package org.esa.snap.rcp.imgfilter.model;
import com.bc.ceres.core.Assert;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static java.lang.Math.*;
/**
* @author Norman
*/
public class Filter {
public enum Operation {
CONVOLVE,
MIN,
MAX,
MEAN,
MEDIAN,
STDDEV,
ERODE,
DILATE,
OPEN,
CLOSE
}
String name;
String shorthand;
Operation operation;
boolean editable;
HashSet<String> tags;
double[] kernelElements;
int kernelWidth;
int kernelHeight;
double kernelQuotient;
int kernelOffsetX;
int kernelOffsetY;
transient List<Listener> listeners;
private static int id = 1;
public static Filter create(int kernelSize, String... tags) {
return create(Operation.CONVOLVE, kernelSize, tags);
}
public static Filter create(Operation operation, int kernelSize, String... tags) {
int id = Filter.id++;
String name = System.getProperty("user.name") + id;
String shorthand = "my" + id;
return new Filter(name, shorthand, operation, kernelSize, kernelSize, null, 1.0, tags);
}
public Filter(String name, String shorthand, int kernelWidth, int kernelHeight, double[] kernelElements, double kernelQuotient, String... tags) {
this(name, shorthand, Operation.CONVOLVE, kernelWidth, kernelHeight, kernelElements, kernelQuotient, tags);
}
public Filter(String name, String shorthand, Operation operation, int kernelWidth, int kernelHeight, String... tags) {
this(name, shorthand, operation, kernelWidth, kernelHeight, null, 1.0, tags);
}
public Filter(String name, String shorthand, Operation operation, int kernelWidth, int kernelHeight, double[] kernelElements, double kernelQuotient, String... tags) {
Assert.notNull(name, "name");
Assert.notNull(shorthand, "shorthand");
Assert.notNull(operation, "operation");
Assert.argument(kernelWidth > 0, "kernelWidth");
Assert.argument(kernelHeight > 0, "kernelHeight");
Assert.argument(Math.abs(kernelQuotient) > 0.0, "kernelQuotient");
this.name = name;
this.shorthand = shorthand;
this.operation = operation;
this.tags = new HashSet<>();
this.tags.addAll(Arrays.asList(tags));
this.kernelWidth = kernelWidth;
this.kernelHeight = kernelHeight;
this.kernelQuotient = kernelQuotient;
this.kernelOffsetX = kernelWidth / 2;
this.kernelOffsetY = kernelHeight / 2;
if (kernelElements != null) {
Assert.argument(kernelElements.length == kernelWidth * kernelHeight, "kernelElements");
this.kernelElements = kernelElements.clone();
} else {
this.kernelElements = new double[kernelWidth * kernelHeight];
if (operation == Operation.CONVOLVE) {
this.kernelElements[kernelOffsetY * kernelWidth + kernelOffsetX] = 1.0;
} else {
Arrays.fill(this.kernelElements, 1.0);
}
}
this.listeners = new ArrayList<>();
}
public String getName() {
return name;
}
public void setName(String name) {
if (!name.equals(this.name)) {
this.name = name;
fireChange("name");
}
}
public String getShorthand() {
return shorthand;
}
public void setShorthand(String shorthand) {
if (!shorthand.equals(this.shorthand)) {
this.shorthand = shorthand;
fireChange("shorthand");
}
}
public Operation getOperation() {
return operation;
}
public void setOperation(Operation operation) {
if (operation != this.operation) {
this.operation = operation;
fireChange("operation");
}
}
public boolean isEditable() {
return editable;
}
public void setEditable(boolean editable) {
if (editable != this.editable) {
this.editable = editable;
fireChange("editable");
}
}
public Set<String> getTags() {
return tags;
}
public void setTags(String... tags) {
setTags(new HashSet<>(Arrays.asList(tags)));
}
private void setTags(HashSet<String> tags) {
if (!tags.equals(this.tags)) {
this.tags = tags;
fireChange("tags");
}
}
public double getKernelElement(int i, int j) {
return getKernelElement(j * kernelWidth + i);
}
public double getKernelElement(int index) {
return kernelElements[index];
}
public void setKernelElement(int i, int j, double value) {
setKernelElement(j * kernelWidth + i, value);
}
public void setKernelElement(int index, double value) {
if (value != kernelElements[index]) {
kernelElements[index] = value;
fireChange("kernelElements");
}
}
public double[] getKernelElements() {
return kernelElements;
}
public void setKernelElements(double[] kernelElements) {
if (!Arrays.equals(kernelElements, this.kernelElements)) {
if (kernelElements.length != kernelWidth * kernelHeight) {
throw new IllegalArgumentException("kernelElements.length != kernelWidth * kernelHeight");
}
this.kernelElements = kernelElements;
fireChange("kernelElements");
}
}
public int getKernelWidth() {
return kernelWidth;
}
public int getKernelHeight() {
return kernelHeight;
}
public void setKernelSize(int width, int height) {
if (width != this.kernelWidth || height != this.kernelHeight) {
int oldWidth = this.kernelWidth;
int oldHeight = this.kernelHeight;
double[] oldElements = kernelElements;
double[] elements = new double[width * height];
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
int oldI = (i * oldWidth) / width;
int oldJ = (j * oldHeight) / height;
int oldIndex = oldJ * oldWidth + oldI;
int newIndex = j * width + i;
elements[newIndex] = oldElements[oldIndex];
}
}
this.kernelWidth = width;
this.kernelHeight = height;
this.kernelOffsetX = width / 2;
this.kernelOffsetY = height / 2;
this.kernelElements = elements;
fireChange("kernelSize");
}
}
public void fill(FillFunction fillFunction) {
int w = kernelWidth;
int h = kernelHeight;
double x0 = -0.5 * (w - 1);
double y0 = -0.5 * (h - 1);
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
double x = x0 + i;
double y = y0 + j;
double v = fillFunction.compute(x, y, w, h);
this.kernelElements[j * w + i] = v;
}
}
fireChange("kernelElements");
}
public void fillRectangle(double fillValue) {
Arrays.fill(kernelElements, fillValue);
fireChange("kernelElements");
}
public void fillEllipse(final double fillValue) {
fill(new FillFunction() {
@Override
public double compute(double x, double y, int w, int h) {
double a = 0.5 * w;
double b = 0.5 * h;
double r = (x / a) * (x / a) + (y / b) * (y / b);
return r < 1.0 ? fillValue : 0;
}
});
}
public void fillGaussian() {
fill(new FillFunction() {
@Override
public double compute(double x, double y, int w, int h) {
double sigX = 0.2 * w;
double sigY = 0.2 * h;
double r = (x / sigX) * (x / sigX) + (y / sigY) * (y / sigY);
double z = exp(-0.5 * r);
return floor((round(2 * (1 + min(w, h)) * z)));
}
});
}
public void fillLaplacian() {
fill(new FillFunction() {
@Override
public double compute(double x, double y, int w, int h) {
double sigX = 0.15 * w;
double sigY = 0.15 * h;
double r = (x / sigX) * (x / sigX) + (y / sigY) * (y / sigY);
double z = (r - 1.0) * exp(-0.5 * r);
return floor((round(2 * (1 + min(w, h)) * z)));
}
});
}
public void fillRandom() {
fill(new FillFunction() {
@Override
public double compute(double x, double y, int w, int h) {
double range = min(w, h);
return round(range * (2 * Math.random() - 1));
}
});
}
public double getKernelQuotient() {
return kernelQuotient;
}
public void setKernelQuotient(double kernelQuotient) {
if (kernelQuotient != this.kernelQuotient) {
this.kernelQuotient = kernelQuotient;
fireChange("kernelQuotient");
}
}
public void adjustKernelQuotient() {
if (operation == Operation.CONVOLVE) {
double sum = 0.0;
for (double v : kernelElements) {
sum += v;
}
setKernelQuotient(sum != 0 ? sum : 1.0);
} else {
setKernelQuotient(1.0);
}
}
public int getKernelOffsetX() {
return kernelOffsetX;
}
public int getKernelOffsetY() {
return kernelOffsetY;
}
public void setKernelOffset(int kernelOffsetX, int kernelOffsetY) {
if (kernelOffsetX != this.kernelOffsetX || kernelOffsetY != this.kernelOffsetY) {
this.kernelOffsetX = kernelOffsetX;
this.kernelOffsetY = kernelOffsetY;
fireChange("kernelOffset");
}
}
public String getKernelElementsAsText() {
return formatKernelElements(kernelElements, new Dimension(kernelWidth, kernelHeight), "\t");
}
public void setKernelElementsFromText(String text) {
Dimension dim = new Dimension();
double[] kernelElements = parseKernelElementsFromText(text, dim);
if (kernelElements != null) {
setKernelSize(dim.width, dim.height);
setKernelElements(kernelElements);
}
}
protected static double[] parseKernelElementsFromText(String text, Dimension dim) {
String[][] tokens = tokenizeKernelElements(text);
if (tokens == null) {
return null;
}
int height = tokens.length;
int width = tokens[0].length;
double[] kernelElements = new double[width * height];
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
try {
kernelElements[j * width + i] = Double.parseDouble(tokens[j][i]);
} catch (NumberFormatException e) {
return null;
}
}
}
if (dim != null) {
dim.width = width;
dim.height = height;
}
return kernelElements;
}
public static String[][] tokenizeKernelElements(String text) {
String[] lines = text.split("\n");
int height = lines.length;
int width = -1;
String[][] strings = new String[height][];
for (int j = 0; j < height; j++) {
String line = lines[j];
String[] cells = line.split(",|;|\\s+");
if (width == -1) {
width = cells.length;
} else if (width != cells.length) {
return null;
}
strings[j] = cells;
}
return strings;
}
public static String formatKernelElements(double[] kernelElements, Dimension dim, String sep) {
StringBuilder text = new StringBuilder();
for (int j = 0; j < dim.height; j++) {
for (int i = 0; i < dim.width; i++) {
if (i != 0) {
text.append(sep);
}
double v = kernelElements[j * dim.width + i];
if (v == (int) v) {
text.append((int) v);
} else {
text.append(v);
}
}
if (j < dim.height - 1) {
text.append('\n');
}
}
return text.toString();
}
public static XStream createXStream() {
final XStream xStream = new XStream();
xStream.setClassLoader(Filter.class.getClassLoader());
xStream.alias("filter", Filter.class);
xStream.registerLocalConverter(Filter.class, "kernelElements", new SingleValueConverter() {
@Override
public String toString(Object o) {
double[] o1 = (double[]) o;
// todo - find out how to obtain width, height
return Filter.formatKernelElements(o1, new Dimension(o1.length, 1), ",");
}
@Override
public Object fromString(String s) {
return Filter.parseKernelElementsFromText(s, null);
}
@Override
public boolean canConvert(Class aClass) {
return aClass.equals(double[].class);
}
});
return xStream;
}
@SuppressWarnings("UnusedDeclaration")
private Object readResolve() {
if (tags == null) {
tags = new HashSet<>();
}
if (listeners == null) {
listeners = new ArrayList<>();
}
if (kernelQuotient == 0.0) {
kernelQuotient = 1.0;
}
return this;
}
@Override
public String toString() {
return name;
}
public static boolean isKernelDataText(String text) {
String[][] strings = tokenizeKernelElements(text);
return strings != null;
}
public void fireChange(String propertyName) {
Listener[] listeners = this.listeners.toArray(new Listener[this.listeners.size()]);
for (Listener listener : listeners) {
listener.filterChanged(this, propertyName);
}
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public interface Listener {
void filterChanged(Filter filter, String propertyName);
}
interface FillFunction {
double compute(double x, double y, int w, int h);
}
}