package ini.trakem2.display;
import ij.IJ;
import ij.measure.Measurements;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;
import ini.trakem2.imaging.ContrastPlot;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Future;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class ContrastAdjustmentMode extends GroupingMode {
private MinMaxData min_max = new MinMaxData();
protected void doPainterUpdate( final Rectangle r, final double m ) {
try {
MinMaxData md = min_max.clone();
final HashMap<Paintable, GroupingMode.ScreenPatchRange<?>> screenPatchRanges = this.screenPatchRanges; // keep a pointer to the current list
for ( final GroupingMode.ScreenPatchRange<?> spr : screenPatchRanges.values()) {
if (screenPatchRanges != this.screenPatchRanges) {
// List has been updated; restart painting
// TODO should it call itself: doPainterUpdate( r, m );
break;
}
((ScreenPatchRange)spr).update( md );
}
} catch (Exception e) {}
}
private class ContrastAdjustmentSource extends GroupingMode.GroupedGraphicsSource {
public void paintOnTop(final Graphics2D g, final Display display, final Rectangle srcRect, final double magnification) {
// do nothing
}
}
protected GroupingMode.GroupedGraphicsSource createGroupedGraphicSource() {
return new ContrastAdjustmentSource();
}
protected ScreenPatchRange createScreenPathRange(final PatchRange range, final Rectangle srcRect, final double magnification) {
return new ScreenPatchRange(range, srcRect, magnification);
}
private class ScreenPatchRange extends GroupingMode.ScreenPatchRange<MinMaxData> {
ScreenPatchRange( final PatchRange range, final Rectangle srcRect, final double magnification )
{
// The super constructor creates an appropriate alpha mask
super(range, srcRect, magnification);
super.transformedImage = makeImage(null, null);
}
@Override
protected BufferedImage makeImage( final ImageProcessor ignored, final FloatProcessor mask )
{
if (null == transformed) return null; // not yet ready
// Use the super.mask, which never changes. super.transformedMask is empty, don't use it!
return super.makeImage(transformed, super.mask);
}
@Override
public void update(MinMaxData m) {
// Transform min and max from slider values to image values
double[] mm = toImage(m.min, m.max);
transformed.reset();
transformed.setMinAndMax(mm[0], mm[1]);
super.transformedImage = makeImage(null, super.mask);
}
}
private final double[] toImage(double slider_min, double slider_max) {
double imin = initial.getMin();
double imax = initial.getMax();
double ratio = (imax-imin) / sliderRange;
return new double[]{imin + slider_min * ratio, slider_max * ratio};
}
/** Expected min,max in slider values, which may be considerably smaller than the proper image min and max. */
private final void updateLabelsAndPlot(double min, double max) {
double[] m = toImage(min, max);
minLabel.setText(Utils.cutNumber(m[0], 1));
maxLabel.setText(Utils.cutNumber(m[1], 1));
plot.update(m[0], m[1]);
}
static private class MinMaxData {
/** Min and max in slider values, not in image values. */
double min = 0,
max = 0;
public MinMaxData() {}
public MinMaxData(double min, double max) {
set(min, max);
}
synchronized public void set(double min, double max) {
this.min = min;
this.max = max;
}
synchronized public MinMaxData clone() {
return new MinMaxData(min ,max);
}
}
private ImageProcessor initial, transformed;
private final JFrame frame;
private final ContrastPlot plot;
private final JLabel minLabel, maxLabel;
private int sliderRange;
public ContrastAdjustmentMode(final Display display, final List<Displayable> selected) throws Exception {
super(display, selected);
// Check that all images are of the same type
int type = originalPatches.get(0).getType();
for (Patch p : originalPatches)
if (p.getType() != type)
throw new Exception("All images must be of the same type!\nFirst offending image: " + p);
// Create an ImageProcessor of the correct type (Short, Float, etc.)
ArrayList<Patch> patches = new ArrayList<Patch>(originalPatches);
Patch first = patches.get(0);
int pad = (int)(ScreenPatchRange.pad / magnification);
Rectangle box = new Rectangle(srcRect.x - pad, srcRect.y - pad, srcRect.width + 2*pad, srcRect.height + 2*pad);
initial = Patch.makeFlatImage(first.getType(), layer, box, magnification, patches, Color.black);
initial.resetMinAndMax();
transformed = initial.duplicate();
transformed.setMinAndMax(first.getMin(), first.getMax());
transformed.snapshot();
Utils.log2("transformed min, max: " + transformed.getMin() + ", " + transformed.getMax());
ImageStatistics stats = ImageStatistics.getStatistics(transformed, Measurements.AREA + Measurements.MEAN + Measurements.MODE + Measurements.MIN_MAX, layer.getParent().getCalibrationCopy());
Utils.log2("stats.min " + stats.min + ", stats.max " + stats.max);
// correct min and max, which for some reason can be wrong (the min can be zero, for example):
if (stats.min < initial.getMin()) stats.min = initial.getMin();
if (stats.max > initial.getMax()) stats.max = initial.getMax();
// stats is giving a minimum of zero even if its wrong
plot = new ContrastPlot(initial.getMin(), initial.getMax(), first.getMin(), first.getMax());
plot.setHistogram(stats, Color.black);
this.sliderRange = computeSliderRange();
// Create GUI
this.frame = new JFrame("Contrast adjustment");
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent we) {
display.getCanvas().cancelTransform();
}
});
final JPanel panel = new JPanel();
panel.setBackground(Color.white);
final GridBagLayout gb = new GridBagLayout();
final GridBagConstraints c = new GridBagConstraints();
panel.setLayout(gb);
// 1. Plot
c.gridx = 0;
c.gridy = 0;
c.fill = GridBagConstraints.NONE;
c.anchor = GridBagConstraints.CENTER;
c.insets = new Insets(10, 10, 0, 10);
gb.setConstraints(plot, c);
panel.add(plot);
// 2. min,max labels
final JPanel mm = new JPanel();
mm.setMinimumSize(new Dimension(plot.getWidth(), 15));
mm.setBackground(Color.white);
final Font monoFont = new Font("Monospaced", Font.PLAIN, 12);
GridBagLayout gbm = new GridBagLayout();
GridBagConstraints cm = new GridBagConstraints();
mm.setLayout(gbm);
minLabel = new JLabel(" ");
minLabel.setFont(monoFont);
minLabel.setBackground(Color.white);
maxLabel = new JLabel(" ");
maxLabel.setFont(monoFont);
maxLabel.setBackground(Color.white);
cm.gridx = 0;
cm.gridy = 0;
cm.anchor = GridBagConstraints.WEST;
gbm.setConstraints(minLabel, cm);
mm.add(minLabel);
cm.gridx = 1;
cm.anchor = GridBagConstraints.CENTER;
cm.fill = GridBagConstraints.HORIZONTAL;
cm.weightx = 1;
JPanel empty = new JPanel();
empty.setBackground(Color.white);
gbm.setConstraints(empty, cm);
mm.add(empty);
cm.weightx = 0;
cm.fill = GridBagConstraints.NONE;
cm.gridx = 2;
cm.anchor = GridBagConstraints.EAST;
gbm.setConstraints(maxLabel, cm);
mm.add(maxLabel);
gbm = null; // defensive programming
cm = null;
c.gridy = 1;
c.insets = new Insets(0, 10, 0, 10);
c.fill = GridBagConstraints.HORIZONTAL;
gb.setConstraints(mm, c);
panel.add(mm);
Utils.log2("first min, max " + first.getMin() + ", " + first.getMax());
// 3. Sliders
double ratio = sliderRange / (initial.getMax() - initial.getMin());
double firstMin = (first.getMin() - initial.getMin()) * ratio;
double firstMax = (first.getMax() - initial.getMin()) * ratio;
plot.update(first.getMin(), first.getMax());
int sliderMin = (int)firstMin;
int sliderMax = (int)firstMax;
// Prevent potential errors
if (sliderMin < 0) sliderMin = 0;
if (sliderMax > sliderRange -1) sliderMax = sliderRange -1;
if (sliderMin > sliderMax) sliderMin = sliderMax;
Utils.log2("After checking, slider min and max values are: " + sliderMin + ", " + sliderMax + " for range " + sliderRange);
final JSlider minslider = createSlider(panel, gb, c, "Minimum", monoFont, sliderRange, sliderMin);
final JSlider maxslider = createSlider(panel, gb, c, "Maximum", monoFont, sliderRange, sliderMax);
ChangeListener adl = new ChangeListener() {
public void stateChanged(ChangeEvent ce) {
double smin = minslider.getValue();
double smax = maxslider.getValue();
Utils.log2("smin, smax: " + smin + ", " + smax);
min_max.set(smin, smax);
updateLabelsAndPlot(smin, smax);
//doPainterUpdate(srcRect, magnification);
painter.update();
}
};
minslider.addChangeListener(adl);
maxslider.addChangeListener(adl);
// 4. Buttons
final JButton cancel = new JButton("Cancel");
final JButton apply = new JButton("Apply");
ActionListener actlis = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
Object source = ae.getSource();
if (cancel == source) {
display.getCanvas().cancelTransform();
} else if (apply == source) {
display.getCanvas().applyTransform();
}
}
};
cancel.addActionListener(actlis);
apply.addActionListener(actlis);
JPanel buttons = new JPanel();
buttons.setBackground(Color.white);
gbm = new GridBagLayout();
buttons.setLayout(gbm);
cm = new GridBagConstraints();
cm.gridx = 0;
cm.gridy = 0;
cm.weightx = 0;
cm.anchor = GridBagConstraints.WEST;
cm.fill = GridBagConstraints.NONE;
gbm.setConstraints(cancel, cm);
buttons.add(cancel);
JPanel space = new JPanel();
space.setBackground(Color.white);
cm.gridx = 1;
cm.weightx = 1;
cm.anchor = GridBagConstraints.CENTER;
cm.fill = GridBagConstraints.HORIZONTAL;
gbm.setConstraints(space, cm);
buttons.add(space);
cm.gridx = 2;
cm.weightx = 0;
cm.anchor = GridBagConstraints.EAST;
cm.fill = GridBagConstraints.NONE;
gbm.setConstraints(apply, cm);
buttons.add(apply);
gbm = null; // defensive programming
cm = null;
c.gridy += 1;
c.fill = GridBagConstraints.HORIZONTAL;
gb.setConstraints(buttons, c);
panel.add(buttons);
frame.getContentPane().add(panel);
Utils.invokeLater(new Runnable() { public void run() {
min_max.set(minslider.getValue(), maxslider.getValue());
frame.pack();
// after calling pack
Dimension dim = new Dimension(plot.getWidth(), 15);
minslider.setMinimumSize(dim);
maxslider.setMinimumSize(dim);
frame.pack(); // again
ij.gui.GUI.center(frame);
frame.setAlwaysOnTop(true);
frame.setVisible(true);
ContrastAdjustmentMode.super.initThreads();
}});
}
private int computeSliderRange() {
double defaultMin = initial.getMin();
double defaultMax = initial.getMax();
int valueRange = (int)(defaultMax - defaultMin);
int newSliderRange = valueRange;
if (newSliderRange>640 && newSliderRange<1280) {
newSliderRange /= 2;
} else if (newSliderRange>=1280) {
newSliderRange /= 5;
}
if (newSliderRange < 256) newSliderRange = 256;
if (newSliderRange > 1024) newSliderRange = 1024;
return newSliderRange;
}
private JSlider createSlider(JPanel panel, GridBagLayout gb, GridBagConstraints c, String title, Font font, int sliderRange, int start) {
Utils.log2("createSlider range: " + sliderRange + ", start: " + start);
JSlider s = new JSlider(JSlider.HORIZONTAL, 0, sliderRange, start);
s.setPaintLabels(false);
s.setPaintTicks(false);
s.setBackground(Color.white);
c.gridy++;
c.insets = new Insets(2, 10, 0, 10);
gb.setConstraints(s, c);
panel.add(s);
JLabel l = new JLabel(title);
l.setBackground(Color.white);
l.setFont(font);
c.gridy++;
c.insets = new Insets(0, 10, IJ.isMacOSX() ? 4 : 0, 0);
JPanel p = new JPanel();
p.setBackground(Color.white);
p.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
gb.setConstraints(p, c);
p.add(l);
panel.add(p);
return s;
}
public void mousePressed( MouseEvent me, int x_p, int y_p, double magnification ) {}
public void mouseDragged( MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old ) {}
public void mouseReleased( MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r ) {}
public boolean isDragging() { return false; }
private final void setUndoState() {
layer.getParent().addEditStep(new Displayable.DoEdits(new HashSet<Displayable>(originalPatches)).init(new String[]{"data"}));
}
public boolean apply() {
/* Set undo step to reflect initial state before any transformations */
setUndoState();
Bureaucrat.createAndStart( new Worker.Task( "Applying transformations" ) {
public void exec() {
// 1. Close dialog
frame.dispose();
// 2. Set min and max
final double[] m = toImage(min_max.min, min_max.max);
final Collection<Future<?>> fus = new ArrayList<Future<?>>();
// Submit all for regeneration
for (Patch p : originalPatches) {
p.setMinAndMax(m[0], m[1]);
fus.add(p.getProject().getLoader().regenerateMipMaps(p));
}
// Wait until all done
for (Future<?> fu : fus) {
try {
fu.get();
} catch (Throwable t) {
IJError.print(t);
}
}
// To reflect final state
setUndoState();
}
}, layer.getProject() );
super.quitThreads();
return true;
}
@Override
public boolean cancel() {
super.cancel();
frame.dispose();
return true;
}
}