package ini.trakem2.imaging.filters;
import ij.IJ;
import ij.gui.GenericDialog;
import ij.gui.YesNoCancelDialog;
import ini.trakem2.display.Display;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Layer;
import ini.trakem2.display.LayerSet;
import ini.trakem2.display.Patch;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
public class FilterEditor {
// They are all IFilter, and all have protected fields.
@SuppressWarnings("rawtypes")
static public final Class[] available =
new Class[]{CLAHE.class, NormalizeLocalContrast.class, EqualizeHistogram.class,
EnhanceContrast.class, ResetMinAndMax.class, DefaultMinAndMax.class,
GaussianBlur.class, Invert.class, Normalize.class,
RankFilter.class, RobustNormalizeLocalContrast.class,
ValueToNoise.class, SubtractBackground.class,
CorrectBackground.class,
LUTRed.class, LUTGreen.class, LUTBlue.class,
LUTMagenta.class, LUTCyan.class, LUTYellow.class,
LUTOrange.class, LUTCustom.class};
static private class TableAvailableFilters extends JTable {
public TableAvailableFilters(final TableChosenFilters tcf) {
setModel(new AbstractTableModel() {
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
return available[rowIndex].getSimpleName();
}
@Override
public int getRowCount() {
return available.length;
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public String getColumnName(final int col) {
return "Available Filters";
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent me) {
if (2 == me.getClickCount()) {
tcf.add(available[getSelectedRow()]);
}
}
});
}
}
static private class FilterWrapper {
IFilter filter;
final Field[] fields;
FilterWrapper(final Class<?> filterClass) {
this.fields = filterClass.getDeclaredFields();
try {
this.filter = (IFilter) filterClass.getConstructor().newInstance();
} catch (final Exception e) {
e.printStackTrace();
}
}
/** Makes a copy of {@code filter}. */
FilterWrapper(final IFilter filter) {
this.fields = filter.getClass().getDeclaredFields();
try {
// Create a copy and set its parameters to the same values
this.filter = (IFilter) filter.getClass().getConstructor().newInstance();
for (final Field f : fields) {
f.set(this.filter, f.get(filter));
}
} catch (final Exception e) {
e.printStackTrace();
}
}
FilterWrapper() {
this.fields = new Field[0]; // empty
}
String name(final int i) {
return fields[i].getName();
}
String value(final int i) {
try {
return "" + fields[i].get(filter);
} catch (final Exception e) {
e.printStackTrace();
}
return null;
}
void set(final int i, Object v) {
Utils.log2("v class: " + v.getClass());
final Field f = fields[i];
f.setAccessible(true);
final Class<?> c = f.getType();
try {
final String sv = v.toString().trim();
Utils.log2("sv is: " + sv + ", and f.getDeclaringClass() == " + c);
if (Double.TYPE == c) v = Double.parseDouble(sv);
else if (Float.TYPE == c) v = Float.parseFloat(sv);
else if (Integer.TYPE == c) v = Integer.parseInt(sv);
else if (Boolean.TYPE == c) v = Boolean.parseBoolean(sv);
else if (Long.TYPE == c) v = Long.parseLong(sv);
else if (Short.TYPE == c) v = Short.parseShort(sv);
else if (Byte.TYPE == c) v = Byte.parseByte(sv);
else if (String.class == c) v = sv;
//
f.set(filter, v);
} catch (final Exception e) {
Utils.logAll("New value '" + v + "' is invalid; keeping the last value.");
e.printStackTrace();
}
}
public boolean sameParameterValues(final FilterWrapper w) {
if (this.filter == w.filter) return true;
if (filter.getClass() != w.filter.getClass()) return false;
for (int i=0; i<fields.length; ++i) {
if (!value(i).equals(w.value(i))) {
return false;
}
}
return true;
}
}
static private class TableChosenFilters extends JTable {
private final ArrayList<FilterWrapper> filters;
public TableChosenFilters(final ArrayList<FilterWrapper> filters) {
this.filters = filters;
setModel(new AbstractTableModel() {
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
switch (columnIndex) {
case 0: return rowIndex + 1;
case 1: return filters.get(rowIndex).filter.getClass().getSimpleName();
}
return null;
}
@Override
public int getRowCount() {
return filters.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(final int col) {
switch (col) {
case 0: return "";
case 1: return "Chosen Filters";
}
return null;
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent me) {
if (me.getClickCount() == 2) {
int row = getSelectedRow();
filters.remove(row);
((AbstractTableModel)getModel()).fireTableStructureChanged();
if (filters.size() > 0) {
if (row > 0) --row;
getSelectionModel().setSelectionInterval(row, row);
}
getColumnModel().getColumn(0).setMaxWidth(10);
}
}
});
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent ke) {
// Check preconditions
final int row = getSelectedRow();
if (-1 == row) return;
int selRow = -1;
//
switch (ke.getKeyCode()) {
case KeyEvent.VK_PAGE_UP:
if (filters.size() > 1 && row > 0) {
filters.add(row -1, filters.remove(row));
selRow = row -1;
ke.consume();
}
break;
case KeyEvent.VK_PAGE_DOWN:
if (filters.size() > 1 && row < filters.size() -1) {
filters.add(row + 1, filters.remove(row));
selRow = row + 1;
ke.consume();
}
break;
case KeyEvent.VK_DELETE:
if (filters.size() > 1) {
if (0 == row) selRow = 0;
else if (filters.size() -1 == row) selRow = filters.size() -2;
else selRow = row -1;
}
filters.remove(row);
ke.consume();
break;
case KeyEvent.VK_UP:
selRow = row > 0 ? row -1 : row;
ke.consume();
break;
case KeyEvent.VK_DOWN:
selRow = row < filters.size() -1 ? row + 1 : row;
ke.consume();
break;
}
((AbstractTableModel)getModel()).fireTableStructureChanged();
getColumnModel().getColumn(0).setMaxWidth(10);
if (-1 != selRow) {
getSelectionModel().setSelectionInterval(selRow, selRow);
}
}
});
getColumnModel().getColumn(0).setMaxWidth(10);
}
final FilterWrapper selected() {
final int row = getSelectedRow();
if (-1 == row) return new FilterWrapper(); // empty
return filters.get(row);
}
final void add(final Class<?> filterClass) {
filters.add(new FilterWrapper(filterClass));
((AbstractTableModel)getModel()).fireTableStructureChanged();
this.getSelectionModel().setSelectionInterval(filters.size()-1, filters.size()-1);
}
final void setupListener(final TableParameters tp) {
getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent e) {
tp.triggerUpdate();
}
});
}
}
static private class TableParameters extends JTable {
TableParameters(final TableChosenFilters tcf) {
setModel(new AbstractTableModel() {
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
final FilterWrapper w = tcf.selected();
switch (columnIndex) {
case 0:
return w.name(rowIndex);
case 1:
return w.value(rowIndex);
}
return null;
}
@Override
public int getRowCount() {
return tcf.selected().fields.length;
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(final int col) {
switch (col) {
case 0: return "Parameter";
case 1: return "Value";
default:
return null;
}
}
@Override
public void setValueAt(final Object v, final int rowIndex, final int columnIndex) {
tcf.selected().set(rowIndex, v);
}
@Override
public boolean isCellEditable(final int rowIndex, final int columnIndex) {
return 1 == columnIndex;
}
});
}
void triggerUpdate() {
((AbstractTableModel)getModel()).fireTableStructureChanged();
}
}
static public final void GUI(final Collection<Patch> patches, final Patch reference) {
final ArrayList<FilterWrapper> filters = new ArrayList<FilterWrapper>();
// Find out if all images have the exact same filters
final StringBuilder sb = new StringBuilder();
final Patch ref = (null == reference? patches.iterator().next() : reference);
final IFilter[] refFilters = ref.getFilters();
if (null != refFilters) {
for (final IFilter f : refFilters) filters.add(new FilterWrapper(f)); // makes a copy of the IFilter
}
//
for (final Patch p : patches) {
if (ref == p) continue;
final IFilter[] fs = p.getFilters();
if (null == fs && null == refFilters) continue; // ok
if ((null != refFilters && null == fs)
|| (null == refFilters && null != fs)
|| (null != refFilters && null != fs && fs.length != refFilters.length)) {
sb.append("WARNING: patch #" + p.getId() + " has a different number of filters than reference patch #" + ref.getId());
continue;
}
// Compare each
for (int i=0; i<refFilters.length; ++i) {
if (fs[i].getClass() != refFilters[i].getClass()) {
sb.append("WARNING: patch #" + p.getId() + " has a different filters than reference patch #" + ref.getId());
break;
}
// Does the filter have the same parameters?
if (!filters.get(i).sameParameterValues(new FilterWrapper(fs[i]))) {
sb.append("WARNING: patch #" + p.getId() + " has filter '" + fs[i].getClass().getSimpleName() + "' with different parameters than the reference patch #" + ref.getId());
}
}
}
if (sb.length() > 0) {
final GenericDialog gd = new GenericDialog("WARNING", null == Display.getFront() ? IJ.getInstance() : Display.getFront().getFrame());
gd.addMessage("Filters are not all the same for all images:");
gd.addTextAreas(sb.toString(), null, 20, 30);
final String[] s = new String[]{"Use the filters of the reference image", "Start from an empty list of filters"};
gd.addChoice("Do:", s, s[0]);
gd.showDialog();
if (gd.wasCanceled()) return;
if (1 == gd.getNextChoiceIndex()) filters.clear();
}
final TableChosenFilters tcf = new TableChosenFilters(filters);
final TableParameters tp = new TableParameters(tcf);
tcf.setupListener(tp);
final TableAvailableFilters taf = new TableAvailableFilters(tcf);
if (filters.size() > 0) {
tcf.getSelectionModel().setSelectionInterval(0, 0);
}
final JFrame frame = new JFrame("Image filters");
final JButton set = new JButton("Set");
final JComboBox pulldown = new JComboBox(new String[]{"Selected images (" + patches.size() + ")", "All images in layer " + (ref.getLayer().getParent().indexOf(ref.getLayer()) + 1), "All images in layer range..."});
final Component[] cs = new Component[]{set, pulldown, tcf, tp, taf};
set.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (check(frame, filters)) {
final ArrayList<Patch> ps = new ArrayList<Patch>();
switch (pulldown.getSelectedIndex()) {
case 0:
ps.addAll(patches);
break;
case 1:
for (final Displayable d : ref.getLayer().getDisplayables(Patch.class)) {
ps.add((Patch)d);
}
break;
case 2:
final GenericDialog gd = new GenericDialog("Apply filters");
Utils.addLayerRangeChoices(ref.getLayer(), gd);
gd.addStringField("Image title matches:", "", 30);
gd.addCheckbox("Visible images only", true);
gd.showDialog();
if (gd.wasCanceled()) return;
final String regex = gd.getNextString();
final boolean visible_only = gd.getNextBoolean();
Pattern pattern = null;
if (0 != regex.length()) {
pattern = Pattern.compile(regex);
}
for (final Layer l : ref.getLayer().getParent().getLayers(gd.getNextChoiceIndex(), gd.getNextChoiceIndex())) {
for (final Displayable d : l.getDisplayables(Patch.class, visible_only)) {
if (null != pattern && !pattern.matcher(d.getTitle()).matches()) {
continue;
}
ps.add((Patch)d);
}
}
}
apply(ps, filters, cs, false);
}
}
});
final JPanel buttons = new JPanel();
final JLabel label = new JLabel("Push F1 for help");
final GridBagConstraints c2 = new GridBagConstraints();
final GridBagLayout gb2 = new GridBagLayout();
buttons.setLayout(gb2);
c2.anchor = GridBagConstraints.WEST;
c2.gridx = 0;
gb2.setConstraints(label, c2);
buttons.add(label);
c2.gridx = 1;
c2.weightx = 1;
final JPanel empty = new JPanel();
gb2.setConstraints(empty, c2);
buttons.add(empty);
c2.gridx = 2;
c2.weightx = 0;
c2.anchor = GridBagConstraints.EAST;
final JLabel a = new JLabel("Apply to:");
gb2.setConstraints(a, c2);
buttons.add(a);
c2.gridx = 3;
c2.insets = new Insets(4, 10, 4, 0);
gb2.setConstraints(pulldown, c2);
buttons.add(pulldown);
c2.gridx = 4;
gb2.setConstraints(set, c2);
buttons.add(set);
//
taf.setPreferredSize(new Dimension(350, 500));
tcf.setPreferredSize(new Dimension(350, 250));
tp.setPreferredSize(new Dimension(350, 250));
//
final GridBagLayout gb = new GridBagLayout();
final GridBagConstraints c = new GridBagConstraints();
final JPanel all = new JPanel();
all.setBackground(Color.white);
all.setPreferredSize(new Dimension(700, 500));
all.setLayout(gb);
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.NORTHWEST;
c.fill = GridBagConstraints.BOTH;
c.gridheight = 2;
c.weightx = 0.5;
final JScrollPane p1 = new JScrollPane(taf);
p1.setPreferredSize(taf.getPreferredSize());
gb.setConstraints(p1, c);
all.add(p1);
c.gridx = 1;
c.gridy = 0;
c.gridheight = 1;
c.weighty = 0.7;
final JScrollPane p2 = new JScrollPane(tcf);
p2.setPreferredSize(tcf.getPreferredSize());
gb.setConstraints(p2, c);
all.add(p2);
c.gridx = 1;
c.gridy = 1;
c.weighty = 0.3;
final JScrollPane p3 = new JScrollPane(tp);
p3.setPreferredSize(tp.getPreferredSize());
gb.setConstraints(p3, c);
all.add(p3);
c.gridx = 0;
c.gridy = 2;
c.gridwidth = 2;
c.weightx = 1;
c.weighty = 0;
gb.setConstraints(buttons, c);
all.add(buttons);
final KeyAdapter help = new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent ke) {
if (ke.getKeyCode() == KeyEvent.VK_F1) {
final GenericDialog gd = new GenericDialog("Help :: image filters");
gd.addMessage(
"In the table 'Available Filters':\n"
+ " - double-click a filter to add it to the table of 'Chosen Filters'\n \n"
+ "In the table 'Chosen Filters':\n"
+ " - double-click a filter to remove it.\n"
+ " - PAGE UP/DOWN keys move the filter up/down in the list.\n"
+ " - 'Delete' key removes the selected filter.\n \n"
+ "In the table of parameter names and values:\n"
+ " - double-click a value to edit it. Then push enter to set the new value.\n \n"
+ "What you need to know:\n"
+ " - filters are set and applied in order, so order matters.\n"
+ " - filters with parameters for which you entered a value of zero\nwill result in a warning message.\n"
+ " - when applying the filters, if you choose 'Selected images', these are the images\nthat were selected when the filter editor was opened.\n"
+ " - when applying the filters, if you want to filter for a regular expression pattern\nin the image name, use the 'All images in layer range...' option,\nwhere a range of one single layer is also possible."
);
gd.hideCancelButton();
gd.setModal(false);
gd.showDialog();
}
}
};
taf.addKeyListener(help);
tcf.addKeyListener(help);
tp.addKeyListener(help);
all.addKeyListener(help);
buttons.addKeyListener(help);
empty.addKeyListener(help);
a.addKeyListener(help);
frame.getContentPane().add(all);
frame.pack();
frame.setVisible(true);
}
private static boolean check(final JFrame frame, final List<FilterWrapper> wrappers) {
if (!wrappers.isEmpty()) {
final String s = sanityCheck(wrappers);
return 0 == s.length()
|| new YesNoCancelDialog(frame, "WARNING", s + "\nContinue?").yesPressed();
}
return true;
}
private static String sanityCheck(final List<FilterWrapper> wrappers) {
// Check all variables, find any numeric ones whose value is zero
// Check for duplicated filters
final HashSet<Class<?>> unique = new HashSet<Class<?>>();
for (final FilterWrapper w : wrappers) {
unique.add(w.filter.getClass());
}
final StringBuilder sb = new StringBuilder();
if (wrappers.size() != unique.size()) {
sb.append("WARNING: there are repeated filters!\n");
}
for (final FilterWrapper w : wrappers) {
for (final Field f : w.fields) {
try {
final String zero = f.get(w.filter).toString();
if ("0".equals(zero) || "0.0".equals(zero)) {
sb.append("WARNING: parameter '" + f.getName() + "' of filter '" + w.filter.getClass().getSimpleName() + "' is zero!\n");
}
} catch (final Exception e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
private static IFilter[] asFilters(final List<FilterWrapper> wrappers) {
if (wrappers.isEmpty()) return null;
final IFilter[] fs = new IFilter[wrappers.size()];
int next = 0;
for (final FilterWrapper fw : wrappers) {
fs[next++] = new FilterWrapper(fw.filter).filter; // a copy
}
return fs;
}
private static void apply(final Collection<Patch> patches, final List<FilterWrapper> wrappers, final Component[] cs, final boolean append) {
Bureaucrat.createAndStart(new Worker.Task("Set filters") {
@Override
public void exec() {
try {
for (final Component c : cs) c.setEnabled(false);
// Undo step
final LayerSet ls = patches.iterator().next().getLayerSet();
ls.addDataEditStep(new HashSet<Displayable>(patches));
//
final ArrayList<Future<?>> fus = new ArrayList<Future<?>>();
for (final Patch p : patches) {
final IFilter[] fs = asFilters(wrappers); // each Patch gets a copy
if (append) p.appendFilters(fs);
else p.setFilters(fs);
p.getProject().getLoader().decacheImagePlus(p.getId());
fus.add(p.updateMipMaps());
}
Utils.wait(fus);
// Current state
ls.addDataEditStep(new HashSet<Displayable>(patches));
} finally {
for (final Component c : cs) c.setEnabled(true);
}
}
}, patches.iterator().next().getProject());
}
static public final IFilter[] duplicate(final IFilter[] fs) {
if (null == fs) return fs;
final IFilter[] copy = new IFilter[fs.length];
for (int i=0; i<fs.length; ++i) copy[i] = new FilterWrapper(fs[i]).filter;
return copy;
}
}