/*
* 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.circulant;
import boofcv.alg.interpolate.InterpolatePixelS;
import boofcv.alg.misc.GImageMiscOps;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.alg.misc.ImageStatistics;
import boofcv.core.image.border.BorderType;
import boofcv.factory.interpolate.FactoryInterpolation;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.GrayF64;
import boofcv.struct.image.InterleavedF64;
import georegression.struct.shapes.RectangleLength2D_F32;
import org.ejml.data.Complex64F;
import org.ejml.ops.ComplexMath64F;
import org.junit.Test;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Peter Abeles
*/
public class TestCirculantTracker {
Random rand = new Random(234);
int width = 60;
int height = 80;
InterpolatePixelS<GrayF32> interp;
public TestCirculantTracker() {
interp = FactoryInterpolation.bilinearPixelS(GrayF32.class, BorderType.EXTENDED);
}
@Test
public void meanShift() {
int w = 32;
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,w,255,interp);
int peakX = 13;
int peakY = 17;
alg.getResponse().reshape(w,w);
for( int i = 0; i < w; i++ ) {
double b = Math.exp( -(i-peakY)*(i-peakY)/3.0 );
for( int j = 0; j < w; j++ ) {
double a = Math.exp( -(j-peakX)*(j-peakX)/3.0 );
alg.getResponse().set(j,i,a*b);
}
}
alg.subpixelPeak(peakX - 2, peakY + 1);
assertEquals(2,alg.offX,0.3);
assertEquals(-1,alg.offY,0.3);
}
@Test
public void basicTrackingCheck() {
GrayF32 a = new GrayF32(30,35);
GrayF32 b = new GrayF32(30,35);
// randomize input image and move it
GImageMiscOps.fillUniform(a, rand, 0, 200);
GImageMiscOps.fillUniform(b,rand,0,200);
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,64,255,interp);
alg.initialize(a, 5, 6, 20, 25);
shiftCopy(2,4,a,b);
alg.performTracking(b);
double tolerance = 1;
RectangleLength2D_F32 r = alg.getTargetLocation();
assertEquals(5+2,r.x0,tolerance);
assertEquals(6 + 4, r.y0, tolerance);
}
@Test
public void computeCosineWindow() {
GrayF64 found = new GrayF64(20,25);
CirculantTracker.computeCosineWindow(found);
// should be between 0 and 1
for( int i = 0; i < found.data.length; i++ ) {
assertTrue( found.data[i] >= 0 && found.data[i] <= 1);
}
centeredSymmetricChecks(found,false);
}
@Test
public void computeGaussianWeights() {
int w = 16;
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,w,255,interp);
alg.gaussianWeight.reshape(w,w);
alg.gaussianWeightDFT.reshape(w, w);
alg.computeGaussianWeights(w);
centeredSymmetricChecks(alg.gaussianWeight,true);
}
private void centeredSymmetricChecks(GrayF64 image , boolean offByOne ) {
// see comments in computeGaussianWeights
int offX = offByOne ? 1-image.width%2 : 0;
int offY = offByOne ? 1-image.height%2 : 0;
int cx = image.width/2;
int cy = image.height/2;
int w = image.width-1;
int h = image.height-1;
// edges should be smaller than center
assertTrue(image.get(cx, cy) > image.get(0, 0));
assertTrue( image.get(cx,cy) > image.get(w,h) );
assertTrue( image.get(cx,cy) > image.get(w,h) );
assertTrue(image.get(cx, cy) > image.get(w, 0));
// symmetry check
for( int i = offY; i < cy; i++ ) {
for( int j = offX; j < cx; j++ ) {
double v0 = image.get(j,i);
double v1 = image.get(w-j+offX,i);
double v2 = image.get(j,h-i+offY);
double v3 = image.get(w-j+offX,h-i+offY);
assertEquals(v0,v1,1e-4);
assertEquals(v0,v2,1e-4);
assertEquals(i+" "+j,v0,v3,1e-4);
}
}
}
/**
* Check a few simple motions. It seems to be accurate to within 1 pixel. Considering alphas seems to be the issue
*/
@Test
public void updateTrackLocation() {
GrayF32 a = new GrayF32(100,100);
GrayF32 b = new GrayF32(100,100);
// randomize input image and move it
GImageMiscOps.fillUniform(a,rand,0,200);
GImageMiscOps.fillUniform(b,rand,0,200);
shiftCopy(0,0,a,b);
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,64,255,interp);
alg.initialize(a,5,6,20,25);
alg.updateTrackLocation(b);
// only pixel level precision.
float tolerance = 1f;
// No motion motion
RectangleLength2D_F32 r = alg.getTargetLocation();
assertEquals(5,r.x0,tolerance);
assertEquals(6,r.y0,tolerance);
// check estimated motion
GImageMiscOps.fillUniform(b,rand,0,200);
shiftCopy(-3,2,a,b);
alg.updateTrackLocation(b);
r = alg.getTargetLocation();
assertEquals(5-3,r.x0,tolerance);
assertEquals(6+2,r.y0,tolerance);
// try out of bounds case
GImageMiscOps.fillUniform(b,rand,0,200);
shiftCopy(-6,0,a,b);
alg.updateTrackLocation(b);
assertEquals(5-6,r.x0,tolerance);
assertEquals(6,r.y0,tolerance);
}
@Test
public void performLearning() {
float interp_factor = 0.075f;
GrayF32 a = new GrayF32(20,25);
GrayF32 b = new GrayF32(20,25);
ImageMiscOps.fill(a, 100);
ImageMiscOps.fill(b,200);
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,64,255,interp);
alg.initialize(a,0,0,20,25);
// copy its internal value
GrayF64 templateC = new GrayF64(alg.template.width,alg.template.height);
templateC.setTo(alg.template);
// give it two images
alg.performLearning(b);
// make sure the images aren't full of zero
assertTrue(Math.abs(ImageStatistics.sum(templateC)) > 0.1 );
assertTrue(Math.abs(ImageStatistics.sum(alg.template)) > 0.1 );
int numNotSame = 0;
// the result should be an average of the two
for( int i = 0; i < a.data.length; i++ ) {
if( Math.abs(a.data[i]-alg.templateNew.data[i]) > 1e-4 )
numNotSame++;
// should be more like the original one than the new one
double expected = templateC.data[i]*(1-interp_factor) + interp_factor*alg.templateNew.data[i];
double found = alg.template.data[i];
assertEquals(expected,found,1e-4);
}
// make sure it is actually different
assertTrue(numNotSame>100);
}
@Test
public void dense_gauss_kernel() {
// try several different shifts
dense_gauss_kernel(0,0);
dense_gauss_kernel(5,0);
dense_gauss_kernel(0,5);
dense_gauss_kernel(-3,-2);
}
public void dense_gauss_kernel( int offX , int offY ) {
GrayF64 region = new GrayF64(32,32);
GrayF64 target = new GrayF64(32,32);
GrayF64 k = new GrayF64(32,32);
CirculantTracker<GrayF32> alg = new CirculantTracker<>(1f/16,0.2,1e-2,0.075,1.0,32,255,interp);
alg.initialize(new GrayF32(32,32),0,0,32,32);
// create a shape inside the image
GImageMiscOps.fillRectangle(region,200,10,15,5,7);
// copy a shifted portion of the region
shiftCopy(offX, offY, region, target);
// process and see if the peak is where it should be
alg.dense_gauss_kernel(0.2f,region,target,k);
int maxX=-1,maxY=-1;
double maxValue = -1;
for( int y = 0; y < k.height;y++ ){
for( int x=0; x < k.width;x++ ) {
if( k.get(x,y) > maxValue ) {
maxValue = k.get(x,y);
maxX = x;
maxY = y;
}
}
}
int expectedX = k.width/2-offX;
int expectedY = k.height/2-offY;
assertEquals(expectedX,maxX);
assertEquals(expectedY,maxY);
}
private void shiftCopy(int offX, int offY, GrayF32 src, GrayF32 dst) {
for( int y = 0; y < src.height; y++ ) {
for( int x = 0; x < src.width; x++ ) {
int xx = x + offX;
int yy = y + offY;
if( xx >= 0 && xx < src.width && yy >= 0 && yy < src.height ) {
dst.set(xx, yy, src.get(x, y));
}
}
}
}
private void shiftCopy(int offX, int offY, GrayF64 src, GrayF64 dst) {
for( int y = 0; y < src.height; y++ ) {
for( int x = 0; x < src.width; x++ ) {
int xx = x + offX;
int yy = y + offY;
if( xx >= 0 && xx < src.width && yy >= 0 && yy < src.height ) {
dst.set(xx, yy, src.get(x, y));
}
}
}
}
@Test
public void imageDotProduct() {
GrayF64 a = new GrayF64(width,height);
ImageMiscOps.fillUniform(a,rand,0,10);
double total = 0;
for( int y = 0; y < height; y++ ) {
for( int x = 0; x < width; x++ ) {
total += a.get(x,y)*a.get(x,y);
}
}
double found = CirculantTracker.imageDotProduct(a);
assertEquals(total,found,1e-8);
}
@Test
public void elementMultConjB() {
InterleavedF64 a = new InterleavedF64(width,height,2);
InterleavedF64 b = new InterleavedF64(width,height,2);
InterleavedF64 c = new InterleavedF64(width,height,2);
ImageMiscOps.fillUniform(a,rand,-10,10);
ImageMiscOps.fillUniform(b,rand,-10,10);
ImageMiscOps.fillUniform(c,rand,-10,10);
CirculantTracker.elementMultConjB(a, b, c);
for( int y = 0; y < height; y++ ) {
for( int x = 0; x < width; x++ ) {
Complex64F aa = new Complex64F(a.getBand(x,y,0),a.getBand(x,y,1));
Complex64F bb = new Complex64F(b.getBand(x,y,0),b.getBand(x,y,1));
Complex64F cc = new Complex64F();
ComplexMath64F.conj(bb, bb);
ComplexMath64F.multiply(aa, bb, cc);
double foundReal = c.getBand(x,y,0);
double foundImg = c.getBand(x,y,1);
assertEquals(cc.real,foundReal,1e-4);
assertEquals(cc.imaginary,foundImg,1e-4);
}
}
}
@Test
public void computeAlphas() {
InterleavedF64 yf = new InterleavedF64(width,height,2);
InterleavedF64 kf = new InterleavedF64(width,height,2);
InterleavedF64 alphaf = new InterleavedF64(width,height,2);
ImageMiscOps.fillUniform(yf,rand,-10,10);
ImageMiscOps.fillUniform(kf,rand,-10,10);
ImageMiscOps.fillUniform(alphaf,rand,-10,10);
float lambda = 0.01f;
CirculantTracker.computeAlphas(yf, kf, lambda, alphaf);
for( int y = 0; y < height; y++ ) {
for( int x = 0; x < width; x++ ) {
Complex64F a = new Complex64F(yf.getBand(x,y,0),yf.getBand(x,y,1));
Complex64F b = new Complex64F(kf.getBand(x,y,0)+lambda,kf.getBand(x,y,1));
Complex64F c = new Complex64F();
ComplexMath64F.divide(a, b, c);
double foundReal = alphaf.getBand(x,y,0);
double foundImg = alphaf.getBand(x,y,1);
assertEquals(c.real,foundReal,1e-4);
assertEquals(c.imaginary,foundImg,1e-4);
}
}
}
}