Use internal SNAPSHOT couplings again
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / AreaList.java
blobdae998f8d48b5aa7fa7798632c35936bf0815083
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
26 import amira.AmiraMeshEncoder;
27 import fiji.geom.AreaCalculations;
28 import ij.IJ;
29 import ij.ImagePlus;
30 import ij.ImageStack;
31 import ij.gui.GenericDialog;
32 import ij.gui.PolygonRoi;
33 import ij.gui.Roi;
34 import ij.gui.ShapeRoi;
35 import ij.io.FileSaver;
36 import ij.measure.Calibration;
37 import ij.measure.ResultsTable;
38 import ij.process.ByteProcessor;
39 import ij.process.FloatPolygon;
40 import ij.process.FloatProcessor;
41 import ij.process.ImageProcessor;
42 import ij.process.ShortProcessor;
43 import ini.trakem2.Project;
44 import ini.trakem2.display.paint.USHORTPaint;
45 import ini.trakem2.persistence.XMLOptions;
46 import ini.trakem2.utils.AreaUtils;
47 import ini.trakem2.utils.CircularSequence;
48 import ini.trakem2.utils.IJError;
49 import ini.trakem2.utils.M;
50 import ini.trakem2.utils.ProjectToolbar;
51 import ini.trakem2.utils.Utils;
53 import java.awt.AlphaComposite;
54 import java.awt.Color;
55 import java.awt.Composite;
56 import java.awt.Graphics2D;
57 import java.awt.Point;
58 import java.awt.Polygon;
59 import java.awt.Rectangle;
60 import java.awt.event.KeyEvent;
61 import java.awt.event.MouseEvent;
62 import java.awt.geom.AffineTransform;
63 import java.awt.geom.Area;
64 import java.awt.geom.NoninvertibleTransformException;
65 import java.awt.geom.PathIterator;
66 import java.awt.image.BufferedImage;
67 import java.awt.image.DataBufferByte;
68 import java.io.File;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.Collection;
72 import java.util.Collections;
73 import java.util.HashMap;
74 import java.util.HashSet;
75 import java.util.Iterator;
76 import java.util.List;
77 import java.util.Map;
78 import java.util.Set;
79 import java.util.TreeMap;
80 import java.util.TreeSet;
81 import java.util.concurrent.ExecutorService;
82 import java.util.concurrent.Future;
84 import javax.vecmath.Point3f;
86 /** A list of brush painted areas similar to a set of labelfields in Amira.
88 * For each layer where painting has been done, there is an entry in the ht_areas HashMap that contains the layer's id as a Long, and a java.awt.geom.Area object.
89 * All Area objects are local to this AreaList's AffineTransform.
91 public class AreaList extends ZDisplayable implements AreaContainer, VectorData {
93 /** Contains the table of layer ids and their associated Area object.*/
94 private HashMap<Long,Area> ht_areas = new HashMap<Long,Area>();
96 /** Flag to signal dynamic loading from the database for the Area of a given layer id in the ht_areas HashMap. */
97 static private final Area UNLOADED = new Area();
99 /** Paint as outlines (false) or as solid areas (true; default, with a default alpha of 0.4f).*/
100 private boolean fill_paint = true;
102 public AreaList(Project project, String title, double x, double y) {
103 super(project, title, x, y);
104 this.alpha = AreaWrapper.PP.default_alpha;
105 addToDatabase();
108 /** Reconstruct from XML. */
109 public AreaList(final Project project, final long id, HashMap<String,String> ht_attributes, final HashMap<Displayable,String> ht_links) {
110 super(project, id, ht_attributes, ht_links);
111 // read the fill_paint
112 Object ob_data = ht_attributes.get("fill_paint");
113 try {
114 if (null != ob_data) this.fill_paint = "true".equals(((String)ob_data).trim().toLowerCase()); // fails: //Boolean.getBoolean((String)ob_data);
115 } catch (Exception e) {
116 Utils.log("AreaList: could not read fill_paint value from XML:" + e);
120 /** Reconstruct from the database. */
121 public AreaList(Project project, long id, String title, float width, float height, float alpha, boolean visible, Color color, boolean locked, ArrayList<Long> al_ul, AffineTransform at) { // al_ul contains Long() wrapping layer ids
122 super(project, id, title, locked, at, width, height);
123 this.alpha = alpha;
124 this.visible = visible;
125 this.color = color;
126 for (final Long lid : al_ul) {
127 ht_areas.put(lid, AreaList.UNLOADED); // assumes al_ul contains only Long instances wrapping layer_id long values
131 public void paint(final Graphics2D g, final Rectangle srcRect, final double magnification, final boolean active, final int channels, final Layer active_layer, final List<Layer> layers) {
132 //arrange transparency
133 Composite original_composite = null;
134 try {
135 if (layer_set.area_color_cues) {
136 original_composite = g.getComposite();
137 Color c = layer_set.use_color_cue_colors ? Color.red : this.color;
138 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Math.min(alpha, 0.25f)));
139 for (final Layer la : layers) {
140 if (active_layer == la) {
141 c = layer_set.use_color_cue_colors ? Color.blue : this.color;
142 continue;
144 Area area = ht_areas.get(la.getId());
145 if (null == area) continue;
146 if (AreaList.UNLOADED == area) {
147 area = loadLayer(la.getId());
148 if (null == area) continue;
150 g.setColor(c);
152 if (fill_paint) g.fill(area.createTransformedArea(this.at));
153 else g.draw(area.createTransformedArea(this.at)); // the contour only
155 if (1.0f == alpha) g.setComposite(original_composite);
157 if (alpha != 1.0f) {
158 if (null == original_composite) original_composite = g.getComposite();
159 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
162 // The active layer, on top:
163 if (null != aw) {
164 aw.paint(g, this.at, fill_paint, this.color);
165 } else {
166 Area area = ht_areas.get(active_layer.getId());
167 if (null == area) return;
168 if (AreaList.UNLOADED == area) {
169 area = loadLayer(active_layer.getId());
170 if (null == area) return;
172 g.setColor(this.color);
174 if (fill_paint) g.fill(area.createTransformedArea(this.at));
175 else g.draw(area.createTransformedArea(this.at)); // the contour only
177 } finally {
178 //Transparency: fix alpha composite back to original.
179 if (null != original_composite) {
180 g.setComposite(original_composite);
185 public void transformPoints(Layer layer, double dx, double dy, double rot, double xo, double yo) {
186 Utils.log("AreaList.transformPoints: not implemented yet.");
189 /** Returns the layer of lowest Z coordinate Layer where this ZDisplayable has a point in. */
190 public Layer getFirstLayer() {
191 double min_z = Double.MAX_VALUE;
192 Layer first_layer = null;
193 for (final Long lid : ht_areas.keySet()) {
194 final Layer la = this.layer_set.getLayer(lid.longValue());
195 double z = la.getZ();
196 if (z < min_z) {
197 min_z = z;
198 first_layer = la;
201 return first_layer;
204 /** Returns the layer of highest Z coordinate Layer where this ZDisplayable has a point in. */
205 public Layer getLastLayer() {
206 double max_z = -Double.MAX_VALUE;
207 Layer last_layer = null;
208 for (final Long lid : ht_areas.keySet()) {
209 Layer la = this.layer_set.getLayer(lid.longValue());
210 double z = la.getZ();
211 if (z > max_z) {
212 max_z = z;
213 last_layer = la;
216 return last_layer;
217 } // I do REALLY miss Lisp macros. Writting the above two methods in a lispy way would make the java code unreadable
219 /** Get the range of layers between the first and last layers in which this AreaList paints to. */
220 public List<Layer> getLayerRange() {
221 return layer_set.getLayers(getFirstLayer(), getLastLayer());
224 public boolean linkPatches() {
225 unlinkAll(Patch.class);
226 // cheap way: intersection of the patches' bounding box with the area
227 Rectangle r = new Rectangle();
228 boolean must_lock = false;
229 for (final Map.Entry<Long,Area> e : ht_areas.entrySet()) {
230 Layer la = this.layer_set.getLayer(e.getKey());
231 if (null == la) {
232 Utils.log2("AreaList.linkPatches: ignoring null layer for id " + e.getKey());
233 continue;
235 final Area area = e.getValue().createTransformedArea(this.at);
236 for (final Patch d : la.getAll(Patch.class)) {
237 r = d.getBoundingBox(r);
238 if (area.intersects(r)) {
239 link(d, true);
240 if (d.locked) must_lock = true;
245 // set the locked flag to this and all linked ones
246 if (must_lock && !locked) {
247 setLocked(true);
248 return true;
251 return false;
254 /** Returns whether the point x,y is contained in this object at the given Layer. */
255 @Override
256 public boolean contains(final Layer layer, final double x, final double y) {
257 Object ob = ht_areas.get(new Long(layer.getId()));
258 if (null == ob) return false;
259 if (AreaList.UNLOADED == ob) {
260 ob = loadLayer(layer.getId());
261 if (null == ob) return false;
263 Area area = (Area)ob;
264 if (!this.at.isIdentity()) area = area.createTransformedArea(this.at);
265 return area.contains(x, y);
268 public boolean intersects(final Layer layer, final Rectangle r) {
269 Object ob = ht_areas.get(layer.getId());
270 if (null == ob) return false;
271 if (AreaList.UNLOADED == ob) {
272 ob = loadLayer(layer.getId());
273 if (null == ob) return false;
275 final Area a = ((Area)ob).createTransformedArea(this.at);
276 return a.intersects(r.x, r.y, r.width, r.height);
279 public boolean intersects(final Layer layer, final Area area) {
280 Object ob = ht_areas.get(layer.getId());
281 if (null == ob) return false;
282 if (AreaList.UNLOADED == ob) {
283 ob = loadLayer(layer.getId());
284 if (null == ob) return false;
286 final Area a = ((Area)ob).createTransformedArea(this.at);
287 a.intersect(area);
288 final Rectangle b = a.getBounds();
289 return 0 != b.width && 0 != b.height;
292 /** Returns the bounds of this object as it shows in the given layer. */
293 public Rectangle getBounds(final Rectangle r, final Layer layer) {
294 if (null == layer) return super.getBounds(r, null);
295 final Area area = (Area)ht_areas.get(layer.getId());
296 if (null == area) {
297 if (null == r) return new Rectangle();
298 r.x = 0;
299 r.y = 0;
300 r.width = 0;
301 r.height = 0;
302 return r;
304 final Rectangle b = area.createTransformedArea(this.at).getBounds();
305 if (null == r) return b;
306 r.setBounds(b.x, b.y, b.width, b.height);
307 return r;
310 public boolean isDeletable() {
311 if (0 == ht_areas.size()) return true;
312 return false;
315 private AreaWrapper aw = null;
316 private Long lid = null;
318 @Override
319 public void mousePressed(final MouseEvent me, final Layer la, final int x_p_w, final int y_p_w, final double mag) {
320 lid = la.getId(); // isn't this.layer pointing to the current layer always? It *should*
321 Object ob = ht_areas.get(new Long(lid));
322 Area area = null;
323 if (null == ob) {
324 area = new Area();
325 ht_areas.put(new Long(lid), area);
326 this.width = layer_set.getLayerWidth(); // will be set properly at mouse release
327 this.height = layer_set.getLayerHeight(); // without this, the first brush slash doesn't get painted because the isOutOfRepaintingClip returns true
328 } else {
329 if (AreaList.UNLOADED == ob) {
330 ob = loadLayer(lid);
331 if (null == ob) return;
333 area = (Area)ob;
336 // help ease the transition from PEN to BRUSH:
337 if (ProjectToolbar.getToolId() == ProjectToolbar.PEN) ProjectToolbar.setTool(ProjectToolbar.BRUSH);
339 aw = new AreaWrapper(area);
340 aw.setSource(this);
341 final Area a = aw.getArea();
342 final Long lid = this.lid;
343 aw.mousePressed(me, la, x_p_w, y_p_w, mag, Arrays.asList(new Runnable[]{new Runnable() { public void run() {
344 // To be run on mouse released:
345 // check if empty. If so, remove
346 Rectangle bounds = a.getBounds();
347 if (0 == bounds.width && 0 == bounds.height) {
348 ht_areas.remove(lid);
350 calculateBoundingBox(la);
351 }}}));
352 aw.setSource(null);
354 @Override
355 public void mouseDragged(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) {
356 if (null == aw) return;
357 aw.setSource(this);
358 aw.mouseDragged(me, la, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
359 aw.setSource(null);
361 @Override
362 public void mouseReleased(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
363 if (null == aw) return;
364 aw.setSource(this);
365 aw.mouseReleased(me, la, x_p, y_p, x_d, y_d, x_r, y_r);
366 aw.setSource(null);
368 lid = null;
369 aw = null;
372 /** Calculate box, make this width,height be that of the box, and translate all areas to fit in.
373 * @param la is the currently active Layer.
375 @Override
376 public boolean calculateBoundingBox(final Layer la) {
377 try {
378 // check preconditions
379 if (0 == ht_areas.size()) return false;
381 Rectangle box = null;
382 for (final Area a : ht_areas.values()) {
383 if (null == a || a.isEmpty()) continue;
384 if (null == box) box = a.getBounds();
385 else box.add(a.getBounds());
388 // If null, the AreaList was empty
389 // If box.width,height are zero, the AreaList was empty
390 if (null == box || 0 == box.width || 0 == box.height) {
391 return false;
394 // If box.x,y are 0, then no transform is necessary
395 if (0 == box.x && 0 == box.y) {
396 // no translation necessary, but must adjust width and height
397 this.width = box.width;
398 this.height = box.height;
399 updateInDatabase("dimensions");
400 return true;
403 // make local to overall box, so that box starts now at 0,0
404 final AffineTransform atb = new AffineTransform(1, 0, 0, 1, -box.x, -box.y);
406 // Guess if multithreaded processing would help
407 if (ht_areas.size() > 1 && (box.width > 2048 || box.height > 2048 || ht_areas.size() > 10)) {
408 // Multithreaded
409 final ExecutorService exec = Utils.newFixedThreadPool("AreaList-CBB");
410 final List<Future<?>> fus = new ArrayList<Future<?>>();
411 for (final Area a : ht_areas.values()) {
412 fus.add(exec.submit(new Runnable() {
413 public void run() {
414 a.transform(atb);
416 }));
418 Utils.wait(fus);
419 exec.shutdown();
420 } else {
421 // Single threaded
422 for (final Area a : ht_areas.values()) {
423 a.transform(atb);
426 this.at.translate(box.x, box.y);
427 this.width = box.width;
428 this.height = box.height;
429 updateInDatabase("transform+dimensions");
430 return true;
431 } finally {
432 updateBucket(la);
436 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
437 final String type = "t2_area_list";
438 if (hs.contains(type)) return;
439 hs.add(type);
440 sb_header.append(indent).append("<!ELEMENT t2_area_list (").append(Displayable.commonDTDChildren()).append(",t2_area)>\n");
441 Displayable.exportDTD(type, sb_header, hs, indent); // all ATTLIST of a Displayable
442 sb_header.append(indent).append("<!ATTLIST t2_area_list fill_paint NMTOKEN #REQUIRED>\n");
443 sb_header.append(indent).append("<!ELEMENT t2_area (t2_path)>\n")
444 .append(indent).append("<!ATTLIST t2_area layer_id NMTOKEN #REQUIRED>\n")
445 .append(indent).append("<!ELEMENT t2_path EMPTY>\n")
446 .append(indent).append("<!ATTLIST t2_path d NMTOKEN #REQUIRED>\n")
450 @Override
451 public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) {
452 sb_body.append(indent).append("<t2_area_list\n");
453 final String in = indent + "\t";
454 super.exportXML(sb_body, in, options);
455 sb_body.append(in).append("fill_paint=\"").append(fill_paint).append("\"\n");
456 String[] RGB = Utils.getHexRGBColor(color);
457 sb_body.append(in).append("style=\"stroke:none;fill-opacity:").append(alpha).append(";fill:#").append(RGB[0]).append(RGB[1]).append(RGB[2]).append(";\"\n");
458 sb_body.append(indent).append(">\n");
459 for (final Map.Entry<Long,Area> entry : ht_areas.entrySet()) {
460 final Area area = entry.getValue();
461 if (null == area || area.isEmpty()) continue;
462 sb_body.append(in).append("<t2_area layer_id=\"").append(entry.getKey()).append("\">\n");
463 exportArea(sb_body, in + "\t", area);
464 sb_body.append(in).append("</t2_area>\n");
466 super.restXML(sb_body, in, options);
467 sb_body.append(indent).append("</t2_area_list>\n");
470 /** Exports the given area as a list of SVG path elements with integers only. Only reads SEG_MOVETO, SEG_LINETO and SEG_CLOSE elements, all others ignored (but could be just as easily saved in the SVG path). */
471 static final void exportArea(final StringBuilder sb, final String indent, final Area area) {
472 // I could add detectors for straight lines and thus avoid saving so many points.
473 final float[] coords = new float[6];
474 final float precision = 0.0001f;
475 for (final PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
476 switch (pit.currentSegment(coords)) {
477 case PathIterator.SEG_MOVETO:
478 //Utils.log2("SEG_MOVETO: " + coords[0] + "," + coords[1]); // one point
479 sb.append(indent).append("<t2_path d=\"M ");
480 M.appendShortest(coords[0], precision, sb);
481 sb.append(' ');
482 M.appendShortest(coords[1], precision, sb);
483 break;
484 case PathIterator.SEG_LINETO:
485 //Utils.log2("SEG_LINETO: " + coords[0] + "," + coords[1]); // one point
486 sb.append(" L ");
487 M.appendShortest(coords[0], precision, sb);
488 sb.append(' ');
489 M.appendShortest(coords[1], precision, sb);
490 break;
491 case PathIterator.SEG_CLOSE:
492 //Utils.log2("SEG_CLOSE");
493 // make a line to the first point
494 //sb.append(" L ").append(x0).append(" ").append(y0);
495 sb.append(" z\" />\n");
496 break;
497 default:
498 Utils.log2("WARNING: AreaList.exportArea unhandled seg type.");
499 break;
501 pit.next();
502 if (pit.isDone()) {
503 //Utils.log2("finishing");
504 return;
509 /** Returns an ArrayList of ArrayList of Point as value with all paths for the Area of the given layer_id. */
510 public ArrayList<ArrayList<Point>> getPaths(long layer_id) {
511 Area ob = ht_areas.get(layer_id);
512 if (null == ob) return null;
513 if (AreaList.UNLOADED == ob) {
514 ob = loadLayer(layer_id);
515 if (null == ob) return null;
517 Area area = ob;
518 ArrayList<ArrayList<Point>> al_paths = new ArrayList<ArrayList<Point>>();
519 ArrayList<Point> al_points = null;
520 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
521 float[] coords = new float[6];
522 int seg_type = pit.currentSegment(coords);
523 switch (seg_type) {
524 case PathIterator.SEG_MOVETO:
525 al_points = new ArrayList<Point>();
526 al_points.add(new Point((int)coords[0], (int)coords[1]));
527 break;
528 case PathIterator.SEG_LINETO:
529 al_points.add(new Point((int)coords[0], (int)coords[1]));
530 break;
531 case PathIterator.SEG_CLOSE:
532 al_paths.add(al_points);
533 al_points = null;
534 break;
535 default:
536 Utils.log2("WARNING: AreaList.getPaths() unhandled seg type.");
537 break;
539 pit.next();
540 if (pit.isDone()) {
541 break;
544 return al_paths;
547 /** Returns a table of Long layer ids versus the ArrayList that getPaths(long) returns for it.*/
548 public HashMap<Long,ArrayList<ArrayList<Point>>> getAllPaths() {
549 HashMap<Long,ArrayList<ArrayList<Point>>> ht = new HashMap<Long,ArrayList<ArrayList<Point>>>();
550 for (final Map.Entry<Long,Area> entry : ht_areas.entrySet()) {
551 ht.put(entry.getKey(), getPaths(entry.getKey()));
553 return ht;
556 public void fillHoles(final Layer la) {
557 Object o = ht_areas.get(la.getId());
558 if (UNLOADED == o) o = loadLayer(la.getId());
559 if (null == o) return;
560 Area area = (Area) o;
562 new AreaWrapper(this, area).fillHoles();
565 public boolean paintsAt(final Layer layer) {
566 if (!super.paintsAt(layer)) return false;
567 return null != ht_areas.get(new Long(layer.getId()));
570 /** Dynamic loading from the database. */
571 private Area loadLayer(final long layer_id) {
572 Area area = project.getLoader().fetchArea(this.id, layer_id);
573 if (null == area) return null;
574 ht_areas.put(new Long(layer_id), area);
575 return area;
578 public void adjustProperties() {
579 GenericDialog gd = makeAdjustPropertiesDialog(); // in superclass
580 gd.addCheckbox("Paint as outlines", !fill_paint);
581 gd.addCheckbox("Apply paint mode to all AreaLists", false);
582 gd.showDialog();
583 if (gd.wasCanceled()) return;
584 // superclass processing
585 final Displayable.DoEdit prev = processAdjustPropertiesDialog(gd);
586 // local proccesing
587 final boolean fp = !gd.getNextBoolean();
588 final boolean to_all = gd.getNextBoolean();
589 if (to_all) {
590 for (final ZDisplayable zd : this.layer_set.getZDisplayables()) {
591 if (zd.getClass() == AreaList.class) {
592 AreaList ali = (AreaList)zd;
593 ali.fill_paint = fp;
594 ali.updateInDatabase("fill_paint");
595 Display.repaint(this.layer_set, ali, 2);
598 } else {
599 if (this.fill_paint != fp) {
600 prev.add("fill_paint", fp);
601 this.fill_paint = fp;
602 updateInDatabase("fill_paint");
606 // Add current step, with the same modified keys
607 DoEdit current = new DoEdit(this).init(prev);
608 if (isLinked()) current.add(new Displayable.DoTransforms().addAll(getLinkedGroup(null)));
609 getLayerSet().addEditStep(current);
612 public boolean isFillPaint() { return this.fill_paint; }
614 /** Merge all arealists contained in the ArrayList to the first one found, and remove the others from the project, and only if they belong to the same LayerSet. Returns the merged AreaList object. */
615 static public AreaList merge(final ArrayList<Displayable> al) {
616 AreaList base = null;
617 final ArrayList<Displayable> list = new ArrayList<Displayable>(al);
618 for (final Iterator<Displayable> it = list.iterator(); it.hasNext(); ) {
619 Object ob = it.next();
620 if (ob.getClass() != AreaList.class) it.remove();
621 if (null == base) {
622 base = (AreaList)ob;
623 if (null == base.layer_set) {
624 Utils.log2("AreaList.merge: null LayerSet for base AreaList.");
625 return null;
627 it.remove();
629 // check that it belongs to the same layer set as the base
630 AreaList ali = (AreaList)ob;
631 if (base.layer_set != ali.layer_set) it.remove();
633 if (list.size() < 1) return null; // nothing to fuse
634 for (final Iterator<Displayable> it = list.iterator(); it.hasNext(); ) {
635 // add to base
636 AreaList ali = (AreaList)it.next();
637 base.add(ali);
638 // remove from project
639 ali.project.removeProjectThing(ali, false, true, 1); // the node from the Project Tree
640 Utils.log2("Merged AreaList " + ali + " to base " + base);
642 // update
643 base.calculateBoundingBox(null);
644 // relink
645 base.linkPatches();
647 return base;
650 /** For each area that ali contains, add it to the corresponding area here.*/
651 private void add(final AreaList ali) {
652 for (final Map.Entry<Long,Area> entry : ali.ht_areas.entrySet()) {
653 Area ob_area = entry.getValue();
654 long lid = entry.getKey();
655 if (UNLOADED == ob_area) ob_area = ali.loadLayer(lid);
656 Area area = (Area)ob_area;
657 area = area.createTransformedArea(ali.at);
658 // now need to inverse transform it by this.at
659 try {
660 area = area.createTransformedArea(this.at.createInverse());
661 } catch (NoninvertibleTransformException nte) {
662 IJError.print(nte);
663 // do what?
665 Object this_area = this.ht_areas.get(entry.getKey());
666 if (UNLOADED == this_area) { this_area = loadLayer(lid); }
667 if (null == this_area) this.ht_areas.put(entry.getKey(), (Area)area.clone());
668 else ((Area)this_area).add(area);
669 updateInDatabase("points=" + ((Long)entry.getKey()).intValue());
673 /** How many layers does this object paint to. */
674 public int getNAreas() { return ht_areas.size(); }
676 public Area getArea(Layer la) {
677 if (null == la) return null;
678 return getArea(la.getId());
680 public Area getArea(long layer_id) {
681 Object ob = ht_areas.get(new Long(layer_id));
682 if (null != ob) {
683 if (UNLOADED == ob) ob = loadLayer(layer_id);
684 return (Area)ob;
686 return null;
691 /** Performs a deep copy of this object, without the links. */
692 public Displayable clone(final Project pr, final boolean copy_id) {
693 final ArrayList<Long> al_ul = new ArrayList<Long>();
694 for (final Long lid : ht_areas.keySet()) { // TODO WARNING the layer ids are wrong if the project is different or copy_id is false! Should lookup closest layer by Z ...
695 al_ul.add(new Long(lid)); // clones of the Long that wraps layer id
697 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
698 final AreaList copy = new AreaList(pr, nid, null != title ? title.toString() : null, width, height, alpha, this.visible, new Color(color.getRed(), color.getGreen(), color.getBlue()), this.visible, al_ul, (AffineTransform)this.at.clone());
699 for (final Map.Entry<Long,Area> entry : copy.ht_areas.entrySet()) {
700 entry.setValue(new Area(this.ht_areas.get(entry.getKey())));
702 return copy;
706 public List<Point3f> generateTriangles(final double scale, final int resample) {
707 final HashMap<Layer,Area> areas = new HashMap<Layer,Area>();
708 for (final Map.Entry<Long,Area> e : ht_areas.entrySet()) {
709 areas.put(layer_set.getLayer((Long)e.getKey()), (Area)e.getValue());
711 return AreaUtils.generateTriangles(this, scale, resample, areas);
714 /** Directly place an Area for the specified layer. Keep in mind it will be added in this AreaList coordinate space, not the overall LayerSet coordinate space. Does not make it local, you should call calculateBoundingBox() after setting an area. */
715 public void setArea(final long layer_id, final Area area) {
716 if (null == area) return;
717 ht_areas.put(layer_id, area);
718 updateInDatabase("points=" + layer_id);
721 /** Add a copy of an Area object to the existing, if any, area object at Layer with layer_id as given, or if not existing, just set the copy as it. The area is expected in this AreaList coordinate space. Does not make it local, you should call calculateBoundingBox when done. */
722 public void addArea(final long layer_id, final Area area) {
723 if (null == area) return;
724 Area a = getArea(layer_id);
725 if (null == a) ht_areas.put(layer_id, new Area(area));
726 else a.add(area);
727 updateInDatabase("points=" + layer_id);
730 /** Adds the given ROI, which is expected in world/LayerSet coordinates, to the area present at Layer with id layer_id, or set it if none present yet. */
731 public void add(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException{
732 if (null == roi) return;
733 Area a = getArea(layer_id);
734 Area asr = M.getArea(roi).createTransformedArea(this.at.createInverse());
735 if (null == a) {
736 ht_areas.put(layer_id, asr);
737 } else {
738 a.add(asr);
739 ht_areas.put(layer_id, a);
741 calculateBoundingBox(null != layer_set ? layer_set.getLayer(layer_id) : null);
742 updateInDatabase("points=" + layer_id);
744 /** Subtracts the given ROI, which is expected in world/LayerSet coordinates, to the area present at Layer with id layer_id, or set it if none present yet. */
745 public void subtract(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException {
746 if (null == roi) return;
747 Area a = getArea(layer_id);
748 if (null == a) return;
749 a.subtract(M.getArea(roi).createTransformedArea(this.at.createInverse()));
750 calculateBoundingBox(null != layer_set ? layer_set.getLayer(layer_id) : null);
751 updateInDatabase("points=" + layer_id);
754 /** Subtracts the given ROI, and then creates a new AreaList with identical properties and the content of the subtracted part. Returns null if there is no intersection between sroi and the Area for layer_id. */
755 public AreaList part(final long layer_id, final ShapeRoi sroi) throws NoninvertibleTransformException {
756 // The Area to subtract, in world coordinates:
757 Area sub = M.getArea(sroi);
758 // The area to subtract from:
759 Area a = getArea(layer_id);
760 if (null == a || M.isEmpty(a)) return null;
761 // The intersection:
762 Area inter = a.createTransformedArea(this.at);
763 inter.intersect(sub);
764 if (M.isEmpty(inter)) return null;
766 // Subtract from this:
767 this.subtract(layer_id, sroi);
769 // Create new AreaList with the intersection area, and add it to the same LayerSet as this:
770 AreaList ali = new AreaList(this.project, this.title, 0, 0);
771 ali.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
772 ali.visible = this.visible;
773 ali.alpha = this.alpha;
774 ali.addArea(layer_id, inter);
775 this.layer_set.add(ali); // needed to call updateBucket
776 ali.calculateBoundingBox(null != layer_set ? layer_set.getLayer(layer_id) : null);
778 return ali;
781 public void keyPressed(KeyEvent ke) {
782 Object source = ke.getSource();
783 if (! (source instanceof DisplayCanvas)) return;
784 DisplayCanvas dc = (DisplayCanvas)source;
785 Layer layer = dc.getDisplay().getLayer();
786 int keyCode = ke.getKeyCode();
787 long layer_id = layer.getId();
789 if (KeyEvent.VK_K == keyCode) {
790 Roi roi = dc.getFakeImagePlus().getRoi();
791 if (null == roi) return;
792 if (!M.isAreaROI(roi)) {
793 Utils.log("AreaList only accepts region ROIs, not lines.");
794 return;
796 ShapeRoi sroi = new ShapeRoi(roi);
798 try {
799 AreaList p = part(layer_id, sroi);
800 if (null != p) {
801 project.getProjectTree().addSibling(this, p);
803 Display.repaint(layer, getBoundingBox(), 5);
804 linkPatches();
805 } catch (NoninvertibleTransformException nite) { IJError.print(nite); }
806 ke.consume();
807 } else {
808 Area a = getArea(layer_id);
809 if (null == a) {
810 a = new Area();
811 ht_areas.put(layer_id, a);
813 new AreaWrapper(this, a).keyPressed(ke, dc, layer);
817 /** Measure the volume (in voxels) of this AreaList,
818 * and the surface area, the latter estimated as the number of voxels that
819 * make the outline.
821 * If the width and height of this AreaList cannot be fit in memory, scaled down versions will be used,
822 * and thus the results will be approximate.
824 * */
825 public String getInfo() {
826 if (0 == ht_areas.size()) return "Empty AreaList " + this.toString();
827 final double[] m = measure();
828 return new StringBuilder("Volume: ").append(IJ.d2s(m[0], 2))
829 .append(" Lower Bound Surface: ").append(IJ.d2s(m[1], 2))
830 .append(" Upper Bound Surface Smoothed: ").append(IJ.d2s(m[2], 2))
831 .append(" Upper Bound Surface: ").append(IJ.d2s(m[3], 2))
832 .append(" Maximum diameter: ").append(IJ.d2s(m[4], 2))
833 .toString();
836 /** @param area is expected in world coordinates. */
837 public boolean intersects(final Area area, final double z_first, final double z_last) {
838 for (Map.Entry<Long,Area> entry : ht_areas.entrySet()) {
839 final Layer layer = layer_set.getLayer(((Long)entry.getKey()).longValue());
840 if (layer.getZ() >= z_first && layer.getZ() <= z_last) {
841 Area a = ((Area)entry.getValue()).createTransformedArea(this.at);
842 a.intersect(area);
843 Rectangle r = a.getBounds();
844 if (0 != r.width && 0 != r.height) return true;
847 return false;
850 /** Export all given AreaLists as one per pixel value, what is called a "labels" file; a file dialog is offered to save the image as a tiff stack. */
851 static public void exportAsLabels(final List<Displayable> listToPaint, final ij.gui.Roi roi, final float scale, int first_layer, int last_layer, final boolean visible_only, final boolean to_file, final boolean as_amira_labels) {
852 // survive everything:
853 if (null == listToPaint || 0 == listToPaint.size()) {
854 Utils.log("Null or empty list.");
855 return;
857 if (scale < 0 || scale > 1) {
858 Utils.log("Improper scale value. Must be 0 < scale <= 1");
859 return;
862 // Select the subset to paint
863 final ArrayList<AreaList> list = new ArrayList<AreaList>();
864 for (final Displayable d : listToPaint) {
865 if (visible_only && !d.isVisible()) continue;
866 if (d instanceof AreaList) list.add((AreaList)d);
869 Utils.log2("exportAsLabels: list.size() is " + list.size());
871 // Current AmiraMeshEncoder supports ByteProcessor only: 256 labels max, including background at zero.
872 if (as_amira_labels && list.size() > 255) {
873 Utils.log("Saving ONLY first 255 AreaLists!\nDiscarded:");
874 final StringBuilder sb = new StringBuilder();
875 for (final Displayable d : list.subList(255, list.size())) {
876 sb.append(" ").append(d.getProject().getShortMeaningfulTitle(d)).append('\n');
878 Utils.log(sb.toString());
879 ArrayList<AreaList> li = new ArrayList<AreaList>(list);
880 list.clear();
881 list.addAll(li.subList(0, 255));
884 String path = null;
885 if (to_file) {
886 String ext = as_amira_labels ? ".am" : ".tif";
887 File f = Utils.chooseFile("labels", ext);
888 if (null == f) return;
889 path = f.getAbsolutePath().replace('\\','/');
892 LayerSet layer_set = list.get(0).getLayerSet();
893 if (first_layer > last_layer) {
894 int tmp = first_layer;
895 first_layer = last_layer;
896 last_layer = tmp;
897 if (first_layer < 0) first_layer = 0;
898 if (last_layer >= layer_set.size()) last_layer = layer_set.size()-1;
900 // Create image according to roi and scale
901 final int width, height;
902 final Rectangle broi;
903 if (null == roi) {
904 broi = null;
905 width = (int)(layer_set.getLayerWidth() * scale);
906 height = (int)(layer_set.getLayerHeight() * scale);
907 } else {
908 broi = roi.getBounds();
909 width = (int)(broi.width * scale);
910 height = (int)(broi.height * scale);
913 // Compute highest label value, which affects of course the stack image type
914 TreeSet<Integer> label_values = new TreeSet<Integer>();
915 for (final Displayable d : list) {
916 String label = d.getProperty("label");
917 if (null != label) label_values.add(Integer.parseInt(label));
919 int lowest=0, highest=0;
920 if (label_values.size() > 0) {
921 lowest = label_values.first();
922 highest = label_values.last();
924 int n_non_labeled = list.size() - label_values.size();
925 int max_label_value = highest + n_non_labeled;
927 int type_ = ImagePlus.GRAY8;
928 if (max_label_value > 255) {
929 type_ = ImagePlus.GRAY16;
930 if (max_label_value > 65535) {
931 type_ = ImagePlus.GRAY32;
934 final int type = type_;
936 final ImageStack stack = new ImageStack(width, height);
938 Calibration cal = layer_set.getCalibration();
940 String amira_params = null;
941 if (as_amira_labels) {
942 final StringBuilder sb = new StringBuilder("CoordType \"uniform\"\nMaterials {\nExterior {\n Id 0,\nColor 0 0 0\n}\n");
943 final float[] c = new float[3];
944 int value = 0;
945 for (final Displayable d : list) {
946 value++; // 0 is background
947 d.getColor().getRGBColorComponents(c);
948 String s = d.getProject().getShortMeaningfulTitle(d);
949 s = s.replace('-', '_').replaceAll(" #", " id");
950 sb.append(Utils.makeValidIdentifier(s)).append(" {\n")
951 .append("Id ").append(value).append(",\n")
952 .append("Color ").append(c[0]).append(' ').append(c[1]).append(' ').append(c[2]).append("\n}\n");
954 sb.append("}\n");
955 amira_params = sb.toString();
958 final float len = last_layer - first_layer + 1;
960 // Assign labels
961 final HashMap<AreaList,Integer> labels = new HashMap<AreaList,Integer>();
962 for (final AreaList d : list) {
963 String slabel = d.getProperty("label");
964 int label;
965 if (null != slabel) {
966 label = Integer.parseInt(slabel);
967 } else {
968 label = (++highest); // 0 is background
970 labels.put(d, label);
973 final ExecutorService exec = Utils.newFixedThreadPool("labels");
974 final Map<Integer,ImageProcessor> slices = Collections.synchronizedMap(new TreeMap<Integer,ImageProcessor>());
975 final List<Future<?>> fus = new ArrayList<Future<?>>();
976 final List<Layer> layers = layer_set.getLayers().subList(first_layer, last_layer+1);
978 for (int k = 0; k < layers.size(); k++) {
979 final Layer la = layers.get(k);
980 final int slice = k;
981 fus.add(exec.submit(new Runnable() {
982 public void run() {
983 Utils.showProgress(slice / len);
985 final ImageProcessor ip;
987 if (ImagePlus.GRAY8 == type) {
988 final BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
989 final Graphics2D g = bi.createGraphics();
991 for (final AreaList ali : list) {
992 final Area area = ali.getArea(la);
993 if (null == area || area.isEmpty()) continue;
994 // Transform: the scale and the roi
995 final AffineTransform aff = new AffineTransform();
996 // reverse order of transformations:
997 /* 3 - To scale: */ if (1 != scale) aff.scale(scale, scale);
998 /* 2 - To roi coordinates: */ if (null != broi) aff.translate(-broi.x, -broi.y);
999 /* 1 - To world coordinates: */ aff.concatenate(ali.at);
1000 g.setTransform(aff);
1001 final int label = labels.get(ali);
1002 g.setColor(new Color(label, label, label));
1003 g.fill(area);
1005 g.dispose();
1006 ip = new ByteProcessor(bi);
1007 bi.flush();
1009 } else if (ImagePlus.GRAY16 == type) {
1010 final USHORTPaint paint = new USHORTPaint((short)0);
1011 final BufferedImage bi = new BufferedImage(paint.getComponentColorModel(), paint.getComponentColorModel().createCompatibleWritableRaster(width, height), false, null);
1012 final Graphics2D g = bi.createGraphics();
1013 //final ColorSpace ugray = ColorSpace.getInstance(ColorSpace.CS_GRAY);
1015 int painted = 0;
1017 for (final AreaList ali : list) {
1018 final Area area = ali.getArea(la);
1019 if (null == area || area.isEmpty()) continue;
1020 // Transform: the scale and the roi
1021 final AffineTransform aff = new AffineTransform();
1022 // reverse order of transformations:
1023 /* 3 - To scale: */ if (1 != scale) aff.scale(scale, scale);
1024 /* 2 - To roi coordinates: */ if (null != broi) aff.translate(-broi.x, -broi.y);
1025 /* 1 - To world coordinates: */ aff.concatenate(ali.at);
1026 // Fill
1027 g.setTransform(aff);
1029 // The color doesn't work: paints in a stretched 8-bit mode
1030 //g.setColor(new Color(ugray, new float[]{((float)labels.get(d)) / range}, 1));
1032 Utils.log2("value: " + labels.get(ali).shortValue());
1033 paint.setValue(labels.get(ali).shortValue());
1034 g.setPaint(paint);
1036 g.fill(area); //.createTransformedArea(aff));
1038 painted += 1;
1040 g.dispose();
1041 ip = new ShortProcessor(bi);
1042 bi.flush();
1044 Utils.log2("painted: " + painted);
1046 } else {
1047 // Option 1: could use the same as above, but shifted by 65536, so that 65537 is 1, 65538 is 2, etc.
1048 // and keep doing it until no more need to be shifted.
1049 // The PROBLEM: cannot keep the order without complicated gymnastics to remember
1050 // which label in which image has to be merged to the final image, which prevent
1051 // a simple one-pass blitter.
1053 // Option 2: paint each arealist, extract the image, use it as a mask for filling:
1055 final FloatProcessor fp = new FloatProcessor(width, height);
1056 final float[] fpix = (float[]) fp.getPixels();
1057 ip = fp;
1059 final BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
1060 final Graphics2D gbi = bi.createGraphics();
1062 for (final AreaList ali : list) {
1063 final Area area = ali.getArea(la);
1064 if (null == area || area.isEmpty()) {
1065 continue;
1067 // Transform: the scale and the roi
1068 // reverse order of transformations:
1069 final AffineTransform aff = new AffineTransform();
1070 /* 3 - To scale: */ if (1 != scale) aff.scale(scale, scale);
1071 /* 2 - To ROI coordinates: */ if (null != broi) aff.translate(-broi.x, -broi.y);
1072 /* 1 - To world coordinates: */ aff.concatenate(ali.at);
1073 final Area s = area.createTransformedArea(aff);
1074 final Rectangle sBounds = s.getBounds();
1075 // Need to paint at all?
1076 if (0 == sBounds.width || 0 == sBounds.height || !sBounds.intersects(0, 0, width, height)) continue;
1077 // Paint shape
1078 gbi.setColor(Color.white);
1079 gbi.fill(s);
1080 // Read out painted region
1081 final int x0 = Math.max(0, sBounds.x);
1082 final int y0 = Math.max(0, sBounds.y);
1083 final int xN = Math.min(width, sBounds.x + sBounds.width);
1084 final int yN = Math.min(height, sBounds.y + sBounds.height);
1085 // Get the array
1086 final byte[] bpix = ((DataBufferByte)bi.getRaster().getDataBuffer()).getData();
1087 final float value = labels.get(ali);
1088 // For every non-black pixel, set a 'value' pixel in the FloatProcessor
1089 for (int y = y0; y < yN; ++y) {
1090 for (int x = x0; x < xN; ++x) {
1091 final int pos = y * width + x;
1092 if (0 == bpix[pos]) continue; // black
1093 fpix[pos] = value;
1096 // Clear image region
1097 gbi.setColor(Color.black);
1098 gbi.fill(s);
1100 gbi.dispose();
1101 bi.flush();
1104 slices.put(slice, ip);
1106 }));
1109 Utils.wait(fus);
1110 exec.shutdownNow();
1112 for (final Map.Entry<Integer,ImageProcessor> e : slices.entrySet()) {
1113 final Layer la = layers.get(e.getKey());
1114 stack.addSlice(la.getZ() * cal.pixelWidth + "", e.getValue());
1115 if (ImagePlus.GRAY8 != type) {
1116 e.getValue().setMinAndMax(lowest, highest);
1120 Utils.showProgress(1);
1121 // Save via file dialog:
1122 ImagePlus imp = new ImagePlus("Labels", stack);
1123 if (as_amira_labels) imp.setProperty("Info", amira_params);
1124 imp.setCalibration(layer_set.getCalibrationCopy());
1125 if (to_file) {
1126 if (as_amira_labels) {
1127 AmiraMeshEncoder ame = new AmiraMeshEncoder(path);
1128 if (!ame.open()) {
1129 Utils.log("Could not write to file " + path);
1130 return;
1132 if (!ame.write(imp)) {
1133 Utils.log("Error in writing Amira file!");
1134 return;
1136 } else {
1137 new FileSaver(imp).saveAsTiff(path);
1139 } else imp.show();
1142 @Override
1143 public ResultsTable measure(ResultsTable rt) {
1144 if (0 == ht_areas.size()) return rt;
1145 if (null == rt) rt = Utils.createResultsTable("AreaList results",
1146 new String[]{"id", "volume", "LB-surface", "UBs-surface",
1147 "UB-surface", "AVGs-surface", "AVG-surface", "max diameter",
1148 "Sum of tops", "name-id", "Xcm", "Ycm", "Zcm"});
1149 rt.incrementCounter();
1150 rt.addLabel("units", layer_set.getCalibration().getUnit());
1151 rt.addValue(0, this.id);
1152 double[] m = measure();
1153 rt.addValue(1, m[0]); // aprox. volume
1154 rt.addValue(2, m[1]); // lower bound surface
1155 rt.addValue(3, m[2]); // upper bound surface smoothed
1156 rt.addValue(4, m[3]); // upper bound surface
1157 rt.addValue(5, (m[1] + m[2]) / 2); // average of LB and UBs
1158 rt.addValue(6, (m[1] + m[3]) / 2); // average of LB and UB
1159 rt.addValue(7, m[4]); // max diameter
1160 rt.addValue(8, m[5]); // sum of all cappings (tops and bottoms)
1161 rt.addValue(9, getNameId());
1162 rt.addValue(10, m[6]); // X of the center of mass
1163 rt.addValue(11, m[7]); // Y
1164 rt.addValue(12, m[8]); // Z
1165 return rt;
1168 /** Returns a double array with 0=volume, 1=lower_bound_surface, 2=upper_bound_surface_smoothed, 3=upper_bound_surface, 4=max_diameter, 5=all_tops_and_bottoms
1169 * All measures are approximate.
1170 * [0] Volume: sum(area * thickness) for all sections
1171 * [1] Lower Bound Surface: measure area per section, compute radius of circumference of identical area, compute then area of the sides of the truncated cone of height thickness, for each section. Plus top and bottom areas when visiting sections without a painted area.
1172 * [2] Upper Bound Surface Smoothed: measure smoothed perimeter lengths per section, multiply by thickness to get lateral area. Plus tops and bottoms.
1173 * [3] Upper Bound Surface: measure raw pixelated perimeter lengths per section, multiply by thickness to get lateral area. Plus top and bottoms.
1174 * [4] Maximum diameter: longest distance between any two points in the contours of all painted areas.
1175 * [5] All tops and bottoms: Sum of all included surface areas that are not part of side area.
1176 * [6] X coordinate of the center of mass.
1177 * [7] Y coordinate of the center of mass.
1178 * [8] Z coordinate of the center of mass. */
1179 public double[] measure() {
1180 if (0 == ht_areas.size()) return new double[6]; // zeros
1182 // prepare suitable transform
1183 AffineTransform aff = new AffineTransform(this.at);
1184 AffineTransform aff2 = new AffineTransform();
1185 // remove translation (for no reason other than historical, and that it may
1186 // help avoid numerical overflows)
1187 Rectangle box = getBoundingBox(null);
1188 aff2.translate(-box.x, -box.y);
1189 aff.preConcatenate(aff2);
1190 aff2 = null;
1192 double volume = 0;
1193 double lower_bound_surface_h = 0;
1194 double upper_bound_surface = 0;
1195 double upper_bound_surface_smoothed = 0;
1196 double prev_surface = 0;
1197 double prev_perimeter = 0;
1198 double prev_smooth_perimeter = 0;
1199 double prev_thickness = 0;
1200 double all_tops_and_bottoms = 0; // i.e. surface area that is not part of the side area
1202 Calibration cal = layer_set.getCalibration();
1203 final double pixelWidth = cal.pixelWidth;
1204 final double pixelHeight = cal.pixelHeight;
1206 // Put areas in order of their layer index:
1207 final TreeMap<Integer,Area> ias = new TreeMap<Integer,Area>();
1208 for (final Map.Entry<Long,Area> e : ht_areas.entrySet()) {
1209 int ilayer = layer_set.indexOf(layer_set.getLayer(e.getKey()));
1210 if (-1 == ilayer) {
1211 Utils.log("Could not find a layer with id " + e.getKey());
1212 continue;
1214 ias.put(ilayer, e.getValue());
1217 ArrayList<Layer> layers = layer_set.getLayers();
1219 int last_layer_index = -1;
1221 ArrayList<Point3f> points = new ArrayList<Point3f>();
1222 final float[] coords = new float[6];
1223 final float fpixelWidth = (float) pixelWidth;
1224 final float fpixelHeight = (float) pixelHeight;
1226 final float resampling_delta = project.getProperty("measurement_resampling_delta", 1.0f);
1228 // for each area, measure its area and its perimeter, to compute volume and surface
1229 for (final Map.Entry<Integer,Area> e : ias.entrySet()) {
1231 // fetch Layer
1232 int layer_index = e.getKey();
1233 if (layer_index > layers.size()) {
1234 Utils.log("Could not find a layer at index " + layer_index);
1235 continue;
1238 final Layer la = layers.get(layer_index);
1240 // fetch Area
1241 Area area = e.getValue();
1242 if (UNLOADED == area) area = loadLayer(la.getId());
1243 // Transform area to world coordinates
1244 area = area.createTransformedArea(aff);
1246 // measure surface
1247 double pixel_area = Math.abs(AreaCalculations.area(area.getPathIterator(null)));
1248 double surface = pixel_area * pixelWidth * pixelHeight;
1250 //Utils.log2(layer_index + " pixel_area: " + pixel_area + " surface " + surface);
1252 // measure volume
1253 double thickness = la.getThickness() * pixelWidth;// the last one is NOT pixelDepth because layer thickness and Z are in pixels
1254 volume += surface * thickness;
1256 double pix_perimeter = AreaCalculations.circumference(area.getPathIterator(null));
1257 double perimeter = pix_perimeter * pixelWidth;
1259 double smooth_perimeter = 0;
1261 // smoothed perimeter:
1263 double smooth_pix_perimeter = 0;
1264 for (final Polygon pol : M.getPolygons(area)) {
1266 try {
1267 // Should use VectorString2D, but takes for ever -- bug in resample?
1268 // And VectorString3D is likely not respecting the 'closed' flag for resampling.
1269 // Also, VectorString3D gets stuck in an infinite loop if the sequence is 6 points!
1270 //VectorString3D v = new VectorString3D(xp, yp, new double[pol.npoints], true);
1274 if (pol.npoints < 5) {
1275 // No point in smoothing out such a short polygon:
1276 // (Plus can't convolve it with a gaussian that needs 5 points adjacent)
1277 smooth_perimeter += new PolygonRoi(pol, PolygonRoi.POLYGON).getLength();
1278 continue;
1281 // Works but it is not the best smoothing of the Area's countour
1282 double[] xp = new double[pol.npoints];
1283 double[] yp = new double[pol.npoints];
1284 for (int p=0; p<pol.npoints; p++) {
1285 xp[p] = pol.xpoints[p];
1286 yp[p] = pol.ypoints[p];
1288 VectorString2D v = new VectorString2D(xp, yp, 0, true);
1289 v.resample(resampling_delta);
1290 smooth_pix_perimeter += v.length() * resampling_delta;
1293 // The best solution I've found:
1294 // 1. Run getInterpolatedPolygon with an interval of 1 to get a point at every pixel
1295 // 2. convolve with a gaussian
1296 // Resample to 1 so that at every one pixel of the contour there is a point
1297 FloatPolygon fpol = new FloatPolygon(new float[pol.npoints], new float[pol.npoints], pol.npoints);
1298 for (int i=0; i<pol.npoints; ++i) {
1299 fpol.xpoints[i] = pol.xpoints[i];
1300 fpol.ypoints[i] = pol.ypoints[i];
1302 fpol = M.createInterpolatedPolygon(fpol, 1, false);
1303 final FloatPolygon fp;
1304 if (fpol.npoints < 5) {
1305 smooth_pix_perimeter += fpol.getLength(false);
1306 fp = fpol;
1307 } else {
1308 // Convolve with a sigma of 1 to smooth it out
1309 final FloatPolygon gpol = new FloatPolygon(new float[fpol.npoints], new float[fpol.npoints], fpol.npoints);
1310 final CircularSequence seq = new CircularSequence(fpol.npoints);
1311 M.convolveGaussianSigma1(fpol.xpoints, gpol.xpoints, seq);
1312 M.convolveGaussianSigma1(fpol.ypoints, gpol.ypoints, seq);
1313 // Resample it to the desired resolution (also facilitates measurement: npoints * resampling_delta)
1314 if (gpol.npoints > resampling_delta) {
1315 fp = M.createInterpolatedPolygon(gpol, resampling_delta, false);
1316 } else {
1317 fp = gpol;
1319 // Measure perimeter: last line segment is potentially shorter or longer than resampling_delta
1320 smooth_pix_perimeter += (fp.npoints -1) * resampling_delta
1321 + Math.sqrt( Math.pow(fp.xpoints[0] - fp.xpoints[fp.npoints-1], 2)
1322 + Math.pow(fp.ypoints[0] - fp.ypoints[fp.npoints-1], 2));
1325 // TEST:
1326 //ij.plugin.frame.RoiManager.getInstance().addRoi(new PolygonRoi(fp, PolygonRoi.POLYGON));
1328 // TESTING: make a polygon roi and show it
1329 // ... just in case to see that resampling works as expected, without weird endings
1331 int[] x = new int[v.length()];
1332 int[] y = new int[x.length];
1333 double[] xd = v.getPoints(0);
1334 double[] yd = v.getPoints(1);
1335 for (int p=0; p<x.length; p++) {
1336 x[p] = (int)xd[p];
1337 y[p] = (int)yd[p];
1339 PolygonRoi proi = new PolygonRoi(x, y, x.length, PolygonRoi.POLYGON);
1340 Rectangle b = proi.getBounds();
1341 for (int p=0; p<x.length; p++) {
1342 x[p] -= b.x;
1343 y[p] -= b.y;
1345 ImagePlus imp = new ImagePlus("test", new ByteProcessor(b.width, b.height));
1346 imp.setRoi(new PolygonRoi(x, y, x.length, PolygonRoi.POLYGON));
1347 imp.show();
1349 } catch (Exception le) { le.printStackTrace(); }
1352 smooth_perimeter = smooth_pix_perimeter * pixelWidth;
1355 //Utils.log2("p, sp: " + perimeter + ", " + smooth_perimeter);
1357 //Utils.log2(layer_index + " pixelWidth,pixelHeight: " + pixelWidth + ", " + pixelHeight);
1358 //Utils.log2(layer_index + " thickness: " + thickness);
1360 //Utils.log2(layer_index + " pix_perimeter: " + pix_perimeter + " perimeter: " + perimeter);
1362 if (-1 == last_layer_index) {
1363 // Start of the very first continuous set:
1364 lower_bound_surface_h += surface;
1365 upper_bound_surface += surface;
1366 upper_bound_surface_smoothed += surface;
1367 all_tops_and_bottoms += surface;
1368 } else if (layer_index - last_layer_index > 1) {
1369 // End of a continuous set ...
1370 // Sum the last surface and its side:
1371 lower_bound_surface_h += prev_surface + prev_thickness * 2 * Math.sqrt(prev_surface * Math.PI); // (2x + 2x) / 2 == 2x
1372 upper_bound_surface += prev_surface + prev_perimeter * prev_thickness;
1373 upper_bound_surface_smoothed += prev_surface + prev_smooth_perimeter * prev_thickness;
1374 all_tops_and_bottoms += prev_surface;
1376 // ... and start of a new set
1377 lower_bound_surface_h += surface;
1378 upper_bound_surface += surface;
1379 upper_bound_surface_smoothed += surface;
1380 all_tops_and_bottoms += surface;
1381 } else {
1382 // Continuation of a set: use this Area and the previous as continuous
1383 double diff_surface = Math.abs(prev_surface - surface);
1385 upper_bound_surface += prev_perimeter * (prev_thickness / 2)
1386 + perimeter * (prev_thickness / 2)
1387 + diff_surface;
1389 upper_bound_surface_smoothed += prev_smooth_perimeter * (prev_thickness / 2)
1390 + smooth_perimeter * (prev_thickness / 2)
1391 + diff_surface;
1393 // Compute area of the mantle of the truncated cone defined by the radiuses of the circles of same area as the two areas
1394 // PI * s * (r1 + r2) where s is the hypothenusa
1395 double r1 = Math.sqrt(prev_surface / Math.PI);
1396 double r2 = Math.sqrt(surface / Math.PI);
1397 double hypothenusa = Math.sqrt(Math.pow(Math.abs(r1 - r2), 2) + Math.pow(thickness, 2));
1398 lower_bound_surface_h += Math.PI * hypothenusa * (r1 + r2);
1400 // Adjust volume too:
1401 volume += diff_surface * prev_thickness / 2;
1404 // store for next iteration:
1405 prev_surface = surface;
1406 prev_perimeter = perimeter;
1407 prev_smooth_perimeter = smooth_perimeter;
1408 last_layer_index = layer_index;
1409 prev_thickness = thickness;
1411 // Iterate points:
1412 final float z = (float) la.getZ();
1413 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); pit.next()) {
1414 switch (pit.currentSegment(coords)) {
1415 case PathIterator.SEG_MOVETO:
1416 case PathIterator.SEG_LINETO:
1417 case PathIterator.SEG_CLOSE:
1418 points.add(new Point3f(coords[0] * fpixelWidth, coords[1] * fpixelHeight, z * fpixelWidth));
1419 break;
1420 default:
1421 Utils.log2("WARNING: unhandled seg type.");
1422 break;
1427 // finish last:
1428 lower_bound_surface_h += prev_surface + prev_perimeter * prev_thickness;
1429 upper_bound_surface += prev_surface + prev_perimeter * prev_thickness;
1430 upper_bound_surface_smoothed += prev_surface + prev_smooth_perimeter * prev_thickness;
1431 all_tops_and_bottoms += prev_surface;
1433 // Compute maximum diameter
1434 final boolean measure_largest_diameter = project.getBooleanProperty("measure_largest_diameter");
1435 double max_diameter_sq = measure_largest_diameter ? 0 : Double.NaN;
1436 final int lp = points.size();
1437 final Point3f c;
1438 if (lp > 0) {
1439 c = new Point3f(points.get(0)); // center of mass
1440 for (int i=0; i<lp; i++) {
1441 final Point3f p = points.get(i);
1442 if (measure_largest_diameter) {
1443 for (int j=i; j<lp; j++) {
1444 double len = p.distanceSquared(points.get(j));
1445 if (len > max_diameter_sq) max_diameter_sq = len;
1448 if (0 == i) continue;
1449 c.x += p.x;
1450 c.y += p.y;
1451 c.z += p.z;
1453 } else {
1454 c = new Point3f(Float.NaN, Float.NaN, Float.NaN);
1456 // Translate the center of mass
1457 c.x = box.x + c.x / lp;
1458 c.y = box.y + c.y / lp;
1459 c.z /= lp;
1461 return new double[]{volume, lower_bound_surface_h, upper_bound_surface_smoothed,
1462 upper_bound_surface, Math.sqrt(max_diameter_sq), all_tops_and_bottoms,
1463 c.x, c.y, c.z};
1466 @Override
1467 Class<?> getInternalDataPackageClass() {
1468 return DPAreaList.class;
1471 @Override
1472 Object getDataPackage() {
1473 // The width,height,links,transform and list of areas
1474 return new DPAreaList(this);
1477 static private final class DPAreaList extends Displayable.DataPackage {
1478 final protected HashMap<Long,Area> ht;
1479 DPAreaList(final AreaList ali) {
1480 super(ali);
1481 this.ht = new HashMap<Long,Area>();
1482 for (final Map.Entry<Long,Area> e : ali.ht_areas.entrySet()) {
1483 this.ht.put(e.getKey(), new Area(e.getValue()));
1486 final boolean to2(final Displayable d) {
1487 super.to1(d);
1488 final AreaList ali = (AreaList)d;
1489 ali.ht_areas.clear();
1490 for (final Map.Entry<Long,Area> e : ht.entrySet()) {
1491 ali.ht_areas.put(e.getKey(), new Area(e.getValue()));
1493 return true;
1497 /** Retain the data within the layer range, and through out all the rest. */
1498 synchronized public boolean crop(List<Layer> range) {
1499 final Set<Long> lids = new HashSet<Long>();
1500 for (final Layer l : range) lids.add(l.getId());
1501 for (final Iterator<Long> it = ht_areas.keySet().iterator(); it.hasNext(); ) {
1502 if (!lids.contains(it.next())) it.remove();
1504 calculateBoundingBox(null);
1505 return true;
1508 /** Returns a stack of images representing the pixel data of this LayerSet inside this AreaList. */
1509 public ImagePlus getStack(final int type, final double scale) {
1510 ImageProcessor ref_ip = Utils.createProcessor(type, 2, 2);
1511 if (null == ref_ip) {
1512 Utils.log("AreaList.getStack: Unknown type " + type);
1513 return null;
1515 Rectangle b = getBoundingBox();
1516 int w = (int)(0.5 + b.width * scale);
1517 int h = (int)(0.5 + b.height * scale);
1518 ImageStack stack = new ImageStack(w, h);
1519 for (Layer la : getLayerRange()) {
1520 Area area = getArea(la);
1521 double z = layer.getZ();
1522 project.getLoader().releaseToFit(w * h * 10);
1523 ImageProcessor ip = ref_ip.createProcessor(w, h);
1524 if (null == area) {
1525 stack.addSlice(Double.toString(z), ip);
1526 continue;
1528 // Create a ROI from the area at Layer la:
1529 AffineTransform aff = getAffineTransformCopy();
1530 aff.translate(-b.x, -b.y);
1531 aff.scale(scale, scale);
1532 ShapeRoi roi = new ShapeRoi(area.createTransformedArea(aff));
1533 // Create a cropped snapshot of the images at Layer la under the area:
1534 ImageProcessor flat = Patch.makeFlatImage(type, la, b, scale, la.getAll(Patch.class), Color.black);
1535 flat.setRoi(roi);
1536 Rectangle rb = roi.getBounds();
1537 ip.insert(flat.crop(), rb.x, rb.y);
1538 // Clear the outside
1539 ImagePlus bimp = new ImagePlus("", ip);
1540 bimp.setRoi(roi);
1541 ip.setValue(0);
1542 ip.setBackgroundValue(0);
1543 IJ.run(bimp, "Clear Outside", "");
1545 stack.addSlice(Double.toString(z), ip);
1548 ImagePlus imp = new ImagePlus("AreaList stack for " + this, stack);
1549 imp.setCalibration(layer_set.getCalibrationCopy());
1550 return imp;
1553 public List<Area> getAreas(final Layer layer, final Rectangle box) {
1554 Area a = (Area) ht_areas.get(layer.getId());
1555 if (null == a) return null;
1556 ArrayList<Area> l = new ArrayList<Area>();
1557 l.add(a);
1558 return l;
1561 protected boolean layerRemoved(Layer la) {
1562 super.layerRemoved(la);
1563 ht_areas.remove(la.getId());
1564 return true;
1567 public boolean apply(final Layer la, final Area roi, final mpicbg.models.CoordinateTransform ct) throws Exception {
1568 final Area a = getArea(la);
1569 if (null == a) return true;
1570 final AffineTransform inverse = this.at.createInverse();
1571 if (M.intersects(a, roi.createTransformedArea(inverse))) {
1572 M.apply(M.wrap(this.at, ct, inverse), roi, a);
1573 calculateBoundingBox(la);
1575 return true;
1578 public boolean apply(final VectorDataTransform vdt) throws Exception {
1579 final Area a = getArea(vdt.layer);
1580 if (null == a) return true;
1581 M.apply(vdt.makeLocalTo(this), a);
1582 calculateBoundingBox(vdt.layer);
1583 return true;
1586 @Override
1587 synchronized public Collection<Long> getLayerIds() {
1588 return new ArrayList<Long>(ht_areas.keySet());
1591 /** In world coordinates, a copy of the area at {@param layer}. May be null. */
1592 @Override
1593 public Area getAreaAt(final Layer layer) {
1594 final Area a = getArea(layer);
1595 if (null == a) return null;
1596 return a.createTransformedArea(this.at);
1599 @Override
1600 public boolean isRoughlyInside(final Layer layer, final Rectangle box) {
1601 final Area a = getArea(layer);
1602 if (null == a) return false;
1604 final float[] coords = new float[6];
1605 final float precision = 0.0001f;
1606 for (final PathIterator pit = a.getPathIterator(this.at); !pit.isDone(); pit.next()) {
1607 switch (pit.currentSegment(coords)) {
1608 case PathIterator.SEG_MOVETO:
1609 case PathIterator.SEG_LINETO:
1610 case PathIterator.SEG_CLOSE:
1611 if (box.contains(coords[0], coords[1])) return true;
1612 break;
1613 default:
1614 break;
1617 return false;
1619 // The above is about 2x to 3x faster than:
1620 //return a.createTransformedArea(this.at).intersects(box);
1622 // But this is 3x faster even than using path iterator:
1623 try {
1624 return this.at.createInverse().createTransformedShape(box).intersects(a.getBounds());
1625 } catch (NoninvertibleTransformException nite) {
1626 IJError.print(nite);
1627 return false;
1631 @Override
1632 public ResultsTable measureAreas(ResultsTable rt) {
1633 if (0 == ht_areas.size()) return rt;
1634 if (null == rt) rt = Utils.createResultsTable("Area results", new String[]{"id", "name-id", "layer index", "area"});
1635 final double nameId = getNameId();
1636 final Calibration cal = layer_set.getCalibration();
1637 final String units = cal.getUnit();
1638 // Sort by Layer
1639 final TreeMap<Layer,Area> sm = new TreeMap<Layer, Area>(Layer.COMPARATOR);
1640 for (final Map.Entry<Long,Area> e : ht_areas.entrySet()) {
1641 sm.put(layer_set.getLayer(e.getKey()), e.getValue());
1643 for (final Map.Entry<Layer,Area> e : sm.entrySet()) {
1644 Area area = e.getValue();
1645 if (area.isEmpty()) continue;
1646 rt.incrementCounter();
1647 rt.addLabel("units", units);
1648 rt.addValue(0, this.id);
1649 rt.addValue(1, nameId);
1650 rt.addValue(2, layer_set.indexOf(e.getKey()) + 1); // 1-based
1651 // measure surface
1652 double pixel_area = Math.abs(AreaCalculations.area(area.createTransformedArea(this.at).getPathIterator(null)));
1653 double surface = pixel_area * cal.pixelWidth * cal.pixelHeight;
1654 rt.addValue(3, surface);
1656 return rt;
1659 /** Interpolate areas between the given first and last layers,
1660 * both of which must contain an area.
1662 * @return false if the interpolation could not be done.
1663 * @throws Exception */
1664 public boolean interpolate(final Layer first, final Layer last, final boolean always_use_distance_map) throws Exception {
1666 int i1 = layer_set.indexOf(first);
1667 int i2 = layer_set.indexOf(last);
1668 if (i1 > i2) {
1669 int tmp = i1;
1670 i1 = i2;
1671 i2 = tmp;
1673 final List<Layer> range = layer_set.getLayers().subList(i1, i2+1);
1675 Area start = null;
1676 int istart = 0;
1677 int inext = -1;
1679 final HashSet<Layer> touched = new HashSet<Layer>();
1681 for (final Layer la : range) {
1682 inext++;
1683 final Area next = getArea(la);
1684 if (null == next || next.isEmpty()) continue;
1685 if (null == start || 0 == inext - istart -1) { // skip for first area or for no space in between
1686 start = next;
1687 istart = inext;
1688 continue;
1690 // Interpolate from start to next
1691 Area[] as = always_use_distance_map? null : AreaUtils.singularInterpolation(start, next, inext - istart -1);
1693 if (null == as) {
1694 // NOT SINGULAR: must use BinaryInterpolation2D
1695 as = AreaUtils.manyToManyInterpolation(start, next, inext - istart -1);
1697 if (null != as) {
1698 for (int k=0; k<as.length; k++) {
1699 final Layer la2 = range.get(k + istart + 1);
1700 addArea(la2.getId(), as[k]);
1701 touched.add(la2);
1705 // Prepare next interval
1706 start = next;
1707 istart = inext;
1710 for (Layer la : touched) calculateBoundingBox(la);
1712 return true;