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
;
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
;
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.
58 if (0 == n
) return null;
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
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,
86 final int w
= (int)Math
.ceil(r
.width
* K
);
87 final int h
= (int)Math
.ceil(r
.height
* K
);
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
);
96 if (null == first_layer
) {
99 //Utils.log("area at depth " + depth + " for layer " + la);
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
110 break; // no more areas to paint
115 Utils
.log("ERROR could not find any areas for " + d
);
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()});
136 //ImagePlus imp = ImageJFunctions.displayAsVirtualStack(shapeListImage);
137 //imp.getProcessor().setMinAndMax( 0, 255 );
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.
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) {
184 ArrayList<Float> a = new ArrayList<Float>(zs);
185 java.util.Collections.sort(a);
187 Utils.log("f: " + f);
191 //Utils.log2("Number of slices: " + imp.getNSlices());
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);
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)) {
222 fix3DPoints(list
, output
, verts
, la
.getZ(), la
.getThickness(), slice_index
, dx
, dy
, rsw
, rsh
, sz
, 1);
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:
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
) {
236 //Utils.log("number of verts in output: " + output.size() + " mod 3: " + (output.size() % 3));
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());
255 return java
.util
.Arrays
.asList(verts
);
258 } catch (final Exception e
) {
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
) {
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!
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
++) {
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
);
327 for (int x
=1; x
<width
; x
++) {
329 final float pix
= ip
.getPixelValue(x
, y
);
336 if (!Float
.isNaN(prev
) && (add_background
|| 0 != prev
)) {
338 Area area
= map
.get(new Float(prev
));
340 area
= new Area(box
);
341 map
.put(new Float(prev
), area
);
343 area
.add(new Area(box
));
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
));
358 area
= new Area(box
);
359 map
.put(new Float(prev
), area
);
361 area
.add(new Area(box
));
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);
397 box.width = val == prev ? 1 : 0;
399 for (int x=1; x<width; x++) {
401 float pix = ip.getPixelValue(x, y);
408 // Else, add previous one
409 segments.add(new Area(box));
410 // ... and start a new box
418 // At end of line, add the last
419 segments.add(new Area(box));
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));
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));
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()) {
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
++) {
466 a
[i
] = new Area(new Polygon(xi
, yi
, xi
.length
));
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());
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
>>();
504 for (int i
=1; i
<=nInterpolates
; i
++) {
505 final float weight
= 1 - inc
* i
;
506 fus
.add(exec
.submit(new Callable
<Area
>() {
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();
514 final ImageProcessor ip
= imp
.getProcessor();
515 ip
.setThreshold(1, 255, ImageProcessor
.NO_LUT_UPDATE
);
517 final Roi roi
= imp
.getRoi();
518 return null == roi ?
new Area() : M
.getArea(roi
).createTransformedArea(back
);
524 for (final Future
<Area
> fu
: fus
) {
528 } catch (final Throwable t
) {