All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.app.FiducialDetection Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

There is a newer version: 1.1.7
Show newest version
/*
 * Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.app;

import boofcv.abst.fiducial.FiducialDetector;
import boofcv.abst.fiducial.SquareImage_to_FiducialDetector;
import boofcv.abst.fiducial.calib.ConfigChessboard;
import boofcv.abst.fiducial.calib.ConfigSquareGrid;
import boofcv.alg.geo.PerspectiveOps;
import boofcv.factory.fiducial.ConfigFiducialBinary;
import boofcv.factory.fiducial.ConfigFiducialImage;
import boofcv.factory.fiducial.FactoryFiducial;
import boofcv.factory.filter.binary.ConfigThreshold;
import boofcv.factory.filter.binary.ThresholdType;
import boofcv.gui.fiducial.VisualizeFiducial;
import boofcv.gui.image.ImagePanel;
import boofcv.gui.image.ShowImages;
import boofcv.io.MediaManager;
import boofcv.io.UtilIO;
import boofcv.io.image.ConvertBufferedImage;
import boofcv.io.image.SimpleImageSequence;
import boofcv.io.image.UtilImageIO;
import boofcv.io.wrapper.DefaultMediaManager;
import boofcv.struct.calib.IntrinsicParameters;
import boofcv.struct.image.GrayU8;
import boofcv.struct.image.ImageType;
import georegression.geometry.ConvertRotation3D_F64;
import georegression.struct.se.Se3_F64;
import georegression.struct.so.Quaternion_F64;
import org.ddogleg.struct.GrowQueue_F64;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Command line application for detecting different types of fiducials in different types of input methods.
 *
 * @author Peter Abeles
 */
public class FiducialDetection extends BaseStandardInputApp {

	public static final int DEFAULT_THRESHOLD = 100;

	// path to intrinsic file
	String intrinsicPath;
	// path to where the results should be stored
	String outputPath;

	PrintStream outputFile;

	FiducialDetector detector;

	void printHelp() {
		System.out.println("java -jar BLAH   ");
		System.out.println();
		System.out.println("   Detects different types of fiducials inside of webcam streams, video files, or still images.");
		System.out.println("   Results are visualized in a window and optionally saved to file.");
		System.out.println();
		System.out.println("----------------------------------- Input Flags -----------------------------------------");
		System.out.println();
		printInputHelp();
		System.out.println();
		System.out.println("----------------------------------- Other Flags -----------------------------------------");
		System.out.println();
		System.out.println("These flags are common for all input methods.  They can be specified any time before the");
		System.out.println("fidcuial flags are specified");
		System.out.println();
		System.out.println("  --Intrinsic=                 Specifies location of the intrinsic parameters file.");
		System.out.println("                                     DEFAULT: Make a crude guess.");
		System.out.println();
		System.out.println("  --OutputFile=                Writes the ID and pose of detected fiducials out to a file");
		System.out.println("                                     File format is described in the file's header.");
		System.out.println();
		System.out.println("----------------------------------- Fiducial Flags --------------------------------------");
		System.out.println();
		System.out.println("Fiducial Types:");
		System.out.println("   BINARY");
		System.out.println("   IMAGE");
		System.out.println("   CHESSBOARD");
		System.out.println("   SQUAREGRID");
		System.out.println();
		System.out.println("Flags for BINARY");
		System.out.println();
		System.out.println("  --Robust=              If slower but more robust technique should be used");
		System.out.println("                                     DEFAULT: true");
		System.out.println("  --Size=                     Specifies the size of all the fiducials");
		System.out.println("                                     DEFAULT: 1");
		System.out.println("  --GridWidth=                  Specifies how many inner squares to expect in the fiducial.");
		System.out.println("                                     Valid options: 3 to 8");
		System.out.println("                                     Default is 4");
		System.out.println("  --Border=                   Specifies relative width of the border.");
		System.out.println("                                     DEFAULT: 0.25");
		System.out.println();
		System.out.println("Flags for IMAGE");
		System.out.println();
		System.out.println("  --Robust=              If slower but more robust technique should be used");
		System.out.println("                                     DEFAULT: true");
		System.out.println("  --Image=:  Adds a single image with the specified size");
		System.out.println("                                     Can be called multiple times for several images");
		System.out.println("  --Border=                   Specifies relative width of the border.");
		System.out.println("                                     DEFAULT: 0.25");
		System.out.println("Flags for CHESSBOARD");
		System.out.println();
		System.out.println("  --Shape=:      Number of rows/columns it expects to see");
		System.out.println("  --SquareWidth=              The width of each square");
		System.out.println("                                     Can be called multiple times for several images");
		System.out.println("                                     DEFAULT: 1");
		System.out.println("Flags for SQUAREGRID");
		System.out.println();
		System.out.println("  --Shape=:      Number of rows/columns it expects to see");
		System.out.println("  --SquareWidth=              The width of each square");
		System.out.println("                                     DEFAULT: 1");
		System.out.println("  --Space=                    The space between each square");
		System.out.println("                                     DEFAULT: Same as SquareWidth");
		System.out.println();
		System.out.println("Examples:");
		System.out.println();
		System.out.println("./application BINARY --Size=1 --GridWidth=3");
		System.out.println("        Opens the default camera at default resolution looking for a 3x3 binary patterns with a width of 1");
		System.out.println();
		System.out.println("./application --Camera=1 --Resolution=640:480 BINARY --Robust=false --Size=1");
		System.out.println("        Opens the camera 1 at a resolution of 640x480 using a fast thresholding technique, ");
		System.out.println("        looking for 4x4 binary patterns with a width of 1");
		System.out.println();
		System.out.println("./application -ImageFile=image.jpeg BINARY");
		System.out.println("        Opens \"image.jpg\" and detects binary square fiducials inside of it");
		System.out.println();
	}

	void parse( String []args ) {
		if( args.length < 1 ) {
			throw new RuntimeException("Must specify some arguments");
		}

		for( int i = 0; i < args.length; i++ ) {
			String arg = args[i];

			if( arg.startsWith("--") ) {
				if( !checkCameraFlag(arg) ) {
					if( flagName.compareToIgnoreCase("Intrinsic") == 0 ) {
						intrinsicPath = parameters;
					} else if( flagName.compareToIgnoreCase("OutputFile") == 0 ) {
						outputPath = parameters;
					} else {
						throw new RuntimeException("Unknown camera option "+flagName);
					}
				}
			} else if( arg.compareToIgnoreCase("BINARY") == 0 ) {
				parseBinary(i+1,args);
				break;
			} else if( arg.compareToIgnoreCase("IMAGE") == 0 ) {
				parseImage(i + 1, args);
				break;
			} else if( arg.compareToIgnoreCase("CHESSBOARD") == 0 ) {
				parseChessboard(i + 1,args);
				break;
			} else if( arg.compareToIgnoreCase("SQUAREGRID") == 0 ) {
				parseSquareGrid(i + 1,args);
				break;
			} else {
				throw new RuntimeException("Unknown fiducial type "+arg);
			}
		}
	}

	void parseBinary( int index , String []args ) {
		boolean robust=true;
		double size=1;
		int gridWidth = 4;
		double borderWidth = 0.25;

		for(; index < args.length; index++ ) {
			String arg = args[index];

			if( !arg.startsWith("--") ) {
				throw new  RuntimeException("Expected flags for binary fiducial");
			}

			splitFlag(arg);
			if( flagName.compareToIgnoreCase("Robust") == 0 ) {
				robust = Boolean.parseBoolean(parameters);
			} else if( flagName.compareToIgnoreCase("Size") == 0 ) {
				size = Double.parseDouble(parameters);
			} else if( flagName.compareToIgnoreCase("GridWidth") == 0 ) {
				gridWidth = Integer.parseInt(parameters);
			} else if( flagName.compareToIgnoreCase("Border") == 0 ) {
				borderWidth = Double.parseDouble(parameters);
			} else {
				throw new RuntimeException("Unknown image option "+flagName);
			}
		}

		System.out.println("binary: robust = "+robust+" size = "+size + " grid width = " + gridWidth+" border = "+borderWidth);

		ConfigFiducialBinary configFid = new ConfigFiducialBinary();
		configFid.targetWidth = size;
		configFid.gridWidth = gridWidth;
		configFid.squareDetector.minimumEdgeIntensity = 10;
		configFid.borderWidthFraction = borderWidth;

		ConfigThreshold configThreshold ;

		if( robust )
			configThreshold = ConfigThreshold.local(ThresholdType.LOCAL_SQUARE, 10);
		else
			configThreshold = ConfigThreshold.fixed(DEFAULT_THRESHOLD);

		detector = FactoryFiducial.squareBinary(configFid, configThreshold, GrayU8.class);
	}

	void parseImage( int index , String []args ) {
		boolean robust=true;

		List paths = new ArrayList();
		GrowQueue_F64 sizes = new GrowQueue_F64();
		double borderWidth = 0.25;

		for(; index < args.length; index++ ) {
			String arg = args[index];

			if( !arg.startsWith("--") ) {
				throw new  RuntimeException("Expected flags for image fiducial");
			}

			splitFlag(arg);
			if( flagName.compareToIgnoreCase("Robust") == 0 ) {
				robust = Boolean.parseBoolean(parameters);
			} else if( flagName.compareToIgnoreCase("Image") == 0 ) {
				String words[] = parameters.split(":");
				if( words.length != 2 )throw new RuntimeException("Expected two for width and image path");
				sizes.add(Double.parseDouble(words[0]));
				paths.add(words[1]);
			} else if( flagName.compareToIgnoreCase("Border") == 0 ) {
				borderWidth = Double.parseDouble(parameters);
			} else {
				throw new RuntimeException("Unknown image option "+flagName);
			}
		}

		if( paths.isEmpty() )
			throw new RuntimeException("Need to specify patterns");

		System.out.println("image: robust = "+robust+" total patterns = "+paths.size()+" border = "+borderWidth);

		ConfigFiducialImage config = new ConfigFiducialImage();
		config.borderWidthFraction = borderWidth;
		ConfigThreshold configThreshold;

		if( robust )
			configThreshold = ConfigThreshold.local(ThresholdType.LOCAL_SQUARE, 10);
		else
			configThreshold = ConfigThreshold.fixed(DEFAULT_THRESHOLD);

		SquareImage_to_FiducialDetector detector =
				FactoryFiducial.squareImage(config, configThreshold, GrayU8.class);

		for (int i = 0; i < paths.size(); i++) {
			BufferedImage buffered = UtilImageIO.loadImage(paths.get(i));
			if( buffered == null )
				throw new RuntimeException("Can't find pattern "+paths.get(i));

			GrayU8 pattern = new GrayU8(buffered.getWidth(),buffered.getHeight());

			ConvertBufferedImage.convertFrom(buffered, pattern);

			detector.addPatternImage(pattern,125,sizes.get(i));
		}

		this.detector = detector;
	}

	void parseChessboard( int index , String []args ) {

		int rows=-1,cols=-1;
		double width = 1;

		for(; index < args.length; index++ ) {
			String arg = args[index];

			if (!arg.startsWith("--")) {
				throw new RuntimeException("Expected flags for chessboard calibration fiducial");
			}

			splitFlag(arg);
			if (flagName.compareToIgnoreCase("Shape") == 0) {
				String words[] = parameters.split(":");
				if( words.length != 2 )throw new RuntimeException("Expected two for rows and columns");
				rows = Integer.parseInt(words[0]);
				cols = Integer.parseInt(words[1]);
			} else if (flagName.compareToIgnoreCase("SquareWidth") == 0) {
				width = Double.parseDouble(parameters);
			} else {
				throw new RuntimeException("Unknown chessboard option "+flagName);
			}
		}

		if( rows < 1 || cols < 1)
			throw new RuntimeException("Must specify number of rows and columns");

		System.out.println("chessboard: rows = "+rows+" columns = "+cols+"  square width "+width);
		ConfigChessboard config = new ConfigChessboard(rows, cols, width);

		detector = FactoryFiducial.calibChessboard(config, GrayU8.class);
	}
	void parseSquareGrid( int index , String []args ) {
		int rows=-1,cols=-1;
		double width = 1, space = -1;

		for(; index < args.length; index++ ) {
			String arg = args[index];

			if (!arg.startsWith("--")) {
				throw new RuntimeException("Expected flags for square grid calibration fiducial");
			}

			splitFlag(arg);
			if (flagName.compareToIgnoreCase("Shape") == 0) {
				String words[] = parameters.split(":");
				if( words.length != 2 )throw new RuntimeException("Expected two for rows and columns");
				rows = Integer.parseInt(words[0]);
				cols = Integer.parseInt(words[1]);
			} else if (flagName.compareToIgnoreCase("SquareWidth") == 0) {
				width = Double.parseDouble(parameters);
			} else if (flagName.compareToIgnoreCase("Space") == 0) {
				space = Double.parseDouble(parameters);
			} else {
				throw new RuntimeException("Unknown square grid option "+flagName);
			}
		}

		if( rows < 1 || cols < 1)
			throw new RuntimeException("Must specify number of rows and columns");

		if( space <= 0 )
			space = width;

		System.out.println("square grid: rows = "+rows+" columns = "+cols+"  square width "+width+"  space "+space);
		ConfigSquareGrid config = new ConfigSquareGrid(rows, cols, width,space);

		detector = FactoryFiducial.calibSquareGrid(config, GrayU8.class);
	}

	private static IntrinsicParameters handleIntrinsic(IntrinsicParameters intrinsic, int width, int height) {
		if( intrinsic == null ) {
			System.out.println();
			System.out.println("SERIOUSLY YOU NEED TO CALIBRATE THE CAMERA YOURSELF!");
			System.out.println("There will be a lot more jitter and inaccurate pose");
			System.out.println();

			return PerspectiveOps.createIntrinsic(width, height, 35);
		} else {
			if( intrinsic.width != width || intrinsic.height != height ) {
				System.out.println();
				System.out.println("The image resolution in the intrinsics file doesn't match the input.");
				System.out.println("Massaging the intrinsic for this input.  If the results are poor calibrate");
				System.out.println("your camera at the correct resolution!");
				System.out.println();

				double ratioW = width/(double)intrinsic.width;
				double ratioH = height/(double)intrinsic.height;

				if( Math.abs(ratioW-ratioH) > 1e-8 ) {
					System.err.println("Can't adjust intrinsic parameters because camera ratios are different");
					System.exit(1);
				}
				PerspectiveOps.scaleIntrinsic(intrinsic,ratioW);
			}
			return intrinsic;
		}
	}

	/**
	 * Displays a continuous stream of images
	 */
	private void processStream(IntrinsicParameters intrinsic , SimpleImageSequence sequence , ImagePanel gui , long pauseMilli) {

		Font font = new Font("Serif", Font.BOLD, 24);

		Se3_F64 fiducialToCamera = new Se3_F64();
		int frameNumber = 0;
		while( sequence.hasNext() ) {
			long before = System.currentTimeMillis();
			GrayU8 input = sequence.next();
			BufferedImage buffered = sequence.getGuiImage();
			try {
				detector.detect(input);
			} catch( RuntimeException e ) {
				System.err.println("BUG!!! saving image to crash_image.png");
				UtilImageIO.saveImage(buffered,"crash_image.png");
				throw e;
			}

			Graphics2D g2 = buffered.createGraphics();

			for (int i = 0; i < detector.totalFound(); i++) {
				detector.getFiducialToCamera(i,fiducialToCamera);
				long id = detector.getId(i);
				double width = detector.getWidth(i);

				VisualizeFiducial.drawCube(fiducialToCamera,intrinsic,width,3,g2);
				VisualizeFiducial.drawLabelCenter(fiducialToCamera,intrinsic,""+id,g2);
			}
			saveResults(frameNumber++);

			if( intrinsicPath == null ) {
				g2.setColor(Color.RED);
				g2.setFont(font);
				g2.drawString("Uncalibrated",10,20);
			}

			gui.setBufferedImage(buffered);

			long after = System.currentTimeMillis();
			long time = Math.max(0,pauseMilli-(after-before));
			if( time > 0 ) {
				try { Thread.sleep(time); } catch (InterruptedException ignore) {}
			}
		}
	}

	/**
	 * Displays a simple image
	 */
	private void processImage( IntrinsicParameters intrinsic , BufferedImage buffered , ImagePanel gui ) {

		Font font = new Font("Serif", Font.BOLD, 24);

		GrayU8 gray = new GrayU8(buffered.getWidth(),buffered.getHeight());
		ConvertBufferedImage.convertFrom(buffered,gray);

		Se3_F64 fiducialToCamera = new Se3_F64();
		try {
			detector.detect(gray);
		} catch( RuntimeException e ) {
			System.err.println("BUG!!! saving image to crash_image.png");
			UtilImageIO.saveImage(buffered,"crash_image.png");
			throw e;
		}

		Graphics2D g2 = buffered.createGraphics();

		for (int i = 0; i < detector.totalFound(); i++) {
			detector.getFiducialToCamera(i,fiducialToCamera);
			long id = detector.getId(i);
			double width = detector.getWidth(i);

			VisualizeFiducial.drawCube(fiducialToCamera,intrinsic,width,3,g2);
			VisualizeFiducial.drawLabelCenter(fiducialToCamera,intrinsic,""+id,g2);
		}
		saveResults(0);

		if( intrinsicPath == null ) {
			g2.setColor(Color.RED);
			g2.setFont(font);
			g2.drawString("Uncalibrated",10,20);
		}

		gui.setBufferedImage(buffered);
	}

	private void saveResults( int frameNumber ) {
		if( outputFile == null )
			return;

		Quaternion_F64 quat = new Quaternion_F64();
		Se3_F64 fiducialToCamera = new Se3_F64();

		outputFile.printf("%d %d",frameNumber,detector.totalFound());
		for (int i = 0; i < detector.totalFound(); i++) {
			long id = detector.getId(i);
			detector.getFiducialToCamera(i,fiducialToCamera);

			ConvertRotation3D_F64.matrixToQuaternion(fiducialToCamera.getR(),quat);

			outputFile.printf(" %d %.10f %.10f %.10f %.10f %.10f %.10f %.10f",id,
					fiducialToCamera.T.x,fiducialToCamera.T.y,fiducialToCamera.T.z,
					quat.w,quat.x,quat.y,quat.z);
		}
		outputFile.println();
	}

	private void process() {
		if( detector == null ) {
			System.err.println("Need to specify which fiducial you wish to detect");
			System.exit(1);
		}

		if( outputPath != null ) {
			try {
				outputFile = new PrintStream(outputPath);
				outputFile.println("# Results from fiducial detection ");
				outputFile.println("# These comments should include the data source and the algorithm used, but I'm busy.");
				outputFile.println("# ");
				outputFile.println("#           ...");
				outputFile.println("# ");
				outputFile.println("# The special Euclidean transform saved each fiducial is from fiducial to camera");
				outputFile.println("# (X,Y,Z) is the translation and (Q1,Q2,Q3,Q4) specifies a quaternion");
				outputFile.println("# ");
			} catch (FileNotFoundException e) {
				System.err.println("Failed to open output file.");
				System.err.println(e.getMessage());
				System.exit(1);
			}
		}

		MediaManager media = DefaultMediaManager.INSTANCE;

		IntrinsicParameters intrinsic = intrinsicPath == null ? null : (IntrinsicParameters)UtilIO.loadXML(intrinsicPath);

		SimpleImageSequence sequence = null;
		long pause = 0;
		BufferedImage buffered = null;
		if( inputType == InputType.VIDEO || inputType == InputType.WEBCAM ) {
			if( inputType == InputType.WEBCAM ) {
				String device = getCameraDeviceString();
				sequence = media.openCamera(device,desiredWidth, desiredHeight,ImageType.single(GrayU8.class));
			} else {
				// just assume 30ms is appropriate.  Should let the use specify this number
				pause = 30;
				sequence = media.openVideo(filePath,ImageType.single(GrayU8.class));
				sequence.setLoop(true);
			}
			intrinsic = handleIntrinsic(intrinsic, sequence.getNextWidth(), sequence.getNextHeight());
		} else {
			buffered = UtilImageIO.loadImage(filePath);
			if( buffered == null ) {
				System.err.println("Can't find image or it can't be read.  "+filePath);
				System.exit(1);
			}
			intrinsic = handleIntrinsic(intrinsic, buffered.getWidth(),buffered.getHeight());
		}


		ImagePanel gui = new ImagePanel();
		gui.setPreferredSize(new Dimension(intrinsic.width,intrinsic.height));
		ShowImages.showWindow(gui,"Fiducial Detector",true);
		detector.setIntrinsic(intrinsic);

		if( sequence != null ) {
			processStream(intrinsic,sequence,gui,pause);
		} else {
			processImage(intrinsic,buffered, gui);
		}

	}

	public static void main(String[] args) {
		FiducialDetection app = new FiducialDetection();
		try {
			app.parse(args);
		} catch( RuntimeException e ) {
			app.printHelp();
			System.out.println();
			System.out.println(e.getMessage());
			System.exit(0);
		}
		try {
			app.process();
		} catch( RuntimeException e ) {
			System.out.println();
			System.out.println(e.getMessage());
			System.exit(0);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy