package ini.trakem2.imaging;
import ij.ImagePlus;
import ij.gui.GenericDialog;
import ij.measure.Calibration;
import ij.measure.Measurements;
import ij.plugin.ContrastEnhancer;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;
import ij.process.StackStatistics;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Layer;
import ini.trakem2.display.Patch;
import ini.trakem2.imaging.filters.EqualizeHistogram;
import ini.trakem2.imaging.filters.IFilter;
import ini.trakem2.parallel.Process;
import ini.trakem2.parallel.TaskFactory;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class ContrastEnhancerWrapper {
private final ContrastEnhancer ce = new ContrastEnhancer();
private Patch reference = null;
private ImageStatistics reference_stats = null;
private double saturated = 0.4;
private boolean normalize = true;
private boolean equalize = false;
private int stats_mode = 0; // Stack Histogram
private boolean use_full_stack = false;
private boolean from_existing_min_and_max = false;
private boolean visible_only = true;
final ExecutorService waiter = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "contrast-enhancer-waiter");
public ContrastEnhancerWrapper() {
this(null);
}
public ContrastEnhancerWrapper(final Patch reference) {
this.reference = reference;
if (null != reference) {
ImageProcessor ip = reference.getImageProcessor();
reference_stats = ImageStatistics.getStatistics(ip, Measurements.MIN_MAX, reference.getLayer().getParent().getCalibrationCopy());
}
}
private String[] getChoices() {
return null == reference ?
new String[]{"Stack Histogram", "Each image histogram"}
: new String[]{"Stack Histogram", "Each image histogram", "Active Patch Histogram"};
}
/**
* @param stats_mode can be 0==stack histogram, 1==each image's histogram, and 2==reference Patch histogram.
*
**/
public void set(double saturated, boolean normalize, boolean equalize, int stats_mode, boolean use_full_stack, boolean from_existing_min_and_max, boolean visible_only) throws Exception {
if (null == reference && 2 == stats_mode) throw new IllegalArgumentException("Need a non-null reference Patch to use 2==stats_mode !");
this.saturated = saturated;
set("saturated", saturated);
this.normalize = normalize;
set("normalize", normalize);
this.equalize = equalize;
set("equalize", equalize);
this.stats_mode = stats_mode;
this.use_full_stack = use_full_stack;
if (from_existing_min_and_max && null != reference) {
// recreate reference_stats
ImageProcessor ip = reference.getImageProcessor();
ip.setMinAndMax(reference.getMin(), reference.getMax());
reference_stats = ImageStatistics.getStatistics(ip, Measurements.MIN_MAX, reference.getLayer().getParent().getCalibrationCopy());
}
this.from_existing_min_and_max = from_existing_min_and_max;
this.visible_only = visible_only;
}
/** Uses the @param reference Patch as the one to extract the reference histogram from.
* Otherwise will use the stack histogram.
* @return false when canceled. */
public boolean showDialog() {
GenericDialog gd = new GenericDialog("Enhance Contrast");
gd.addNumericField("Saturated Pixels:", saturated, 1, 4, "%");
gd.addCheckbox("Normalize", normalize);
gd.addCheckbox("Equalize Histogram", equalize);
final String[] choices = getChoices();
gd.addChoice("Use:", choices, choices[stats_mode]);
gd.addCheckbox("Use full stack", use_full_stack);
gd.addCheckbox("From existing min and max", from_existing_min_and_max);
gd.addCheckbox("Visible images only", visible_only);
gd.showDialog();
if (gd.wasCanceled()) return false;
try {
set(gd.getNextNumber(), gd.getNextBoolean(), gd.getNextBoolean(),
gd.getNextChoiceIndex(), gd.getNextBoolean(), gd.getNextBoolean(),
gd.getNextBoolean());
} catch (Exception e) {
IJError.print(e);
return false;
}
return true;
}
private void set(String field, Object value) throws Exception {
Field f = ContrastEnhancer.class.getDeclaredField(field);
f.setAccessible(true);
f.set(ce, value);
}
public boolean applyLayerWise(final Collection<Layer> layers) {
boolean b = true;
for (final Layer layer : layers) {
if (Thread.currentThread().isInterrupted()) return false;
b = b && apply(layer.getDisplayables(Patch.class, visible_only));
// Wait until all mipmaps have regenerated:
try {
waiter.submit(new Runnable() { public void run() { /*buh!*/ } }).get();
} catch (Exception e) {
IJError.print(e);
return false;
}
}
return b;
}
public boolean apply(final Collection<Displayable> patches_) {
if (null == patches_) return false;
// Create appropriate patch list
ArrayList<Patch> patches = new ArrayList<Patch>();
for (final Displayable d : patches_) {
if (d.getClass() == Patch.class) patches.add((Patch)d);
}
if (0 == patches.size()) return false;
// Check that all images are of the same size and type
Patch firstp = (Patch) patches.get(0);
final int ptype = firstp.getType();
final double pw = firstp.getOWidth();
final double ph = firstp.getOHeight();
for (final Patch p : patches) {
if (p.getType() != ptype) {
// can't continue
Utils.log("Can't homogenize histograms: images are not all of the same type.\nFirst offending image is: " + p);
return false;
}
if (!equalize && 0 == stats_mode && p.getOWidth() != pw || p.getOHeight() != ph) {
Utils.log("Can't homogenize histograms: images are not all of the same size.\nFirst offending image is: " + p);
return false;
}
}
try {
if (equalize) {
for (final Patch p : patches) {
if (Thread.currentThread().isInterrupted()) return false;
p.appendFilters(new IFilter[]{new EqualizeHistogram()});
/*
p.getProject().getLoader().releaseToFit(p.getOWidth(), p.getOHeight(), p.getType(), 3);
ImageProcessor ip = p.getImageProcessor().duplicate(); // a throw-away copy
if (this.from_existing_min_and_max) {
ip.setMinAndMax(p.getMin(), p.getMax());
}
ce.equalize(ip);
p.setMinAndMax(ip.getMin(), ip.getMax());
*/
// submit for regeneration
p.getProject().getLoader().decacheImagePlus(p.getId());
regenerateMipMaps(p);
}
return true;
}
// Else, call stretchHistogram with an appropriate stats object
final ImageStatistics stats;
if (1 == stats_mode) { // use each image independent stats
stats = null;
} else if (0 == stats_mode) { // use stack statistics
final ArrayList<Patch> sub = new ArrayList<Patch>();
if (use_full_stack) {
sub.addAll(patches);
} else {
// build stack statistics, ordered by stdDev
final SortedMap<Stats,Patch> sp = Collections.synchronizedSortedMap(new TreeMap<Stats,Patch>());
Process.progressive(
patches,
new TaskFactory<Patch, Stats>() {
public Stats process(final Patch p) {
if (Thread.currentThread().isInterrupted()) return null;
ImagePlus imp = p.getImagePlus();
p.getProject().getLoader().releaseToFit(p.getOWidth(), p.getOHeight(), p.getType(), 2);
Stats s = new Stats(imp.getStatistics());
sp.put(s, p);
return s;
}
});
if (Thread.currentThread().isInterrupted()) return false;
final ArrayList<Patch> a = new ArrayList<Patch>(sp.values());
final int count = a.size();
if (count < 3) {
sub.addAll(a);
} else if (3 == count) {
sub.add(a.get(1)); // the middle one
} else if (4 == count ) {
sub.addAll(a.subList(1, 3));
} else if (count > 4) {
int first = (int)(count / 4.0 + 0.5);
int last = (int)(count / 4.0 * 3 + 0.5);
sub.addAll(a.subList(first, last));
}
}
stats = new StackStatistics(new PatchStack(sub.toArray(new Patch[sub.size()]), 1));
} else {
stats = reference_stats;
}
final Calibration cal = patches.get(0).getLayer().getParent().getCalibrationCopy();
Process.progressive(
patches,
new TaskFactory<Patch, Object>() {
public Object process(final Patch p) {
if (Thread.currentThread().isInterrupted()) return null;
p.getProject().getLoader().releaseToFit(p.getOWidth(), p.getOHeight(), p.getType(), 3);
ImageProcessor ip = p.getImageProcessor().duplicate(); // a throw-away copy
if (ContrastEnhancerWrapper.this.from_existing_min_and_max) {
ip.setMinAndMax(p.getMin(), p.getMax());
}
ImageStatistics st = stats;
if (null == stats) {
Utils.log2("Null stats, using image's self");
st = ImageStatistics.getStatistics(ip, Measurements.MIN_MAX, cal);
}
ce.stretchHistogram(ip, saturated, st);
// This is all we care about from stretching the histogram:
p.setMinAndMax(ip.getMin(), ip.getMax());
regenerateMipMaps(p);
return null;
}
});
} catch (Exception e) {
IJError.print(e);
return false;
}
return true;
}
private void regenerateMipMaps(final Patch p) {
// submit for regeneration
final Future<?> fu = p.getProject().getLoader().regenerateMipMaps(p);
// ... and when done, decache any images
tasks.add(waiter.submit(new Runnable() {
public void run() {
if (null != fu) {
try {
fu.get();
} catch (Exception e) {
IJError.print(e);
}
}
p.getProject().getLoader().decacheAWT(p.getId());
}
}));
}
final Vector<Future<?>> tasks = new Vector<Future<?>>();
/** Waits until all tasks have finished executing. */
public void shutdown() {
// Add a job at the end of the queue that closes the queue
tasks.add(waiter.submit(new Runnable() {
public void run() {
waiter.shutdown();
tasks.clear();
}
}));
// ... and wait until all tasks are executed
for (Future<?> fu : new Vector<Future<?>>(tasks)) {
if (null != fu) {
try {
fu.get();
} catch (InterruptedException ie) {
waiter.shutdownNow();
tasks.clear();
return;
} catch (Exception e) {
IJError.print(e);
}
}
}
}
static private class Stats implements Comparable<Stats> {
ImageStatistics s;
Stats(ImageStatistics s) {
this.s = s;
}
public int compareTo(Stats o) {
return (int)(o.s.stdDev - this.s.stdDev);
}
}
}