/*
* 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;
import boofcv.abst.distort.FDistort;
import boofcv.alg.distort.LensDistortionNarrowFOV;
import boofcv.alg.geo.PerspectiveOps;
import boofcv.alg.geo.WorldToCameraToPixel;
import boofcv.struct.image.ImageBase;
import boofcv.struct.image.ImageType;
import boofcv.testing.BoofTesting;
import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point3D_F64;
import georegression.struct.se.Se3_F64;
import org.ejml.ops.MatrixFeatures;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author Peter Abeles
*/
public abstract class GenericFiducialDetectorChecks {
// tolerance for difference between pixel location and geometric center from reprojection
protected double pixelAndProjectedTol = 3.0;
protected List<ImageType> types = new ArrayList<>();
public abstract ImageBase loadImage(ImageType imageType);
public abstract LensDistortionNarrowFOV loadDistortion(boolean distorted );
public abstract FiducialDetector createDetector( ImageType imageType );
/**
* Tests several items:
*
* 1) Does it properly handle setIntrinsic() being called multiple times
* 2) Can it handle no distortion
*/
@Test
public void checkHandleNewIntrinsic() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
assertTrue(detector.totalFound()>= 1);
Results results = extractResults(detector);
// run it again with a changed intrinsic that's incorrect
detector.setLensDistortion(loadDistortion(false));
detector.detect(image);
// results should have changed
if( results.id.length == detector.totalFound()) {
assertEquals(results.id.length, detector.totalFound());
for (int i = 0; i < detector.totalFound(); i++) {
assertEquals(results.id[i],detector.getId(i));
Se3_F64 pose = new Se3_F64();
Point2D_F64 pixel = new Point2D_F64();
detector.getFiducialToCamera(i, pose);
detector.getImageLocation(i, pixel);
assertTrue(pose.getT().distance(results.pose.get(i).T) > 1e-4);
assertFalse(MatrixFeatures.isIdentical(pose.getR(), results.pose.get(i).R, 1e-4));
// pixel location is based on the observed location, thus changing the intrinsics should not
// affect it
assertTrue(results.pixel.get(i).distance(pixel) <= 2.0 );
}
} else {
// clearly changed
}
// then reproduce original
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
// see if it produced exactly the same results
assertEquals(results.id.length,detector.totalFound());
for (int i = 0; i < detector.totalFound(); i++) {
assertEquals(results.id[i],detector.getId(i));
Se3_F64 pose = new Se3_F64();
detector.getFiducialToCamera(i, pose);
assertEquals(0,pose.getT().distance(results.pose.get(i).T),1e-8);
assertTrue(MatrixFeatures.isIdentical(pose.getR(),results.pose.get(i).R,1e-8));
}
}
}
/**
* Provide an intrinsic model then remove it
*/
@Test
public void checkRemoveIntrinsic() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
// detect with no intrinsics
FiducialDetector detector = createDetector(type);
detector.detect(image);
assertFalse(detector.is3D());
assertTrue(detector.totalFound() >= 1);
Results expected = extractResults(detector);
// detect with intrinsics
detector.setLensDistortion(loadDistortion(true));
assertTrue(detector.is3D());
assertTrue(detector.totalFound() >= 1);
// detect without intrinsics again
detector.setLensDistortion(null);
assertFalse(detector.is3D());
assertTrue(detector.totalFound() >= 1);
Results found = extractResults(detector);
// compare results
assertEquals(expected.id.length, found.id.length);
for (int i = 0; i < expected.id.length; i++) {
assertEquals(expected.id[i],found.id[i]);
assertTrue(expected.pose.get(i).T.distance(found.pose.get(i).T) <= 1e-4);
assertTrue(MatrixFeatures.isIdentical(expected.pose.get(i).getR(), found.pose.get(i).R, 1e-4));
assertTrue(found.pixel.get(i).distance(expected.pixel.get(i)) <= 1e-4 );
}
}
}
/**
* Makes sure the input is not modified
*/
@Test
public void modifyInput() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
ImageBase orig = image.clone();
FiducialDetector detector = createDetector(type);
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
BoofTesting.assertEquals(image,orig,0);
}
}
@Test
public void checkMultipleRuns() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
assertTrue(detector.totalFound()>= 1);
Results results = extractResults(detector);
// run it again
detector.detect(image);
// see if it produced exactly the same results
assertEquals(results.id.length,detector.totalFound());
for (int i = 0; i < detector.totalFound(); i++) {
assertEquals(results.id[i],detector.getId(i));
Se3_F64 pose = new Se3_F64();
detector.getFiducialToCamera(i, pose);
assertEquals(0,pose.getT().distance(results.pose.get(i).T),1e-8);
assertTrue(MatrixFeatures.isIdentical(pose.getR(),results.pose.get(i).R,1e-8));
}
}
}
@Test
public void checkSubImage() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
assertTrue(detector.totalFound()>= 1);
long foundID[] = new long[ detector.totalFound() ];
List<Se3_F64> foundPose = new ArrayList<>();
for (int i = 0; i < detector.totalFound(); i++) {
foundID[i] = detector.getId(i);
Se3_F64 pose = new Se3_F64();
detector.getFiducialToCamera(i, pose);
foundPose.add(pose);
}
// run it again with a sub-image
detector.detect(BoofTesting.createSubImageOf(image));
// see if it produced exactly the same results
assertEquals(foundID.length,detector.totalFound());
for (int i = 0; i < detector.totalFound(); i++) {
assertEquals(foundID[i],detector.getId(i));
Se3_F64 pose = new Se3_F64();
detector.getFiducialToCamera(i, pose);
assertEquals(0,pose.getT().distance(foundPose.get(i).T),1e-8);
assertTrue(MatrixFeatures.isIdentical(pose.getR(),foundPose.get(i).R,1e-8));
}
}
}
/**
* See if the stability estimation is reasonable. First detect targets in the full sized image. Then shrink it
* by 15% and see if the instability increases. The instability should always increase for smaller objects with
* the same orientation since the geometry is worse.
*/
@Test
public void checkStability() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
assertTrue(detector.totalFound() >= 1);
long foundIds[] = new long[ detector.totalFound() ];
double location[] = new double[ detector.totalFound() ];
double orientation[] = new double[ detector.totalFound() ];
FiducialStability results = new FiducialStability();
for (int i = 0; i < detector.totalFound(); i++) {
detector.computeStability(i,0.2,results);
foundIds[i] = detector.getId(i);
location[i] = results.location;
orientation[i] = results.orientation;
}
ImageBase shrunk = image.createSameShape();
new FDistort(image,shrunk).affine(0.8,0,0,0.8,0,0).apply();
detector.detect(shrunk);
assertTrue(detector.totalFound() == foundIds.length);
for (int i = 0; i < detector.totalFound(); i++) {
detector.computeStability(i,0.2,results);
long id = detector.getId(i);
boolean matched = false;
for (int j = 0; j < foundIds.length; j++) {
if( foundIds[j] == id ) {
matched = true;
assertTrue(location[j] < results.location);
assertTrue(orientation[j] < results.orientation);
break;
}
}
assertTrue(matched);
}
}
}
@Test
public void checkImageLocation() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
LensDistortionNarrowFOV distortion = loadDistortion(true);
detector.setLensDistortion(distortion);
detector.detect(image);
assertTrue(detector.totalFound() >= 1);
for (int i = 0; i < detector.totalFound(); i++) {
Se3_F64 fidToCam = new Se3_F64();
Point2D_F64 pixel = new Point2D_F64();
detector.getFiducialToCamera(i, fidToCam);
detector.getImageLocation(i, pixel);
Point2D_F64 rendered = new Point2D_F64();
WorldToCameraToPixel worldToPixel = PerspectiveOps.createWorldToPixel(distortion, fidToCam);
worldToPixel.transform(new Point3D_F64(0,0,0),rendered);
// see if the reprojected is near the pixel location
assertTrue( rendered.distance(pixel) <= pixelAndProjectedTol);
}
}
}
/**
* Makes sure that if it hasn't been provided intrinsic it can't support pose
*/
@Test
public void is3dWithNoIntrinsic() {
FiducialDetector detector = createDetector(types.get(0));
assertFalse( detector.is3D() );
}
/**
* Make sure lens distortion is removed if it was set previously and then removed
*/
@Test // TODO remove test? This should be a non-issue now
public void clearLensDistortion() {
for( ImageType type : types ) {
ImageBase image = loadImage(type);
FiducialDetector detector = createDetector(type);
// save the results
detector.setLensDistortion(loadDistortion(false));
detector.detect(image);
assertTrue(detector.totalFound() >= 1);
Results before = extractResults(detector);
// run with lens distortion
detector.setLensDistortion(loadDistortion(true));
detector.detect(image);
// remove lens distortion
detector.setLensDistortion(loadDistortion(false));
detector.detect(image);
Results after = extractResults(detector);
// see if it's the same
for (int i = 0; i < after.id.length; i++) {
assertEquals(before.id[i], after.id[i]);
assertEquals(0,before.pose.get(i).T.distance(after.pose.get(i).T),1e-8);
assertTrue(MatrixFeatures.isIdentical(before.pose.get(i).R,after.pose.get(i).R,1e-8));
assertEquals(0,before.pixel.get(i).distance(after.pixel.get(i)),1e-8);
}
}
}
private Results extractResults( FiducialDetector detector ) {
Results out = new Results(detector.totalFound());
for (int i = 0; i < detector.totalFound(); i++) {
Se3_F64 pose = new Se3_F64();
Point2D_F64 pixel = new Point2D_F64();
detector.getFiducialToCamera(i, pose);
detector.getImageLocation(i, pixel);
out.id[i] = detector.getId(i);
out.pose.add(pose);
out.pixel.add(pixel);
}
return out;
}
private static class Results {
public long id[];
public List<Se3_F64> pose = new ArrayList<>();
public List<Point2D_F64> pixel = new ArrayList<>();
public Results( int N ) {
id = new long[ N ];
pose = new ArrayList<>();
pixel = new ArrayList<>();
}
}
}