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

boofcv.alg.fiducial.square.DetectFiducialSquareImage 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.6
Show newest version
/*
 * Copyright (c) 2021, 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.alg.fiducial.square;

import boofcv.abst.distort.FDistort;
import boofcv.abst.filter.binary.InputToBinary;
import boofcv.alg.descriptor.DescriptorDistance;
import boofcv.alg.filter.binary.GThresholdImageOps;
import boofcv.alg.filter.misc.AverageDownSampleOps;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.alg.misc.ImageStatistics;
import boofcv.alg.misc.PixelMath;
import boofcv.alg.shapes.polygon.DetectPolygonBinaryGrayRefine;
import boofcv.core.image.ConvertImage;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.GrayU8;
import boofcv.struct.image.ImageGray;

import java.util.ArrayList;
import java.util.List;

/**
 * 

* Fiducial which uses images to describe arbitrary binary patterns. When useing this fiducial it's up to the user to * select good images which will provide unique orientation and are easily distinguished against other patterns and * noise insensitive. *

*
* *
*

* The above image visually shows the fiducials internal coordinate system. The center of the fiducial is the origin * of the coordinate system, e.g. all sides are width/2 distance away from the origin. +x is to the right, +y is up * , and +z out of the paper towards the viewer. *

*

* A good pattern will have thick lines or thick shapes. When detecting the image it's not uncommon for the distortion * removal to be off by one or two pixels. So think lines are be completely out of synch. The image should also * be chosen so that there is to rotational ambiguity. A perfect circle in the center is an example of a bad fiducial * in which orientation can't be uniquely determined. *

* @author Peter Abeles */ public class DetectFiducialSquareImage> extends BaseDetectFiducialSquare { // Width of black border (units = pixels) private final static int w=16; private final static int squareLength=w*4; // this must be a multiple of 16 // length of description in 16bit units private final static int DESC_LENGTH = squareLength*squareLength/16; // converts the input image into a binary one private GrayU8 binary = new GrayU8(squareLength,squareLength); // list of all known targets private List targets = new ArrayList<>(); // description of the current target candidate private short squareDef[] = new short[DESC_LENGTH]; // storage for no border sub-image private GrayF32 grayNoBorder = new GrayF32(); // if the hamming score is better than this it is considered to be a good match private int hammingThreshold; /** * Configures the fiducial detector * * @param matchThreshold Considered a match if the hamming distance is less than this fraction of the maximum */ public DetectFiducialSquareImage(InputToBinary inputToBinary, DetectPolygonBinaryGrayRefine quadDetector, double borderWidthFraction , double minimumBlackBorderFraction , double matchThreshold, Class inputType) { super(inputToBinary,quadDetector, false, borderWidthFraction, minimumBlackBorderFraction, (int)Math.round(squareLength/(1-2.0*borderWidthFraction)), inputType ); hammingThreshold = (int)(squareLength*squareLength*matchThreshold); //noinspection ConstantConditions if( squareLength%16 != 0 ) throw new RuntimeException("Square Length must be a multiple of 16"); } /** * Adds a new image to the detector. Image must be gray-scale and is converted into * a binary image using the specified threshold. All input images are rescaled to be * square and of the appropriate size. Thus the original shape of the image doesn't * matter. Square shapes are highly recommended since that's what the target looks like. * * @param inputBinary Binary input image pattern. 0 = black, 1 = white. * @param lengthSide How long one of the sides of the target is in world units. * @return The ID of the provided image */ public int addPattern(GrayU8 inputBinary, double lengthSide) { if( inputBinary == null ) { throw new IllegalArgumentException("Input image is null."); } else if( lengthSide <= 0 ) { throw new IllegalArgumentException("Parameter lengthSide must be more than zero"); } else if(ImageStatistics.max(inputBinary) > 1 ) throw new IllegalArgumentException("A binary image is composed on 0 and 1 pixels. This isn't binary!"); // see if it needs to be resized if ( inputBinary.width != squareLength || inputBinary.height != squareLength ) { // need to create a new image and rescale it to better handle the resizing GrayF32 inputGray = new GrayF32(inputBinary.width,inputBinary.height); ConvertImage.convert(inputBinary,inputGray); PixelMath.multiply(inputGray,255,inputGray); GrayF32 scaled = new GrayF32(squareLength,squareLength); // See if it can use the better algorithm for scaling down the image if( inputBinary.width > squareLength && inputBinary.height > squareLength ) { AverageDownSampleOps.down(inputGray,scaled); } else { new FDistort(inputGray,scaled).scaleExt().apply(); } GThresholdImageOps.threshold(scaled,binary,255/2.0,false); } else { binary.setTo(inputBinary); } // describe it in 4 different orientations FiducialDef def = new FiducialDef(); def.lengthSide = lengthSide; // CCW rotation so that the index refers to how many CW rotation it takes to put it into the nominal pose binaryToDef(binary, def.desc[0]); ImageMiscOps.rotateCCW(binary); binaryToDef(binary, def.desc[1]); ImageMiscOps.rotateCCW(binary); binaryToDef(binary, def.desc[2]); ImageMiscOps.rotateCCW(binary); binaryToDef(binary, def.desc[3]); int index = targets.size(); targets.add( def ); return index; } /** * Converts a binary image into the compressed bit format */ protected static void binaryToDef(GrayU8 binary , short[] desc ) { for (int i = 0; i < binary.data.length; i+=16) { int value = 0; for (int j = 0; j < 16; j++) { value |= binary.data[i+j] << j; } desc[i/16] = (short)value; } } @Override protected boolean processSquare(GrayF32 gray, Result result, double edgeInside, double edgeOutside) { int off = (gray.width-binary.width)/2; gray.subimage(off,off,off+binary.width,off+binary.width,grayNoBorder); // grayNoBorder.printInt(); // compute a global threshold from the difference between the outside and inside perimeter pixel values float threshold = (float)((edgeInside+edgeOutside)/2.0); GThresholdImageOps.threshold(grayNoBorder,binary,threshold,false); // binary.printBinary(); binaryToDef(binary, squareDef); boolean matched = false; int bestScore = hammingThreshold+1; for (int i = 0; i < targets.size(); i++) { FiducialDef def = targets.get(i); for (int j = 0; j < 4; j++) { int score = hamming(def.desc[j], squareDef); if( score < bestScore ) { bestScore = score; result.rotation = j; result.which = i; result.lengthSide = def.lengthSide; matched = true; } } } return matched; } /** * Computes the hamming score between two descriptions. Larger the number better the fit */ protected int hamming(short[] a, short[] b) { int distance = 0; for (int i = 0; i < a.length; i++) { distance += DescriptorDistance.hamming((a[i]&0xFFFF) ^ (b[i]&0xFFFF)); } return distance; } public List getTargets() { return targets; } /** * description of an image in 4 different orientations */ public static class FiducialDef { public short[][] desc = new short[4][DESC_LENGTH]; public double lengthSide; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy