/*
* 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.feature.tracker;
import boofcv.alg.misc.GImageMiscOps;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.ImageGray;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.*;
/**
* Standard tests for implementations of {@link PointTracker}.
*
* @author Peter Abeles
*/
public abstract class StandardPointTracker<T extends ImageGray> {
public PointTracker<T> tracker;
Random rand = new Random(234);
int width = 100;
int height = 80;
GrayF32 image = new GrayF32(width,height);
boolean shouldDropTracks;
boolean shouldCreateInactive;
protected StandardPointTracker(boolean shouldCreateInactive, boolean shouldDropTracks) {
this.shouldCreateInactive = shouldCreateInactive;
this.shouldDropTracks = shouldDropTracks;
}
@Before
public void initStandard() {
ImageMiscOps.fillUniform(image, rand, 0, 100);
}
/**
* Creates a new tracker with the specified number of tracks initially.
*/
public abstract PointTracker<T> createTracker();
/**
* The cookie for tracks should not be set
*/
@Test
public void checkCookieNull() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
assertTrue(tracker.getAllTracks(null).size() > 0);
for( PointTrack t : tracker.getActiveTracks(null) ) {
assertTrue(t.cookie==null);
}
}
/**
* After a track has been dropped the cookie should not be modified and returned when
* the track is recycled.
*
* NOTE: This test will return incorrect results if a large group of tracks are predeclared
* and recycled tracks are put at the end of the queue
*/
@Test
public void checkCookieSaved() {
// create tracks
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
assertTrue(tracker.getAllTracks(null).size() > 0);
tracker.getActiveTracks(null).get(0).setCookie(1);
// drop tracks
tracker.dropAllTracks();
// respawn and look for a cookie that's not null
processImage((T)image);
tracker.spawnTracks();
int numFound = 0;
for( PointTrack t : tracker.getActiveTracks(null) ) {
if( t.getCookie() != null )
numFound++;
}
assertEquals(1,numFound);
}
/**
* High level spawn tracks test.
*/
@Test
public void spawnTracks() {
// Process an image and make sure no new tracks have been spawned until requested
tracker = createTracker();
processImage((T)image);
assertEquals(0,tracker.getAllTracks(null).size());
assertEquals(0,tracker.getActiveTracks(null).size());
assertTrue(tracker.getNewTracks(null).size() == 0 );
// Request that new tracks be spawned and ensure that all lists have been updated
tracker.spawnTracks();
assertTrue(tracker.getAllTracks(null).size() > 0);
assertTrue(tracker.getActiveTracks(null).size()>0);
assertTrue(tracker.getActiveTracks(null).size() ==
tracker.getNewTracks(null).size() );
checkInside(tracker.getAllTracks(null));
// Tweak the input image and make sure that everything has the expected size
ImageMiscOps.addGaussian(image,rand,2,0,255);
processImage((T)image);
int beforeAll = tracker.getAllTracks(null).size();
int beforeActive = tracker.getActiveTracks(null).size();
assertTrue(beforeAll > 0);
assertTrue(beforeActive>0);
assertEquals(0, tracker.getNewTracks(null).size());
checkInside(tracker.getAllTracks(null));
// Call spawn again. There should be more tracks now
tracker.spawnTracks();
assertTrue(beforeAll < tracker.getAllTracks(null).size());
assertTrue(beforeActive < tracker.getActiveTracks(null).size());
checkInside(tracker.getAllTracks(null));
// there should be some pre-existing tracks
assertTrue(tracker.getActiveTracks(null).size() !=
tracker.getNewTracks(null).size() );
}
/**
* When spawn is called, make sure that it doesn't return identical tracks
*/
@Test
public void spawnTracks_NoDuplicates() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
assertTrue(tracker.getActiveTracks(null).size()>0);
int activeBefore = tracker.getActiveTracks(null).size();
// drop a track
PointTrack dropped = tracker.getActiveTracks(null).get(0);
tracker.dropTrack(dropped);
double x = dropped.x;
double y = dropped.y;
// process the exact same image
// I think in just about every tracker nothing should change. Might need to change this test for some
processImage((T)image);
// should just spawn one track
tracker.spawnTracks();
assertEquals(activeBefore,tracker.getActiveTracks(null).size());
assertEquals(1,tracker.getNewTracks(null).size());
PointTrack found = tracker.getNewTracks(null).get(0);
assertTrue(x == found.x);
assertTrue(y == found.y);
}
@Test
public void dropAllTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
assertTrue(tracker.getAllTracks(null).size() > 0);
assertTrue(tracker.getActiveTracks(null).size() > 0);
tracker.dropAllTracks();
assertEquals(0,tracker.getAllTracks(null).size());
assertEquals(0,tracker.getActiveTracks(null).size());
// tracks which have been dropped by request should not be included in this list
assertEquals(0,tracker.getDroppedTracks(null).size());
}
/**
* Cause tracks to be dropped during the update
*/
@Test
public void testUpdateTrackDrop() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
int beforeAll = tracker.getAllTracks(null).size();
int beforeActive = tracker.getActiveTracks(null).size();
assertTrue(beforeAll > 0);
assertEquals(0,tracker.getDroppedTracks(null).size());
// make the image a poor match, causing tracks to be dropped
GImageMiscOps.fill(image, 0);
processImage((T)image);
int afterAll = tracker.getAllTracks(null).size();
int afterActive = tracker.getActiveTracks(null).size();
int afterDropped = tracker.getDroppedTracks(null).size();
int afterInactive = tracker.getInactiveTracks(null).size();
// make sure some change happened
assertTrue(afterActive < beforeActive);
// algorithm specific checks
if( shouldDropTracks ) {
assertTrue(afterDropped>0);
assertTrue(afterAll < beforeAll );
} else {
assertEquals(0,afterDropped);
assertTrue(afterAll == beforeAll );
}
if( shouldCreateInactive )
assertTrue(afterInactive>0);
else
assertEquals(0,afterInactive);
// this might not be true for all trackers...
assertEquals(0,afterActive);
// some tracks should either be dropped or become inactive
assertTrue(afterDropped+afterInactive>0);
// note that some trackers will not add any features to the dropped list since
// it will try to respawn them
assertEquals(beforeAll-afterAll,tracker.getDroppedTracks(null).size());
}
@Test
public void testRequestDrop() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
List<PointTrack> tracks = tracker.getActiveTracks(null);
int before = tracks.size();
assertTrue(before > 0);
assertTrue(tracker.dropTrack(tracks.get(0)));
// a second request to drop the track should do nothing
assertFalse(tracker.dropTrack(tracks.get(0)));
// the track should be removed from the all and active lists
assertEquals(before-1,tracker.getAllTracks(null).size());
assertEquals(before-1,tracker.getActiveTracks(null).size());
// tracks which have been dropped by request should not be included in this list
assertEquals(0,tracker.getDroppedTracks(null).size());
}
@Test
public void testTrackUpdate() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
int before = tracker.getAllTracks(null).size();
assertTrue(before > 0);
checkUniqueFeatureID();
// by adding a little bit of noise the features should move slightly
ImageMiscOps.addUniform(image,rand,0,5);
processImage((T)image);
checkUniqueFeatureID();
int after = tracker.getAllTracks(null).size();
int dropped = tracker.getDroppedTracks(null).size();
assertEquals(before , after+dropped);
}
@Test
public void reset() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
assertTrue(tracker.getAllTracks(null).size() > 0);
assertEquals(0, tracker.getAllTracks(null).get(0).featureId);
tracker.reset();
// add several tracks
ImageMiscOps.addUniform(image,rand,0,5);
processImage((T)image);
tracker.spawnTracks();
// old tracks should be discarded
assertTrue(tracker.getAllTracks(null).size() > 0);
// checks to see if feature ID counter was reset
assertEquals(0, tracker.getAllTracks(null).get(0).featureId);
}
/**
* Makes sure each feature has a unique feature number
*/
private void checkUniqueFeatureID() {
List<PointTrack> l = tracker.getActiveTracks(null);
for( int i = 0; i < l.size(); i++ ) {
PointTrack a = l.get(i);
for( int j = i+1; j < l.size(); j++ ) {
PointTrack b = l.get(j);
assertTrue(a.featureId != b.featureId);
}
}
}
@Test
public void getAllTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
List<PointTrack> input = new ArrayList<>();
assertTrue(input == tracker.getAllTracks(input));
List<PointTrack> ret = tracker.getAllTracks(null);
//sanity check
assertTrue(ret.size() > 0 );
checkIdentical(input,ret);
}
@Test
public void getActiveTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
List<PointTrack> input = new ArrayList<>();
assertTrue(input == tracker.getActiveTracks(input));
List<PointTrack> ret = tracker.getActiveTracks(null);
//sanity check
assertTrue(ret.size() > 0 );
checkIdentical(input, ret);
}
@Test
public void getInactiveTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
// create a situation where tracks might become inactive
GImageMiscOps.fill(image, 0);
processImage((T)image);
List<PointTrack> input = new ArrayList<>();
assertTrue( input == tracker.getInactiveTracks(input));
List<PointTrack> ret = tracker.getInactiveTracks(null);
checkIdentical(input, ret);
}
@Test
public void getDroppedTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
// create a situation where tracks might be dropped
GImageMiscOps.fill(image, 0);
processImage((T)image);
List<PointTrack> input = new ArrayList<>();
assertTrue( input == tracker.getDroppedTracks(input));
if( shouldDropTracks )
assertTrue( input.size() > 0 );
List<PointTrack> ret = tracker.getDroppedTracks(null);
checkIdentical(input, ret);
}
@Test
public void getNewTracks() {
tracker = createTracker();
processImage((T)image);
tracker.spawnTracks();
List<PointTrack> input = new ArrayList<>();
assertTrue( input == tracker.getNewTracks(input));
List<PointTrack> ret = tracker.getNewTracks(null);
//sanity check
assertTrue(ret.size() > 0 );
checkIdentical(input, ret);
}
private void checkIdentical( List<PointTrack> a , List<PointTrack> b ){
assertEquals(a.size(),b.size());
for( int i = 0; i < a.size(); i++ ) {
assertTrue(a.get(i) == b.get(i));
}
}
/**
* Makes sure all the tracks are inside the image
*/
protected void checkInside( List<PointTrack> tracks ) {
for( PointTrack t : tracks ) {
if( t.x < 0 || t.y < 0 || t.x > width-1 || t.y > height-1 )
fail("track is outside of the image: "+t.x+" "+t.y);
}
}
/**
* Performs all the standard tracking steps. Associates and updates tracks descriptions. This function
* is here for {@link DetectDescribeAssociateTwoPass} tests. If overriden it should be
* equivalent to just calling tracker.process()
*
* @param image
*/
protected void processImage( T image ) {
tracker.process(image);
}
}