/*
* 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.shapes.ellipse;
import boofcv.alg.distort.PixelTransformAffine_F32;
import boofcv.alg.filter.binary.ThresholdImageOps;
import boofcv.alg.shapes.TestShapeFittingOps;
import boofcv.io.image.ConvertBufferedImage;
import boofcv.struct.image.GrayU8;
import georegression.metric.UtilAngle;
import georegression.struct.affine.Affine2D_F32;
import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point2D_I32;
import georegression.struct.shapes.EllipseRotated_F64;
import org.ddogleg.struct.FastQueue;
import org.junit.Test;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author Peter Abeles
*/
public class TestBinaryEllipseDetectorPixel {
/**
* Test the whole pipeline with a rendered image
*/
@Test
public void basicOnImage() {
List<EllipseRotated_F64> expected = new ArrayList<>();
expected.add( new EllipseRotated_F64(30,38,10,8,0));
expected.add( new EllipseRotated_F64(115,80,20,15,Math.PI/2.0));
GrayU8 input = renderEllipses(200,300,expected, 0);
GrayU8 binary = input.createSameShape();
ThresholdImageOps.threshold(input,binary,100,true);
// detect ovals in binary image
BinaryEllipseDetectorPixel alg = new BinaryEllipseDetectorPixel();
alg.process(binary);
// compare against expected results
List<BinaryEllipseDetectorPixel.Found> found = alg.getFound();
List<EllipseRotated_F64> foundEllipses = new ArrayList<>();
for( BinaryEllipseDetectorPixel.Found f : found ) {
assertTrue( f.contour.size() > 10);
foundEllipses.add(f.ellipse);
}
checkEquals(expected,foundEllipses,1.0,0.1);
}
/**
* Undistort the image when no distoriton is provided
*/
@Test
public void undistortContour() {
List<Point2D_I32> input = new ArrayList<>();
FastQueue<Point2D_F64> output = new FastQueue<>(Point2D_F64.class, true);
for (int i = 0; i < 10; i++) {
input.add( new Point2D_I32(i,i));
}
BinaryEllipseDetectorPixel alg = new BinaryEllipseDetectorPixel();
alg.undistortContour(input,output);
assertEquals(input.size(),output.size);
for (int i = 0; i < input.size(); i++) {
Point2D_I32 p = input.get(i);
assertEquals(p.x,output.get(i).x,1e-8);
assertEquals(p.y,output.get(i).y,1e-8);
}
}
/**
* Undistort the image when distortion model is provided
*/
@Test
public void undistortContour_WithDistortion() {
List<Point2D_I32> input = new ArrayList<>();
FastQueue<Point2D_F64> output = new FastQueue<>(Point2D_F64.class, true);
for (int i = 0; i < 10; i++) {
input.add( new Point2D_I32(i,i));
}
BinaryEllipseDetectorPixel alg = new BinaryEllipseDetectorPixel();
alg.setLensDistortion(new PixelTransformAffine_F32(new Affine2D_F32(1,0,0,1,10.0f,0)));
alg.undistortContour(input,output);
assertEquals(input.size(),output.size);
for (int i = 0; i < input.size(); i++) {
Point2D_I32 p = input.get(i);
assertEquals(p.x+10,output.get(i).x,1e-8);
assertEquals(p.y,output.get(i).y,1e-8);
}
}
/**
* Test to see if it is approximately elliptical when the number of pixels is smaller
* than the threshold
*/
@Test
public void isApproximatelyElliptical_small() {
EllipseRotated_F64 ellipse = new EllipseRotated_F64(5,3,10,6,0);
List<Point2D_F64> negative = TestShapeFittingOps.createRectangle_F64(20,10,60-4);
List<Point2D_F64> positive = TestShapeFittingOps.createEllipse_F64(ellipse,60-4);
BinaryEllipseDetectorPixel alg = new BinaryEllipseDetectorPixel();
alg.setMaxDistanceFromEllipse(1.5);
assertFalse(alg.isApproximatelyElliptical(ellipse,negative,100));
assertTrue(alg.isApproximatelyElliptical(ellipse,positive,100));
}
/**
* Test to see if it is approximately elliptical when the number of pixels is larger
* than the threshold
*/
@Test
public void isApproximatelyElliptical_large() {
EllipseRotated_F64 ellipse = new EllipseRotated_F64(5,3,10,6,0);
List<Point2D_F64> negative = TestShapeFittingOps.createRectangle_F64(20,10,60-4);
List<Point2D_F64> positive = TestShapeFittingOps.createEllipse_F64(ellipse,60-4);
BinaryEllipseDetectorPixel alg = new BinaryEllipseDetectorPixel();
alg.setMaxDistanceFromEllipse(1.5);
assertFalse(alg.isApproximatelyElliptical(ellipse,negative,20));
assertTrue(alg.isApproximatelyElliptical(ellipse,positive,20));
}
public static void checkEquals( List<EllipseRotated_F64> expected ,
List<EllipseRotated_F64> found , double tol , double tolPhi )
{
assertEquals(expected.size(),found.size());
boolean matched[] = new boolean[expected.size()];
for( EllipseRotated_F64 f : found ) {
boolean foundMatch = false;
for (int i = 0; i < expected.size(); i++) {
EllipseRotated_F64 e = expected.get(i);
if( Math.abs(f.a-e.a) <= tol && Math.abs(f.b-e.b) <= tol) {
boolean angleMatch = true;
// if it's a circle ignore the angle
if( Math.abs(e.a - e.b)/Math.max(e.a,e.b) > 0.01 ) {
angleMatch = UtilAngle.distHalf(f.phi,e.phi) <= tolPhi;
}
if( angleMatch && e.center.distance(f.center) <= tol ) {
foundMatch = matched[i] = true;
}
}
}
if( !foundMatch ) {
System.out.println("Found");
System.out.println(f);
System.out.println("\nExpected");
for (int i = 0; i < expected.size(); i++) {
System.out.println(expected.get(i));
}
}
assertTrue(foundMatch);
}
for (int i = 0; i < matched.length; i++) {
assertTrue(matched[i]);
}
}
public static void checkEquals( EllipseRotated_F64 expected ,
EllipseRotated_F64 found , double tol , double tolPhi ) {
assertEquals(expected.a , found.a, tol);
assertEquals(expected.b , found.b, tol);
assertEquals(expected.center.x , found.center.x, tol);
assertEquals(expected.center.y , found.center.y, tol);
assertTrue(UtilAngle.dist(found.phi, expected.phi) <= tolPhi);
}
public static GrayU8 renderEllipses(int width, int height, List<EllipseRotated_F64> ellipses, int color)
{
// render a binary image with two ovals
BufferedImage buffered = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = buffered.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.WHITE);
g2.fillRect(0,0,width,height);
g2.setColor(new Color(color,color,color));
for( EllipseRotated_F64 ellipse : ellipses ) {
AffineTransform tx = new AffineTransform();
tx.concatenate( AffineTransform.getTranslateInstance(ellipse.center.x,ellipse.center.y));
tx.concatenate( AffineTransform.getRotateInstance(ellipse.phi));
int a = (int)Math.round(ellipse.a);
int b = (int)Math.round(ellipse.b);
g2.setTransform(tx);
g2.fillOval(-a,-b,a*2,b*2);
}
// ShowImages.showDialog(buffered);
GrayU8 input = new GrayU8(width,height);
ConvertBufferedImage.convertFrom(buffered,input);
return input;
}
}