/**
*
*/
package ini.trakem2.imaging;
import ij.process.ByteProcessor;
import ini.trakem2.display.Display;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Layer;
import ini.trakem2.display.Patch;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.Filter;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import mpicbg.models.NoninvertibleModelException;
import mpicbg.trakem2.transform.TransformMesh;
/** Utility functions for blending images together, to remove contrast seams.
* Inspired and guided by Stephan Preibisch's blending functions in his Stitching plugins. */
public final class Blending {
static public final Bureaucrat blend(final List<Layer> layers, final boolean respect_current_mask, final Filter<Patch> filter) {
return Bureaucrat.createAndStart(
new Worker.Task("Blending layer-wise") {
@Override
public void exec() {
blendLayerWise(layers, respect_current_mask, filter);
}
}, layers.get(0).getProject());
}
static public final void blendLayerWise(final List<Layer> layers, final boolean respect_current_mask, final Filter<Patch> filter) {
for (final Layer layer : layers) {
final List<Patch> patches = layer.getAll(Patch.class);
final Set<Patch> s = new HashSet<Patch>();
if (null == filter) {
s.addAll(patches);
} else {
for (final Iterator<Patch> it = patches.iterator(); it.hasNext(); ) {
final Patch p = it.next();
if (filter.accept(p)) s.add(p);
}
}
blendPatches(s, respect_current_mask);
}
}
/** For each file, find the weight for the alpha mask according to
* wether the pixel overlaps with other images (weighted alpha
* dependent on the distante to the image border and of that on
* the other images) or not (full alpha).
* An image that doesn't overlap at all gets no alpha set at all.
*/
static public final Bureaucrat blend(final Set<Patch> patches, final boolean respect_current_mask) {
if (null == patches || patches.size() < 2) return null;
return Bureaucrat.createAndStart(
new Worker.Task("Blending images") {
@Override
public void exec() {
blendPatches(patches, respect_current_mask);
}
}, patches.iterator().next().getProject());
}
static public final void blendPatches(final Set<Patch> patches, final boolean respect_current_mask) {
ExecutorService exe = null;
try {
if (null == patches || patches.size() < 2) return;
final Layer layer = patches.iterator().next().getLayer();
for (final Patch p : patches) {
if (null != p.getCoordinateTransform()) {
Utils.log("CANNOT blend: at least one image has a coordinate transform.\nBlending of coordinate-transformed images will be enabled in the near future.");
return;
}
if (p.getLayer() != layer) {
Utils.log("CANNOT blend: all images must belong to the same layer!\n Otherwise the overlap cannot be computed.");
return;
}
}
final HashMap<Patch,TransformMesh> meshes = new HashMap<Patch,TransformMesh>();
for (final Patch p : patches) {
meshes.put(p, null == p.getCoordinateTransform() ? null
: new TransformMesh(p.getCoordinateTransform(), p.getMeshResolution(), p.getOWidth(), p.getOHeight()));
}
exe = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
final List<Future<?>> futures = Collections.synchronizedList(new ArrayList<Future<?>>());
final List<Future<?>> futures2 = Collections.synchronizedList(new ArrayList<Future<?>>());
// Cache the indices that determine overlap order within the layer
final HashMap<Patch,Integer> indices = new HashMap<Patch,Integer>();
int i = 0;
for (final Displayable d : layer.getDisplayables()) {
if (d.getClass() == Patch.class && patches.contains((Patch)d)) {
indices.put((Patch)d, i);
}
i += 1;
}
for (final Patch p : patches) {
if (Thread.currentThread().isInterrupted()) break;
futures.add(exe.submit(new Runnable() { @Override
public void run() {
final int pLayerIndex = indices.get(p);
final Set<Patch> overlapping = new HashSet<Patch>();
for (final Patch op : patches) {
if (indices.get(op) < pLayerIndex) overlapping.add(op);
}
if (setBlendingMask(p, overlapping, meshes, respect_current_mask)) {
futures2.add(p.updateMipMaps());
}
}}, null));
}
// join all:
Utils.waitIfAlive(futures, false);
Utils.waitIfAlive(futures2, false);
} catch (final Exception e) {
IJError.print(e);
} finally {
if (null != exe) exe.shutdown();
Display.repaint();
}
}
/** Returns true if a new mask has been set to Patch p. */
static private boolean setBlendingMask(final Patch p, Set<Patch> overlapping, final Map<Patch,TransformMesh> meshes, final boolean respect_current_mask) {
Utils.log2("Blending " + p);
if (overlapping.contains(p)) {
overlapping = new HashSet<Patch>(overlapping);
overlapping.remove(p);
}
final AffineTransform at = p.getAffineTransform();
final TransformMesh mesh = meshes.get(p);
ByteProcessor mask = null;
if (respect_current_mask) {
mask = p.getAlphaMask();
}
if (null == mask) {
mask = new ByteProcessor(p.getOWidth(), p.getOHeight());
mask.setValue(255);
mask.fill();
}
final byte[] pix = (byte[]) mask.getPixels();
final Point2D.Double po = new Point2D.Double();
final double[] fo = new double[2];
final int p_o_width = p.getOWidth();
final int p_o_height = p.getOHeight();
int next = 0;
final double[] weights = new double[overlapping.size() + 1]; // the self as well
int masked = 0;
for (int y=0; y<p_o_height; y++) {
if (Thread.currentThread().isInterrupted()) return false;
for (int x=0; x<p_o_width; x++) {
// transform x,y to world coords
if (null != mesh) {
fo[0] = x;
fo[1] = y;
mesh.applyInPlace(fo);
po.x = fo[0];
po.y = fo[1];
} else {
po.x = x;
po.y = y;
}
at.transform(po, po);
fo[0] = po.x;
fo[1] = po.y;
// debug:
if (0 == x && 0 == y) {
Utils.log2("point 0,0 goes to " + fo[0] + ", " + fo[1]);
}
// check if it intersects any Patch
next = 0;
for (final Patch other : overlapping) {
final double weight = intersects(fo, other, meshes.get(other));
if (weight > 0) weights[next++] = weight;
}
final int i = y * p_o_width + x;
if (respect_current_mask) {
// Don't compute if no overlap or if current mask value is zero
if (next > 0 && pix[i] != 0) {
weights[next++] = computeWeight(x, y, p_o_width, p_o_height); // the weight of Patch p, added last
double sum = 0;
for (int f=0; f<next; f++) sum += weights[f];
pix[i] = (byte)((int)(255 * (weights[next-1] / sum) * ((pix[i]&0xff) / 255.0f) ));
masked++;
}
// else leave current value untouched
} else if (next > 0) {
// Overwritting current mask
weights[next++] = computeWeight(x, y, p_o_width, p_o_height); // the weight of Patch p, added last
double sum = 0;
for (int f=0; f<next; f++) sum += weights[f];
pix[i] = (byte)((int)(255 * (weights[next-1] / sum)));
masked++;
}
}
}
Utils.log2("Masked = " + masked + " for " + p);
if (masked > 0) {
p.setAlphaMask(mask);
//new ij.ImagePlus("mask for " + p.getId(), mask).show();
return true;
}
Utils.log("Nothing to blend in image " + p);
return false;
}
static private final double computeWeight(final double x, final double y, final int width, final int height) {
//return Math.min(Math.min(x, width - x),
// Math.min(y, height - y));
// Normalized, as suggested by Stephan Preibisch:
return (Math.min(x, width - x) / (width/2)) * (Math.min(y, height - y) / (height/2));
}
/** Returns true if fo[0,1] x,y world coords intersect the affine and potentially coordinate transformed pixels of the other Patch. */
static private double intersects(final double[] fo, final Patch other, final TransformMesh mesh) {
// First inverse affine transform
final AffineTransform at = other.getAffineTransform();
final Point2D.Double po = new Point2D.Double(fo[0], fo[1]);
final int o_width = other.getOWidth();
final int o_height = other.getOHeight();
try {
at.inverseTransform(po, po);
} catch (final NoninvertibleTransformException nite) {
return -1;
}
if (null == mesh) {
if (po.x >= 0 && po.x < o_width
&& po.y >= 0 && po.y < o_height) {
return computeWeight(po.x, po.y, o_width, o_height);
} else {
return -1;
}
}
// Then inverse the coordinate transform
try {
fo[0] = po.x;
fo[1] = po.y;
mesh.applyInverseInPlace(fo);
return computeWeight(fo[0], fo[1], o_width, o_height);
} catch (final NoninvertibleModelException nime) {
// outside boundaries
return -1;
}
}
}