preparing release pom-trakem2-2.0.0, VectorString-2.0.0, TrakEM2_-1.0h
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / utils / AreaUtils.java
blobe61d3ebad1e82a96e03d3c48be31a18df67a64e9
1 package ini.trakem2.utils;
3 import java.awt.Polygon;
4 import java.awt.Rectangle;
5 import java.awt.geom.AffineTransform;
6 import java.awt.geom.Area;
7 import java.awt.geom.Path2D;
8 import java.util.ArrayList;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.TreeMap;
13 import java.util.concurrent.Callable;
14 import java.util.concurrent.ExecutionException;
15 import java.util.concurrent.ExecutorService;
16 import java.util.concurrent.Executors;
17 import java.util.concurrent.Future;
19 import org.scijava.vecmath.Point3f;
21 import ij.ImagePlus;
22 import ij.gui.Roi;
23 import ij.measure.Calibration;
24 import ij.plugin.filter.ThresholdToSelection;
25 import ij.process.ImageProcessor;
26 import ini.trakem2.display.Displayable;
27 import ini.trakem2.display.Layer;
28 import ini.trakem2.display.LayerSet;
29 import ini.trakem2.imaging.BinaryInterpolation2D;
30 import ini.trakem2.vector.Editions;
31 import ini.trakem2.vector.SkinMaker;
32 import ini.trakem2.vector.VectorString2D;
33 //import mpicbg.imglib.algorithm.labeling.BinaryInterpolation2D; // using ini.trakem2.imaging.BinaryInterpolation2D until imglib's algorithms jar is released
34 import mpicbg.imglib.container.shapelist.ShapeList;
35 import mpicbg.imglib.container.shapelist.ShapeListCached;
36 import mpicbg.imglib.image.Image;
37 import mpicbg.imglib.image.display.imagej.ImageJFunctions;
38 import mpicbg.imglib.type.logic.BitType;
39 import mpicbg.imglib.type.numeric.integer.ByteType;
42 public final class AreaUtils {
44 /** Project property key. */
45 static public final String always_interpolate_areas_with_distance_map = "always_interpolate_areas_with_distance_map";
47 private AreaUtils() {}
49 /** Expects areas in local coordinates to the Displayable @param d.
50 * @param scale The scaling of the entire universe, to limit the overall box
51 * @param resample The optimization parameter for marching cubes (i.e. a value of 2 will scale down to half, then apply marching cubes, then scale up by 2 the vertices coordinates).
52 * @return The List of triangles involved, specified as three consecutive vertices. A list of Point3f vertices. */
53 static public List<Point3f> generateTriangles(final Displayable d, final double scale, final int resample_, final Map<Layer,Area> areas) {
54 // in the LayerSet, layers are ordered by Z already.
55 try {
57 int n = areas.size();
58 if (0 == n) return null;
60 final int resample;
61 if (resample_ <=0 ) {
62 resample = 1;
63 Utils.log2("Fixing zero or negative resampling value to 1.");
64 } else resample = resample_;
66 final LayerSet layer_set = d.getLayerSet();
67 final AffineTransform aff = d.getAffineTransformCopy();
68 final Rectangle r = d.getBoundingBox(null);
70 // remove translation from a copy of the Displayable's AffineTransform
71 final AffineTransform at_translate = new AffineTransform();
72 at_translate.translate(-r.x, -r.y);
73 aff.preConcatenate(at_translate);
74 // incorporate resampling scaling into the transform
75 final AffineTransform atK = new AffineTransform();
76 //Utils.log("resample: " + resample + " scale: " + scale);
77 final double K = (1.0 / resample) * scale; // 'scale' is there to limit gigantic universes
78 atK.scale(K, K);
79 aff.preConcatenate(atK);
81 final Calibration cal = layer_set.getCalibrationCopy();
83 // Find first layer, compute depth, and fill in the depth vs area map
84 Layer first_layer = null,
85 last_layer = null;
86 final int w = (int)Math.ceil(r.width * K);
87 final int h = (int)Math.ceil(r.height * K);
88 int depth = 0;
90 final Map<Integer,Area> ma = new HashMap<Integer,Area>();
92 for (final Layer la : layer_set.getLayers()) { // layers sorted by Z ASC
93 final Area area = areas.get(la);
94 if (null != area) {
95 ma.put(depth, area);
96 if (null == first_layer) {
97 first_layer = la;
99 //Utils.log("area at depth " + depth + " for layer " + la);
100 depth++;
101 n--;
102 } else if (0 != depth) {
103 //Utils.log("Empty area at depth " + depth);
104 depth++; // an empty layer
106 // else, continue iterating until finding the first layer
108 if (0 == n) {
109 last_layer = la;
110 break; // no more areas to paint
114 if (0 == depth) {
115 Utils.log("ERROR could not find any areas for " + d);
116 return null;
118 if (0 != n) {
119 Utils.log("WARNING could not find all areas for " + d);
122 // No zero-padding: Marching Cubes now can handle edges
123 final ShapeList<ByteType> shapeList = new ShapeListCached<ByteType>(new int[]{w, h, depth}, new ByteType(), 32);
124 final Image<ByteType> shapeListImage = new Image<ByteType>(shapeList, shapeList.getBackground(), "ShapeListContainer");
125 final ByteType intensity = new ByteType((byte)127); // 255 or -1 don't work !? So, giving the highest value (127) that is both a byte and an int.
127 for (final Map.Entry<Integer,Area> e : ma.entrySet()) {
128 Area a = e.getValue();
129 if (!aff.isIdentity()) {
130 a = M.areaInIntsByRounding(a.createTransformedArea(aff));
132 shapeList.addShape(a, intensity, new int[]{e.getKey()});
135 //debug:
136 //ImagePlus imp = ImageJFunctions.displayAsVirtualStack(shapeListImage);
137 //imp.getProcessor().setMinAndMax( 0, 255 );
138 //imp.show();
140 //Utils.log2("Using imglib Shape List Image Container");
142 // Now marching cubes
143 final List<Point3f> list = new MCTriangulator().getTriangles(shapeListImage, 1, new float[3]); // origins at 0,0,0: uncalibrated
146 // The list of triangles has coordinates:
147 // - in x,y: in pixels, scaled by K = (1 / resample) * scale,
148 // translated by r.x, r.y (the top-left coordinate of this AreaList bounding box)
149 // - in z: in stack slice indices
151 // So all x,y,z must be corrected in x,y and z of the proper layer
154 //final double offset = first_layer.getZ();
155 final int i_first_layer = layer_set.indexOf(first_layer);
157 // The x,y translation to correct each point by:
158 final float dx = (float)(r.x * scale * cal.pixelWidth);
159 final float dy = (float)(r.y * scale * cal.pixelHeight);
161 // Correct x,y by resampling and calibration, but not scale
162 final float rsw = (float)(resample * cal.pixelWidth); // scale is already in the pixel coordinates
163 final float rsh = (float)(resample * cal.pixelHeight);
164 final double sz = scale * cal.pixelWidth; // no resampling in Z. and Uses pixelWidth, not pixelDepth.
167 // debug:
169 // which p.z types exist?
170 final TreeSet<Float> ts = new TreeSet<Float>();
171 for (final Iterator it = list.iterator(); it.hasNext(); ) {
172 ts.add(((Point3f)it.next()).z);
174 for (final Float pz : ts) Utils.log2("A z: " + pz);
178 // debug: How many different Z?
180 HashSet<Float> zs = new HashSet<Float>();
181 for (Point3f p : list) {
182 zs.add(p.z);
184 ArrayList<Float> a = new ArrayList<Float>(zs);
185 java.util.Collections.sort(a);
186 for (Float f : a) {
187 Utils.log("f: " + f);
191 //Utils.log2("Number of slices: " + imp.getNSlices());
193 // Fix all points:
194 // Read from list, modify and put into verts
195 // and don't modify it if the verts already has it (it's just coincident)
197 final Point3f[] verts = new Point3f[list.size()];
199 //Utils.log("number of verts: " + verts.length + " mod 3: " + (verts.length % 3));
201 final TreeMap<Integer,Point3f> output = new TreeMap<Integer,Point3f>();
204 // The first section generates vertices at -1 and 0
205 // The last section generates them at last_section_index and last_section_index +1
207 // Capture from -1 to 0
208 fix3DPoints(list, output, verts, first_layer.getZ(), 0, -1, dx, dy, rsw, rsh, sz, 1);
210 int slice_index = 0;
212 for (final Layer la : layer_set.getLayers().subList(i_first_layer, i_first_layer + depth)) {
214 // If layer is empty, continue
215 /* // YEAH don't! At least the immediate next layer would have points, like the extra Z level after last layer, to account for the thickness of the layer!
216 if (empty_layers.contains(la)) {
217 slice_index++;
218 continue;
222 fix3DPoints(list, output, verts, la.getZ(), la.getThickness(), slice_index, dx, dy, rsw, rsh, sz, 1);
224 slice_index++;
227 // The last set of vertices to process:
228 // Do the last layer again. The last layer has two Z planes in which it has pixels:
229 try {
230 // Capture from last_section_index to last_section_index+1, inclusive
231 fix3DPoints(list, output, verts, last_layer.getZ() + last_layer.getThickness(), 0, slice_index, dx, dy, rsw, rsh, sz, 2);
232 } catch (final Exception ee) {
233 IJError.print(ee);
236 //Utils.log("number of verts in output: " + output.size() + " mod 3: " + (output.size() % 3));
239 // debug:
240 //Utils.log2("Remaining p.z to process: ");
241 //for (final Float pz : ts) Utils.log2("remains: z: " + pz);
244 // Handle potential errors:
245 if (0 != list.size() - output.size()) {
246 Utils.log2("Unprocessed/unused points: " + (list.size() - output.size()));
247 for (int i=0; i<verts.length; i++) {
248 if (null == verts[i]) {
249 final Point3f p = (Point3f) list.get(i);
250 Utils.log2("verts[" + i + "] = " + p.x + ", " + p.y + ", " + p.z + " p.z as int: " + ((int)(p.z + 0.05f)));
253 return new ArrayList<Point3f>(output.values());
254 } else {
255 return java.util.Arrays.asList(verts);
258 } catch (final Exception e) {
259 e.printStackTrace();
261 return null;
265 * @param list The original points
266 * @param output The accumulated list of modified points to construct a mesh from
267 * @param verts The array of vertices, each index is filled if the point has been processed already.
268 * @param la_Z The Layer to process points for.
269 * @param la_thickness the thickness of that layer
270 * @param layer_index The stack slice index corresponding to the Layer @param la.
272 static private final void fix3DPoints(final List<Point3f> list, final TreeMap<Integer,Point3f> output, final Point3f[] verts, final double la_z, final double la_thickness, final int layer_index, final float dx, final float dy, final float rsw, final float rsh, final double sz, final int n_slices) {
273 int fixed = 0;
274 // Find all pixels that belong to the layer, and transform them back:
275 for (int i=0; i<verts.length; i++) {
276 if (null != verts[i]) continue; // already processed! The unprocessed Z is merely coincident with a processed Z.
277 final Point3f p = list.get(i);
278 final int pz = (int)(p.z + 0.05f);
279 //final int pz = (int)(p.z + (0.5f * Math.signum(p.z)));
280 if ( pz >= layer_index && pz < layer_index + n_slices) {
281 // correct pixel position:
282 // -- The 'rsw','rsh' scales back to LayerSet coords
283 // -- The 'dx','dy' translates back to this AreaList bounding box
284 p.x = p.x * rsw + dx;
285 p.y = p.y * rsh + dy;
287 // The Z is more complicated: the Z of the layer, scaled relative to the layer thickness
288 p.z = (float)((la_z + la_thickness * (p.z - layer_index)) * sz); // using pixelWidth, not pixelDepth!
290 verts[i] = p;
291 output.put(i, p);
292 fixed++;
295 //Utils.log("fix between " + layer_index + " and " + (layer_index + n_slices) + " (" + fixed + ")");
298 /** Extracts all non-background areas. */
299 static public final Map<Float,Area> extractAreas(final ImageProcessor ip) {
300 return extractAreas(ip, null, false, null, Thread.currentThread(), false);
303 /** Scan line-wise for all areas, returning a Map of area pixel values in @param ip vs. Area instances.
304 * If @param map_ is not null, it puts the areas there and returns it.
305 * If @param box_ is not null, it uses it as the unit pixel area. To make any sense, it must be setup as Rectangle(0,0,1,1).
306 * If @param report is true, it will report progress every 100 lines. */
307 static public final Map<Float,Area> extractAreas(final ImageProcessor ip, final HashMap<Float,Area> map_, final boolean add_background, final Rectangle box_, final Thread parent, final boolean report) {
308 final int height = ip.getHeight();
309 final int width = ip.getWidth();
310 int inc = height / 100;
311 if (inc < 10) inc = 10;
313 final Map<Float,Area> map = null == map_ ? new HashMap<Float,Area>() : map_;
314 final Rectangle box = null == box_ ? new Rectangle(0, 0, 1, 1) : box_;
316 for (int y=0; y<height; y++) {
317 if (0 == y % inc) {
318 if (parent.isInterrupted()) return map;
319 if (report) Utils.showStatus(new StringBuilder().append("line: ").append(y).append('/').append(height).toString());
322 float prev = ip.getPixelValue(0, y);
323 box.x = 0;
324 box.y = y;
325 box.width = 0;
327 for (int x=1; x<width; x++) {
329 final float pix = ip.getPixelValue(x, y);
331 if (pix == prev) {
332 box.width++;
333 continue;
334 } else {
335 // add previous one
336 if (!Float.isNaN(prev) && (add_background || 0 != prev)) {
337 box.width++;
338 Area area = map.get(new Float(prev));
339 if (null == area) {
340 area = new Area(box);
341 map.put(new Float(prev), area);
342 } else {
343 area.add(new Area(box));
346 // start new box
347 box.x = x;
348 box.y = y;
349 box.width = 0;
350 prev = pix;
354 // At end of line, add the last
355 if (!Float.isNaN(prev) && (add_background || 0 != prev)) {
356 Area area = map.get(new Float(prev));
357 if (null == area) {
358 area = new Area(box);
359 map.put(new Float(prev), area);
360 } else {
361 area.add(new Area(box));
366 return map;
369 static public Area infiniteArea()
371 final Path2D.Double path = new Path2D.Double();
372 path.moveTo(Double.MAX_VALUE, Double.MAX_VALUE);
373 path.lineTo(-Double.MAX_VALUE, Double.MAX_VALUE);
374 path.lineTo(-Double.MAX_VALUE, -Double.MAX_VALUE);
375 path.lineTo(Double.MAX_VALUE, -Double.MAX_VALUE);
376 path.lineTo(Double.MAX_VALUE, Double.MAX_VALUE);
378 return new Area(path);
381 /** Extract the Area of the image for the given pixel value.
382 * ThresholdToSelection is way faster than this, just use it.
383 * It's BROKEN do not use. */
385 static public final Area extractArea(final ImageProcessor ip, final float val) {
386 final int height = ip.getHeight();
387 final int width = ip.getWidth();
389 final Area area = new Area();
390 final ArrayList<Area> segments = new ArrayList<Area>();
391 final Rectangle box = new Rectangle(0, 0, 1, 1);
393 for (int y=0; y<height; y++) {
394 float prev = ip.getPixelValue(0, y);
395 box.x = 0;
396 box.y = y;
397 box.width = val == prev ? 1 : 0;
399 for (int x=1; x<width; x++) {
401 float pix = ip.getPixelValue(x, y);
403 if (val == pix) {
404 if (pix == prev) {
405 box.width++;
406 continue;
408 // Else, add previous one
409 segments.add(new Area(box));
410 // ... and start a new box
411 box.x = x;
412 box.y = y;
413 box.width = 1;
414 prev = pix;
418 // At end of line, add the last
419 segments.add(new Area(box));
421 // Join a few
422 if (segments.size() > 32) {
423 final Area a = new Area(segments.get(0));
424 final int len = segments.size();
425 for (int i=1; i<len; i++) a.add(segments.get(i));
426 area.add(a);
427 segments.clear();
431 if (segments.size() > 0) {
432 final Area a = new Area(segments.get(0));
433 final int len = segments.size();
434 for (int i=1; i<len; i++) a.add(segments.get(i));
435 area.add(a);
438 return area;
442 /** Interpolate areas only if they are made of a single shape each.
443 * Assumes that areas are in the same coordinate system.
444 * @throws Exception */
445 static public final Area[] singularInterpolation(final Area a1, final Area a2, final int nInterpolates) throws Exception {
446 if (!a1.isSingular() || !a2.isSingular()) {
447 return null;
449 final VectorString2D vs1 = M.asVectorString2D(M.getPolygons(a1).iterator().next(), 0);
450 final VectorString2D vs2 = M.asVectorString2D(M.getPolygons(a2).iterator().next(), 1);
452 final Editions ed = new Editions(vs1, vs2, Math.min(vs1.getAverageDelta(), vs2.getAverageDelta()), true);
454 final double[][][] d = SkinMaker.getMorphedPerimeters(vs1, vs2, nInterpolates, ed);
456 final Area[] a = new Area[d.length];
457 for (int i=0; i<d.length; i++) {
458 final double[] x = d[i][0];
459 final double[] y = d[i][1];
460 final int[] xi = new int[x.length];
461 final int[] yi = new int[y.length];
462 for (int k=0; k<x.length; k++) {
463 xi[k] = (int) x[k];
464 yi[k] = (int) y[k];
466 a[i] = new Area(new Polygon(xi, yi, xi.length));
469 return a;
472 static public final Area[] manyToManyInterpolation(final Area a1, final Area a2, final int nInterpolates) throws InterruptedException, ExecutionException {
473 final Rectangle b = a1.getBounds();
474 b.add(a2.getBounds());
475 final AffineTransform translate = new AffineTransform(1, 0, 0, 1, -b.x, -b.y);
477 final ShapeList<BitType> shapeList1 = new ShapeListCached<BitType>(new int[]{b.width, b.height}, new BitType(false), 32);
478 shapeList1.addShape(a1.createTransformedArea(translate), new BitType(true), new int[]{0});
479 final Image<BitType> img1 = new Image<BitType>(shapeList1, shapeList1.getBackground(), "ShapeListContainer");
481 final ShapeList<BitType> shapeList2 = new ShapeListCached<BitType>(new int[]{b.width, b.height}, new BitType(false), 32);
482 shapeList2.addShape(a2.createTransformedArea(translate), new BitType(true), new int[]{0});
483 final Image<BitType> img2 = new Image<BitType>(shapeList2, shapeList2.getBackground(), "ShapeListContainer");
485 final float inc = 1.0f / (nInterpolates + 1);
487 final BinaryInterpolation2D interpol = new BinaryInterpolation2D(img1, img2, inc);
488 if (!interpol.checkInput()) {
489 System.out.println("Error: " + interpol.getErrorMessage());
490 return null;
493 final Area[] as = new Area[nInterpolates];
494 final AffineTransform back = new AffineTransform(1, 0, 0, 1, b.x, b.y);
496 // TODO parallelize, which needs the means to call process() in parallel too--currently it cannot,
497 // the result would get overwritten.
499 final ExecutorService exec = Executors.newFixedThreadPool(Math.min(nInterpolates, Runtime.getRuntime().availableProcessors()));
500 final ArrayList<Future<Area>> fus = new ArrayList<Future<Area>>();
502 try {
504 for (int i=1; i<=nInterpolates; i++) {
505 final float weight = 1 - inc * i;
506 fus.add(exec.submit(new Callable<Area>() {
507 @Override
508 public Area call() throws Exception {
509 final Image<BitType> imb = interpol.process(weight);
510 final ImagePlus imp = ImageJFunctions.copyToImagePlus(imb, ImagePlus.GRAY8);
511 // BitType gets copied to 0 and 255 in 8-bit ByteProcessor
512 final ThresholdToSelection ts = new ThresholdToSelection();
513 ts.setup("", imp);
514 final ImageProcessor ip = imp.getProcessor();
515 ip.setThreshold(1, 255, ImageProcessor.NO_LUT_UPDATE);
516 ts.run(ip);
517 final Roi roi = imp.getRoi();
518 return null == roi ? new Area() : M.getArea(roi).createTransformedArea(back);
520 }));
523 int i = 0;
524 for (final Future<Area> fu : fus) {
525 as[i++] = fu.get();
528 } catch (final Throwable t) {
529 IJError.print(t);
530 } finally {
531 exec.shutdown();
534 return as;