![JAR search and dependency download from the Maven repository](/logo.png)
boofcv.alg.fiducial.square.DetectFiducialSquareBinary Maven / Gradle / Ivy
Show all versions of recognition Show documentation
/*
* 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.alg.fiducial.square;
import boofcv.abst.filter.binary.InputToBinary;
import boofcv.alg.filter.binary.ThresholdImageOps;
import boofcv.alg.shapes.polygon.BinaryPolygonDetector;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.GrayU8;
import boofcv.struct.image.ImageGray;
import java.util.Arrays;
/**
*
* Square fiducial that encodes numerical values in a binary N by N grids, where N ≥ 3. The outer border
* is entirely black while the inner portion is divided into a grid of equally sized back and white squares.
* Typical grid sizes are 3x3, 4x4, and 5x5, which can encode up to 32, 4096, and 2,097,152 unique values respectively.
* In other words, a grid of size N can encode N*N-4 bits, or a number with 2N*N-4 values.
* The lower left corner is always back and while all the other corners are always white.
* This allows orientation to be uniquely determined.
*
*
*
*
*
* The above image is an example of a 4x4 grid and 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.
* The black orientation corner is pointed out in the above image.
* The fiducial's width refers to the width of each side along the black border NOT the internal encoded image.
* The size of each square is the same and has a width of (fiducal width)*(1.0 - 2.0*(border fractional width))/N.
*
*
* NOTE: While a larger grid size will allow you to encode more numbers it will increase the rate at which ID numbers
* are incorrectly identified.
* NOTE: The size of the border can be adjusted, but 0.25 is recommended. The thinner the black border is the worse
* it will perform when viewed at an angle, but the larger the squares can be.
*
*
* @author Peter Abeles Original author/maintainer
* @author Nathan Pahucki Added the ability to use more than 4x4 grid for Capta360, [email protected]
*/
public class DetectFiducialSquareBinary
extends BaseDetectFiducialSquare {
// helper data structures for computing the value of each grid element
int[] counts, classified, tmp;
// converts the input image into a binary one
private GrayU8 binaryInner = new GrayU8(1,1);
// storage for no border sub-image
private GrayF32 grayNoBorder = new GrayF32();
// number of rows/columns in the encoded binary pattern
private int gridWidth;
// width of a square in the inner undistorted image.
protected final static int w=10;
// total number of pixels in a square
protected final static int N=w*w;
// length of a side for the fiducial's black border in world units.
private double lengthSide = 1;
// ambiguity threshold. 0 to 1. 0 = very strict and 1 = anything goes
// Sets how strict a square must be black or white for it to be accepted.
double ambiguityThreshold = 0.4;
/**
* Configures the fiducial detector
*
* @param gridWidth Number of elements wide the encoded square grid is. 3,4, or 5 is recommended.
* @param borderWidthFraction Fraction of the fiducial's width that the border occupies. 0.25 is recommended.
* @param inputToBinary Converts the input image into a binary image
* @param quadDetector Detects quadrilaterals in the input image
* @param inputType Type of image it's processing
*/
public DetectFiducialSquareBinary(int gridWidth,
double borderWidthFraction ,
double minimumBlackBorderFraction ,
final InputToBinary inputToBinary,
final BinaryPolygonDetector quadDetector, Class inputType) {
// Black borders occupies 2.0*borderWidthFraction of the total width
// The number of pixels for each square is held constant and the total pixels for the inner region
// is determined by the size of the grid
// The number of pixels in the undistorted image (squarePixels) is selected using the above information
super(inputToBinary,quadDetector,borderWidthFraction,minimumBlackBorderFraction,
(int)Math.round((w * gridWidth) /(1.0-borderWidthFraction*2.0)) ,inputType);
if( gridWidth < 3 || gridWidth > 8)
throw new IllegalArgumentException("The grid must be at least 3 and at most 8 elements wide");
this.gridWidth = gridWidth;
binaryInner.reshape(w * gridWidth,w * gridWidth);
counts = new int[getTotalGridElements()];
classified = new int[getTotalGridElements()];
tmp = new int[getTotalGridElements()];
}
@Override
protected boolean processSquare(GrayF32 gray, Result result, double edgeInside, double edgeOutside) {
int off = (gray.width - binaryInner.width) / 2;
gray.subimage(off, off, off + binaryInner.width, off + binaryInner.width, grayNoBorder);
// convert input image into binary number
double threshold = (edgeInside+edgeOutside)/2;
findBitCounts(grayNoBorder,threshold);
if (thresholdBinaryNumber())
return false;
// adjust the orientation until the black corner is in the lower left
if (rotateUntilInLowerCorner(result))
return false;
result.which = extractNumeral();
result.lengthSide = lengthSide;
//printClassified();
return true;
}
/**
* Extract the numerical value it encodes
* @return the int value of the numeral.
*/
protected int extractNumeral() {
int val = 0;
final int topLeft = getTotalGridElements() - gridWidth;
int shift = 0;
// -2 because the top and bottom rows have 2 unusable bits (the first and last)
for(int i = 1; i < gridWidth - 1; i++) {
final int idx = topLeft + i;
val |= classified[idx] << shift;
//System.out.println("val |= classified[" + idx + "] << " + shift + ";");
shift++;
}
// Don't do the first or last row, handled above and below - special cases
for(int ii = 1; ii < gridWidth - 1; ii++) {
for(int i = 0; i < gridWidth; i++) {
final int idx = getTotalGridElements() - (gridWidth * (ii + 1)) + i;
val |= classified[idx] << shift;
// System.out.println("val |= classified[" + idx + "] << " + shift + ";");
shift++;
}
}
// The last row
for(int i = 1; i < gridWidth - 1; i++) {
val |= classified[i] << shift;
//System.out.println("val |= classified[" + i + "] << " + shift + ";");
shift++;
}
return val;
}
/**
* Rotate the pattern until the black corner is in the lower right. Sanity check to make
* sure there is only one black corner
*/
private boolean rotateUntilInLowerCorner(Result result) {
// sanity check corners. There should only be one exactly one black
final int topLeft = getTotalGridElements() - gridWidth;
final int topRight = getTotalGridElements() - 1;
final int bottomLeft = 0;
final int bottomRight = gridWidth - 1;
if (classified[bottomLeft] + classified[bottomRight] + classified[topRight] + classified[topLeft] != 1)
return true;
// Rotate until the black corner is in the lower left hand corner on the image.
// remember that origin is the top left corner
result.rotation = 0;
while (classified[topLeft] != 1) {
result.rotation++;
rotateClockWise();
}
return false;
}
protected void rotateClockWise() {
final int totalElements = getTotalGridElements();
// Swap the four corners
for (int ii = 0; ii < gridWidth; ii++) {
for (int i = 0; i < gridWidth; i++) {
final int fromIdx = ii * gridWidth + i;
final int toIdx = (totalElements - (gridWidth * (i + 1))) + ii;
tmp[fromIdx] = classified[toIdx];
}
}
System.arraycopy(tmp, 0, classified, 0, totalElements);
}
/**
* Sees how many pixels were positive and negative in each square region. Then decides if they
* should be 0 or 1 or unknown
*/
protected boolean thresholdBinaryNumber() {
int lower = (int) (N * (ambiguityThreshold / 2.0));
int upper = (int) (N * (1 - ambiguityThreshold / 2.0));
final int totalElements = getTotalGridElements();
for (int i = 0; i < totalElements; i++) {
if (counts[i] < lower) {
classified[i] = 0;
} else if (counts[i] > upper) {
classified[i] = 1;
} else {
// it's ambiguous so just fail
return true;
}
}
return false;
}
/**
* Converts the gray scale image into a binary number
*/
protected void findBitCounts(GrayF32 gray , double threshold ) {
// compute binary image using an adaptive algorithm to handle shadows
ThresholdImageOps.threshold(gray,binaryInner,(float)threshold,true);
Arrays.fill(counts, 0);
for (int row = 0; row < gridWidth; row++) {
int y0 = row * binaryInner.width / gridWidth;
int y1 = (row + 1) * binaryInner.width / gridWidth;
for (int col = 0; col < gridWidth; col++) {
int x0 = col * binaryInner.width / gridWidth;
int x1 = (col + 1) * binaryInner.width / gridWidth;
int total = 0;
for (int i = y0; i < y1; i++) {
int index = i * binaryInner.width + x0;
for (int j = x0; j < x1; j++) {
total += binaryInner.data[index++];
}
}
counts[row * gridWidth + col] = total;
}
}
}
public void setLengthSide(final double lengthSide) {
this.lengthSide = lengthSide;
}
/**
* Number of elements wide the grid is
*/
public int getGridWidth() {
return gridWidth;
}
/**
* parameters which specifies how tolerant it is of a square being ambiguous black or white.
* @param ambiguityThreshold 0 to 1, inclusive
*/
public void setAmbiguityThreshold(double ambiguityThreshold) {
if( ambiguityThreshold < 0 || ambiguityThreshold > 1 )
throw new IllegalArgumentException("Must be from 0 to 1, inclusive");
this.ambiguityThreshold = ambiguityThreshold;
}
/**
* Total number of elements in the grid
*/
private int getTotalGridElements() {
return gridWidth * gridWidth;
}
public long getNumberOfDistinctFiducials() {
// The -4 is for the 4 orientation squares
return (long) Math.pow(2, gridWidth * gridWidth - 4);
}
// For troubleshooting.
public GrayF32 getGrayNoBorder() { return grayNoBorder; }
public GrayU8 getBinaryInner() {
return binaryInner;
}
// This is only works well as a visual representation if the output font is mono spaced.
public void printClassified() {
System.out.println();
System.out.println("██████");
for (int row = 0; row < gridWidth; row++) {
System.out.print("█");
for (int col = 0; col < gridWidth; col++) {
System.out.print(classified[row * gridWidth + col] == 1 ? "█︎" : "◻");
}
System.out.print("█");
System.out.println();
}
System.out.println("██████");
}
}