/*
* 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.tracker.klt;
import boofcv.alg.filter.derivative.GradientSobel;
import boofcv.alg.interpolate.InterpolateRectangle;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.core.image.border.BorderIndex1D_Extend;
import boofcv.core.image.border.ImageBorder1D_F32;
import boofcv.factory.interpolate.FactoryInterpolation;
import boofcv.struct.image.GrayF32;
import boofcv.testing.BoofTesting;
import org.junit.Test;
import java.util.Random;
import static org.junit.Assert.*;
/**
* @author Peter Abeles
*/
public class TestKltTracker {
Random rand = new Random(234);
int imageWidth = 40;
int imageHeight = 50;
GrayF32 image = new GrayF32(imageWidth, imageHeight);
GrayF32 derivX = new GrayF32(imageWidth, imageHeight);
GrayF32 derivY = new GrayF32(imageWidth, imageHeight);
/**
* Process the same features in two different sets of image. only difference is that one is a sub image
* results should be identical
*/
@Test
public void testSubImages() {
ImageMiscOps.fillUniform(image, rand, 0, 100);
GradientSobel.process(image, derivX, derivY, new ImageBorder1D_F32(BorderIndex1D_Extend.class));
KltTracker<GrayF32, GrayF32> trackerA = createDefaultTracker();
trackerA.setImage(image, derivX, derivY);
KltTracker<GrayF32, GrayF32> trackerB = createDefaultTracker();
GrayF32 image = BoofTesting.createSubImageOf(this.image);
GrayF32 derivX = BoofTesting.createSubImageOf(this.derivX);
GrayF32 derivY = BoofTesting.createSubImageOf(this.derivY);
trackerB.setImage(image, derivX, derivY);
for( int y = 0; y < imageHeight; y += 4) {
for( int x = 0; x < imageWidth; x += 4) {
KltFeature featureA = new KltFeature(3);
KltFeature featureB = new KltFeature(3);
featureA.setPosition(x,y);
featureB.setPosition(x,y);
trackerA.setDescription(featureA);
trackerB.setDescription(featureB);
float dx = rand.nextFloat()*2-1;
float dy = rand.nextFloat()*2-1;
featureA.setPosition(x+dx,y+dy);
featureB.setPosition(x+dx,y+dy);
KltTrackFault faultA = trackerA.track(featureA);
KltTrackFault faultB = trackerB.track(featureB);
assertTrue(faultA == faultB);
if( x == 4 )
System.out.println();
if( faultA == KltTrackFault.SUCCESS ) {
assertTrue(x+" "+y,featureA.x == featureB.x);
assertTrue(x+" "+y,featureA.y == featureB.y);
}
}
}
}
/**
* Create a description of a feature next to the border then place the feature just outside of the image
* and see if it can track to its original position.
*/
@Test
public void testTracking_border1() {
ImageMiscOps.fillUniform(image,rand,0,100);
GradientSobel.process(image, derivX, derivY, new ImageBorder1D_F32(BorderIndex1D_Extend.class));
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(3);
// lower right border, but fully inside the image
feature.setPosition(imageWidth-4, imageHeight-4);
tracker.setDescription(feature);
// put it partially outside the image
feature.setPosition(imageWidth-2, imageHeight-1);
// see if it got sucked back
assertTrue(tracker.track(feature) == KltTrackFault.SUCCESS);
assertEquals(imageWidth-4,feature.x,0.01);
assertEquals(imageHeight-4,feature.y,0.01);
// same thing but with the top left image
feature.setPosition(3, 3);
tracker.setDescription(feature);
// put it partially outside the image
feature.setPosition(1, 2);
// see if it got sucked back
assertTrue(tracker.track(feature) == KltTrackFault.SUCCESS);
assertEquals(3,feature.x,0.01);
assertEquals(3,feature.y,0.01);
}
/**
* Place a feature on the border then put it inside the image. See if it moves towards the border
*/
@Test
public void testTracking_border2() {
ImageMiscOps.fillUniform(image,rand,0,100);
GradientSobel.process(image, derivX, derivY, new ImageBorder1D_F32(BorderIndex1D_Extend.class));
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(3);
// just outside the image
feature.setPosition(imageWidth-3-1+2, imageHeight-3-1+1);
tracker.setDescription(feature);
// now fuly inside the image
feature.setPosition(imageWidth-3-1, imageHeight-3-1);
// see if it got sucked back
assertTrue(tracker.track(feature) == KltTrackFault.SUCCESS);
assertEquals(imageWidth-3-1+2,feature.x,0.01);
assertEquals(imageHeight-3-1+1,feature.y,0.01);
// same thing but with the top left image
feature.setPosition(2, 1);
tracker.setDescription(feature);
// put it fully inside the image
feature.setPosition(3, 3);
// see if it got sucked back
assertTrue(tracker.track(feature) == KltTrackFault.SUCCESS);
assertEquals(2,feature.x,0.01);
assertEquals(1,feature.y,0.01);
}
/**
* Set description should fail if a feature is entirely outside the image
*/
@Test
public void setDescription_outsideFail() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(3);
feature.setPosition(-100,200);
assertFalse(tracker.setDescription(feature));
}
/**
* Compares the border algorithm to the inner algorithm
*/
@Test
public void setDescription_compare() {
ImageMiscOps.fillUniform(image,rand,0,100);
GradientSobel.process(image, derivX, derivY, new ImageBorder1D_F32(BorderIndex1D_Extend.class));
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature featureA = new KltFeature(3);
KltFeature featureB = new KltFeature(3);
featureA.setPosition(20.6f,25.1f);
featureB.setPosition(20.6f,25.1f);
tracker.setAllowedBounds(featureA);
tracker.internalSetDescription(featureA);
tracker.internalSetDescriptionBorder(featureB);
for( int i = 0; i < featureA.desc.data.length; i++ ) {
assertTrue(featureA.desc.data[i] == featureB.desc.data[i]);
assertTrue(featureA.derivX.data[i] == featureB.derivX.data[i]);
assertTrue(featureA.derivY.data[i] == featureB.derivY.data[i]);
}
}
/**
* When placed outside the image pixels should be NaN
*/
@Test
public void setDescription_borderNaN() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(3);
feature.setPosition(2,1);
tracker.setDescription(feature);
int numNaN = 0;
for( int i = 0; i < feature.desc.data.length; i++ ) {
if( Float.isNaN(feature.desc.data[i])) {
numNaN++;
}
}
assertEquals(19,numNaN);
}
/**
* Pass in a feature with a small determinant and see if it returns a fault.
*/
@Test
public void detectBadFeature() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(2);
// put a feature right on the corner
feature.setPosition(20, 20);
// Gxx, Gyy, and Gxy will all be zero, which is bad
// update the feature's position
tracker.setImage(image, derivX, derivY);
assertTrue(tracker.track(feature) != KltTrackFault.SUCCESS);
}
@Test
public void compare_computeGandE_border_toInsideImage() {
ImageMiscOps.fillUniform(image,rand,0,100);
GradientSobel.process(image, derivX, derivY, new ImageBorder1D_F32(BorderIndex1D_Extend.class));
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
tracker.setImage(image, derivX, derivY);
KltFeature feature = new KltFeature(2);
feature.setPosition(20, 22);
tracker.setDescription(feature);
tracker.currDesc.reshape(5,5);
// need to compute E from a shifted location or else it will be zero
tracker.computeE(feature,21,23);
float expectedGxx = feature.Gxx;
float expectedGxy = feature.Gxy;
float expectedGyy = feature.Gyy;
float expectedEx = tracker.Ex;
float expectedEy = tracker.Ey;
assertTrue( 0 != expectedGxx );
assertTrue( 0 != expectedEy );
assertEquals(25,tracker.computeGandE_border(feature,20,22));
assertEquals(expectedGxx,tracker.Gxx,1e-8);
assertEquals(expectedGxy,tracker.Gxy,1e-8);
assertEquals(expectedGyy,tracker.Gyy,1e-8);
assertEquals(25,tracker.computeGandE_border(feature,21,23));
assertEquals(expectedEx,tracker.Ex,1e-8);
assertEquals(expectedEy,tracker.Ey,1e-8);
}
@Test
public void isDescriptionComplete() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
KltFeature f = new KltFeature(2);
tracker.lengthFeature = 25;
assertTrue(tracker.isDescriptionComplete(f));
for( int i = 0; i < f.desc.data.length; i++ ) {
f.desc.data[i] = Float.NaN;
assertFalse(tracker.isDescriptionComplete(f));
f.desc.data[i] = 0;
assertTrue(tracker.isDescriptionComplete(f));
}
}
@Test
public void isFullyInside() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
KltFeature f = new KltFeature(2);
tracker.image = new GrayF32(imageWidth,imageHeight);
tracker.setAllowedBounds(f);
assertTrue(tracker.isFullyInside(imageWidth/2,imageHeight/2));
assertTrue(tracker.isFullyInside(2,2));
assertTrue(tracker.isFullyInside(imageWidth-3,imageHeight-3));
// check border cases
assertFalse(tracker.isFullyInside(1.99f,imageHeight/2));
assertFalse(tracker.isFullyInside(imageWidth/2f,1.99f));
assertFalse(tracker.isFullyInside(imageWidth-2.99f,imageHeight-3));
assertFalse(tracker.isFullyInside(imageWidth-3,imageHeight-2.99f));
// negative numbers
assertFalse(tracker.isFullyInside(-imageWidth/2,imageHeight/2));
assertFalse(tracker.isFullyInside(imageWidth/2,-imageHeight/2));
}
@Test
public void isFullyOutside() {
KltTracker<GrayF32, GrayF32> tracker = createDefaultTracker();
int r = 2;
KltFeature f = new KltFeature(r);
tracker.image = new GrayF32(imageWidth,imageHeight);
tracker.setAllowedBounds(f);
assertFalse(tracker.isFullyOutside(-r,-r));
assertFalse(tracker.isFullyOutside(imageWidth/2,imageHeight/2));
assertFalse(tracker.isFullyOutside(imageWidth+r-1,imageHeight+r-1));
assertTrue(tracker.isFullyOutside(imageWidth/2,1000));
assertTrue(tracker.isFullyOutside(1000,imageHeight/2));
assertTrue(tracker.isFullyOutside(-r-0.001f,-r));
assertTrue(tracker.isFullyOutside(-r,-r-0.001f));
assertTrue(tracker.isFullyOutside(imageWidth+r-0.999f,imageHeight+r-1));
assertTrue(tracker.isFullyOutside(imageWidth+r-1,imageHeight+r-0.999f));
}
public static KltTracker<GrayF32, GrayF32> createDefaultTracker() {
KltConfig config = new KltConfig();
config.maxPerPixelError = 10;
config.maxIterations = 30;
config.minDeterminant = 0.01f;
config.minPositionDelta = 0.001f;
InterpolateRectangle<GrayF32> interp1 = FactoryInterpolation.bilinearRectangle(GrayF32.class);
InterpolateRectangle<GrayF32> interp2 = FactoryInterpolation.bilinearRectangle(GrayF32.class);
return new KltTracker<>(interp1, interp2, config);
}
}