/*
* This file is part of the JFeatureLib project: https://github.com/locked-fg/JFeatureLib
* JFeatureLib is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* JFeatureLib is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JFeatureLib; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* You are kindly asked to refer to the papers of the according authors which
* should be mentioned in the Javadocs of the respective classes as well as the
* JFeatureLib project itself.
*
* Hints how to cite the projects can be found at
* https://github.com/locked-fg/JFeatureLib/wiki/Citation
*/
package de.lmu.ifi.dbs.jfeaturelib.features;
import com.google.common.base.Preconditions;
import de.lmu.ifi.dbs.jfeaturelib.LibProperties;
import de.lmu.ifi.dbs.jfeaturelib.Progress;
import de.lmu.ifi.dbs.utilities.Arrays2;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.ImageProcessor;
import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.util.EnumSet;
/**
* Class that generates several types of histograms.
*
* This class replaces RGBHistogram and GrayHistogram in previous versions.
*
* @author graf
*/
public class Histogram extends AbstractFeatureDescriptor {
public static enum TYPE {
RGB, Red, Green, Blue, HSB, Hue, Saturation, Brightness, Gray
};
TYPE type;
int bins;
public Histogram() {
}
@Override
public void setProperties(LibProperties properties) {
type = TYPE.valueOf(properties.getString(LibProperties.HISTOGRAMS_TYPE));
bins = properties.getInteger(LibProperties.HISTOGRAMS_BINS);
}
/**
* Starts the histogram detection.
*
* @param ip ImageProcessor of the source image
*/
@Override
public void run(ImageProcessor ip) {
firePropertyChange(Progress.START);
InnerHistogram histogram = null;
if (type == TYPE.Gray) {
histogram = new Gray();
} else if (type == TYPE.RGB) {
histogram = new RGB();
} else if (type == TYPE.Red) {
histogram = new RgbMix(1, 0, 0);
} else if (type == TYPE.Green) {
histogram = new RgbMix(0, 1, 0);
} else if (type == TYPE.Blue) {
histogram = new RgbMix(0, 0, 1);
} else if (type == TYPE.HSB) {
histogram = new HSB();
} else if (type == TYPE.Hue || type == TYPE.Saturation || type == TYPE.Brightness) {
histogram = new HsbMix(type);
} else {
throw new IllegalStateException("no valid histogram type selected: " + type);
}
setMask(ip);
int[] hist = histogram.run(ip);
double[] features = Arrays2.convertToDouble(hist);
features = scale(features, bins);
addData(features);
firePropertyChange(Progress.END);
}
@Override
public EnumSet<Supports> supports() {
EnumSet set = EnumSet.of(
Supports.NoChanges,
Supports.DOES_8C,
Supports.DOES_8G,
Supports.DOES_RGB,
Supports.Masking);
return set;
}
/**
* Reduces the dimensions of src to a length of newLength.
*
* For example if the input array is 265 and newLength is 64, then the 256d are shifted/compressed down to the new
* 64. In example, dimension 1-4 will be added up in bin 1 of the target array
*
* @param src
* @param newLength
* @return new double array with newLength size
*/
double[] scale(double[] src, int newLength) {
if (src.length != newLength) {
double[] target = new double[newLength];
double factor = 1d * newLength / src.length;
// newLength: 64
// doubleArr: 256
// factor: 1/4
for (int srcI = 0; srcI < src.length; srcI++) {
int j = (int) Math.floor(srcI * factor);
target[j] += src[srcI];
}
src = target;
}
return src;
}
/**
* Copied from imageJ as ImageJ unfortunately uses static fields for the weights which causes race conditions in
* multi threaded environments.
*
* @see ColorProcessor#getHistogram(ij.process.ImageProcessor)
* @param ip
* @param rWeight
* @param gWeight
* @param bWeight
* @return 3*256 bin histogram
*/
private int[] getHistogram(ImageProcessor ip, double rWeight, double gWeight, double bWeight) {
ImageProcessor mask = getMask();
if (mask != null) {
return getHistogram(ip, mask, rWeight, gWeight, bWeight);
}
Rectangle roi = ip.getRoi();
int roiX = roi.x;
int roiY = roi.y;
int roiWidth = roi.width;
int roiHeight = roi.height;
int width = ip.getWidth();
int c, r, g, b, v;
int[] histogram = new int[256];
for (int y = roiY; y < (roiY + roiHeight); y++) {
int i = y * width + roiX;
for (int x = roiX; x < (roiX + roiWidth); x++) {
c = ip.get(i++);
// c = pixels[i++];
r = (c & 0xff0000) >> 16;
g = (c & 0xff00) >> 8;
b = c & 0xff;
v = (int) (r * rWeight + g * gWeight + b * bWeight + 0.5);
histogram[v]++;
}
}
return histogram;
}
/**
* Copied from imageJ as ImageJ unfortunately uses static fields for the weights which causes race conditions in
* multi threaded environments.
*
* @see ColorProcessor#getHistogram(ij.process.ImageProcessor)
* @param ip
* @param mask
* @return 3*256 bin histogram
*/
private int[] getHistogram(ImageProcessor ip, ImageProcessor mask,
double rWeight, double gWeight, double bWeight) {
Rectangle roi = ip.getRoi();
int roiX = roi.x;
int roiY = roi.y;
int roiWidth = roi.width;
int roiHeight = roi.height;
if (mask.getWidth() != roiWidth || mask.getHeight() != roiHeight) {
throw new IllegalArgumentException("Mask size != ROI size");
}
int width = ip.getWidth();
int c, r, g, b, v;
int[] histogram = new int[256];
for (int y = roiY, my = 0; y < (roiY + roiHeight); y++, my++) {
int i = y * width + roiX;
int mi = my * roiWidth;
for (int x = roiX; x < (roiX + roiWidth); x++) {
if(mask.get(mi++) != 0) {
c = ip.get(i);
// c = pixels[i];
r = (c & 0xff0000) >> 16;
g = (c & 0xff00) >> 8;
b = c & 0xff;
v = (int) (r * rWeight + g * gWeight + b * bWeight + 0.5);
histogram[v]++;
}
i++;
}
}
return histogram;
}
private interface InnerHistogram {
public int[] run(ImageProcessor ip);
}
private class Gray implements InnerHistogram {
@Override
public int[] run(ImageProcessor ip) {
if (!ByteProcessor.class.isAssignableFrom(ip.getClass())) {
ip = ip.convertToByte(true);
}
return ((ByteProcessor) ip).getHistogram();
}
}
private class RGB implements InnerHistogram {
@Override
public int[] run(ImageProcessor image) {
int[] features = new int[256 * 3];
System.arraycopy(getHistogram(image, 1, 0, 0), 0, features, 0, 256);
firePropertyChange(new Progress(33));
System.arraycopy(getHistogram(image, 0, 1, 0), 0, features, 256, 256);
firePropertyChange(new Progress(66));
System.arraycopy(getHistogram(image, 0, 0, 1), 0, features, 512, 256);
firePropertyChange(new Progress(99));
return features;
}
}
private class RgbMix implements InnerHistogram {
private final double r;
private final double g;
private final double b;
public RgbMix(double r, double g, double b) {
this.r = r;
this.g = g;
this.b = b;
}
@Override
public int[] run(ImageProcessor image) {
return getHistogram(image, r, g, b);
}
}
private class HSB implements InnerHistogram {
@Override
public int[] run(ImageProcessor ip) {
ColorProcessor cp;
if (ip instanceof ColorProcessor) {
cp = (ColorProcessor) ip;
} else {
cp = (ColorProcessor) ip.convertToRGB();
}
int width = ip.getWidth();
int height = ip.getHeight();
byte[] H = new byte[width * height];
byte[] S = new byte[width * height];
byte[] B = new byte[width * height];
cp.getHSB(H, S, B);
int[] features = new int[256 * 3];
ByteProcessor channel = new ByteProcessor(width, height, H, cp.getDefaultColorModel());
channel.setMask(ip.getMask());
System.arraycopy(channel.getHistogram(), 0, features, 0, 256);
firePropertyChange(new Progress(33));
channel.setPixels(S);
System.arraycopy(channel.getHistogram(), 0, features, 256, 256);
firePropertyChange(new Progress(66));
channel.setPixels(B);
System.arraycopy(channel.getHistogram(), 0, features, 512, 256);
firePropertyChange(new Progress(99));
return features;
}
}
private class HsbMix implements InnerHistogram {
private final TYPE type;
private HsbMix(TYPE type) {
this.type = type;
}
@Override
public int[] run(ImageProcessor ip) {
ColorProcessor cp;
if (ip instanceof ColorProcessor) {
cp = (ColorProcessor) ip;
} else {
cp = (ColorProcessor) ip.convertToRGB();
}
int width = ip.getWidth();
int height = ip.getHeight();
byte[] H = new byte[width * height];
byte[] S = new byte[width * height];
byte[] B = new byte[width * height];
cp.getHSB(H, S, B);
ByteProcessor channel;
ColorModel colorModel = cp.getDefaultColorModel();
if (type == TYPE.Hue) {
channel = new ByteProcessor(width, height, H, colorModel);
} else if (type == TYPE.Saturation) {
channel = new ByteProcessor(width, height, S, colorModel);
} else if (type == TYPE.Brightness) {
channel = new ByteProcessor(width, height, B, colorModel);
} else {
throw new IllegalArgumentException("type must be H,S or B");
}
return channel.getHistogram(ip.getMask());
}
}
@Override
public String getDescription() {
return "Histograms";
}
}