/*
* 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.calib.chess;
import boofcv.alg.fiducial.calib.squares.*;
import boofcv.alg.shapes.polygon.BinaryPolygonDetector;
import boofcv.struct.image.GrayU8;
import boofcv.struct.image.ImageGray;
import georegression.struct.point.Point2D_F64;
import georegression.struct.shapes.Polygon2D_F64;
import georegression.struct.shapes.Polygon2D_I32;
import org.ddogleg.struct.FastQueue;
import java.util.List;
/**
* Given a binary image it detects the presence of chess board calibration grids. 1) Detect blobs
* in binary image and select square like ones. 2) Create two grids, inner and outer, 3) Put the
* grids together, 4) extract initial estimate for corners
*
* @author Peter Abeles
*/
public class DetectChessSquarePoints<T extends ImageGray> {
// detector for squares
BinaryPolygonDetector<T> detectorSquare;
// Converts detected squares into a graph and into grids
SquaresIntoCrossClusters s2c;
SquareCrossClustersIntoGrids c2g;
// size of square grids
private int numRows,numCols;
// bounding quadrilateral
private Polygon2D_I32 boundPolygon = new Polygon2D_I32();
SquareGridTools tools = new SquareGridTools();
FastQueue<Point2D_F64> calibrationPoints = new FastQueue<>(Point2D_F64.class, true);
// maximum distance two corners can be from each other
double maxCornerDistanceSq;
// List of nodes put into clusters
List<List<SquareNode>> clusters;
/**
* Configures chess board detector.
*
* @param numRows Number of rows in square grid
* @param numCols Number of columns in square grid
* @param maxCornerDistance Maximum distance in pixels that two "overlapping" corners can be from each other.
*/
public DetectChessSquarePoints(int numRows, int numCols, double maxCornerDistance,
BinaryPolygonDetector<T> detectorSquare)
{
this.maxCornerDistanceSq = maxCornerDistance*maxCornerDistance;
this.numRows = numRows;
this.numCols = numCols;
this.detectorSquare = detectorSquare;
s2c = new SquaresIntoCrossClusters(maxCornerDistance,-1);
c2g = new SquareCrossClustersIntoGrids();
}
/**
* Detects chessboard in the binary image. Square corners must be disconnected.
* Returns true if a chessboard was found, false otherwise.
*
* @param input Original input image.
* @param binary Binary image of chessboard
* @return True if successful.
*/
public boolean process( T input , GrayU8 binary ) {
boundPolygon.vertexes.reset();
detectorSquare.process(input, binary);
FastQueue<Polygon2D_F64> found = detectorSquare.getFoundPolygons();
FastQueue<BinaryPolygonDetector.Info> foundInfo = detectorSquare.getPolygonInfo();
clusters = s2c.process(found.toList(),foundInfo.toList());
c2g.process(clusters);
List<SquareGrid> grids = c2g.getGrids().toList();
for (int i = 0; i < grids.size(); i++) {
SquareGrid grid = grids.get(i);
if( grid.rows == numCols && grid.columns == numRows ) {
tools.transpose(grid);
}
if( grid.rows == numRows && grid.columns == numCols ) {
// this detector requires that the (0,0) grid cell has a square inside of it
if( grid.get(0,0) == null ){
if( grid.get(0,-1) != null ) {
tools.flipColumns(grid);
} else if( grid.get(-1,0) != null ) {
tools.flipRows(grid);
} else {
continue;
}
}
// make sure its in the expected orientation
if( !ensureCCW(grid) )
continue;
// If symmetric, ensure that the (0,0) is closest to top-left image corner
putIntoCanonical(grid);
// now extract the calibration points
return computeCalibrationPoints(grid);
}
}
return false;
}
/**
* Ensures that the grid is in a CCW order. It is assumed that (0,0) is a square.
*
* @return true if it was able to make it CCW or false if it failed to
*/
boolean ensureCCW( SquareGrid grid ) {
if( grid.columns <= 2 && grid.rows <= 2 )
return true;
Point2D_F64 a,b,c;
a = grid.get(0,0).center;
if( grid.columns > 2)
b = grid.get(0,2).center;
else
b = grid.get(1,1).center;
if( grid.rows > 2)
c = grid.get(2,0).center;
else
c = grid.get(1,1).center;
double x0 = b.x-a.x;
double y0 = b.y-a.y;
double x1 = c.x-a.x;
double y1 = c.y-a.y;
double z = x0 * y1 - y0 * x1;
if( z < 0 ) {
// flip it along an axis which is symmetric
if( grid.columns%2 == 1 )
tools.flipColumns(grid);
else if( grid.rows%2 == 1 )
tools.flipRows(grid);
else
return false;
}
return true;
}
/**
* Examines the grid and makes sure the (0,0) square is the closest one ot the top left corner.
* Only flip operations are allowed along symmetric axises
*/
void putIntoCanonical( SquareGrid grid ) {
boolean rowOdd = grid.rows%2 == 1;
boolean colOdd = grid.columns%2 == 1;
if( colOdd == rowOdd ) {
// if odd and square then 4 solutions. Otherwise just two solution that are on
// opposite sides on the grid
if( rowOdd && grid.rows == grid.columns ) {
int best = -1;
double bestDistance = Double.MAX_VALUE;
for (int i = 0; i < 4; i++) {
SquareNode n = grid.getCornerByIndex(i);
double d = n.center.normSq();
if( d < bestDistance ) {
best = i;
bestDistance = d;
}
}
for (int i = 0; i < best; i++) {
tools.rotateCCW(grid);
}
} else {
double first = grid.get(0,0).center.normSq();
double last = grid.getCornerByIndex(2).center.normSq();
if( last < first ) {
tools.reverse(grid);
}
}
}
// if only one is odd then there is a unique solution. Since uber is already in a legit
// configuration nothing needs ot be done
}
/**
* Find inner corner points across the grid. Start from the "top" row and work its way down. Corners
* are found by finding the average point between two adjacent corners on adjacent squares.
*/
boolean computeCalibrationPoints(SquareGrid grid) {
calibrationPoints.reset();
for (int row = 0; row < grid.rows-1; row++) {
int offset = row%2;
for (int col = offset; col < grid.columns; col += 2) {
SquareNode a = grid.get(row,col);
if( col > 0 ) {
SquareNode b = grid.get(row+1,col-1);
if( !setIntersection(a,b,calibrationPoints.grow()))
return false;
}
if( col < grid.columns-1) {
SquareNode b = grid.get(row+1,col+1);
if( !setIntersection(a,b,calibrationPoints.grow()))
return false;
}
}
}
return true;
}
private boolean setIntersection( SquareNode a , SquareNode n , Point2D_F64 point ) {
for (int i = 0; i < 4; i++) {
SquareEdge edge = a.edges[i];
if( edge != null && edge.destination(a) == n ) {
Point2D_F64 p0 = edge.a.corners.get(edge.sideA);
Point2D_F64 p1 = edge.b.corners.get(edge.sideB);
point.x = (p0.x+p1.x)/2.0;
point.y = (p0.y+p1.y)/2.0;
return true;
}
}
return false;
}
public List<List<SquareNode>> getGraphs() {
return clusters;
}
public SquaresIntoCrossClusters getShapeToClusters() {
return s2c;
}
public SquareCrossClustersIntoGrids getGrids() {
return c2g;
}
public BinaryPolygonDetector<T> getDetectorSquare() {
return detectorSquare;
}
public FastQueue<Point2D_F64> getCalibrationPoints() {
return calibrationPoints;
}
public int getNumRows() {
return numRows;
}
public int getNumCols() {
return numCols;
}
}