boofcv.testing.BoofTesting Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of boofcv-ip Show documentation
Show all versions of boofcv-ip Show documentation
BoofCV is an open source Java library for real-time computer vision and robotics applications.
/*
* Copyright (c) 2011-2017, 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.testing;
import boofcv.core.image.*;
import boofcv.struct.image.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Functions to aid in unit testing code for correctly handling sub-images
*
* @author Peter Abeles
*/
// todo remove all comwapare with border functions and use sub-images instead
@SuppressWarnings({"unchecked"})
public class BoofTesting {
public static T convertToGenericType(Class> type) {
if (type == GrayS8.class || type == GrayU8.class)
return (T) GrayI8.class;
if (type == GrayS16.class || type == GrayU16.class)
return (T) GrayI16.class;
if (type == InterleavedS8.class || type == InterleavedU8.class)
return (T) InterleavedI8.class;
if (type == InterleavedS16.class || type == InterleavedU16.class)
return (T) InterleavedI16.class;
return (T) type;
}
public static ImageDataType convertToGenericType(ImageDataType type) {
if (type.isInteger()) {
if (type.getNumBits() == 8)
return ImageDataType.I8;
else if (type.getNumBits() == 16)
return ImageDataType.I16;
}
return type;
}
/**
* If an image is to be created then the generic type can't be used a specific one needs to be. An arbitrary
* specific image type is returned here.
*/
public static T convertGenericToSpecificType(Class> type) {
if (type == GrayI8.class)
return (T) GrayU8.class;
if (type == GrayI16.class)
return (T) GrayS16.class;
if (type == InterleavedI8.class)
return (T) InterleavedU8.class;
if (type == InterleavedI16.class)
return (T) InterleavedS16.class;
return (T) type;
}
/**
*
* Returns an image which is a sub-image but contains the same values of the input image. Use for
* testing compliance with sub-images. The subimage is created by creating a larger image,
* copying over the input image into the inner portion, then creating a subimage of the copied part.
*
*/
@SuppressWarnings({"unchecked"})
public static > T createSubImageOf(T input) {
if( input instanceof ImageGray) {
return (T)createSubImageOf_S((ImageGray)input);
} else if( input instanceof Planar) {
return (T)createSubImageOf_PL((Planar) input);
} else if( input instanceof ImageInterleaved ) {
return (T)createSubImageOf_I((ImageInterleaved) input);
} else {
throw new IllegalArgumentException("Add support for this image type");
}
}
public static > T createSubImageOf_S(T input) {
// create the larger image
T ret = (T) input.createNew(input.width + 10, input.height + 12);
// create a sub-image of the inner portion
ret = (T) ret.subimage(5, 7, input.width + 5, input.height + 7, null);
// copy input image into the subimage
ret.setTo(input);
return ret;
}
public static > T createSubImageOf_I(T input) {
// create the larger image
T ret = (T) input.createNew(input.width + 10, input.height + 12);
// create a sub-image of the inner portion
ret = (T) ret.subimage(5, 7, input.width + 5, input.height + 7, null);
// copy input image into the subimage
ret.setTo(input);
return ret;
}
public static T createSubImageOf_PL(T input) {
T ret = (T)new Planar(input.type,input.width,input.height,input.getNumBands());
for( int i = 0; i < input.getNumBands(); i++ ) {
ret.bands[i] = createSubImageOf_S(input.getBand(i));
}
ret.stride = ret.bands[0].stride;
ret.startIndex = ret.bands[0].startIndex;
return ret;
}
/**
* Searches for functions that accept only images and makes sure they only accept
* images which have he same width and height.
*
* @param testClass Instance of the class being tested
*/
public static void checkImageDimensionValidation(Object testClass, int numFunctions) {
int count = 0;
Method methods[] = testClass.getClass().getMethods();
for (Method m : methods) {
// see if the inputs are all images
if (!areAllInputsImages(m))
continue;
// test a positive case
Class params[] = m.getParameterTypes();
Object[] inputs = new Object[params.length];
for (int i = 0; i < params.length; i++) {
inputs[i] = GeneralizedImageOps.createSingleBand(params[i], 10, 20);
}
try {
m.invoke(testClass, inputs);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
// test negative cases
for (int target = 0; target < params.length; target++) {
for (int i = 0; i < params.length; i++) {
if (i != target)
inputs[i] = GeneralizedImageOps.createSingleBand(params[i], 10, 20);
else
inputs[i] = GeneralizedImageOps.createSingleBand(params[i], 11, 22);
}
try {
m.invoke(testClass, inputs);
throw new RuntimeException("Expected an exception here");
} catch (InvocationTargetException e) {
if (e.getTargetException().getClass() != IllegalArgumentException.class)
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
count++;
}
if (count != numFunctions)
throw new RuntimeException("Unexpected number of functions");
}
private static boolean areAllInputsImages(Method m) {
Class> params[] = m.getParameterTypes();
if (params.length == 0)
return false;
for (Class> p : params) {
if (!ImageGray.class.isAssignableFrom(p)) {
return false;
}
}
return true;
}
/**
* Tests the specified function with the original image provided and with an equivalent
* sub-image. The two results are then compared. The function being tested must only
* have one input parameter of type {@link GrayU8}.
*
* @param testClass Instance of the class that contains the function being tested.
* @param function The name of the function being tested.
* @param checkEquals Checks to see if the two images have been modified the same way on output
* @param inputParam The original input parameters
*/
// TODO make sure pixels outside are not modified of sub-matrix
// todo have the submatrices be from different shaped inputs
@SuppressWarnings({"unchecked"})
public static void checkSubImage(Object testClass,
String function,
boolean checkEquals,
Object... inputParam) {
try {
ImageBase[] larger = new ImageBase[inputParam.length];
ImageBase[] subImg = new ImageBase[inputParam.length];
Class> paramDesc[] = new Class>[inputParam.length];
Object[] inputModified = new Object[inputParam.length];
for (int i = 0; i < inputParam.length; i++) {
if (ImageBase.class.isAssignableFrom(inputParam[i].getClass())) {
ImageBase> img = (ImageBase>) inputParam[i];
// copy the original image inside of a larger image
larger[i] = img.createNew(img.getWidth() + 10, img.getHeight() + 12);
// extract a sub-image and make it equivalent to the original image.
subImg[i] = larger[i].subimage(5, 6, 5 + img.getWidth(), 6 + img.getHeight(), null);
subImg[i].setTo(img);
}
// the first time it is called use the original inputs
inputModified[i] = inputParam[i];
paramDesc[i] = inputParam[i].getClass();
}
// first try it with the original image
Method m = findMethod(testClass.getClass(), function, paramDesc);
m.invoke(testClass, inputModified);
// now try it with the sub-image
for (int i = 0; i < inputModified.length; i++) {
if (subImg[i] != null)
inputModified[i] = subImg[i];
}
m.invoke(testClass, inputModified);
// the result should be the identical
if (checkEquals) {
for (int i = 0; i < inputParam.length; i++) {
if (subImg[i] == null)
continue;
assertEquals((ImageBase)inputModified[i], subImg[i], 0);
}
}
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* Searches for a function which is a perfect match. if none it exists it checks
* to see if any matches that could accept an input of the specified type. If there
* is only one such match that is returned.
*/
public static Method findMethod(Class> type, String name, Class>... params) {
Method methods[] = type.getMethods();
List found = new ArrayList<>();
for (Method m : methods) {
if (m.getName().compareTo(name) != 0)
continue;
Class> a[] = m.getParameterTypes();
if (a.length != params.length)
continue;
boolean match = true;
for (int i = 0; i < a.length; i++) {
if (a[i] != params[i]) {
match = false;
break;
}
}
if (match) {
// its a perfect match
return m;
}
// see if it could be called with these parameters
match = true;
for (int i = 0; i < a.length; i++) {
if (params[i] == a[i])
continue;
if (a[i].isPrimitive()) {
if (a[i] == Boolean.TYPE && params[i] == Boolean.class)
continue;
if (a[i] == Byte.TYPE && params[i] == Byte.class)
continue;
if (a[i] == Short.TYPE && params[i] == Short.class)
continue;
if (a[i] == Integer.TYPE && params[i] == Integer.class)
continue;
if (a[i] == Long.TYPE && params[i] == Long.class)
continue;
if (a[i] == Float.TYPE && params[i] == Float.class)
continue;
if (a[i] == Double.TYPE && params[i] == Double.class)
continue;
}
if (!a[i].isAssignableFrom(params[i])) {
match = false;
break;
}
}
if (match) {
found.add(m);
}
}
if (found.size() == 1) {
return found.get(0);
}
throw new RuntimeException("Couldn't find matching *public* function to " + name);
}
/**
* Looks up the static method then passes in the specified inputs.
*/
public static void callStaticMethod(Class> classType, String name, Object... inputs) {
Class> params[] = new Class[inputs.length];
for( int i = 0; i < inputs.length; i++ ) {
params[i] = inputs[i].getClass();
}
Method m = findMethod(classType,name,params);
if( m == null ) {
for( int i = 0; i < inputs.length; i++ ) {
if( params[i] == Integer.class ) {
params[i] = int.class;
} else if( params[i] == Float.class ) {
params[i] = float.class;
} else if( params[i] == Double.class ) {
params[i] = double.class;
}
}
m = findMethod(classType,name,params);
}
if( m == null )
throw new IllegalArgumentException("Method not found");
try {
m.invoke(null,inputs);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/**
* Searches for all functions with the specified name in the target class. Once it finds
* that function it invokes the specified function in the owner class. That function must
* take in a Method as its one and only parameter. The method will be one of the matching
* ones in the target class.
*
* @param owner
* @param ownerMethod
* @param target
* @param targetMethod
* @return The number of times 'targetMethod' was found and called.
*/
public static int findMethodThenCall(Object owner, String ownerMethod, Class target, String targetMethod) {
int total = 0;
Method[] list = target.getMethods();
try {
Method om = owner.getClass().getMethod(ownerMethod, Method.class);
for (Method m : list) {
if (!m.getName().equals(targetMethod))
continue;
om.invoke(owner, m);
total++;
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
return total;
}
public static void assertEquals(double a[], double b[], double tol) {
for (int i = 0; i < a.length; i++) {
double diff = Math.abs(a[i] - b[i]);
if (diff > tol)
throw new RuntimeException("Element " + i + " not equals. " + a[i] + " " + b[i]);
}
}
public static void assertEquals(double a[], float b[], double tol) {
for (int i = 0; i < a.length; i++) {
double diff = Math.abs(a[i] - b[i]);
if (diff > tol)
throw new RuntimeException("Element " + i + " not equals. " + a[i] + " " + b[i]);
}
}
public static void assertEquals(double a[], int b[]) {
for (int i = 0; i < a.length; i++) {
double diff = Math.abs((int) a[i] - b[i]);
if (diff != 0)
throw new RuntimeException("Element " + i + " not equals. " + a[i] + " " + b[i]);
}
}
public static void assertEquals(float a[], float b[], float tol) {
for (int i = 0; i < a.length; i++) {
double diff = Math.abs(a[i] - b[i]);
if (diff > tol)
throw new RuntimeException("Element " + i + " not equals. " + a[i] + " " + b[i]);
}
}
public static void assertEquals(ImageBase imgA, ImageBase imgB, double tol ) {
// if no specialized check exists, use a slower generalized approach
if( imgA instanceof ImageGray) {
GImageGray a = FactoryGImageGray.wrap((ImageGray)imgA);
GImageGray b = FactoryGImageGray.wrap((ImageGray)imgB);
for( int y = 0; y < imgA.height; y++ ) {
for( int x = 0; x < imgA.width; x++ ) {
double valA = a.get(x,y).doubleValue();
double valB = b.get(x,y).doubleValue();
double difference = valA - valB;
if( Math.abs(difference) > tol )
throw new RuntimeException("Values not equal at ("+x+","+y+") "+valA+" "+valB);
}
}
} else if( imgA instanceof Planar && imgB instanceof Planar){
Planar a = (Planar)imgA;
Planar b = (Planar)imgB;
if( a.getNumBands() != b.getNumBands() )
throw new RuntimeException("Number of bands not equal");
for( int band = 0; band < a.getNumBands(); band++ ) {
assertEquals(a.getBand(band), b.getBand(band), tol);
}
} else if( imgA instanceof ImageMultiBand && imgB instanceof ImageMultiBand) {
ImageMultiBand a = (ImageMultiBand)imgA;
ImageMultiBand b = (ImageMultiBand)imgB;
if( a.getNumBands() != b.getNumBands() )
throw new RuntimeException("Number of bands not equal");
int numBands = a.getNumBands();
for( int y = 0; y < imgA.height; y++ ) {
for( int x = 0; x < imgA.width; x++ ) {
for( int band = 0; band < numBands; band++ ) {
double valA = GeneralizedImageOps.get( a, x, y, band);
double valB = GeneralizedImageOps.get( b, x, y, band);
double difference = valA - valB;
if( Math.abs(difference) > tol )
throw new RuntimeException("Values not equal at ("+x+","+y+") "+valA+" "+valB);
}
}
}
} else {
throw new RuntimeException("Unknown image type");
}
}
public static void assertEqualsInner(ImageBase imgA, ImageBase imgB, double tol , int borderX , int borderY ,
boolean relative ) {
// if no specialized check exists, use a slower generalized approach
if( imgA instanceof ImageGray) {
GImageGray a = FactoryGImageGray.wrap((ImageGray)imgA);
GImageGray b = FactoryGImageGray.wrap((ImageGray)imgB);
for( int y = borderY; y < imgA.height-borderY; y++ ) {
for( int x = borderX; x < imgA.width-borderX; x++ ) {
double valA = a.get(x,y).doubleValue();
double valB = b.get(x,y).doubleValue();
double error = Math.abs(valA - valB);
if( relative ) {
double denominator = Math.abs(valA) + Math.abs(valB);
if( denominator == 0 )
denominator = 1;
error /= denominator;
}
if( error > tol )
throw new RuntimeException("Values not equal at ("+x+","+y+") "+valA+" "+valB);
}
}
} else if( imgA instanceof Planar){
Planar a = (Planar)imgA;
Planar b = (Planar)imgB;
if( a.getNumBands() != b.getNumBands() )
throw new RuntimeException("Number of bands not equal");
for( int band = 0; band < a.getNumBands(); band++ ) {
assertEqualsInner(a.getBand(band), b.getBand(band), tol, borderX, borderY, relative);
}
} else {
throw new RuntimeException("Unknown image type");
}
}
public static void assertEqualsInner(ImageBase imgA, ImageBase imgB, double tol ,
int borderX0 , int borderY0 , int borderX1 , int borderY1 ,
boolean relative ) {
// if no specialized check exists, use a slower generalized approach
if( imgA instanceof ImageGray) {
GImageGray a = FactoryGImageGray.wrap((ImageGray)imgA);
GImageGray b = FactoryGImageGray.wrap((ImageGray)imgB);
for( int y = borderY0; y < imgA.height-borderY1; y++ ) {
for( int x = borderX0; x < imgA.width-borderX1; x++ ) {
double valA = a.get(x,y).doubleValue();
double valB = b.get(x,y).doubleValue();
double error = Math.abs(valA - valB);
if( relative ) {
double denominator = Math.abs(valA) + Math.abs(valB);
if( denominator == 0 )
denominator = 1;
error /= denominator;
}
if( error > tol )
throw new RuntimeException("Values not equal at ("+x+","+y+") "+valA+" "+valB);
}
}
} else if( imgA instanceof Planar){
Planar a = (Planar)imgA;
Planar b = (Planar)imgB;
if( a.getNumBands() != b.getNumBands() )
throw new RuntimeException("Number of bands not equal");
for( int band = 0; band < a.getNumBands(); band++ ) {
assertEqualsInner(a.getBand(band), b.getBand(band), tol, borderX0, borderY0, borderX1, borderY1, relative);
}
} else {
throw new RuntimeException("Unknown image type");
}
}
public static void assertEqualsRelative(ImageBase imgA, ImageBase imgB, double tolFrac ) {
// if no specialized check exists, use a slower generalized approach
if( imgA instanceof ImageGray) {
GImageGray a = FactoryGImageGray.wrap((ImageGray) imgA);
GImageGray b = FactoryGImageGray.wrap((ImageGray) imgB);
for (int y = 0; y < imgA.height; y++) {
for (int x = 0; x < imgA.width; x++) {
double valA = a.get(x, y).doubleValue();
double valB = b.get(x, y).doubleValue();
double difference = valA - valB;
double max = Math.max(Math.abs(valA), Math.abs(valB));
if (max == 0)
max = 1;
if (Math.abs(difference) / max > tolFrac)
throw new RuntimeException("Values not equal at (" + x + "," + y + ") " + valA + " " + valB);
}
}
} else if( imgA instanceof ImageInterleaved) {
GImageMultiBand a = FactoryGImageMultiBand.wrap(imgA);
GImageMultiBand b = FactoryGImageMultiBand.wrap(imgB);
float valueA[] = new float[ a.getNumberOfBands() ];
float valueB[] = new float[ b.getNumberOfBands() ];
for (int y = 0; y < imgA.height; y++) {
for (int x = 0; x < imgA.width; x++) {
a.get(x,y, valueA);
b.get(x,y, valueB);
for (int i = 0; i < a.getNumberOfBands(); i++) {
double valA = valueA[i];
double valB = valueB[i];
double difference = valA - valB;
double max = Math.max(Math.abs(valA), Math.abs(valB));
if (max == 0)
max = 1;
if (Math.abs(difference) / max > tolFrac)
throw new RuntimeException("Values not equal at (" + x + "," + y + ") " + valA + " " + valB);
}
}
}
} else if( imgA instanceof Planar){
Planar a = (Planar)imgA;
Planar b = (Planar)imgB;
if( a.getNumBands() != b.getNumBands() )
throw new RuntimeException("Number of bands not equal");
for( int band = 0; band < a.getNumBands(); band++ ) {
assertEqualsRelative(a.getBand(band),b.getBand(band),tolFrac );
}
} else {
throw new RuntimeException("Unknown image type");
}
}
/**
* Checks to see if only the image borders are equal to each other within tolerance
*/
public static void assertEqualsBorder(ImageGray imgA, ImageGray imgB, double tol, int borderX, int borderY) {
if (imgA.getWidth() != imgB.getWidth())
throw new RuntimeException("Widths are not equals");
if (imgA.getHeight() != imgB.getHeight())
throw new RuntimeException("Heights are not equals");
GImageGray a = FactoryGImageGray.wrap(imgA);
GImageGray b = FactoryGImageGray.wrap(imgB);
for (int y = 0; y < imgA.getHeight(); y++) {
for (int x = 0; x < borderX; x++) {
compareValues(tol, a, b, x, y);
}
for (int x = imgA.getWidth() - borderX; x < imgA.getWidth(); x++) {
compareValues(tol, a, b, x, y);
}
}
for (int x = borderX; x < imgA.getWidth() - borderX; x++) {
for (int y = 0; y < borderY; y++) {
compareValues(tol, a, b, x, y);
}
for (int y = imgA.getHeight() - borderY; y < imgA.getHeight(); y++) {
compareValues(tol, a, b, x, y);
}
}
}
private static void compareValues(double tol, GImageGray a, GImageGray b, int x, int y) {
double normalizer = Math.abs(a.get(x, y).doubleValue()) + Math.abs(b.get(x, y).doubleValue());
if (normalizer < 1.0) normalizer = 1.0;
if (Math.abs(a.get(x, y).doubleValue() - b.get(x, y).doubleValue()) / normalizer > tol)
throw new RuntimeException("values not equal at (" + x + " " + y + ") " + a.get(x, y) + " " + b.get(x, y));
}
public static void checkBorderZero(ImageGray outputImage, int border) {
GImageGray img = FactoryGImageGray.wrap(outputImage);
for (int y = 0; y < img.getHeight(); y++) {
if (y >= border && y < img.getHeight() - border)
continue;
for (int x = 0; x < img.getWidth(); x++) {
if (x >= border && x < img.getWidth() - border)
continue;
if (img.get(x, y).intValue() != 0)
throw new RuntimeException("The border is not zero: "+x+" "+y);
}
}
}
public static void checkBorderZero(ImageGray outputImage, int borderX0 , int borderY0 , int borderX1 , int borderY1 ) {
GImageGray img = FactoryGImageGray.wrap(outputImage);
for (int y = 0; y < img.getHeight(); y++) {
if (y >= borderY0 && y < img.getHeight() - borderY1)
continue;
for (int x = 0; x < img.getWidth(); x++) {
if (x >= borderX0 && x < img.getWidth() - borderX1)
continue;
if (img.get(x, y).intValue() != 0)
throw new RuntimeException("The border is not zero: "+x+" "+y);
}
}
}
public static void printDiff(ImageGray imgA, ImageGray imgB) {
GImageGray a = FactoryGImageGray.wrap(imgA);
GImageGray b = FactoryGImageGray.wrap(imgB);
System.out.println("------- Difference -----------");
for (int y = 0; y < imgA.getHeight(); y++) {
for (int x = 0; x < imgA.getWidth(); x++) {
double diff = Math.abs(a.get(x, y).doubleValue() - b.get(x, y).doubleValue());
System.out.printf("%2d ", (int) diff);
}
System.out.println();
}
}
public static void printDiffBinary(GrayU8 imgA, GrayU8 imgB) {
System.out.println("------- Difference -----------");
for (int y = 0; y < imgA.getHeight(); y++) {
for (int x = 0; x < imgA.getWidth(); x++) {
if( imgA.unsafe_get(x,y) != imgB.unsafe_get(x,y))
System.out.print(" x");
else
System.out.print(" .");
}
System.out.println();
}
}
}