/*
* 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.SquareGrid;
import boofcv.alg.fiducial.calib.squares.SquareGridTools;
import boofcv.alg.fiducial.calib.squares.SquareNode;
import boofcv.alg.fiducial.calib.squares.TestSquareRegularClustersIntoGrids;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.alg.misc.PixelMath;
import boofcv.alg.shapes.polygon.BinaryPolygonDetector;
import boofcv.factory.shape.ConfigPolygonDetector;
import boofcv.factory.shape.FactoryShapeDetector;
import boofcv.struct.image.GrayU8;
import georegression.metric.UtilAngle;
import georegression.struct.point.Point2D_F64;
import georegression.struct.shapes.Polygon2D_F64;
import org.ejml.simple.SimpleMatrix;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import java.util.Random;
import static boofcv.alg.fiducial.calib.squares.TestSquareCrossClustersIntoGrids.connect;
import static org.junit.Assert.*;
/**
* @author Peter Abeles
*/
public class TestDetectChessSquarePoints {
int offsetX = 15;
int offsetY = 10;
int squareLength = 30;
Random rand = new Random(234);
int w = 400;
int h = 500;
@Before
public void setup() {
offsetX = 15;
offsetY = 10;
squareLength = 30;
}
/**
* Give it a simple target and see if it finds the expected number of squares
*/
@Test
public void basicTest() {
basicTest(3, 3);
basicTest(5, 5);
basicTest(5, 7);
basicTest(7, 5);
// handle non-symmetric cases here
basicTest(2, 2);
basicTest(4, 4);
basicTest(6, 6);
basicTest(4, 2);
basicTest(6, 2);
basicTest(2, 4);
basicTest(2, 6);
basicTest(2, 3);
basicTest(2, 5);
basicTest(2, 7);
basicTest(4, 5);
basicTest(6, 5);
basicTest(3, 2);
basicTest(5, 2);
basicTest(7, 2);
basicTest(5, 4);
basicTest(5, 6);
}
public void basicTest(int rows, int cols) {
// System.out.println("grid shape rows = "+ rows +" cols = "+ cols);
GrayU8 binary = createTarget(rows, cols);
GrayU8 gray = binary.clone();
PixelMath.multiply(gray, 200, gray);
PixelMath.minus(255,gray,gray);
// ShowImages.showWindow(gray,"Input");
// try {
// Thread.sleep(2000);
// } catch (InterruptedException ignore) {}
BinaryPolygonDetector<GrayU8> detectorSquare = FactoryShapeDetector.
polygon(new ConfigPolygonDetector(4,4),GrayU8.class);
DetectChessSquarePoints<GrayU8> alg =
new DetectChessSquarePoints<>(rows, cols,2, detectorSquare);
// System.out.println("test grid "+ gridWidth + " " + gridHeight);
assertTrue(alg.process(gray, binary));
List<Point2D_F64> calib = alg.getCalibrationPoints().toList();
double x0 = offsetX+squareLength;
double y0 = offsetY+squareLength;
int pointRows = 2*(rows /2)-1+ rows %2;
int pointCols = 2*(cols /2)-1+ cols %2;
assertEquals(pointCols*pointRows, calib.size());
int index = 0;
for (int row = 0; row < pointRows; row++) {
for (int col = 0; col < pointCols; col++) {
assertTrue(calib.get(index++).distance(x0+col*squareLength,y0+row*squareLength) < 3 );
}
}
}
private GrayU8 createTarget(int rows, int cols) {
int squareLength2 = squareLength-2;
GrayU8 binary = new GrayU8(w,h);
SimpleMatrix a = new SimpleMatrix(1,2);
a.set(5);
// create the grid
for(int y = 0; y < rows; y += 2) {
for(int x = 0; x < cols; x += 2 ) {
int pixelX = x*squareLength+offsetX;
int pixelY = y*squareLength+offsetY;
ImageMiscOps.fillRectangle(binary, 1, pixelX, pixelY, squareLength, squareLength);
}
}
// don't want the square touching each other
for(int y = 1; y < rows; y += 2) {
for(int x = 1; x < cols; x += 2 ) {
int pixelX = x*squareLength+offsetX+1;
int pixelY = y*squareLength+offsetY+1;
ImageMiscOps.fillRectangle(binary, 1, pixelX, pixelY, squareLength2, squareLength2);
}
}
return binary;
}
/**
* Crash case. The outer grid touches the image edge but not the inner.
*/
@Test
public void touchImageEdge() {
offsetX = -10;
offsetY = -15;
int gridWidth=4;
int gridHeight=5;
GrayU8 binary = createTarget(gridHeight, gridWidth);
GrayU8 gray = binary.clone();
PixelMath.multiply(gray, 200, gray);
PixelMath.minus(255,gray,gray);
// ShowImages.showWindow(gray, "Input");
// try {
// Thread.sleep(2000);
// } catch (InterruptedException ignore) {}
BinaryPolygonDetector<GrayU8> detectorSquare = FactoryShapeDetector.
polygon(new ConfigPolygonDetector(4,4),GrayU8.class);
DetectChessSquarePoints<GrayU8> alg =
new DetectChessSquarePoints<>(gridWidth,gridHeight,2, detectorSquare);
assertFalse(alg.process(gray, binary));
}
@Test
public void putIntoCanonical() {
SquareGridTools tools = new SquareGridTools();
DetectChessSquarePoints alg = new DetectChessSquarePoints(2,2,10,null);
for (int rows = 2; rows <= 5; rows++) {
for (int cols = 2; cols <= 5; cols++) {
SquareGrid uber = createGrid(rows, cols);
alg.putIntoCanonical(uber);
checkCanonical(uber);
// make it do some work
boolean oddRow = rows%2 == 1;
boolean oddCol = cols%2 == 1;
if( oddRow == oddCol ) {
if( oddRow && rows==cols ) {
tools.rotateCCW(uber);
} else{
tools.reverse(uber);
}
}
alg.putIntoCanonical(uber);
checkCanonical(uber);
}
}
}
private void checkCanonical( SquareGrid uber ) {
double best = uber.nodes.get(0).center.norm();
for( SquareNode n : uber.nodes ) {
if( n == null ) continue;
double d = n.center.norm();
if( d < best )
fail("0 should be best");
}
}
@Test
public void ensureCCW() {
int shapes[][] = new int[][]{{4,5},{2,3},{3,2},{2,2}};
DetectChessSquarePoints<GrayU8> alg = new DetectChessSquarePoints<>(2,2,0.01,null);
for( int[]shape : shapes ) {
// System.out.println(shape[0]+" "+shape[1]);
SquareGrid grid = createGrid(shape[0],shape[1]);
assertTrue(isCCW((grid)));
assertTrue(alg.ensureCCW(grid));
assertTrue(isCCW((grid)));
if( grid.columns%2 == 1)
alg.tools.flipColumns(grid);
else if( grid.rows%2 == 1)
alg.tools.flipRows(grid);
else
continue;
assertFalse(isCCW((grid)));
assertTrue(alg.ensureCCW(grid));
assertTrue(isCCW((grid)));
}
}
private static boolean isCCW( SquareGrid grid ) {
SquareNode a,b,c;
a=b=c=null;
for (int i = 0; i < grid.columns; i++) {
if( grid.get(0,i) != null ) {
if( a == null )
a = grid.get(0, i);
else {
b = grid.get(0, i);
break;
}
}
}
if( b == null ) {
for (int i = 0; i < grid.columns; i++) {
if (grid.get(1, i) != null) {
b = grid.get(1, i);
}
}
}
for (int i = 0; i < grid.columns; i++) {
SquareNode n = grid.get(grid.rows-1,i);
if( n != null ) {
c = n;
break;
}
}
assertTrue(a!=null);
assertTrue(b!=null);
assertTrue(c!=null);
double x0 = b.center.x - a.center.x;
double y0 = b.center.y - a.center.y;
double x1 = c.center.x - a.center.x;
double y1 = c.center.y - a.center.y;
double angle0 = Math.atan2(y0,x0);
double angle1 = Math.atan2(y1,x1);
return UtilAngle.distanceCCW(angle0,angle1) < Math.PI;
}
@Test
public void computeCalibrationPoints() {
DetectChessSquarePoints<GrayU8> alg = new DetectChessSquarePoints<>(2,2,0.01,null);
double w = TestSquareRegularClustersIntoGrids.DEFAULT_WIDTH;
for (int rows = 2; rows <= 5; rows++) {
for (int cols = 2; cols <= 5; cols++) {
// System.out.println(rows+" "+cols);
SquareGrid grid = createGrid(rows, cols);
assertTrue(alg.computeCalibrationPoints(grid));
assertEquals((rows - 1) * (cols - 1), alg.calibrationPoints.size());
double x0 = w/2;
double y0 = w/2;
int index = 0;
for (int i = 0; i < rows - 1; i++) {
for (int j = 0; j < cols - 1; j++) {
double x = x0 + j*w;
double y = y0 + i*w;
Point2D_F64 p = alg.calibrationPoints.get(index++);
assertTrue(p.distance(x, y) < 1e-8);
}
}
}
}
}
public static SquareGrid createGrid(int rows , int cols ) {
SquareGrid grid = new SquareGrid();
grid.columns = cols;
grid.rows = rows;
double w = TestSquareRegularClustersIntoGrids.DEFAULT_WIDTH;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if( row%2 == 0 ) {
if( col%2 == 0 ) {
grid.nodes.add( createSquare(col*w,row*w,w));
} else {
grid.nodes.add(null);
}
} else {
if( col%2 == 0 ) {
grid.nodes.add(null);
} else {
grid.nodes.add( createSquare(col*w,row*w,w));
}
}
}
}
for (int row = 0; row < rows-1; row++) {
for (int col = 0; col < cols; col++) {
SquareNode n = grid.get(row,col);
if( n == null )
continue;
if( col > 0 ) {
SquareNode a = grid.get(row+1,col-1);
connect(n,3,a,1);
}
if( col < cols-1 ) {
SquareNode a = grid.get(row+1,col+1);
connect(n,2,a,0);
}
}
}
return grid;
}
public static SquareNode createSquare( double x , double y , double width ) {
double r = width/2;
Polygon2D_F64 poly = new Polygon2D_F64(4);
poly.get(0).set(-r, -r);
poly.get(1).set( r, -r);
poly.get(2).set( r, r);
poly.get(3).set(-r, r);
SquareNode square = new SquareNode();
for (int i = 0; i < 4; i++) {
poly.get(i).x += x;
poly.get(i).y += y;
}
square.corners = poly;
square.center.set(x, y);
square.largestSide = width;
return square;
}
}