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

boofcv.alg.fiducial.aztec.AztecFinderPatternDetector 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) 2022, 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.aztec;

import boofcv.alg.fiducial.calib.squares.SquareGraph;
import boofcv.alg.fiducial.calib.squares.SquareNode;
import boofcv.alg.fiducial.qrcode.PositionPatternNode;
import boofcv.alg.fiducial.qrcode.SquareLocatorPatternDetectorBase;
import boofcv.alg.shapes.polygon.DetectPolygonBinaryGrayRefine;
import boofcv.alg.shapes.polygon.DetectPolygonFromContour;
import boofcv.misc.BoofMiscOps;
import boofcv.struct.image.ImageGray;
import georegression.struct.point.Point2D_F64;
import georegression.struct.shapes.Polygon2D_F64;
import lombok.Getter;
import org.ddogleg.nn.FactoryNearestNeighbor;
import org.ddogleg.nn.NearestNeighbor;
import org.ddogleg.nn.NnData;
import org.ddogleg.struct.DogArray;
import org.jetbrains.annotations.Nullable;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Searches for Aztec finder patterns inside an image and returns a list of candidates. Finder patterns are found
 * by looking at the external contour of block quadrilaterals and looking for quadrilaterals that have a similar
 * center pixel. If one, two, or three match then that's consider a match for compact or full-range Aztec codes.
 *
 * @author Peter Abeles
 */
public class AztecFinderPatternDetector> extends SquareLocatorPatternDetectorBase {

	/** At least this fraction of points is required to match a template when examining a potential pyramid */
	public double minimumTemplateMatch = 0.8;

	/** Two pyramid layers are considered to be close if they are within this fraction of side length of each other */
	public double distanceTolerance = 0.15;

	// Layers that describe a pyramid
	private final DogArray layers = new DogArray<>(Layer::new, Layer::reset);

	/** Found candidate pyramids/locator patterns. Recycled every search */
	private final @Getter DogArray found = new DogArray<>(AztecPyramid::new, AztecPyramid::reset);

	// used to search for neighbors that which are candidates for connecting
	private final NearestNeighbor nn = (NearestNeighbor)FactoryNearestNeighbor.kdtree(new SquareNode.KdTreeSquareNode());
	private final NearestNeighbor.Search search = nn.createSearch();
	private final DogArray> searchResults = new DogArray<>(NnData::new);

	// workspace for homography calculation
	GridToPixelHelper gridToPixel = new GridToPixelHelper();
	Point2D_F64 pixel = new Point2D_F64();

	/**
	 * Configures the detector
	 *
	 * @param squareDetector Square detector
	 */
	public AztecFinderPatternDetector( DetectPolygonBinaryGrayRefine squareDetector ) {
		super(squareDetector);
		maxContourFraction = 2.0;
	}

	@Override protected void findLocatorPatternsFromSquares() {
		layers.reset();
		found.reset();
		squaresToLayerList();
		findLayersInsideOfLayers();
		createPyramids();
	}

	/**
	 * Takes the detected squares and turns it into a list of {@link PositionPatternNode}.
	 */
	void squaresToLayerList() {
		List infoList = squareDetector.getPolygonInfo();
		for (int i = 0; i < infoList.size(); i++) {
			DetectPolygonFromContour.Info info = infoList.get(i);

			// See if the appearance matches a finder pattern
			double grayThreshold = (info.edgeInside + info.edgeOutside)/2;

			int diameter = computeLayerDiameter(info.polygon, (float)grayThreshold);
			if (diameter <= 0)
				continue;

			// refine the edge estimate
			squareDetector.refine(info);

			// Save the results
			Layer pp = this.layers.grow();
			pp.square = info.polygon;
			pp.threshold = grayThreshold;
			pp.diameter = diameter;

			SquareGraph.computeNodeInfo(pp);
		}
	}

	/**
	 * Add layers which are contained inside other layers as children.
	 */
	void findLayersInsideOfLayers() {
		// Initialize search
		nn.setPoints(layers.toList(), false);

		// Go through all layers
		for (int layerIdx = 0; layerIdx < layers.size; layerIdx++) {
			Layer a = layers.get(layerIdx);

			// Find all layers with a center close to the center of 'a'
			search.findNearest(a, a.largestSide/2.0, 10, searchResults);
			for (int searchIdx = 0; searchIdx < searchResults.size; searchIdx++) {
				NnData result = searchResults.get(searchIdx);
				Layer b = result.point;

				// Don't compare against itself
				if (a == b)
					continue;

				// Only add children to 'a'
				if (b.largestSide > a.largestSide)
					continue;

				// See if their centers are close to each other
				// use the max of the two to make it symmetric so that order doesn't matter
				double maxSide = Math.max(a.largestSide, b.largestSide);
				double distance = a.center.distance(b.center);
				if (distance > maxSide*distanceTolerance)
					continue;

				b.child = true;
				a.children.add(b);

				if (verbose != null) verbose.printf("%s child of %s\n", format(a), format(b));
			}
		}
	}

	private static String format( Layer a ) {
		return String.format("(%.1f %.1f, s=%d)", a.center.x, a.center.y, a.diameter);
	}

	/**
	 * Go from layers to output pyramid while rejecting false positives
	 */
	void createPyramids() {
		for (int layerIdx = 0; layerIdx < layers.size; layerIdx++) {
			Layer a = layers.get(layerIdx);

			// If a layer is inside another it can't be the outermost layer.
			// Reject if there are too many children and it's probably noise
			if (a.child || a.children.size() > 1)
				continue;

			// Reject if not the smallest layer and it has a child or the reverse
			if (a.children.isEmpty() == (a.diameter == 9))
				continue;

			AztecPyramid p = found.grow();
			copyToOutput(a, p.layers.grow());
			for (int i = 0; i < a.children.size(); i++) {
				copyToOutput(a.children.get(i), p.layers.grow());
			}
			p.alignCorners();
		}
	}

	void copyToOutput( Layer src, AztecPyramid.Layer dst ) {
		dst.square.setTo(src.square);
		dst.center.setTo(src.center);
		dst.threshold = src.threshold;
	}

	/**
	 * Decides if the polygon is part of a pyramid. If so the number of layers in the pyramid it belongs to
	 *
	 * @return Number of layers in the pyramid. 3 = width 5, 5 = width 7
	 */
	int computeLayerDiameter( Polygon2D_F64 polygon, float threshold ) {
		double score5 = scoreTemplate(polygon, threshold, 5);
		double score9 = scoreTemplate(polygon, threshold, 9);

		if (score5 < minimumTemplateMatch && score9 < minimumTemplateMatch)
			return 0;

		return score5 > score9 ? 5 : 9;
	}

	/**
	 * Samples the center of squares inside assuming it's a pyramid with the specified number of squares in the outer
	 * ring.
	 *
	 * @param polygon Corners of square region
	 * @param threshold B&W threshold value
	 * @param squaresWide Number of squares wide the black ring is.
	 * @return 0.0 to 1.0. 1.0 indicates a perfect fit
	 */
	double scoreTemplate( Polygon2D_F64 polygon, float threshold, int squaresWide ) {
		// Initialize the coordinate system. Note an artifact of how defining the coordinate
		// system in the center of a square is that integer values will be at the center in the image
		gridToPixel.initOriginCenter(polygon, squaresWide);

		// Start sampling inside the next white ring inside the outer black ring
		int radius = (squaresWide - 2)/2;

		// Number of times an observation matches the template
		int numMatches = 0;

		for (int row = -radius; row <= radius; row++) {
			// rrow and rcol is distance from the border along their respective axis
			int rrow = Math.abs(row);

			for (int col = -radius; col <= radius; col++) {
				int rcol = Math.abs(col);

				// find the pixel coordinate for the specified grid coordinate
				gridToPixel.convert(col, row, pixel);

				// Sample the image at this point
				float pixelValue = interpolate.get((float)pixel.x, (float)pixel.y);

				// distance from the edge determines if we expect a white or black region
				int r = Math.max(rrow, rcol);
				if (pixelValue > threshold == (r%2 == 1))
					numMatches++;
			}
		}

		// total number of times the template was tested
		int numSamples = (radius*2 + 1)*(radius*2 + 1);
		double fitFraction = numMatches/(double)numSamples;

		if (verbose != null) verbose.printf("poly_sore: p[0]=(%.1f %.1f) template: score=%.2f squares=%d pixels=%.1f\n",
				polygon.get(0).x, polygon.get(1).y, fitFraction, squaresWide, polygon.getSideLength(0));

		return fitFraction;
	}

	@Override public void setVerbose( @Nullable PrintStream out, @Nullable Set set ) {
		this.verbose = BoofMiscOps.addPrefix(this, out);
	}

	/**
	 * Candidate locator patterns. Layers are the number of transitions between dark and white
	 */
	public static class Layer extends SquareNode {
		// Number of squares wide in squares this pyramid layer is
		public int diameter;
		// threshold used to split black/white pixels
		public double threshold;
		// true if it's a child to another node
		public boolean child;
		// List of nodes which consider this layer to be a parent
		public List children = new ArrayList<>();

		@Override
		public void reset() {
			super.reset();
			diameter = -1;
			threshold = 0;
			child = false;
			children.clear();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy