/* * 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.abst.fiducial.calib; import boofcv.abst.geo.Estimate1ofEpipolar; import boofcv.abst.geo.calibration.DetectorFiducialCalibration; import boofcv.alg.distort.DistortImageOps; import boofcv.alg.distort.PointToPixelTransform_F32; import boofcv.alg.distort.PointTransformHomography_F32; import boofcv.alg.distort.PointTransformHomography_F64; import boofcv.alg.geo.calibration.CalibrationObservation; import boofcv.alg.interpolate.InterpolationType; import boofcv.alg.misc.ImageMiscOps; import boofcv.core.image.border.BorderType; import boofcv.factory.geo.FactoryMultiView; import boofcv.gui.image.ShowImages; import boofcv.io.image.ConvertBufferedImage; import boofcv.struct.distort.PixelTransform2_F32; import boofcv.struct.distort.Point2Transform2_F32; import boofcv.struct.distort.Point2Transform2_F64; import boofcv.struct.geo.AssociatedPair; import boofcv.struct.image.GrayF32; import georegression.struct.point.Point2D_F64; import org.ejml.data.DenseMatrix64F; import org.ejml.ops.CommonOps; import org.junit.Before; import org.junit.Test; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.*; /** * @author Peter Abeles */ public abstract class GenericPlanarCalibrationDetectorChecks { int width = 300,height= 300; GrayF32 original; GrayF32 distorted; List<CalibrationObservation> solutions = new ArrayList<>(); Point2Transform2_F32 d2o; Point2Transform2_F64 o2d; public abstract void renderTarget(GrayF32 original , List<CalibrationObservation> solutions ); public abstract DetectorFiducialCalibration createDetector(); @Before public void setup() { original = new GrayF32(width,height); distorted = new GrayF32(width, height); renderTarget(original, solutions); } /** * Nothing was detected. make sure it doesn't return null. */ @Test public void checkDetectionsNotNull() { DetectorFiducialCalibration detector = createDetector(); detector.process(original.createSameShape()); assertTrue( detector.getDetectedPoints() != null ); assertTrue( detector.getDetectedPoints().size() == 0 ); } /** * First call something was detected, second call nothing was detected. it should return an empty list */ @Test public void checkDetectionsResetOnFailure() { DetectorFiducialCalibration detector = createDetector(); detector.process(original); assertTrue( detector.getDetectedPoints().size() > 0 ); detector.process(original.createSameShape()); assertTrue( detector.getDetectedPoints() != null ); assertTrue( detector.getDetectedPoints().size() == 0 ); } /** * Makes sure origin in the target's physical center. This is done by seeing that most extreme * points are all equally distant. Can't use the mean since the target might not evenly distributed. * * Should this really be a requirement? There is some mathematical justification for it and make sense * when using it as a fiducial. */ @Test public void targetIsCentered() { List<Point2D_F64> layout = createDetector().getLayout(); double minX=Double.MAX_VALUE,maxX=-Double.MAX_VALUE; double minY=Double.MAX_VALUE,maxY=-Double.MAX_VALUE; for( Point2D_F64 p : layout ) { if( p.x < minX ) minX = p.x; if( p.x > maxX ) maxX = p.x; if( p.y < minY ) minY = p.y; if( p.y > maxY ) maxY = p.y; } assertEquals(Math.abs(minX), Math.abs(maxX), 1e-8); assertEquals(Math.abs(minY), Math.abs(maxY), 1e-8); } /** * Make sure new instances of calibration points are returned each time */ @Test public void dataNotRecycled() { DetectorFiducialCalibration detector = createDetector(); assertTrue(detector.process(original)); CalibrationObservation found0 = detector.getDetectedPoints(); assertTrue(detector.process(original)); CalibrationObservation found1 = detector.getDetectedPoints(); assertEquals(found0.size(),found1.size()); assertTrue(found0 != found1); for (int i = 0; i < found0.size(); i++) { assertFalse(found1.points.contains(found0.points.get(0))); } } /** * Easy case with no distortion */ @Test public void undistorted() { DetectorFiducialCalibration detector = createDetector(); // display(original); assertTrue(detector.process(original)); CalibrationObservation found = detector.getDetectedPoints(); checkList(found, false); } /** * Pinch it a little bit like what is found with perspective distortion */ @Test public void distorted() { DetectorFiducialCalibration detector = createDetector(); createTransform(width / 5, height / 5, width * 4 / 5, height / 6, width - 1, height - 1, 0, height - 1); PixelTransform2_F32 pixelTransform = new PointToPixelTransform_F32(d2o); ImageMiscOps.fill(distorted, 0xff); DistortImageOps.distortSingle(original, distorted, pixelTransform, InterpolationType.BILINEAR, BorderType.EXTENDED); // display(distorted); assertTrue(detector.process(distorted)); CalibrationObservation found = detector.getDetectedPoints(); checkList(found, true); } private void display( GrayF32 image ) { BufferedImage visualized = ConvertBufferedImage.convertTo(image, null, true); ShowImages.showWindow(visualized, "Input"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } public void checkList(CalibrationObservation found , boolean applyTransform ) { List<CalibrationObservation> expectedList = new ArrayList<>(); if( !applyTransform ) { expectedList.addAll(this.solutions); } else { for (int i = 0; i < solutions.size(); i++) { CalibrationObservation orig = solutions.get(i); CalibrationObservation mod = new CalibrationObservation(); for (int j = 0; j < orig.size(); j++) { Point2D_F64 p = orig.points.get(i).copy(); o2d.compute(p.x, p.y, p); mod.add(p, orig.get(j).index ); } expectedList.add(mod); } } assertEquals(expectedList.get(0).size(),found.size()); // the order is important. check to see that they are close and in the correct order boolean anyMatched = false; for (int i = 0; i < expectedList.size(); i++) { CalibrationObservation expected = expectedList.get(i); boolean matched = true; for (int j = 0; j < expected.size(); j++) { if( found.get(j).index != expected.get(j).index ) { matched = false; break; } Point2D_F64 f = found.get(i); Point2D_F64 e = expected.get(i); if( f.distance(e) >= 3 ) { matched = false; break; } } if( matched ) { anyMatched = true; break; } } assertTrue(anyMatched); } public void createTransform( double x0 , double y0 , double x1 , double y1 , double x2 , double y2 , double x3 , double y3 ) { // Homography estimation algorithm. Requires a minimum of 4 points Estimate1ofEpipolar computeHomography = FactoryMultiView.computeHomography(true); // Specify the pixel coordinates from destination to target ArrayList<AssociatedPair> associatedPairs = new ArrayList<>(); associatedPairs.add(new AssociatedPair(x0, y0, 0, 0)); associatedPairs.add(new AssociatedPair(x1, y1, width-1, 0)); associatedPairs.add(new AssociatedPair(x2, y2, width-1, height-1)); associatedPairs.add(new AssociatedPair(x3, y3, 0, height - 1)); // Compute the homography DenseMatrix64F H = new DenseMatrix64F(3, 3); computeHomography.process(associatedPairs, H); // Create the transform for distorting the image d2o = new PointTransformHomography_F32(H); CommonOps.invert(H); o2d = new PointTransformHomography_F64(H); } /** * Observations points should always be in increasing order */ @Test public void checkPointIndexIncreasingOrder() { DetectorFiducialCalibration detector = createDetector(); assertTrue(detector.process(original)); CalibrationObservation found = detector.getDetectedPoints(); assertEquals(detector.getLayout().size(),found.size()); for (int i = 0; i < found.size(); i++) { assertEquals(i, found.get(i).index); } } }