Difference between revisions of "Example Color Histogram Lookup"
From BoofCV
Jump to navigationJump to searchm |
m |
||
(5 intermediate revisions by the same user not shown) | |||
Line 5: | Line 5: | ||
</center> | </center> | ||
Image color histograms can be treated as features. | Image color histograms can be treated as features. These features can then be used to look up similar images. The example below shows how different color spaces can be used to look up similar images from a data set of vacation photos. The animated image above show the results from a query. | ||
Example Code: | Example Code: | ||
* [https://github.com/lessthanoptimal/BoofCV/blob/v0. | * [https://github.com/lessthanoptimal/BoofCV/blob/v0.40/examples/src/main/java/boofcv/examples/recognition/ExampleColorHistogramLookup.java ExampleColorHistogramLookup.java ] | ||
Concepts: | Concepts: | ||
Line 23: | Line 23: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
/** | /** | ||
* Demonstration of how to find similar images using color histograms. | * Demonstration of how to find similar images using color histograms. Image color histograms here are treated as | ||
* features and extracted using a more flexible algorithm than when they are used for image processing. | * features and extracted using a more flexible algorithm than when they are used for image processing. It's | ||
* more flexible in that the bin size can be varied and n-dimensions are supported. | * more flexible in that the bin size can be varied and n-dimensions are supported. | ||
* | * | ||
* In this example, histograms for about 150 images are generated. | * In this example, histograms for about 150 images are generated. A target image is selected and the 10 most | ||
* similar images, according to Euclidean distance of the histograms, are found. This illustrates several concepts; | * similar images, according to Euclidean distance of the histograms, are found. This illustrates several concepts; | ||
* 1) How to construct a histogram in 1D, 2D, 3D, ..etc, 2) Histograms are just feature descriptors. | * 1) How to construct a histogram in 1D, 2D, 3D, ..etc, 2) Histograms are just feature descriptors. | ||
* 3) Advantages of different color spaces. | * 3) Advantages of different color spaces. | ||
* | * | ||
* Euclidean distance is used here since that's what the nearest-neighbor search uses. | * Euclidean distance is used here since that's what the nearest-neighbor search uses. It's possible to compare | ||
* two histograms using any of the distance metrics in DescriptorDistance too. | * two histograms using any of the distance metrics in DescriptorDistance too. | ||
* | * | ||
Line 40: | Line 40: | ||
/** | /** | ||
* HSV stores color information in Hue and Saturation while intensity is in Value. | * HSV stores color information in Hue and Saturation while intensity is in Value. This computes a 2D histogram | ||
* from hue and saturation only, which makes it lighting independent. | * from hue and saturation only, which makes it lighting independent. | ||
*/ | */ | ||
public static List<double[]> coupledHueSat( List<String> images | public static List<double[]> coupledHueSat( List<String> images ) { | ||
List<double[]> points = new ArrayList<>(); | List<double[]> points = new ArrayList<>(); | ||
Planar<GrayF32> rgb = new Planar<>(GrayF32.class,1,1,3); | Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3); | ||
Planar<GrayF32> hsv = new Planar<>(GrayF32.class,1,1,3); | Planar<GrayF32> hsv = new Planar<>(GrayF32.class, 1, 1, 3); | ||
for( String path : images ) { | for (String path : images) { | ||
BufferedImage buffered = UtilImageIO. | BufferedImage buffered = UtilImageIO.loadImageNotNull(path); | ||
rgb.reshape(buffered.getWidth(), buffered.getHeight()); | rgb.reshape(buffered.getWidth(), buffered.getHeight()); | ||
Line 57: | Line 56: | ||
ConvertBufferedImage.convertFrom(buffered, rgb, true); | ConvertBufferedImage.convertFrom(buffered, rgb, true); | ||
ColorHsv. | ColorHsv.rgbToHsv(rgb, hsv); | ||
Planar<GrayF32> hs = hsv.partialSpectrum(0,1); | Planar<GrayF32> hs = hsv.partialSpectrum(0, 1); | ||
// The number of bins is an important parameter. | // The number of bins is an important parameter. Try adjusting it | ||
Histogram_F64 histogram = new Histogram_F64(12,12); | Histogram_F64 histogram = new Histogram_F64(12, 12); | ||
histogram.setRange(0, 0, 2.0*Math.PI); // range of hue is from 0 to 2PI | histogram.setRange(0, 0, 2.0*Math.PI); // range of hue is from 0 to 2PI | ||
histogram.setRange(1, 0, 1.0); // range of saturation is from 0 to 1 | histogram.setRange(1, 0, 1.0); // range of saturation is from 0 to 1 | ||
// Compute the histogram | // Compute the histogram | ||
GHistogramFeatureOps.histogram(hs,histogram); | GHistogramFeatureOps.histogram(hs, histogram); | ||
UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter | UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter | ||
points.add(histogram. | points.add(histogram.data); | ||
} | } | ||
Line 78: | Line 77: | ||
/** | /** | ||
* Computes two independent 1D histograms from hue and saturation. | * Computes two independent 1D histograms from hue and saturation. Less affects by sparsity, but can produce | ||
* worse results since the basic assumption that hue and saturation are decoupled is most of the time false. | * worse results since the basic assumption that hue and saturation are decoupled is most of the time false. | ||
*/ | */ | ||
public static List<double[]> independentHueSat( List<File> images | public static List<double[]> independentHueSat( List<File> images ) { | ||
List<double[]> points = new ArrayList<>(); | List<double[]> points = new ArrayList<>(); | ||
// The number of bins is an important parameter. | // The number of bins is an important parameter. Try adjusting it | ||
TupleDesc_F64 histogramHue = new TupleDesc_F64(30); | TupleDesc_F64 histogramHue = new TupleDesc_F64(30); | ||
TupleDesc_F64 histogramValue = new TupleDesc_F64(30); | TupleDesc_F64 histogramValue = new TupleDesc_F64(30); | ||
List<TupleDesc_F64> histogramList = new ArrayList<>(); | List<TupleDesc_F64> histogramList = new ArrayList<>(); | ||
histogramList.add(histogramHue); histogramList.add(histogramValue); | histogramList.add(histogramHue); | ||
histogramList.add(histogramValue); | |||
Planar<GrayF32> rgb = new Planar<>(GrayF32.class,1,1,3); | Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3); | ||
Planar<GrayF32> hsv = new Planar<>(GrayF32.class,1,1,3); | Planar<GrayF32> hsv = new Planar<>(GrayF32.class, 1, 1, 3); | ||
for( File f : images ) { | for (File f : images) { | ||
BufferedImage buffered = UtilImageIO. | BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath()); | ||
rgb.reshape(buffered.getWidth(), buffered.getHeight()); | rgb.reshape(buffered.getWidth(), buffered.getHeight()); | ||
hsv.reshape(buffered.getWidth(), buffered.getHeight()); | hsv.reshape(buffered.getWidth(), buffered.getHeight()); | ||
ConvertBufferedImage.convertFrom(buffered, rgb, true); | ConvertBufferedImage.convertFrom(buffered, rgb, true); | ||
ColorHsv. | ColorHsv.rgbToHsv(rgb, hsv); | ||
GHistogramFeatureOps.histogram(hsv.getBand(0), 0, 2*Math.PI,histogramHue); | GHistogramFeatureOps.histogram(hsv.getBand(0), 0, 2*Math.PI, histogramHue); | ||
GHistogramFeatureOps.histogram(hsv.getBand(1), 0, 1, histogramValue); | GHistogramFeatureOps.histogram(hsv.getBand(1), 0, 1, histogramValue); | ||
// need to combine them into a single descriptor for processing later on | // need to combine them into a single descriptor for processing later on | ||
TupleDesc_F64 imageHist = UtilFeature.combine(histogramList,null); | TupleDesc_F64 imageHist = UtilFeature.combine(histogramList, null); | ||
UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter | UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter | ||
points.add(imageHist. | points.add(imageHist.data); | ||
} | } | ||
Line 118: | Line 117: | ||
/** | /** | ||
* Constructs a 3D histogram using RGB. | * Constructs a 3D histogram using RGB. RGB is a popular color space, but the resulting histogram will | ||
* depend on lighting conditions and might not produce the accurate results. | * depend on lighting conditions and might not produce the accurate results. | ||
*/ | */ | ||
Line 124: | Line 123: | ||
List<double[]> points = new ArrayList<>(); | List<double[]> points = new ArrayList<>(); | ||
Planar<GrayF32> rgb = new Planar<>(GrayF32.class,1,1,3); | Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3); | ||
for( File f : images ) { | for (File f : images) { | ||
BufferedImage buffered = UtilImageIO. | BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath()); | ||
rgb.reshape(buffered.getWidth(), buffered.getHeight()); | rgb.reshape(buffered.getWidth(), buffered.getHeight()); | ||
ConvertBufferedImage.convertFrom(buffered, rgb, true); | ConvertBufferedImage.convertFrom(buffered, rgb, true); | ||
// The number of bins is an important parameter. | // The number of bins is an important parameter. Try adjusting it | ||
Histogram_F64 histogram = new Histogram_F64(10,10,10); | Histogram_F64 histogram = new Histogram_F64(10, 10, 10); | ||
histogram.setRange(0, 0, 255); | histogram.setRange(0, 0, 255); | ||
histogram.setRange(1, 0, 255); | histogram.setRange(1, 0, 255); | ||
histogram.setRange(2, 0, 255); | histogram.setRange(2, 0, 255); | ||
GHistogramFeatureOps.histogram(rgb,histogram); | GHistogramFeatureOps.histogram(rgb, histogram); | ||
UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter | UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter | ||
points.add(histogram. | points.add(histogram.data); | ||
} | } | ||
Line 150: | Line 148: | ||
/** | /** | ||
* Computes a histogram from the gray scale intensity image alone. | * Computes a histogram from the gray scale intensity image alone. Probably the least effective at looking up | ||
* similar images. | * similar images. | ||
*/ | */ | ||
Line 156: | Line 154: | ||
List<double[]> points = new ArrayList<>(); | List<double[]> points = new ArrayList<>(); | ||
GrayU8 gray = new GrayU8(1,1); | GrayU8 gray = new GrayU8(1, 1); | ||
for( File f : images ) { | for (File f : images) { | ||
BufferedImage buffered = UtilImageIO. | BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath()); | ||
gray.reshape(buffered.getWidth(), buffered.getHeight()); | gray.reshape(buffered.getWidth(), buffered.getHeight()); | ||
Line 169: | Line 166: | ||
UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter | UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter | ||
points.add(imageHist. | points.add(imageHist.data); | ||
} | } | ||
Line 175: | Line 172: | ||
} | } | ||
public static void main(String[] args) { | public static void main( String[] args ) { | ||
String imagePath = UtilIO.pathExample("recognition/vacation"); | String imagePath = UtilIO.pathExample("recognition/vacation"); | ||
List<String> images = UtilIO.listByPrefix(imagePath,null,".jpg"); | List<String> images = UtilIO.listByPrefix(imagePath, null, ".jpg"); | ||
Collections.sort(images); | Collections.sort(images); | ||
Line 197: | Line 194: | ||
double[] targetPoint = points.get(target); | double[] targetPoint = points.get(target); | ||
// Use a generic NN search algorithm. | // Use a generic NN search algorithm. This uses Euclidean distance as a distance metric. | ||
NearestNeighbor<double[]> nn = FactoryNearestNeighbor.exhaustive(new KdTreeEuclideanSq_F64(targetPoint.length)); | NearestNeighbor<double[]> nn = FactoryNearestNeighbor.exhaustive(new KdTreeEuclideanSq_F64(targetPoint.length)); | ||
NearestNeighbor.Search<double[]> search = nn.createSearch(); | |||
DogArray<NnData<double[]>> results = new DogArray(NnData::new); | |||
nn.setPoints(points, true); | nn.setPoints(points, true); | ||
search.findNearest(targetPoint, -1, 10, results); | |||
ListDisplayPanel gui = new ListDisplayPanel(); | ListDisplayPanel gui = new ListDisplayPanel(); | ||
// Add the target which the other images are being matched against | // Add the target which the other images are being matched against | ||
gui.addImage(UtilImageIO. | gui.addImage(UtilImageIO.loadImageNotNull(images.get(target)), "Target", ScaleOptions.ALL); | ||
// The results will be the 10 best matches, but their order can be arbitrary. | // The results will be the 10 best matches, but their order can be arbitrary. For display purposes | ||
// it's better to do it from best fit to worst fit | // it's better to do it from best fit to worst fit | ||
Collections.sort(results.toList(), | Collections.sort(results.toList(), Comparator.comparingDouble(( NnData o ) -> o.distance)); | ||
// Add images to GUI -- first match is always the target image, so skip it | // Add images to GUI -- first match is always the target image, so skip it | ||
Line 228: | Line 219: | ||
} | } | ||
ShowImages.showWindow(gui,"Similar Images",true); | ShowImages.showWindow(gui, "Similar Images", true); | ||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 15:15, 17 January 2022
Image color histograms can be treated as features. These features can then be used to look up similar images. The example below shows how different color spaces can be used to look up similar images from a data set of vacation photos. The animated image above show the results from a query.
Example Code:
Concepts:
- Scene Classification
- Dense Image Features
- Clustering
- k-NN classifier
Related Examples:
Example Code
/**
* Demonstration of how to find similar images using color histograms. Image color histograms here are treated as
* features and extracted using a more flexible algorithm than when they are used for image processing. It's
* more flexible in that the bin size can be varied and n-dimensions are supported.
*
* In this example, histograms for about 150 images are generated. A target image is selected and the 10 most
* similar images, according to Euclidean distance of the histograms, are found. This illustrates several concepts;
* 1) How to construct a histogram in 1D, 2D, 3D, ..etc, 2) Histograms are just feature descriptors.
* 3) Advantages of different color spaces.
*
* Euclidean distance is used here since that's what the nearest-neighbor search uses. It's possible to compare
* two histograms using any of the distance metrics in DescriptorDistance too.
*
* @author Peter Abeles
*/
public class ExampleColorHistogramLookup {
/**
* HSV stores color information in Hue and Saturation while intensity is in Value. This computes a 2D histogram
* from hue and saturation only, which makes it lighting independent.
*/
public static List<double[]> coupledHueSat( List<String> images ) {
List<double[]> points = new ArrayList<>();
Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3);
Planar<GrayF32> hsv = new Planar<>(GrayF32.class, 1, 1, 3);
for (String path : images) {
BufferedImage buffered = UtilImageIO.loadImageNotNull(path);
rgb.reshape(buffered.getWidth(), buffered.getHeight());
hsv.reshape(buffered.getWidth(), buffered.getHeight());
ConvertBufferedImage.convertFrom(buffered, rgb, true);
ColorHsv.rgbToHsv(rgb, hsv);
Planar<GrayF32> hs = hsv.partialSpectrum(0, 1);
// The number of bins is an important parameter. Try adjusting it
Histogram_F64 histogram = new Histogram_F64(12, 12);
histogram.setRange(0, 0, 2.0*Math.PI); // range of hue is from 0 to 2PI
histogram.setRange(1, 0, 1.0); // range of saturation is from 0 to 1
// Compute the histogram
GHistogramFeatureOps.histogram(hs, histogram);
UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter
points.add(histogram.data);
}
return points;
}
/**
* Computes two independent 1D histograms from hue and saturation. Less affects by sparsity, but can produce
* worse results since the basic assumption that hue and saturation are decoupled is most of the time false.
*/
public static List<double[]> independentHueSat( List<File> images ) {
List<double[]> points = new ArrayList<>();
// The number of bins is an important parameter. Try adjusting it
TupleDesc_F64 histogramHue = new TupleDesc_F64(30);
TupleDesc_F64 histogramValue = new TupleDesc_F64(30);
List<TupleDesc_F64> histogramList = new ArrayList<>();
histogramList.add(histogramHue);
histogramList.add(histogramValue);
Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3);
Planar<GrayF32> hsv = new Planar<>(GrayF32.class, 1, 1, 3);
for (File f : images) {
BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath());
rgb.reshape(buffered.getWidth(), buffered.getHeight());
hsv.reshape(buffered.getWidth(), buffered.getHeight());
ConvertBufferedImage.convertFrom(buffered, rgb, true);
ColorHsv.rgbToHsv(rgb, hsv);
GHistogramFeatureOps.histogram(hsv.getBand(0), 0, 2*Math.PI, histogramHue);
GHistogramFeatureOps.histogram(hsv.getBand(1), 0, 1, histogramValue);
// need to combine them into a single descriptor for processing later on
TupleDesc_F64 imageHist = UtilFeature.combine(histogramList, null);
UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter
points.add(imageHist.data);
}
return points;
}
/**
* Constructs a 3D histogram using RGB. RGB is a popular color space, but the resulting histogram will
* depend on lighting conditions and might not produce the accurate results.
*/
public static List<double[]> coupledRGB( List<File> images ) {
List<double[]> points = new ArrayList<>();
Planar<GrayF32> rgb = new Planar<>(GrayF32.class, 1, 1, 3);
for (File f : images) {
BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath());
rgb.reshape(buffered.getWidth(), buffered.getHeight());
ConvertBufferedImage.convertFrom(buffered, rgb, true);
// The number of bins is an important parameter. Try adjusting it
Histogram_F64 histogram = new Histogram_F64(10, 10, 10);
histogram.setRange(0, 0, 255);
histogram.setRange(1, 0, 255);
histogram.setRange(2, 0, 255);
GHistogramFeatureOps.histogram(rgb, histogram);
UtilFeature.normalizeL2(histogram); // normalize so that image size doesn't matter
points.add(histogram.data);
}
return points;
}
/**
* Computes a histogram from the gray scale intensity image alone. Probably the least effective at looking up
* similar images.
*/
public static List<double[]> histogramGray( List<File> images ) {
List<double[]> points = new ArrayList<>();
GrayU8 gray = new GrayU8(1, 1);
for (File f : images) {
BufferedImage buffered = UtilImageIO.loadImageNotNull(f.getPath());
gray.reshape(buffered.getWidth(), buffered.getHeight());
ConvertBufferedImage.convertFrom(buffered, gray, true);
TupleDesc_F64 imageHist = new TupleDesc_F64(150);
HistogramFeatureOps.histogram(gray, 255, imageHist);
UtilFeature.normalizeL2(imageHist); // normalize so that image size doesn't matter
points.add(imageHist.data);
}
return points;
}
public static void main( String[] args ) {
String imagePath = UtilIO.pathExample("recognition/vacation");
List<String> images = UtilIO.listByPrefix(imagePath, null, ".jpg");
Collections.sort(images);
// Different color spaces you can try
List<double[]> points = coupledHueSat(images);
// List<double[]> points = independentHueSat(images);
// List<double[]> points = coupledRGB(images);
// List<double[]> points = histogramGray(images);
// A few suggested image you can try searching for
int target = 0;
// int target = 28;
// int target = 38;
// int target = 46;
// int target = 65;
// int target = 77;
double[] targetPoint = points.get(target);
// Use a generic NN search algorithm. This uses Euclidean distance as a distance metric.
NearestNeighbor<double[]> nn = FactoryNearestNeighbor.exhaustive(new KdTreeEuclideanSq_F64(targetPoint.length));
NearestNeighbor.Search<double[]> search = nn.createSearch();
DogArray<NnData<double[]>> results = new DogArray(NnData::new);
nn.setPoints(points, true);
search.findNearest(targetPoint, -1, 10, results);
ListDisplayPanel gui = new ListDisplayPanel();
// Add the target which the other images are being matched against
gui.addImage(UtilImageIO.loadImageNotNull(images.get(target)), "Target", ScaleOptions.ALL);
// The results will be the 10 best matches, but their order can be arbitrary. For display purposes
// it's better to do it from best fit to worst fit
Collections.sort(results.toList(), Comparator.comparingDouble(( NnData o ) -> o.distance));
// Add images to GUI -- first match is always the target image, so skip it
for (int i = 1; i < results.size; i++) {
String file = images.get(results.get(i).index);
double error = results.get(i).distance;
BufferedImage image = UtilImageIO.loadImage(file);
gui.addImage(image, String.format("Error %6.3f", error), ScaleOptions.ALL);
}
ShowImages.showWindow(gui, "Similar Images", true);
}
}