Using GPhoto for move detection

Using GPhoto for move detection

This is my first blog entry in more than two years. I've been very busy with my Android planetarium project, now almost finished, but probably needed also some rest. Despite of that during this time I have developed a number of little nice projects, and waited for the right moment to resume my blog activity and release some of them. As ussual they are not oriented to specific goals, going from 3d renderings to solving complex and time consuming tasks in a completely automatic manner, or even to criptocurrencies.

In this post I present a little program aimed to detecting move and triggering shots with a camera automatically. It is based on the gphoto library, and uses the gphoto binding implemented in the JPARSEC library. Some features of the binding have been improved or developed to support this use case. For instance, the live view mode didn't support executing other commands except those to capture previews, and it wasn't possible to execute this mode without showing a panel object with those previews.

The program provides many (31) configuration options in a file named config.txt. Among others, it is possible to set some custom parameters for the camera at startup (ISO, shutter, aperture, among others) and right before the program ends, adjust the sensitivity of the move detection algorithm, configure masks the search for move only in certain areas, or to uppload the shots to a server.

My intenction is to use this program for security, shooting when move is detected with a high resolution camera instead of a webcam. After thinking from time to time on this a saw a post in the gphoto mail list from Alan Corey, asking for a way to do something similar for wild photography. So I decided to start writing the program and Alan collaborated testing it in his cameras. In fact, the program has been tested successfully in a Canon 40D, a Nikon D5200, and a Canon Powershot S70 and A520. It has also been tested in a variety of CPU devices, from a powerful desktop to a Raspberry PI 3. Here I will describe the basics of this program, some other details can be found in the gphoto mailing list and previous messages about the same subject.

The move detection algorithm is very simple. It searches for move in two ways: detecting a global change in the luminosity of the preview image, and detecting changes in the brightness level of the pixels. The preview image is internally converted to gray scale since the color data is not required to detect move. The global change is computed from the difference between the histograms of the preview image and a reference preview image taken when the program starts. This reference image should be free of movement, and is optionally updated in a given time interval to account for possible brightness changes due to the sky or the weather. The histogram difference is computed pixel by pixel, and this change in the gray color is also used to compute the number of individual pixels that present some move or change in that level, according to a different change criteria specific to individual pixels. So the second move detection check is computed from the percentage of individual pixels showing move, and the move is considered as detected when any of those two methods trigger a detection.

When this happens the program pauses the live view in the JPARSEC binding to execute other commands to take a number of shots, resuming back after that the live view mode.

The program has as dependency the jsch library to uppload images to a server, and the itextpdf for its Base64 class used here to include images in the web server. The idea is to compile it with Java 6 at least. Here is the code of the main class.

GPhotoMoveDetection.java
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
 
import com.itextpdf.text.pdf.codec.Base64;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
 
import jparsec.graph.DataSet;
import jparsec.graph.chartRendering.AWTGraphics;
import jparsec.io.ConsoleReport;
import jparsec.io.FileIO;
import jparsec.io.ReadFile;
import jparsec.io.device.implementation.GPhotoCamera;
import jparsec.io.device.implementation.GPhotoCamera.CAMERA_ID;
import jparsec.io.device.implementation.GPhotoCamera.CAMERA_PARAMETER;
import jparsec.io.image.Picture;
import jparsec.time.AstroDate;
import jparsec.vo.FTP;
 
/**
 * An example of using GPhoto library (through the binding integrated in JPARSEC) to control 
 * a DSLR camera and trigger a photo when movement is detected.
 * 
 * @author T. Alonso Albi - OAN (Spain)
 * @version 1.0
 */
public class GPhotoMoveDetection {
 
	// Export only this class to gphoto.jar and execute with:
	// java -classpath gphoto.jar:jparsec.jar:jsch-0.1.41.jar:jsch-0.1.41.jar:itextpdf-5.1.3.jar research.other.GPhotoMoveDetection 
 
	// TODO:
	// Option to pause motion detection ?
	// Server page reloading all the time, problematic with high fps. Reload by sections with different rates ?
	// Is it possible to trigger several shots quickly from shell ? Option to use the other method ?
	// Avoid overwriting old shots ?
	// Send e-mail with move alert ?
 
	public static GPhotoCamera c = null;
	public static String inISO, inAPERTURE, inCAPTURE_TARGET, inNIKON_QUALITY, inRESOLUTION, inSHUTTER_SPEED;
	public static String outISO, outAPERTURE, outCAPTURE_TARGET, outNIKON_QUALITY, outRESOLUTION, outSHUTTER_SPEED;
	public static FTP ftp = null;
	public static ArrayList<String> messages = new ArrayList<String>(), shotList = new ArrayList<String>();
	public static ArrayList<Object> thumbs = new ArrayList<Object>();
	public static int n = 0, nn = 0, moveEvents = 0;
	public static long lastMoveEvent;
	public static boolean forceExit = false, forcePause = false, createThumbs = true, keepInCamera = false;
	public static int webServerPort = 8080; // Server port (int)
	public static int liveMaxTime = 0; // sec, <= 0 => for ever
	public static int updateRef = 3600; // sec, -1 => never update
	public static int resample = 320; // width, -1 => no resample
	public static int thresholdBr = 10; // brightness % change to trigger global movement 
	public static int thresholdPx = 10; // brightness % change to trigger pixel movement 
	public static int movingPixPerc = 5; // % of pixel with change greater than threshold to trigger local movement
	public static int fps = -2; // fps of live view, < 0 for <1 fps (-2 => 0.5 fps)
	public static int nshot = 3; // number of shots to take
	public static int maxShot = 1000; // maximum number of shots to take
	public static boolean rename = true; // True to rename shots to 'shot_xxx.jpg'
	public static String sound = null;
	public static String server = null, user = null, pass = null, remoteDir = null; // to uppload images
	public static String extension = ".jpg"; // extension in lowercase for the new relevant images
	public static String mask[] = new String[] {
		"--------------------------------",
		"--------------------------------",
		"--------------------------------",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"--------------------------------",
		"--------------------------------",
		"--------------------------------",
	};
	public static boolean debug = true;
 
	/**
	 * Test program.
	 * @param args Unused.
	 */
	public static void main(String args[])
	{
		try {			
			String dir = FileIO.getWorkingDirectory();
			loadConfig(false);
 
			String out[] = GPhotoCamera.autoDetect();
			if (debug) System.out.println("Using gphoto "+GPhotoCamera.gphotoVersion);
			if (debug) System.out.println("Detected cameras:");
			ConsoleReport.stringArrayReport(out);
			c = new GPhotoCamera(CAMERA_ID.EOS40D, null, dir, false, debug);
 
			// Set custom camera parameters or read them from camera
			inISO = setParameter(inISO, CAMERA_PARAMETER.ISO);
			inAPERTURE = setParameter(inAPERTURE, CAMERA_PARAMETER.APERTURE);
			inCAPTURE_TARGET = setParameter(inCAPTURE_TARGET, CAMERA_PARAMETER.CAPTURE_TARGET);
			inNIKON_QUALITY = setParameter(inNIKON_QUALITY, CAMERA_PARAMETER.NIKON_QUALITY);
			inRESOLUTION = setParameter(inRESOLUTION, CAMERA_PARAMETER.RESOLUTION);
			inSHUTTER_SPEED = setParameter(inSHUTTER_SPEED, CAMERA_PARAMETER.SHUTTER_SPEED);
 
			c.setTimeLimitForLiveView(liveMaxTime);
			c.setLiveFPS(fps);
			c.setCopyInCamera(keepInCamera);
 
			if (debug) {
				CAMERA_PARAMETER cv[] = CAMERA_PARAMETER.values();
				for (int i=0; i<cv.length; i++)
				{
					System.out.println("Possible values of "+cv[i]);
					String values[] = c.getConfig(cv[i]);
					ConsoleReport.stringArrayReport(values);
				}
			}
 
			try {
				if (debug) System.out.println("Creating web server on port "+webServerPort);
				createServer(webServerPort);
			} catch (Exception e) {
				e.printStackTrace();
			}
 
			n = 0;
			nn = 0;
			long lastShotTime = -1;
			while(!forceExit) {
				if (forcePause) {
					if (debug) System.out.println("Live view is currently paused");					
				} else {
					if (c.isLivePaused()) {
						if (debug) System.out.println("Resuming live view");
						c.resumeLiveView();
						loadConfig(true);
					} else {
						if (debug) System.out.println("Starting live view");
						if (!c.isLive()) c.startLiveView(null, fps);
					}
				}
				long wait = (1000 / fps);
				if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
				while (!forcePause) {
					try {
						if (forceExit) break;
						if (!c.isLive()) {
							if (debug) System.out.println("Detected live view was stopped (limit time). Waiting 5s and restarting live view mode ...");
							long t0 = System.currentTimeMillis();
							long t1 = t0 + 5000; // Wait 5s to restart live view
							while (true) {
								long t2 = System.currentTimeMillis();
								if (t2 < t1) {
									Thread.sleep(500);
									continue;
								}
								break;
							}
							String lastShot = c.getLastShotPath();
							if (lastShot != null && FileIO.exists(lastShot)) FileIO.deleteFile(lastShot);
							if (debug) System.out.println("Starting live view");
							c.startLiveView(null, fps);
						}
 
						String lastShot = c.getLastShotPath();
						boolean move = false;
						if (lastShot != null && FileIO.exists(lastShot)) {
							long time = (new File(lastShot)).lastModified();
							if (time != lastShotTime) {
								Picture pic = new Picture(lastShot);
								move = detectMove(pic);
							}
							lastShotTime = time;
						}
 
						if (move) {
							if (debug) System.out.println("Pausing live view");
							c.pauseLiveView();
							long t0 = System.currentTimeMillis();
							long t1 = t0 + wait * 2;
							while (true) {
								long t2 = System.currentTimeMillis();
								if (t2 < t1) {
									Thread.sleep(100);
									continue;
								}
								break;
							}
							break;
						}
 
						Thread.sleep(wait / 50);
					} catch (Exception exc) {
						exc.printStackTrace();
						continue;
					}
				}
 
				if (forceExit) break;
				if (forcePause) {
					if (!c.isLivePaused()) c.pauseLiveView();
					long t0 = System.currentTimeMillis();
					long t1 = t0 + wait * 2;
					while (true) {
						long t2 = System.currentTimeMillis();
						if (t2 < t1) {
							try {
								Thread.sleep(1000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							continue;
						}
						break;
					}
					continue;
				}
 
				// Shot/s after move detection
				moveEvents ++;
				lastMoveEvent = System.currentTimeMillis();
				addMessage("Move detected!");
				if (n > maxShot) {
					addMessage("Aborting taking shots: too much files (> "+maxShot+")");
				} else {
					// Shot inside shell using the binding
					String com = "capture-image-and-download";
					boolean lock = false;
 
					String path = "";
					for (int i=0; i<nshot; i++) {
						c.executeExternalCommandInPausedLiveView(com);
						long t0 = System.currentTimeMillis();
						while (true) {
							Thread.sleep(50);
							if (c.getExternalCommandInPausedLiveView() == null) break;
							if (System.currentTimeMillis() - t0 > 30000) {
								lock = true;
								break;
							}
						}
						if (lock) break;
						path += c.getLastShotPath()+",";
					}
					if (!path.equals("")) path = path.substring(0, path.length()-1);
 
					if (lock) addMessage("Detected camera freeze when taking shots (too much of them ?)");
					if (debug) System.out.println("Created files: "+path);
					n += nshot;
					if (!lock && !path.equals("")) {
						String p[] = DataSet.toStringArray(path, ",");
						for (int i=0; i<p.length; i++) {
							if (p[i].toLowerCase().endsWith(extension)) {
								if (debug) System.out.println("New shot found: "+p[i]);
								Picture pic = null;
								if (rename) {
									nn ++;
									pic = new Picture(p[i]);
									pic.write("shot"+nn+".jpg");
									FileIO.deleteFile(p[i]);
									p[i] = "shot"+nn+".jpg";
								}								
								if (createThumbs) {
									shotList.add(p[i]);
									if (pic == null) pic = new Picture(p[i]);
									if (resample > 0) pic.getScaledInstance(resample, 0, true);
									thumbs.add(pic.getImage());
								}
								if (ftp != null) {
									if (debug) System.out.println("Upploading "+p[i]+" to "+user+"@"+server);
									ftp.uppload(p[i], p[i]);
								}
							}
						}
					}
				}
 
				loadConfig(true);
				c.resumeLiveView();
 
				try {
					Thread.sleep(wait*2);
				} catch (Exception exc) {
					exc.printStackTrace();
				}
			}
 
			// Set at the end custom camera parameters or those read from camera at the beginning
			setParameter(inISO, outISO, CAMERA_PARAMETER.ISO);
			setParameter(inAPERTURE, outAPERTURE, CAMERA_PARAMETER.APERTURE);
			setParameter(inCAPTURE_TARGET, outCAPTURE_TARGET, CAMERA_PARAMETER.CAPTURE_TARGET);
			setParameter(inNIKON_QUALITY, outNIKON_QUALITY, CAMERA_PARAMETER.NIKON_QUALITY);
			setParameter(inRESOLUTION, outRESOLUTION, CAMERA_PARAMETER.RESOLUTION);
			setParameter(inSHUTTER_SPEED, outSHUTTER_SPEED, CAMERA_PARAMETER.SHUTTER_SPEED);
 
			if (debug) System.out.println("Exiting ...");
			if (c.isLivePaused()) c.resumeLiveView();
			c.stopLiveView();
			try {
				long wait = (1000 / fps);
				if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
				long t0 = System.currentTimeMillis();
				long t1 = t0 + wait * 3;
				while (true) {
					long t2 = System.currentTimeMillis();
					if (t2 < t1) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						continue;
					}
					break;
				}
 
				System.exit(0);
			} catch (Exception exc) {
				exc.printStackTrace();
			}
 
		} catch (Exception exc)
		{
			exc.printStackTrace();
		}
	}
 
	public static String setParameter(String value, CAMERA_PARAMETER p) throws Exception {
		if (c == null || value == null) return value;
		if (value.equals("null")) {
			value = c.getParameterFromCamera(p);
		} else {
			c.setParameter(p, value);
		}
		return value;
	}
 
	public static void setParameter(String valueIn, String valueOut, CAMERA_PARAMETER p) throws Exception {
		if (c == null || valueIn == null || valueOut == null) return;
		if (valueOut.equals("null")) {
			c.setParameter(p, valueIn);
		} else {
			c.setParameter(p, valueOut);
		}
	}
 
	public static void loadConfig(boolean reload) throws Exception {
		String dir = FileIO.getWorkingDirectory(), config = dir + "config.txt";
		if (!FileIO.exists(config) && !reload) {
			System.out.println("Cannot find config file "+config);
			System.exit(0);
		}
 
		String d[] = DataSet.arrayListToStringArray(ReadFile.readAnyExternalFile(config));
		String param[] = new String[] {
			"UPDATE_REF", "RESAMPLE", "THRESHOLD_BR", "THRESHOLD_PX", "MOVING_PIX_PER", "FPS", 
			"NSHOTS", "MAX_SHOTS", "EXTENSION", "MASK", "RENAME", "SERVER", "USER", "PASSW", 
			"REMOTE_DIR", "LIVE_MAX_TIME", "WEB_SERVER_PORT", "CREATE_THUMBS", "KEEP_IN_CAMERA",
			"ISO_IN", "APERTURE_IN", "CAPTURE_TARGET_IN", "NIKON_QUALITY_IN", "RESOLUTION_IN", 
			"SHUTTER_SPEED_IN", "ISO_OUT", "APERTURE_OUT", "CAPTURE_TARGET_OUT", "NIKON_QUALITY_OUT", 
			"RESOLUTION_OUT", "SHUTTER_SPEED_OUT"
 
		};
		for (int i=0; i<d.length; i++) {
			for (int j=0; j<param.length; j++) {
				if (d[i].startsWith(param[j])) {
					if (debug) System.out.println("Reading "+d[i]);
					String val = d[i].substring(d[i].indexOf(" ")).trim();
					if (val.indexOf("//") > 0) val = val.substring(0, val.indexOf("//")).trim();
 
					if (j == 0) updateRef = Integer.parseInt(val);
					if (j == 1) resample = Integer.parseInt(val);
					if (j == 2) thresholdBr = Integer.parseInt(val);
					if (j == 3) thresholdPx = Integer.parseInt(val);
					if (j == 4) movingPixPerc = Integer.parseInt(val);
					if (j == 5) fps = Integer.parseInt(val);
					if (j == 6) nshot = Integer.parseInt(val);
					if (j == 7) maxShot = Integer.parseInt(val);
					if (j == 8) extension = val;
					if (j == 9) mask = DataSet.getSubArray(d, i+1, d.length-1);
					if (j == 10) rename = Boolean.parseBoolean(val);
					if (j == 11) server = val;
					if (j == 12) user = val;
					if (j == 13) pass = val;
					if (j == 14) remoteDir = val;
					if (j == 15) liveMaxTime = Integer.parseInt(val);
					if (j == 16) webServerPort = Integer.parseInt(val);
					if (j == 17) createThumbs = Boolean.parseBoolean(val);
					if (j == 18) keepInCamera = Boolean.parseBoolean(val);
 
					if (reload) continue;
					if (j == 19) inISO = val;
					if (j == 20) inAPERTURE = val;
					if (j == 21) inCAPTURE_TARGET = val;
					if (j == 22) inNIKON_QUALITY = val;
					if (j == 23) inRESOLUTION = val;
					if (j == 24) inSHUTTER_SPEED = val;
					if (j == 25) outISO = val;
					if (j == 26) outAPERTURE = val;
					if (j == 27) outCAPTURE_TARGET = val;
					if (j == 28) outNIKON_QUALITY = val;
					if (j == 29) outRESOLUTION = val;
					if (j == 30) outSHUTTER_SPEED = val;
				}
			}
		}
 
		if (ftp != null) ftp.disconnect();
		ftp = null;
		if (server != null && !server.equals("null") && user != null && !user.equals("null") 
				&& pass != null && !pass.equals("null")) 
			ftp = new FTP(server, user, pass);
		if (remoteDir != null && !remoteDir.equals("null")) ftp.changeDirectory(remoteDir);
 
		if (reload) addMessage("Configuration reloaded");
		if (c != null) {
			c.setLiveFPS(fps);
			c.setTimeLimitForLiveView(liveMaxTime);
			c.setCopyInCamera(keepInCamera);
		}
 
		String pp = dir + "sound.mp3";
		sound = null;
		if (FileIO.exists(pp)) {
			byte[] bytes = Files.readAllBytes(Paths.get(pp));
			sound = Base64.encodeBytes(bytes); 
			sound = "data:audio/mp3;base64,"+sound;
		}
	}
 
	public static Picture ref = null, lastPicColor = null, refColor = null;
	public static long lastRef = 0;
	public static boolean detectMove(Picture pic) throws Exception {
		if (resample > 0) pic.getScaledInstance(resample, 0, true);
 
		// Set reference at startup
		BufferedImage copy = Picture.copy(pic.getImage());
		if (refColor == null) {
			refColor = new Picture(copy);
			Graphics2D g2 = refColor.getImage().createGraphics();
			AWTGraphics.enableAntialiasing(g2);
			float fs = 20;
			g2.setColor(Color.RED);
			g2.setFont(g2.getFont().deriveFont(fs));
			g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
			g2.setColor(Color.WHITE);
			for (int y=0; y<pic.getHeight(); y++) {
				int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
				for (int x=0; x<pic.getWidth(); x++) {
					int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
					if (mask[cy].substring(cx, cx+1).equals("-"))
						continue;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}
			g2.dispose();
 
			pic.toGrayScale();
			ref = pic;
			lastRef = System.currentTimeMillis();
			addMessage("Reference created");
			return false;
		}
 
		// Detect movement
		lastPicColor = new Picture(pic.getImage());
		Graphics2D g2 = lastPicColor.getImage().createGraphics();
		AWTGraphics.enableAntialiasing(g2);
		float fs = 20;
		g2.setColor(Color.RED);
		g2.setFont(g2.getFont().deriveFont(fs));
		g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
		g2.setColor(Color.WHITE);
 
		pic.toGrayScale();
		boolean move = false;
		double histo[] = new double[255];
		int movingPixels = 0, maskedPixels = 0;		
		for (int y=0; y<pic.getHeight(); y++) {
			int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
			for (int x=0; x<pic.getWidth(); x++) {
				int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
				if (mask[cy].substring(cx, cx+1).equals("-")) {
					maskedPixels ++;
					continue;
				}
 
				Color c1 = pic.getColorAt(x, y);
				Color c2 = ref.getColorAt(x, y);
				int gray1 = c1.getRGB() & 255;
				int gray2 = c2.getRGB() & 255;
				int g = Math.abs(gray1 - gray2);
				histo[g] ++;
				if (g > thresholdPx*2.55) {
					movingPixels ++;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}			
		}
		g2.dispose();
		int maxIndex = (int) (DataSet.getIndexOfMaximum(histo) / 2.55);
		double percMov = movingPixels * 100.0 / (double) (pic.getWidth() * pic.getHeight() - maskedPixels);
		if (maxIndex > thresholdBr || percMov > movingPixPerc) move = true;
		if (debug) System.out.println("Global move: "+maxIndex+"/"+thresholdBr+". Local move: "+(float)percMov+"/"+movingPixPerc);
 
		// Update reference if required
		double elapsed = (System.currentTimeMillis() - lastRef) * 0.001;
		if (!move && updateRef > 0 && elapsed > updateRef) {
			ref = pic;
			refColor = new Picture(copy);
			lastRef = System.currentTimeMillis();			
 
			g2 = refColor.getImage().createGraphics();
			AWTGraphics.enableAntialiasing(g2);
			fs = 20;
			g2.setColor(Color.RED);
			g2.setFont(g2.getFont().deriveFont(fs));
			g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
			g2.setColor(Color.WHITE);
			for (int y=0; y<pic.getHeight(); y++) {
				int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
				for (int x=0; x<pic.getWidth(); x++) {
					int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
					if (mask[cy].substring(cx, cx+1).equals("-"))
						continue;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}
 
			addMessage("Reference updated");
		}
		return move;
	}
 
	public static String addMessage(String msg) {
		AstroDate astro = new AstroDate();
		String m = astro.toString();
		if (msg != null) {
			m += ": "+msg;
			messages.add(0, m);
 
			if (debug) System.out.println(msg);
		}
		return m;
	}
 
	public static String commands[] = new String[] {
		"Pause,left,Pausing web server",
		"Resume,center,Resuming web server",
		"Exit,right,Exiting"
	};
	public static String imgFormat = "jpg"; // jpg to reduce file size
	public static void createServer(int port) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
        server.createContext("/", new MyHandler(-1));
 
        for (int i=0; i<commands.length; i++) {
        	String name = "com"+i;
 
            server.createContext("/"+name, new MyHandler(i));
        }
 
        server.setExecutor(null); // creates a default executor
        server.start();
	}
 
   static class MyHandler implements HttpHandler {
    	int index = -1;
 
    	public MyHandler() {
		}
 
    	public MyHandler(int i) {
    		this.index = i;
		}
 
        @Override
        public void handle(HttpExchange t) throws IOException {
        	StringBuffer response = new StringBuffer();
        	String sep = "<BR><BR>", step = FileIO.getLineSeparator();
 
			long wait = (1000 / fps);
			if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
			int sec = (int) (wait / 1000 + 0.5);
    		response.append("<html><head><title>Web server</title><meta http-equiv=\"refresh\" content=\""+sec+", url=/\"></head><body bgcolor=\"#000000\">");
    		response.append("<center><H1 style=\"color: white\">GPhoto web server</H1></center>"+ step);
    		try {
    			response.append("<p style=\"color: white; font-size:30px; float: left\">Reference</p><p style=\"color: white; font-size:30px; float: right\">Live view (last image)</p>" + sep + step);
    			response.append("<div style=\"display: inline-block\">" + step);
        		if (refColor != null)
        			response.append("<img src=\""+refColor.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: left\" />" + step);
        		if (lastPicColor != null)
        			response.append("<img src=\""+lastPicColor.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: right\" />"+step);
        		response.append("</div>" + step);
    		} catch (Exception exc) {
    			exc.printStackTrace();
    		}
    		response.append(sep + "<HR>" + step);
    		if (c != null) {
    			response.append("<p style=\"color: white; font-size:20px\">Camera: "+c.getModel()+", port "+c.getPort()+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Working directory: "+c.getWorkingDirectory()+"</p>"+step);
        		String status = "running";
        		if (c.isLivePaused()) status = "paused";
    			response.append("<p style=\"color: white; font-size:20px\">Status: "+status+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Move events: "+moveEvents+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Shots taken: "+n+"</p>"+step);
    			String config = "liveMaxTime="+liveMaxTime+", ";
    			config += "updateRef="+updateRef+", ";
    			config += "resample="+resample+", ";
    			config += "thresholdBr="+thresholdBr+", ";
    			config += "thresholdPx="+thresholdPx+", ";
    			config += "movingPixPerc="+movingPixPerc+", ";
    			config += "fps="+fps+", ";
    			config += "nshot="+nshot+", ";
    			config += "maxShot="+maxShot+", ";
    			config += "rename="+rename+", ";
    			config += "createThumbs="+createThumbs+", ";
    			config += "keepInCamera="+keepInCamera+", ";
    			config += "server="+user+"@"+server+", ";
    			response.append("<p style=\"color: white; font-size:20px\">Configuration: "+config+"</p>"+step);
    		}
 
    		response.append("<HR>" + sep + step);
    		response.append("<div style=\"display: block; text-align: center\">" + step);
    		for (int i=0; i<3; i++) {
    			String title = FileIO.getField(1, commands[i], ",", false);
    			String align = FileIO.getField(2, commands[i], ",", false);
    			response.append("<input type=\"button\" style=\"color: white; float: "+align+"; font-size:40px\" onclick=\"location.href='/com"+i+"';\" value=\""+title+"\" />" + step);
    		}
    		response.append("</div>" + step);
 
    		if (index >= 0) {
    			String msg = FileIO.getRestAfterField(2, commands[index], ",", false).trim();
        		response.append("<center><H2 style=\"color: white\">" + msg + sep + step);
        		response.append("</H2></center>");
 
        		if (index == 0) {
        			forcePause = true;
        			addMessage("Paused");
        		}
        		if (index == 1) {
        			forcePause = false;
        			addMessage("Resumed");
        		}
        		if (index == 2) {
        			forceExit = true;
        			addMessage("Exited");
        		}
    		}
 
    		if (shotList != null && shotList.size() > 0) {
    			response.append("<HR>" + step);
 
    			try {
        			boolean left = true;
	        		for (int i=shotList.size()-1; i>=0; i--) {
	        			if (left) response.append("<div style=\"display: inline-block\">" + step);		        			
	        			Picture pic = new Picture((BufferedImage) thumbs.get(i));
	        			response.append("<img src=\""+pic.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: "+(left ? "left" : "right")+"\" />" + step);
	        			if (!left) {
	        				response.append("</div>" + step);
	            			response.append("<p style=\"color: white; font-size:24px; float: left\">"+shotList.get(i+1)+"</p><p style=\"color: white; font-size:24px; float: right\">"+shotList.get(i)+"</p>" + step);
	        			} else {
	        				if (i == 0)
		            			response.append("<p style=\"color: white; font-size:24px; float: left\">"+shotList.get(i)+"</p>" + step);
	        			}
	        			left = !left;
	        		}
    			} catch (Exception exc) {
    				exc.printStackTrace();
    			}
 
    			response.append(sep + sep + sep + sep + sep);
    		}  
 
    		if (messages != null && messages.size() > 0) {
        		response.append("<HR>" + step);
 
        		String m[] = DataSet.arrayListToStringArray(messages);
        		for (int i=0; i<m.length; i++) {
        			response.append("<p style=\"color: white\"; font-size:16px>"+m[i]+"</p>" + step);
        		}
    		}
 
    		if (sound != null && lastMoveEvent > 0) {
    			double elapsed = (System.currentTimeMillis() - lastMoveEvent) * 0.001;
    			if (elapsed < sec) {
		    		response.append("<audio controls=\"controls\" autobuffer=\"autobuffer\" autoplay=\"autoplay\">"+step);
		    		response.append("<source src=\""+sound+"\" />"+step);
		    		response.append("</audio>"+step);
    			}
    		}
 
    		response.append("</body></html>");
 
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.toString().getBytes());
            os.close();
        }
    }
}

The program includes a little web server to see the reference image with the move detection mask, the latest live view image showing the pixels with movement, some buttons to pause or stop the program, and some log messages. The server also shows the shots taken when move was detected. The next image shows how the web server looks like.


 

To access the web server in the same computer in which gphoto is running and the camera is connected you have to open a browser and load http://localhost:port, where port is the port number configured in the program (8080 by default). Of course knowing the computer IP you can load it from another device in the same network, or even from another place outside that network with a proper configuration of the router.

The full working program can be downloaded from this url. It should work with no modification in Linux and Mac. In Windows the launching script would need some modification (mainly change ; by : and write it to a .bat file), but anyway you first need to compile and install the gphoto library, so anyone capable of doing that would have no problem executing the program.

Discussion

Enter your comment
   __ __   ____   ____   ___   _____
  / // /  /  _/  /  _/  / _ \ / ___/
 / _  /  _/ /   _/ /   / , _// (_ / 
/_//_/  /___/  /___/  /_/|_| \___/
 
 
blog/gphoto_move_detection.txt · created: 2018/01/19 13:32 (Last modified 2018/11/21 11:18) by Tom├ís Alonso Albi
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki