Blending respects existing alpha masks if asked to.
[trakem2.git] / ini / trakem2 / imaging / Blending.java
blob0441d2b6b09b739226bc8e3e1bcb065dacd213b6
1 /**
3 */
4 package ini.trakem2.imaging;
6 import ini.trakem2.display.Patch;
7 import ini.trakem2.display.Display;
8 import ini.trakem2.utils.Utils;
9 import ini.trakem2.utils.Bureaucrat;
10 import ini.trakem2.utils.Worker;
11 import ini.trakem2.utils.IJError;
12 import mpicbg.trakem2.transform.CoordinateTransform;
13 import mpicbg.trakem2.transform.TransformMesh;
14 import mpicbg.models.NoninvertibleModelException;
16 import java.awt.geom.NoninvertibleTransformException;
17 import java.awt.geom.AffineTransform;
18 import java.awt.geom.Point2D;
19 import java.util.Set;
20 import java.util.Map;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.ArrayList;
25 import ij.process.ByteProcessor;
27 import java.util.concurrent.FutureTask;
28 import java.util.concurrent.Executors;
29 import java.util.concurrent.ExecutorService;
30 import java.util.concurrent.Callable;
32 /** Utility functions for blending images together, to remove contrast seams.
33 * Inspired and guided by Stephan Preibisch's blending functions in his Stitching plugins. */
34 public final class Blending {
36 /** For each file, find the weight for the alpha mask according to
37 * wether the pixel overlaps with other images (weighted alpha
38 * dependent on the distante to the image border and of that on
39 * the other images) or not (full alpha).
40 * An image that doesn't overlap at all gets no alpha set at all.
42 static public final Bureaucrat blend(final Set<Patch> patches, final boolean respect_current_mask) {
43 if (null == patches || patches.size() < 2) return null;
45 return Bureaucrat.createAndStart(new Worker("Blending images") {
46 public void run() {
47 try {
48 startedWorking();
50 for (final Patch p : patches) {
51 if (null != p.getCoordinateTransform()) {
52 Utils.log("CANNOT blend: at least one image has a coordinate transform.\nBlending of coordinate-transformed images will be enabled in the near future.");
53 return;
57 final HashMap<Patch,TransformMesh> meshes = new HashMap<Patch,TransformMesh>();
58 for (final Patch p : patches) {
59 meshes.put(p, null == p.getCoordinateTransform() ? null
60 : new TransformMesh(p.getCoordinateTransform(), 32, p.getOWidth(), p.getOHeight()));
63 ExecutorService exe = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
64 final ArrayList<FutureTask> futures = new ArrayList<FutureTask>();
66 for (final Patch p : patches) {
67 if (Thread.currentThread().isInterrupted()) break;
68 FutureTask future = new FutureTask(new Runnable() { public void run() {
69 final int pLayerIndex = p.getLayer().indexOf( p );
70 final Set< Patch > overlapping = new HashSet< Patch >();
71 for ( Patch op : patches )
72 if ( p.getLayer().indexOf( op ) < pLayerIndex )
73 overlapping.add( op );
74 if (setBlendingMask(p, overlapping, meshes, respect_current_mask)) {
75 p.updateMipmaps();
77 }}, null);
78 futures.add(future);
79 exe.submit(future);
82 // join all:
83 for (final FutureTask future : futures) {
84 if (Thread.currentThread().isInterrupted()) break;
85 try {
86 future.get();
87 } catch (InterruptedException ie) {} // thrown when canceled
90 exe.shutdownNow();
92 } catch (Exception e) {
93 IJError.print(e);
94 } finally {
95 finishedWorking();
96 Display.repaint();
99 }, patches.iterator().next().getProject());
102 /** Returns true if a new mask has been set to Patch p. */
103 static private boolean setBlendingMask(final Patch p, Set<Patch> overlapping, final Map<Patch,TransformMesh> meshes, final boolean respect_current_mask) {
105 Utils.log2("Blending " + p);
107 if (overlapping.contains(p)) {
108 overlapping = new HashSet<Patch>(overlapping);
109 overlapping.remove(p);
112 final AffineTransform at = p.getAffineTransform();
113 final TransformMesh mesh = meshes.get(p);
115 ByteProcessor mask = null;
116 if (respect_current_mask) {
117 mask = p.getProject().getLoader().fetchImageMask(p);
119 if (null == mask) {
120 mask = new ByteProcessor(p.getOWidth(), p.getOHeight());
121 mask.setValue(255);
122 mask.fill();
125 final byte[] pix = (byte[]) mask.getPixels();
127 final Point2D.Double po = new Point2D.Double();
128 final float[] fo = new float[2];
130 final int p_o_width = p.getOWidth();
131 final int p_o_height = p.getOHeight();
133 int next = 0;
134 final float[] weights = new float[overlapping.size() + 1]; // the self as well
135 int masked = 0;
137 for (int y=0; y<p_o_height; y++) {
139 if (Thread.currentThread().isInterrupted()) return false;
141 for (int x=0; x<p_o_width; x++) {
143 // transform x,y to world coords
144 if (null != mesh) {
145 fo[0] = x;
146 fo[1] = y;
147 mesh.applyInPlace(fo);
148 po.x = fo[0];
149 po.y = fo[1];
150 } else {
151 po.x = x;
152 po.y = y;
155 at.transform(po, po);
157 fo[0] = (float) po.x;
158 fo[1] = (float) po.y;
160 // debug:
161 if (0 == x && 0 == y) {
162 Utils.log2("point 0,0 goes to " + fo[0] + ", " + fo[1]);
165 // check if it intersects any Patch
166 next = 0;
167 for (final Patch other : overlapping) {
168 float weight = intersects(fo, other, meshes.get(other));
169 if (weight > 0) weights[next++] = weight;
172 final int i = y * p_o_height + x;
174 if (respect_current_mask) {
175 // Don't compute if no overlap or if current mask value is zero
176 if (next > 0 && pix[i] != 0) {
177 weights[next++] = computeWeight(x, y, p_o_width, p_o_height); // the weight of Patch p, added last
178 float sum = 0;
179 for (int f=0; f<next; f++) sum += weights[f];
180 pix[i] = (byte)((int)(255 * (weights[next-1] / sum) * ((pix[i]&0xff) / 255.0f) ));
181 masked++;
183 // else leave current value untouched
184 } else if (next > 0) {
185 // Overwritting current mask
186 weights[next++] = computeWeight(x, y, p_o_width, p_o_height); // the weight of Patch p, added last
187 float sum = 0;
188 for (int f=0; f<next; f++) sum += weights[f];
189 pix[i] = (byte)((int)(255 * (weights[next-1] / sum)));
190 masked++;
195 Utils.log2("Masked = " + masked + " for " + p);
197 if (masked > 0) {
198 p.setAlphaMask(mask);
200 //new ij.ImagePlus("mask for " + p.getId(), mask).show();
202 return true;
205 return false;
208 static private final float computeWeight(final float x, final float y, final int width, final int height) {
209 //return Math.min(Math.min(x, width - x),
210 // Math.min(y, height - y));
211 // Normalized, as suggested by Stephan Preibisch:
212 return (Math.min(x, width - x) / (width/2)) * (Math.min(y, height - y) / (height/2));
215 /** Returns true if fo[0,1] x,y world coords intersect the affine and potentially coordinate transformed pixels of the other Patch. */
216 static private float intersects(final float[] fo, final Patch other, final TransformMesh mesh) {
217 // First inverse affine transform
218 final AffineTransform at = other.getAffineTransform();
219 final Point2D.Double po = new Point2D.Double(fo[0], fo[1]);
220 final int o_width = other.getOWidth();
221 final int o_height = other.getOHeight();
222 try {
223 at.inverseTransform(po, po);
224 } catch (NoninvertibleTransformException nite) {
225 return -1;
227 if (null == mesh) {
228 if (po.x >= 0 && po.x < o_width
229 && po.y >= 0 && po.y < o_height) {
230 return computeWeight((float)po.x, (float)po.y, o_width, o_height);
231 } else {
232 return -1;
235 // Then inverse the coordinate transform
236 try {
237 fo[0] = (float) po.x;
238 fo[1] = (float) po.y;
239 mesh.applyInverseInPlace(fo);
240 return computeWeight(fo[0], fo[1], o_width, o_height);
241 } catch (NoninvertibleModelException nime) {
242 // outside boundaries
243 return -1;