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.
23 package ini
.trakem2
.display
;
27 import ij
.gui
.OvalRoi
;
29 import ij
.gui
.ShapeRoi
;
30 import ij
.gui
.PolygonRoi
;
31 import ij
.gui
.Toolbar
;
32 import ij
.gui
.GenericDialog
;
33 import ij
.io
.FileSaver
;
34 import ij
.process
.ByteProcessor
;
35 import ij
.process
.ImageProcessor
;
36 import ij
.process
.FloatPolygon
;
39 import ij
.measure
.Calibration
;
40 import ij
.measure
.ResultsTable
;
42 import ini
.trakem2
.Project
;
43 import ini
.trakem2
.persistence
.DBObject
;
44 import ini
.trakem2
.utils
.ProjectToolbar
;
45 import ini
.trakem2
.utils
.IJError
;
46 import ini
.trakem2
.utils
.Utils
;
47 import ini
.trakem2
.utils
.M
;
48 import ini
.trakem2
.render3d
.Perimeter2D
;
49 import ini
.trakem2
.vector
.VectorString3D
;
51 import java
.awt
.Color
;
52 import java
.awt
.Rectangle
;
53 import java
.awt
.Point
;
54 import java
.awt
.Polygon
;
55 import java
.awt
.event
.MouseEvent
;
56 import java
.awt
.event
.KeyEvent
;
58 import java
.awt
.geom
.AffineTransform
;
59 import java
.awt
.geom
.Area
;
60 import java
.awt
.geom
.GeneralPath
;
61 import java
.awt
.geom
.NoninvertibleTransformException
;
62 import java
.awt
.geom
.PathIterator
;
63 import java
.awt
.geom
.Point2D
;
64 import java
.awt
.image
.BufferedImage
;
65 import java
.awt
.image
.DataBufferByte
;
66 import java
.awt
.Shape
;
67 import java
.awt
.Graphics
;
68 import java
.awt
.Graphics2D
;
69 import java
.awt
.Composite
;
70 import java
.awt
.AlphaComposite
;
71 import java
.awt
.RenderingHints
;
74 import marchingcubes
.MCTriangulator
;
75 import isosurface
.Triangulator
;
76 import amira
.AmiraMeshEncoder
;
77 import amira
.AmiraParameters
;
79 import javax
.vecmath
.Point3f
;
81 /** A list of brush painted areas similar to a set of labelfields in Amira.
83 * 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.
84 * All Area objects are local to this AreaList's AffineTransform.
86 public class AreaList
extends ZDisplayable
{
88 /** Contains the table of layer ids and their associated Area object.*/
89 private HashMap ht_areas
= new HashMap();
91 /** Flag to signal dynamic loading from the database for the Area of a given layer id in the ht_areas HashMap. */
92 static private Object UNLOADED
= new Object();
94 /** Flag to repaint faster even if the object is selected. */
95 static private boolean brushing
= false;
97 /** Paint as outlines (false) or as solid areas (true; default, with a default alpha of 0.4f).*/
98 private boolean fill_paint
= true;
100 public AreaList(Project project
, String title
, double x
, double y
) {
101 super(project
, title
, x
, y
);
106 /** Reconstruct from XML. */
107 public AreaList(Project project
, long id
, HashMap ht_attributes
, HashMap ht_links
) {
108 super(project
, id
, ht_attributes
, ht_links
);
109 // read the fill_paint
110 Object ob_data
= ht_attributes
.get("fill_paint");
112 if (null != ob_data
) this.fill_paint
= "true".equals(((String
)ob_data
).trim().toLowerCase()); // fails: //Boolean.getBoolean((String)ob_data);
113 } catch (Exception e
) {
114 Utils
.log("AreaList: could not read fill_paint value from XML:" + e
);
118 /** Reconstruct from the database. */
119 public AreaList(Project project
, long id
, String title
, double width
, double height
, float alpha
, boolean visible
, Color color
, boolean locked
, ArrayList al_ul
, AffineTransform at
) { // al_ul contains Long() wrapping layer ids
120 super(project
, id
, title
, locked
, at
, width
, height
);
122 this.visible
= visible
;
124 for (Iterator it
= al_ul
.iterator(); it
.hasNext(); ) {
125 ht_areas
.put(it
.next(), AreaList
.UNLOADED
); // assumes al_ul contains only Long instances wrapping layer_id long values
129 public void paint(final Graphics2D g
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
) {
130 Object ob
= ht_areas
.get(new Long(active_layer
.getId()));
131 if (null == ob
) return;
132 if (AreaList
.UNLOADED
== ob
) {
133 ob
= loadLayer(active_layer
.getId());
134 if (null == ob
) return;
136 final Area area
= (Area
)ob
;
137 g
.setColor(this.color
);
138 //arrange transparency
139 Composite original_composite
= null;
141 original_composite
= g
.getComposite();
142 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
145 if (fill_paint
) g
.fill(area
.createTransformedArea(this.at
));
146 else g
.draw(area
.createTransformedArea(this.at
)); // the contour only
151 final Area tmp
= last
.getTmpArea();
153 if (fill_paint
) g
.fill(tmp
.createTransformedArea(this.at
));
154 else g
.draw(tmp
.createTransformedArea(this.at
)); // won't be perfect except on mouse release
156 } catch (Exception e
) {}
159 //Transparency: fix alpha composite back to original.
160 if (null != original_composite
) {
161 g
.setComposite(original_composite
);
165 public void transformPoints(Layer layer
, double dx
, double dy
, double rot
, double xo
, double yo
) {
166 Utils
.log("AreaList.transformPoints: not implemented yet.");
169 /** Returns the layer of lowest Z coordinate Layer where this ZDisplayable has a point in. */
170 public Layer
getFirstLayer() {
171 double min_z
= Double
.MAX_VALUE
;
172 Layer first_layer
= null;
173 for (Iterator it
= ht_areas
.keySet().iterator(); it
.hasNext(); ) {
174 Layer la
= this.layer_set
.getLayer(((Long
)it
.next()).longValue());
175 double z
= la
.getZ();
184 /** Returns the layer of highest Z coordinate Layer where this ZDisplayable has a point in. */
185 public Layer
getLastLayer() {
186 double max_z
= -Double
.MAX_VALUE
;
187 Layer last_layer
= null;
188 for (Iterator it
= ht_areas
.keySet().iterator(); it
.hasNext(); ) {
189 Layer la
= this.layer_set
.getLayer(((Long
)it
.next()).longValue());
190 double z
= la
.getZ();
197 } // I do REALLY miss Lisp macros. Writting the above two methods in a lispy way would make the java code unreadable
199 public void linkPatches() {
200 unlinkAll(Patch
.class);
201 // cheap way: intersection of the patches' bounding box with the area
202 Rectangle r
= new Rectangle();
203 for (Iterator it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
204 Map
.Entry entry
= (Map
.Entry
)it
.next();
205 Layer la
= this.layer_set
.getLayer(((Long
)entry
.getKey()).longValue());
206 Area area
= (Area
)entry
.getValue();
207 area
= area
.createTransformedArea(this.at
);
208 for (Iterator dit
= la
.getDisplayables(Patch
.class).iterator(); dit
.hasNext(); ) {
209 Displayable d
= (Displayable
)dit
.next();
210 r
= d
.getBoundingBox(r
);
211 if (area
.intersects(r
)) {
218 /** Returns whether the point x,y is contained in this object at the given Layer. */
219 public boolean contains(final Layer layer
, final int x
, final int y
) {
220 Object ob
= ht_areas
.get(new Long(layer
.getId()));
221 if (null == ob
) return false;
222 if (AreaList
.UNLOADED
== ob
) {
223 ob
= loadLayer(layer
.getId());
224 if (null == ob
) return false;
226 Area area
= (Area
)ob
;
227 if (!this.at
.isIdentity()) area
= area
.createTransformedArea(this.at
);
228 return area
.contains(x
, y
);
231 public boolean intersects(final Layer layer
, final Rectangle r
) {
232 Object ob
= ht_areas
.get(layer
.getId());
233 if (null == ob
) return false;
234 if (AreaList
.UNLOADED
== ob
) {
235 ob
= loadLayer(layer
.getId());
236 if (null == ob
) return false;
238 final Area a
= ((Area
)ob
).createTransformedArea(this.at
);
239 return a
.intersects(r
.x
, r
.y
, r
.width
, r
.height
);
242 public boolean intersects(final Layer layer
, final Area area
) {
243 Object ob
= ht_areas
.get(layer
.getId());
244 if (null == ob
) return false;
245 if (AreaList
.UNLOADED
== ob
) {
246 ob
= loadLayer(layer
.getId());
247 if (null == ob
) return false;
249 final Area a
= ((Area
)ob
).createTransformedArea(this.at
);
251 final Rectangle b
= a
.getBounds();
252 return 0 != b
.width
&& 0 != b
.height
;
255 /** Returns the bounds of this object as it shows in the given layer. */
256 public Rectangle
getBounds(final Rectangle r
, final Layer layer
) {
257 if (null == layer
) return super.getBounds(r
, null);
258 final Area area
= (Area
)ht_areas
.get(layer
.getId());
260 if (null == r
) return new Rectangle();
267 final Rectangle b
= area
.createTransformedArea(this.at
).getBounds();
268 if (null == r
) return b
;
269 r
.setBounds(b
.x
, b
.y
, b
.width
, b
.height
);
273 public boolean isDeletable() {
274 if (0 == ht_areas
.size()) return true;
278 private boolean is_new
= false;
280 public void mousePressed(final MouseEvent me
, final int x_p_w
, final int y_p_w
, final double mag
) {
281 final long lid
= Display
.getFrontLayer(this.project
).getId(); // isn't this.layer pointing to the current layer always? It *should*
282 Object ob
= ht_areas
.get(new Long(lid
));
286 ht_areas
.put(new Long(lid
), area
);
288 this.width
= layer_set
.getLayerWidth(); // will be set properly at mouse release
289 this.height
= layer_set
.getLayerHeight(); // without this, the first brush slash doesn't get painted because the isOutOfRepaintingClip returns true
291 if (AreaList
.UNLOADED
== ob
) {
293 if (null == ob
) return;
298 // transform the x_p, y_p to the local coordinates
301 if (!this.at
.isIdentity()) {
302 final Point2D
.Double p
= inverseTransformPoint(x_p_w
, y_p_w
);
307 if (me
.isShiftDown()) {
308 // fill in a hole if the clicked point lays within one
310 if (area
.contains(x_p
, y_p
)) {
311 if (me
.isAltDown()) {
313 pol
= M
.findPath(area
, x_p
, y_p
); // no null check, exists for sure
314 area
.subtract(new Area(pol
));
316 } else if (!me
.isAltDown()) {
318 pol
= M
.findPath(area
, x_p
, y_p
);
320 area
.add(new Area(pol
)); // may not exist
324 final Rectangle r_pol
= transformRectangle(pol
.getBounds());
325 Display
.repaint(Display
.getFrontLayer(), r_pol
, 1);
326 updateInDatabase("points=" + lid
);
328 // An area in world coords:
330 // Try to find a hole in another visible AreaList, but fill it here
331 for (final ZDisplayable zd
: Display
.getFrontLayer(this.project
).getParent().getZDisplayables(AreaList
.class)) {
332 if ( ! zd
.isVisible()) continue;
333 final AreaList ali
= (AreaList
) zd
;
334 final Area a
= ali
.getArea(lid
);
335 if (null == a
) continue;
336 // bring point to zd space
337 final Point2D
.Double p
= ali
.inverseTransformPoint(x_p_w
, y_p_w
);
338 final Polygon polygon
= M
.findPath(a
, (int)p
.x
, (int)p
.y
);
339 if (null != polygon
) {
340 // Bring polygon to world coords
341 b
= new Area(polygon
).createTransformedArea(ali
.at
);
345 // If nothing found, try to merge all visible areas in current layer and find a hole there
347 final Area all
= new Area(); // in world coords
348 for (final ZDisplayable zd
: Display
.getFrontLayer(this.project
).getParent().getZDisplayables(AreaList
.class)) {
349 if ( ! zd
.isVisible()) continue;
350 final AreaList ali
= (AreaList
) zd
;
351 final Area a
= ali
.getArea(lid
);
352 if (null == a
) continue;
353 all
.add(a
.createTransformedArea(ali
.at
));
355 final Polygon polygon
= M
.findPath(all
, x_p_w
, y_p_w
); // in world coords
356 if (null != polygon
) {
357 b
= new Area(polygon
);
362 // Add b as local to this AreaList
363 area
.add(b
.createTransformedArea(this.at
.createInverse()));
364 Display
.repaint(Display
.getFrontLayer(this.project
), b
.getBounds(), 1); // use b, in world coords
365 } catch (NoninvertibleTransformException nite
) { IJError
.print(nite
); }
369 if (null != last
) last
.quit();
370 last
= new BrushThread(area
, mag
);
374 public void mouseDragged(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_d_old
, int y_d_old
) {
375 // nothing, the BrushThread handles it
376 //irrelevant//if (ProjectToolbar.getToolId() == ProjectToolbar.PEN) brushing = true;
378 public void mouseReleased(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_r
, int y_r
) {
381 //Utils.log("AreaList mouseReleased: no brushing");
389 long lid
= Display
.getFrontLayer(this.project
).getId();
390 Object ob
= ht_areas
.get(new Long(lid
));
395 // check if empty. If so, remove
396 Rectangle bounds
= area
.getBounds(); // TODO this can fail if the layer changes suddenly while painting
397 if (0 == bounds
.width
&& 0 == bounds
.height
) {
398 ht_areas
.remove(new Long(lid
));
399 //Utils.log("removing empty area");
402 final boolean translated
= calculateBoundingBox(); // will reset all areas' top-left coordinates, and update the database if necessary
404 // update for all, since the bounding box has changed
405 updateInDatabase("all_points");
407 // update the points for the current layer only
408 updateInDatabase("points=" + lid
);
411 // Repaint instead the last rectangle, to erase the circle
413 Display
.repaint(Display
.getFrontLayer(), r_old
, 3, false);
416 // repaint the navigator and snapshot
417 Display
.repaint(Display
.getFrontLayer(), this);
420 /** Calculate box, make this width,height be that of the box, and translate all areas to fit in. @param lid is the currently active Layer. */ //This is the only road to sanity for ZDisplayable objects.
421 public boolean calculateBoundingBox() {
422 // forget it if this has been done once already, for at the moment it would work only for translations, not any other types of transforms. TODO: need to fix this somehow, generates repainting problems.
423 //if (this.at.getType() != AffineTransform.TYPE_TRANSLATION) return false; // meaning, there's more bits in the type than just the translation
424 // check preconditions
425 if (0 == ht_areas
.size()) return false;
426 Area
[] area
= new Area
[ht_areas
.size()];
427 Map
.Entry
[] entry
= new Map
.Entry
[area
.length
];
428 ht_areas
.entrySet().toArray(entry
);
429 ht_areas
.values().toArray(area
);
430 Rectangle
[] b
= new Rectangle
[area
.length
];
431 Rectangle box
= null;
432 for (int i
=0; i
<area
.length
; i
++) {
433 b
[i
] = area
[i
].getBounds();
434 if (null == box
) box
= (Rectangle
)b
[i
].clone();
437 if (null == box
) return false; // empty AreaList
438 final AffineTransform atb
= new AffineTransform();
439 atb
.translate(-box
.x
, -box
.y
); // make local to overall box, so that box starts now at 0,0
440 for (int i
=0; i
<area
.length
; i
++) {
441 entry
[i
].setValue(area
[i
].createTransformedArea(atb
));
443 //this.translate(box.x, box.y);
444 this.at
.translate(box
.x
, box
.y
);
445 this.width
= box
.width
;
446 this.height
= box
.height
;
447 updateInDatabase("transform+dimensions");
448 if (null != layer_set
) layer_set
.updateBucket(this);
449 if (0 != box
.x
|| 0 != box
.y
) {
455 private BrushThread last
= null;
456 static private Rectangle r_old
= null;
458 /** Modeled after the ij.gui.RoiBrush class from ImageJ. */
459 private class BrushThread
extends Thread
{
460 /** The area to paint into when done or when removing. */
461 final private Area target_area
;
462 /** The temporary area to paint to, which is only different than target_area when adding. */
463 final private Area area
;
464 /** The list of all painted points. */
465 private final ArrayList
<Point
> points
= new ArrayList
<Point
>();
466 /** The last point on which a paint event was done. */
467 private Point previous_p
= null;
468 private boolean paint
= true;
469 private int brush_size
; // the diameter
471 final private int leftClick
=16, alt
=9;
472 final private DisplayCanvas dc
= Display
.getFront().getCanvas();
473 final private int flags
= dc
.getModifiers();
474 private boolean adding
= (0 == (flags
& alt
));
476 BrushThread(Area area
, double mag
) {
477 super("BrushThread");
478 setPriority(Thread
.NORM_PRIORITY
);
479 // if adding areas, make it be a copy, to be added on mouse release
480 // (In this way, the receiving Area is small and can be operated on fast)
482 this.target_area
= area
;
483 this.area
= new Area();
485 this.target_area
= area
;
489 brush_size
= ProjectToolbar
.getBrushSize();
490 brush
= makeBrush(brush_size
, mag
);
491 if (null == brush
) return;
496 // Make interpolated points effect add or subtract operations
497 synchronized (this) {
498 if (points
.size() < 2) {
499 // merge the temporary Area, if any, with the general one
500 if (adding
) this.target_area
.add(area
);
506 // paint the regions between points
507 // A cheap way would be to just make a rectangle between both points, with thickess radius.
508 // A better, expensive way is to fit a spline first, then add each one as a circle.
509 // The spline way is wasteful, but way more precise and beautiful. Since there's only one repaint, it's not excessively slow.
510 int[] xp
= new int[points
.size()];
511 int[] yp
= new int[xp
.length
];
513 for (final Point p
: points
) {
520 PolygonRoi proi
= new PolygonRoi(xp
, yp
, xp
.length
, Roi
.POLYLINE
);
522 FloatPolygon fp
= proi
.getFloatPolygon();
525 double[] xpd
= new double[fp
.npoints
];
526 double[] ypd
= new double[fp
.npoints
];
527 // Fails: fp contains float[], which for some reason cannot be copied into double[]
528 //System.arraycopy(fp.xpoints, 0, xpd, 0, xpd.length);
529 //System.arraycopy(fp.ypoints, 0, ypd, 0, ypd.length);
530 for (int i
=0; i
<xpd
.length
; i
++) {
531 xpd
[i
] = fp
.xpoints
[i
];
532 ypd
[i
] = fp
.ypoints
[i
];
537 // VectorString2D resampling doesn't work
538 VectorString3D vs
= new VectorString3D(xpd
, ypd
, new double[xpd
.length
], false);
539 double delta
= ((double)brush_size
) / 10;
540 if (delta
< 1) delta
= 1;
542 xpd
= vs
.getPoints(0);
543 ypd
= vs
.getPoints(1);
545 } catch (Exception e
) { IJError
.print(e
); }
548 final AffineTransform atb
= new AffineTransform();
550 final AffineTransform inv_at
= at
.createInverse();
554 for (int i
=0; i
<xpd
.length
; i
++) {
555 atb
.setToTranslation((int)xpd
[i
], (int)ypd
[i
]); // always integers
556 atb
.preConcatenate(inv_at
);
557 area
.add(slashInInts(brush
.createTransformedArea(atb
)));
559 this.target_area
.add(area
);
562 for (int i
=0; i
<xpd
.length
; i
++) {
563 atb
.setToTranslation((int)xpd
[i
], (int)ypd
[i
]); // always integers
564 atb
.preConcatenate(inv_at
);
565 target_area
.subtract(slashInInts(brush
.createTransformedArea(atb
)));
569 } catch (Exception ee
) {
574 final Area
getTmpArea() {
575 if (area
!= target_area
) return area
;
578 /** For best smoothness, each mouse dragged event should be captured!*/
582 final AffineTransform atb
= new AffineTransform();
585 if (0 == (flags
& leftClick
)) {
589 p
= dc
.getCursorLoc(); // as offscreen coords
590 if (p
.equals(previous_p
) /*|| (null != previous_p && p.distance(previous_p) < brush_size/5) */) {
591 try { Thread
.sleep(1); } catch (InterruptedException ie
) {}
594 // bring to offscreen position of the mouse
595 atb
.translate(p
.x
, p
.y
);
596 // capture bounds while still in offscreen coordinates
597 final Rectangle r
= new Rectangle();
598 final Area slash
= createSlash(atb
, r
);
599 if(null == slash
) continue;
601 if (0 == (flags
& alt
)) {
602 // no modifiers, just add
605 // with alt down, substract
606 area
.subtract(slash
);
611 final Rectangle copy
= (Rectangle
)r
.clone();
612 if (null != r_old
) r
.add(r_old
);
615 Display
.repaint(Display
.getFrontLayer(), 3, r
, false, false); // repaint only the last added slash
622 /** Sets the bounds of the created slash, in offscreen coords, to r if r is not null. */
623 private Area
createSlash(final AffineTransform atb
, final Rectangle r
) {
624 Area slash
= brush
.createTransformedArea(atb
); // + int transform, no problem
625 if (null != r
) r
.setRect(slash
.getBounds());
626 // bring to the current transform, if any
627 if (!at
.isIdentity()) {
629 slash
= slash
.createTransformedArea(at
.createInverse());
630 } catch (NoninvertibleTransformException nite
) {
635 // avoid problems with floating-point points, for example inability to properly fill areas or delete them.
636 return slashInInts(slash
);
639 private final Area
slashInInts(final Area area
) {
640 int[] x
= new int[400];
641 int[] y
= new int[400];
643 for (PathIterator pit
= area
.getPathIterator(null); !pit
.isDone(); ) {
644 if (x
.length
== next
) {
645 int[] x2
= new int[x
.length
+ 200];
646 int[] y2
= new int[y
.length
+ 200];
647 System
.arraycopy(x
, 0, x2
, 0, x
.length
);
648 System
.arraycopy(y
, 0, y2
, 0, y
.length
);
652 final float[] coords
= new float[6];
653 int seg_type
= pit
.currentSegment(coords
);
655 case PathIterator
.SEG_MOVETO
:
656 case PathIterator
.SEG_LINETO
:
657 x
[next
] = (int)coords
[0];
658 y
[next
] = (int)coords
[1];
660 case PathIterator
.SEG_CLOSE
:
663 Utils
.log2("WARNING: AreaList.slashInInts unhandled seg type.");
667 if (pit
.isDone()) break; // the loop
670 // resize back (now next is the length):
671 if (x
.length
== next
) {
672 int[] x2
= new int[next
];
673 int[] y2
= new int[next
];
674 System
.arraycopy(x
, 0, x2
, 0, next
);
675 System
.arraycopy(y
, 0, y2
, 0, next
);
679 return new Area(new Polygon(x
, y
, next
));
682 /** This method could get tones of improvement, which should be pumped upstream into ImageJ's RoiBrush class which is creating it at every while(true) {} iteration!!!
683 * The returned area has its coordinates centered around 0,0
685 private Area
makeBrush(int diameter
, double mag
) {
686 if (diameter
< 1) return null;
687 if (mag
>= 1) return new Area(new OvalRoi(-diameter
/2, -diameter
/2, diameter
, diameter
).getPolygon());
688 // else, create a smaller brush and transform it up, i.e. less precise, less points to store -but precision matches what the eye sees, and allows for much better storage -less points.
689 int screen_diameter
= (int)(diameter
* mag
);
690 if (0 == screen_diameter
) return null; // can't paint at this mag with this diameter
692 Area brush
= new Area(new OvalRoi(-screen_diameter
/2, -screen_diameter
/2, screen_diameter
, screen_diameter
).getPolygon());
693 // scale to world coordinates
694 AffineTransform at
= new AffineTransform();
695 at
.scale(1/mag
, 1/mag
);
696 return brush
.createTransformedArea(at
);
701 Polygon pol = new OvalRoi(-diameter/2, -diameter/2, diameter, diameter).getPolygon();
702 Polygon pol2 = new Polygon();
703 // cheap and fast: skip every other point, since all will be square angles
704 for (int i=0; i<pol.npoints; i+=2) {
705 pol2.addPoint(pol.xpoints[i], pol.ypoints[i]);
707 return new Area(pol2);
708 // the above works nice, but then the fill and fill-remove don't work properly (there are traces in the edges)
709 // Needs a workround: before adding/substracting, enlarge the polygon to have square edges
714 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
715 String type
= "t2_area_list";
716 if (hs
.contains(type
)) return;
718 sb_header
.append(indent
).append("<!ELEMENT t2_area_list (").append(Displayable
.commonDTDChildren()).append(",t2_area)>\n");
719 Displayable
.exportDTD(type
, sb_header
, hs
, indent
); // all ATTLIST of a Displayable
720 sb_header
.append(indent
).append("<!ATTLIST t2_area_list fill_paint NMTOKEN #REQUIRED>\n");
721 sb_header
.append(indent
).append("<!ELEMENT t2_area (t2_path)>\n")
722 .append(indent
).append("<!ATTLIST t2_area layer_id NMTOKEN #REQUIRED>\n")
723 .append(indent
).append("<!ELEMENT t2_path EMPTY>\n")
724 .append(indent
).append("<!ATTLIST t2_path d NMTOKEN #REQUIRED>\n")
728 public void exportXML(StringBuffer sb_body
, String indent
, Object any
) {
729 sb_body
.append(indent
).append("<t2_area_list\n");
730 final String in
= indent
+ "\t";
731 super.exportXML(sb_body
, in
, any
);
732 sb_body
.append(in
).append("fill_paint=\"").append(fill_paint
).append("\"\n");
733 String
[] RGB
= Utils
.getHexRGBColor(color
);
734 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");
735 sb_body
.append(indent
).append(">\n");
736 for (Iterator it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
737 Map
.Entry entry
= (Map
.Entry
)it
.next();
738 long lid
= ((Long
)entry
.getKey()).longValue();
739 Area area
= (Area
)entry
.getValue();
740 sb_body
.append(in
).append("<t2_area layer_id=\"").append(lid
).append("\">\n");
741 exportArea(sb_body
, in
+ "\t", area
);
742 sb_body
.append(in
).append("</t2_area>\n");
744 super.restXML(sb_body
, in
, any
);
745 sb_body
.append(indent
).append("</t2_area_list>\n");
748 /** 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). */
749 private void exportArea(StringBuffer sb
, String indent
, Area area
) {
750 // I could add detectors for straight lines and thus avoid saving so many points.
751 for (PathIterator pit
= area
.getPathIterator(null); !pit
.isDone(); ) {
752 float[] coords
= new float[6];
753 int seg_type
= pit
.currentSegment(coords
);
756 case PathIterator
.SEG_MOVETO
:
757 //Utils.log2("SEG_MOVETO: " + coords[0] + "," + coords[1]); // one point
760 sb
.append(indent
).append("<t2_path d=\"M ").append(x0
).append(" ").append(y0
);
762 case PathIterator
.SEG_LINETO
:
763 //Utils.log2("SEG_LINETO: " + coords[0] + "," + coords[1]); // one point
764 sb
.append(" L ").append((int)coords
[0]).append(" ").append((int)coords
[1]);
766 case PathIterator
.SEG_CLOSE
:
767 //Utils.log2("SEG_CLOSE");
768 // make a line to the first point
769 //sb.append(" L ").append(x0).append(" ").append(y0);
770 sb
.append(" z\" />\n");
773 Utils
.log2("WARNING: AreaList.exportArea unhandled seg type.");
778 //Utils.log2("finishing");
783 /** 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). */
784 private void exportAreaT2(final StringBuffer sb
, final String indent
, final Area area
) {
785 // I could add detectors for straight lines and thus avoid saving so many points.
786 for (PathIterator pit
= area
.getPathIterator(null); !pit
.isDone(); ) {
787 float[] coords
= new float[6];
788 int seg_type
= pit
.currentSegment(coords
);
791 case PathIterator
.SEG_MOVETO
:
794 sb
.append(indent
).append("(path '(M ").append(x0
).append(' ').append(y0
);
796 case PathIterator
.SEG_LINETO
:
797 sb
.append(" L ").append((int)coords
[0]).append(' ').append((int)coords
[1]);
799 case PathIterator
.SEG_CLOSE
:
800 // no need to make a line to the first point
804 Utils
.log2("WARNING: AreaList.exportArea unhandled seg type.");
814 /** Returns an ArrayList of ArrayList of Point as value with all paths for the Area of the given layer_id. */
815 public ArrayList
getPaths(long layer_id
) {
816 Object ob
= ht_areas
.get(new Long(layer_id
));
817 if (null == ob
) return null;
818 if (AreaList
.UNLOADED
== ob
) {
819 ob
= loadLayer(layer_id
);
820 if (null == ob
) return null;
822 Area area
= (Area
)ob
;
823 ArrayList al_paths
= new ArrayList();
824 ArrayList al_points
= null;
825 for (PathIterator pit
= area
.getPathIterator(null); !pit
.isDone(); ) {
826 float[] coords
= new float[6];
827 int seg_type
= pit
.currentSegment(coords
);
829 case PathIterator
.SEG_MOVETO
:
830 al_points
= new ArrayList();
831 al_points
.add(new Point((int)coords
[0], (int)coords
[1]));
833 case PathIterator
.SEG_LINETO
:
834 al_points
.add(new Point((int)coords
[0], (int)coords
[1]));
836 case PathIterator
.SEG_CLOSE
:
837 al_paths
.add(al_points
);
841 Utils
.log2("WARNING: AreaList.getPaths() unhandled seg type.");
852 /** Returns a table of Long layer ids versus the ArrayList that getPaths(long) returns for it.*/
853 public HashMap
getAllPaths() {
854 HashMap ht
= new HashMap();
855 for (Iterator it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
856 Map
.Entry entry
= (Map
.Entry
)it
.next();
857 ht
.put(entry
.getKey(), getPaths(((Long
)entry
.getKey()).longValue()));
862 /** These methods below prepended with double-underscore to mean: they are public, but should not be used except by the TMLHandler. I could put a caller detector but parsing XML code doesn't need any more overhead.*/
864 /** For reconstruction from XML. */
865 public void __startReconstructing(long lid
) {
866 ht_areas
.put("CURRENT", new Long(lid
));
867 ht_areas
.put(new Long(lid
), new GeneralPath(GeneralPath
.WIND_EVEN_ODD
));
869 /** For reconstruction from XML. */
870 public void __addPath(String svg_path
) {
871 GeneralPath gp
= (GeneralPath
)ht_areas
.get(ht_areas
.get("CURRENT"));
872 svg_path
= svg_path
.trim();
873 while (-1 != svg_path
.indexOf(" ")) {
874 svg_path
= svg_path
.replaceAll(" "," "); // make all spaces be single
876 final char[] data
= new char[svg_path
.length()];
877 svg_path
.getChars(0, data
.length
, data
, 0);
881 /** Assumes first char is 'M' and last char is a 'z'*/
882 private void parse(final GeneralPath gp
, final char[] data
) {
883 if ('z' != data
[data
.length
-1]) {
884 Utils
.log("AreaList: no closing z, ignoring sub path");
887 data
[data
.length
-1] = 'L'; // replacing the closing z for an L, since we read backwards
888 final int[] xy
= new int[2];
891 for (int i
=0; i
<data
.length
; i
++) {
892 if ('L' == data
[i
]) {
897 readXY(data
, i_L
, xy
);
898 final int x0
= xy
[0];
899 final int y0
= xy
[1];
902 while (-1 != (first
= readXY(data
, first
, xy
))) {
903 gp
.lineTo(xy
[0], xy
[1]);
907 gp
.lineTo(x0
, y0
); //TODO unnecessary?
911 /** Assumes all read chars will be digits except for the separator (single white space char), and won't fail (but generate ugly results) when any char is not a digit. */
912 private final int readXY(final char[] data
, int first
, final int[] xy
) { // final method: inline
913 if (first
>= data
.length
) return -1;
915 char c
= data
[first
];
918 if (data
.length
== last
) return -1;
921 first
= last
+2; // the first digit position after the found L, which will be the next first.
923 // skip the L and the white space separating <y> and L
925 if (last
< 0) return -1;
932 xy
[1] += (((int)c
) -48) * pos
; // digit zero is char with int value 48
938 // skip separating space
946 xy
[0] += (((int)c
) -48) * pos
;
953 /** For reconstruction from XML. */
954 public void __endReconstructing() {
955 Object ob
= ht_areas
.get("CURRENT");
957 GeneralPath gp
= (GeneralPath
)ht_areas
.get((Long
)ob
);
958 ht_areas
.put(ob
, new Area(gp
));
959 ht_areas
.remove("CURRENT");
963 public void fillHoles(final Layer la
) {
964 Object o
= ht_areas
.get(la
.getId());
965 if (UNLOADED
== o
) o
= loadLayer(la
.getId());
966 if (null == o
) return;
967 Area area
= (Area
) o
;
969 Polygon pol
= new Polygon();
970 for (PathIterator pit
= area
.getPathIterator(null); !pit
.isDone(); ) {
971 float[] coords
= new float[6];
972 int seg_type
= pit
.currentSegment(coords
);
974 case PathIterator
.SEG_MOVETO
:
975 case PathIterator
.SEG_LINETO
:
976 pol
.addPoint((int)coords
[0], (int)coords
[1]);
978 case PathIterator
.SEG_CLOSE
:
979 area
.add(new Area(pol
));
984 Utils
.log2("WARNING: unhandled seg type.");
994 public boolean paintsAt(final Layer layer
) {
995 if (!super.paintsAt(layer
)) return false;
996 return null != ht_areas
.get(new Long(layer
.getId()));
999 /** Dynamic loading from the database. */
1000 private Area
loadLayer(final long layer_id
) {
1001 Area area
= project
.getLoader().fetchArea(this.id
, layer_id
);
1002 if (null == area
) return null;
1003 ht_areas
.put(new Long(layer_id
), area
);
1007 public void adjustProperties() {
1008 GenericDialog gd
= makeAdjustPropertiesDialog(); // in superclass
1009 gd
.addCheckbox("Paint as outlines", !fill_paint
);
1010 gd
.addCheckbox("Apply paint mode to all AreaLists", false);
1012 if (gd
.wasCanceled()) return;
1013 // superclass processing
1014 final Displayable
.DoEdit prev
= processAdjustPropertiesDialog(gd
);
1016 final boolean fp
= !gd
.getNextBoolean();
1017 final boolean to_all
= gd
.getNextBoolean();
1019 for (Iterator it
= this.layer_set
.getZDisplayables().iterator(); it
.hasNext(); ) {
1020 Object ob
= it
.next();
1021 if (ob
instanceof AreaList
) {
1022 AreaList ali
= (AreaList
)ob
;
1023 ali
.fill_paint
= fp
;
1024 ali
.updateInDatabase("fill_paint");
1025 Display
.repaint(this.layer_set
, ali
, 2);
1029 if (this.fill_paint
!= fp
) {
1030 prev
.add("fill_paint", fp
);
1031 this.fill_paint
= fp
;
1032 updateInDatabase("fill_paint");
1036 // Add current step, with the same modified keys
1037 DoEdit current
= new DoEdit(this).init(prev
);
1038 if (isLinked()) current
.add(new Displayable
.DoTransforms().addAll(getLinkedGroup(null)));
1039 getLayerSet().addEditStep(current
);
1042 public boolean isFillPaint() { return this.fill_paint
; }
1044 /** 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. */
1045 static public AreaList
merge(final ArrayList al
) {
1046 AreaList base
= null;
1047 final ArrayList list
= (ArrayList
)al
.clone();
1048 for (Iterator it
= list
.iterator(); it
.hasNext(); ) {
1049 Object ob
= it
.next();
1050 if (ob
.getClass() != AreaList
.class) it
.remove();
1052 base
= (AreaList
)ob
;
1053 if (null == base
.layer_set
) {
1054 Utils
.log2("AreaList.merge: null LayerSet for base AreaList.");
1059 // check that it belongs to the same layer set as the base
1060 AreaList ali
= (AreaList
)ob
;
1061 if (base
.layer_set
!= ali
.layer_set
) it
.remove();
1063 if (list
.size() < 1) return null; // nothing to fuse
1064 if (!Utils
.check("Merging AreaList has no undo. Continue?")) return null;
1065 for (Iterator it
= list
.iterator(); it
.hasNext(); ) {
1067 AreaList ali
= (AreaList
)it
.next();
1069 // remove from project
1070 ali
.project
.removeProjectThing(ali
, false, true, 1); // the node from the Project Tree
1071 Utils
.log2("Merged AreaList " + ali
+ " to base " + base
);
1074 base
.calculateBoundingBox();
1081 /** For each area that ali contains, add it to the corresponding area here.*/
1082 private void add(AreaList ali
) {
1083 for (Iterator it
= ali
.ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
1084 Map
.Entry entry
= (Map
.Entry
)it
.next();
1085 Object ob_area
= entry
.getValue();
1086 long lid
= ((Long
)entry
.getKey()).longValue();
1087 if (UNLOADED
== ob_area
) ob_area
= ali
.loadLayer(lid
);
1088 Area area
= (Area
)ob_area
;
1089 area
= area
.createTransformedArea(ali
.at
);
1090 // now need to inverse transform it by this.at
1092 area
= area
.createTransformedArea(this.at
.createInverse());
1093 } catch (NoninvertibleTransformException nte
) {
1097 Object this_area
= this.ht_areas
.get(entry
.getKey());
1098 if (UNLOADED
== this_area
) { this_area
= loadLayer(lid
); }
1099 if (null == this_area
) this.ht_areas
.put(entry
.getKey(), (Area
)area
.clone());
1100 else ((Area
)this_area
).add(area
);
1101 updateInDatabase("points=" + ((Long
)entry
.getKey()).intValue());
1105 /** How many layers does this object paint to. */
1106 public int getNAreas() { return ht_areas
.size(); }
1108 public Area
getArea(Layer la
) {
1109 if (null == la
) return null;
1110 return getArea(la
.getId());
1112 public Area
getArea(long layer_id
) {
1113 Object ob
= ht_areas
.get(new Long(layer_id
));
1115 if (UNLOADED
== ob
) ob
= loadLayer(layer_id
);
1123 /** Performs a deep copy of this object, without the links. */
1124 public Displayable
clone(final Project pr
, final boolean copy_id
) {
1125 final ArrayList al_ul
= new ArrayList();
1126 for (Iterator it
= ht_areas
.keySet().iterator(); it
.hasNext(); ) { // TODO WARNING the layer ids are wrong if the project is different or copy_id is false! Should lookup closest layer by Z ...
1127 al_ul
.add(new Long(((Long
)it
.next()).longValue())); // clones of the Long that wrap layer ids
1129 final long nid
= copy_id ?
this.id
: pr
.getLoader().getNextId();
1130 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());
1131 for (Iterator it
= copy
.ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
1132 Map
.Entry entry
= (Map
.Entry
)it
.next();
1133 entry
.setValue(((Area
)this.ht_areas
.get(entry
.getKey())).clone());
1138 /** Will make the assumption that all layers have the same thickness as the first one found.
1139 * @param scale The scaling of the entire universe, to limit the overall box
1140 * @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).
1141 * @return The List of triangles involved, specified as three consecutive vertices. A list of Point3f vertices.
1143 public List
generateTriangles(final double scale
, final int resample_
) {
1144 // in the LayerSet, layers are ordered by Z already.
1147 if (resample_
<=0 ) {
1149 Utils
.log2("Fixing zero or negative resampling value to 1.");
1150 } else resample
= resample_
;
1152 int n
= getNAreas();
1153 if (0 == n
) return null;
1154 final Rectangle r
= getBoundingBox();
1155 // remove translation from a copy of this Displayable's AffineTransform
1156 final AffineTransform at_translate
= new AffineTransform();
1157 at_translate
.translate(-r
.x
, -r
.y
);
1158 final AffineTransform at2
= (AffineTransform
)this.at
.clone();
1159 at2
.preConcatenate(at_translate
);
1160 // incorporate resampling scaling into the transform
1161 final AffineTransform atK
= new AffineTransform();
1162 //Utils.log("resample: " + resample + " scale: " + scale);
1163 final double K
= (1.0 / resample
) * scale
; // 'scale' is there to limit gigantic universes
1165 at2
.preConcatenate(atK
);
1167 final Calibration cal
= layer_set
.getCalibrationCopy();
1170 ImageStack stack
= null;
1171 final int w
= (int)Math
.ceil(r
.width
* K
);
1172 final int h
= (int)Math
.ceil(r
.height
* K
);
1174 // For the thresholding after painting an scaled-down area into an image:
1175 final int threshold
;
1176 if (K
> 0.8) threshold
= 200;
1177 else if (K
> 0.5) threshold
= 128;
1178 else if (K
> 0.3) threshold
= 75; // 75 gives upper 70% of 255 range. It's better to blow up a bit, since resampling down makes marching cubes undercut the mesh.
1179 else threshold
= 40;
1181 Layer first_layer
= null;
1183 final HashSet
<Layer
> empty_layers
= new HashSet
<Layer
>();
1185 for (final Layer la
: layer_set
.getLayers()) { // layers sorted by Z ASC
1186 if (0 == n
) break; // no more areas to paint
1187 final Area area
= getArea(la
);
1189 if (null == stack
) {
1190 stack
= new ImageStack(w
, h
);
1193 project
.getLoader().releaseToFit(w
, h
, ImagePlus
.GRAY8
, 3);
1194 // must be a new image, for pixel array is shared with BufferedImage and ByteProcessor in java 1.6
1195 final BufferedImage bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1196 final Graphics2D g
= bi
.createGraphics();
1197 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, RenderingHints
.VALUE_INTERPOLATION_BICUBIC
);
1198 g
.setRenderingHint(RenderingHints
.KEY_ANTIALIASING
, RenderingHints
.VALUE_ANTIALIAS_ON
);
1199 g
.setColor(Color
.white
);
1200 g
.fill(area
.createTransformedArea(at2
));
1201 final ByteProcessor bp
= new ByteProcessor(bi
);
1202 bp
.threshold(threshold
);
1203 stack
.addSlice(Double
.toString(la
.getZ()), bp
);
1208 } else if (null != stack
) {
1209 // add a black slice
1210 stack
.addSlice(la
.getZ() + "", new ByteProcessor(w
, h
));
1211 empty_layers
.add(la
);
1216 // No need anymore: MCTriangulator does it on its own now
1217 stack
= zeroPad(stack
);
1219 // Still, the MCTriangulator does NOT zero pad properly
1221 final ImagePlus imp
= new ImagePlus("", stack
);
1222 imp
.getCalibration().pixelWidth
= 1; // ensure all set to 1.
1223 imp
.getCalibration().pixelHeight
= 1;
1224 imp
.getCalibration().pixelDepth
= 1;
1226 // Now marching cubes
1227 final Triangulator tri
= new MCTriangulator();
1228 final List list
= tri
.getTriangles(imp
, 0, new boolean[]{true, true, true}, 1);
1231 // The list of triangles has coordinates:
1232 // - in x,y: in pixels, scaled by K = (1 / resample) * scale,
1233 // translated by r.x, r.y (the top-left coordinate of this AreaList bounding box)
1234 // - in z: in stack slice indices
1236 // So all x,y,z must be corrected in x,y and z of the proper layer
1239 final double offset
= first_layer
.getZ();
1240 final int i_first_layer
= layer_set
.indexOf(first_layer
);
1242 // The x,y translation to correct each point by:
1243 final float dx
= (float)(r
.x
* scale
* cal
.pixelWidth
);
1244 final float dy
= (float)(r
.y
* scale
* cal
.pixelHeight
);
1246 // Correct x,y by resampling and calibration, but not scale (TODO this hints that calibration in 3D is wrong: should be divided by the scale! May affect all Displayable types)
1247 final float rsw
= (float)(resample
* cal
.pixelWidth
); // scale is already in the pixel coordinates
1248 final float rsh
= (float)(resample
* cal
.pixelHeight
);
1249 final double sz
= scale
* cal
.pixelWidth
; // no resampling in Z. and Uses pixelWidth, not pixelDepth.
1252 int slice_index
= 0;
1256 // which p.z types exist?
1257 final TreeSet<Float> ts = new TreeSet<Float>();
1258 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1259 ts.add(((Point3f)it.next()).z);
1261 for (final Float pz : ts) Utils.log2("A z: " + pz);
1263 Utils.log2("Number of slices: " + imp.getNSlices());
1268 //if (i_first_layer > 0)
1269 // fix3DPoints(list, layer_set.previous(layer_set.getLayer(i_first_layer -1)), -1, dx, dy, rsw, rsh, sz);
1270 fix3DPoints(list
, layer_set
.previous(layer_set
.getLayer(i_first_layer
)), 0, dx
, dy
, rsw
, rsh
, sz
);
1272 for (final Layer la
: layer_set
.getLayers().subList(i_first_layer
, i_first_layer
+ imp
.getNSlices() -2)) { // -2: it's padded
1274 //Utils.log2("handling slice_index: " + slice_index);
1276 // If layer is empty, continue
1277 if (empty_layers
.contains(la
)) {
1282 fix3DPoints(list
, la
, slice_index
+ 1, dx
, dy
, rsw
, rsh
, sz
); // +1 because of padding
1284 //Utils.log2("processed slice index " + slice_index + " for layer " + la);
1289 // The last set of vertices to process:
1290 // Find all pixels that belong to the layer, and transform them back:
1291 //final Layer last = layer_set.getLayer(layer_index -1);
1292 //fixLast3DPoints(list, last.getZ() + last.getThickness(), last.getThickness(), layer_index +1, dx, dy, rsw, rsh, sz);
1294 // Do the last layer again, capturing from slice_index+1 to +2, since the last layer has two Z planes in which it has pixels:
1295 Layer la
= layer_set
.getLayer(i_first_layer
+ slice_index
-1);
1296 fix3DPoints(list
, la
, slice_index
+1, dx
, dy
, rsw
, rsh
, sz
);
1297 } catch (Exception ee
) {
1303 } catch (Exception e
) {
1304 e
.printStackTrace();
1309 private final void fix3DPoints(final List list
, final Layer la
, final int layer_index
, final float dx
, final float dy
, final float rsw
, final float rsh
, final double sz
) {
1310 final double la_z
= la
.getZ();
1311 final double la_thickness
= la
.getThickness();
1312 // Find all pixels that belong to the layer, and transform them back:
1313 for (final Iterator it
= list
.iterator(); it
.hasNext(); ) {
1314 final Point3f p
= (Point3f
)it
.next();
1315 if (p
.z
>= layer_index
&& p
.z
< layer_index
+ 1) {
1316 // correct pixel position:
1317 // -- The '-1' corrects for zero padding
1318 // -- The 'rsw','rsh' scales back to LayerSet coords
1319 // -- The 'dx','dy' translates back to this AreaList bounding box
1320 p
.x
= (p
.x
-1) * rsw
+ dx
;
1321 p
.y
= (p
.y
-1) * rsh
+ dy
;
1323 // The Z is more complicated: the Z of the layer, scaled relative to the layer thickness
1324 // -- 'offset' is the Z of the first layer, corresponding to the layer that contributed to the first stack slice.
1325 p
.z
= (float)((la_z
+ la_thickness
* (p
.z
- layer_index
)) * sz
); // using pixelWidth, not pixelDepth!
1331 private final void fixLast3DPoints(final List list, 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) {
1332 // Find all pixels that belong to the layer, and transform them back:
1333 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1334 final Point3f p = (Point3f)it.next();
1335 if (p.z >= layer_index) {
1336 // correct pixel position:
1337 // -- The '-1' corrects for zero padding
1338 // -- The 'rsw','rsh' scales back to LayerSet coords
1339 // -- The 'dx','dy' translates back to this AreaList bounding box
1340 p.x = (p.x -1) * rsw + dx;
1341 p.y = (p.y -1) * rsh + dy;
1343 // The Z is more complicated: the Z of the layer, scaled relative to the layer thickness
1344 // -- 'offset' is the Z of the first layer, corresponding to the layer that contributed to the first stack slice.
1345 p.z = (float)((la_z + la_thickness * (p.z - layer_index)) * sz); // unsing pixelWidth, not pixelDepth!
1351 static private ImageStack
zeroPad(final ImageStack stack
) {
1352 int w
= stack
.getWidth();
1353 int h
= stack
.getHeight();
1354 // enlarge all processors
1355 ImageStack st
= new ImageStack(w
+2, h
+2);
1356 for (int i
=1; i
<=stack
.getSize(); i
++) {
1357 ImageProcessor ip
= new ByteProcessor(w
+2, h
+2);
1358 ip
.insert(stack
.getProcessor(i
), 1, 1);
1359 st
.addSlice(Integer
.toString(i
), ip
);
1361 ByteProcessor bp
= new ByteProcessor(w
+2, h
+2);
1362 // insert slice at 0
1363 st
.addSlice("0", bp
, 0);
1364 // append slice at the end
1365 st
.addSlice(Integer
.toString(stack
.getSize()+1), bp
);
1370 /** 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. */
1371 public void setArea(final long layer_id
, final Area area
) {
1372 if (null == area
) return;
1373 ht_areas
.put(layer_id
, area
);
1374 updateInDatabase("points=" + layer_id
);
1377 /** 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. */
1378 public void addArea(final long layer_id
, final Area area
) {
1379 if (null == area
) return;
1380 Area a
= getArea(layer_id
);
1381 if (null == a
) ht_areas
.put(layer_id
, new Area(area
));
1383 updateInDatabase("points=" + layer_id
);
1386 /** 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. */
1387 public void add(final long layer_id
, final ShapeRoi roi
) throws NoninvertibleTransformException
{
1388 if (null == roi
) return;
1389 Area a
= getArea(layer_id
);
1390 Area asr
= M
.getArea(roi
).createTransformedArea(this.at
.createInverse());
1392 ht_areas
.put(layer_id
, asr
);
1396 calculateBoundingBox();
1397 updateInDatabase("points=" + layer_id
);
1399 /** 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. */
1400 public void subtract(final long layer_id
, final ShapeRoi roi
) throws NoninvertibleTransformException
{
1401 if (null == roi
) return;
1402 Area a
= getArea(layer_id
);
1403 if (null == a
) return;
1404 a
.subtract(M
.getArea(roi
).createTransformedArea(this.at
.createInverse()));
1405 calculateBoundingBox();
1406 updateInDatabase("points=" + layer_id
);
1409 /** 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. */
1410 public AreaList
part(final long layer_id
, final ShapeRoi sroi
) throws NoninvertibleTransformException
{
1411 // The Area to subtract, in world coordinates:
1412 Area sub
= M
.getArea(sroi
);
1413 // The area to subtract from:
1414 Area a
= getArea(layer_id
);
1415 if (null == a
|| M
.isEmpty(a
)) return null;
1416 // The intersection:
1417 Area inter
= a
.createTransformedArea(this.at
);
1418 inter
.intersect(sub
);
1419 if (M
.isEmpty(inter
)) return null;
1421 // Subtract from this:
1422 this.subtract(layer_id
, sroi
);
1424 // Create new AreaList with the intersection area, and add it to the same LayerSet as this:
1425 AreaList ali
= new AreaList(this.project
, this.title
, 0, 0);
1426 ali
.color
= new Color(color
.getRed(), color
.getGreen(), color
.getBlue());
1427 ali
.visible
= this.visible
;
1428 ali
.alpha
= this.alpha
;
1429 ali
.addArea(layer_id
, inter
);
1430 this.layer_set
.add(ali
); // needed to call updateBucket
1431 ali
.calculateBoundingBox();
1436 public void keyPressed(KeyEvent ke
) {
1437 Object source
= ke
.getSource();
1438 if (! (source
instanceof DisplayCanvas
)) return;
1439 DisplayCanvas dc
= (DisplayCanvas
)source
;
1440 Layer la
= dc
.getDisplay().getLayer();
1441 int keyCode
= ke
.getKeyCode();
1445 case KeyEvent
.VK_C
: // COPY
1446 Area area
= (Area
) ht_areas
.get(la
.getId());
1448 DisplayCanvas
.setCopyBuffer(AreaList
.class, area
.createTransformedArea(this.at
));
1452 case KeyEvent
.VK_V
: // PASTE
1453 // Casting a null is fine, and addArea survives a null.
1454 Area a
= (Area
) DisplayCanvas
.getCopyBuffer(AreaList
.class);
1456 addArea(la
.getId(), a
.createTransformedArea(this.at
.createInverse()));
1457 calculateBoundingBox();
1461 case KeyEvent
.VK_F
: // fill all holes
1465 case KeyEvent
.VK_X
: // remove area from current layer, if any
1466 if (null != ht_areas
.remove(la
.getId())) {
1467 calculateBoundingBox();
1472 } catch (Exception e
) {
1475 if (ke
.isConsumed()) {
1476 Display
.repaint(la
, getBoundingBox(), 5);
1482 Roi roi
= dc
.getFakeImagePlus().getRoi();
1483 if (null == roi
) return;
1485 if (!M
.isAreaROI(roi
)) {
1486 Utils
.log("AreaList only accepts region ROIs, not lines.");
1489 ShapeRoi sroi
= new ShapeRoi(roi
);
1490 long layer_id
= la
.getId();
1494 add(layer_id
, sroi
);
1497 case KeyEvent
.VK_D
: // VK_S is for 'save' always
1498 subtract(layer_id
, sroi
);
1501 case KeyEvent
.VK_K
: // knive
1502 AreaList p
= part(layer_id
, sroi
);
1504 project
.getProjectTree().addSibling(this, p
);
1508 Display
.repaint(la
, getBoundingBox(), 5);
1510 } catch (NoninvertibleTransformException e
) {
1511 Utils
.log("Could not add ROI to area at layer " + dc
.getDisplay().getLayer() + " : " + e
);
1515 /** Measure the volume (in voxels) of this AreaList,
1516 * and the surface area, the latter estimated as the number of voxels that
1519 * If the width and height of this AreaList cannot be fit in memory, scaled down versions will be used,
1520 * and thus the results will be approximate.
1523 public String
getInfo() {
1524 if (0 == ht_areas
.size()) return "Empty AreaList " + this.toString();
1525 Rectangle box
= getBoundingBox(null);
1527 while (!getProject().getLoader().releaseToFit(2 * (long)(scale
* (box
.width
* box
.height
)) + 1000000)) { // factor of 2, because a mask will be involved
1532 final int w
= (int)Math
.ceil(box
.width
* scale
);
1533 final int h
= (int)Math
.ceil(box
.height
* scale
);
1534 BufferedImage bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1535 Graphics2D g
= bi
.createGraphics();
1536 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1537 byte[] pixels
= ((DataBufferByte
)bi
.getRaster().getDataBuffer()).getData(); // buffer.getData();
1539 // prepare suitable transform
1540 AffineTransform aff
= (AffineTransform
)this.at
.clone();
1541 AffineTransform aff2
= new AffineTransform();
1542 // A - remove translation
1543 aff2
.translate(-box
.x
, -box
.y
);
1544 aff
.preConcatenate(aff2
);
1546 if (1.0f
!= scale
) {
1547 aff2
.setToIdentity();
1548 aff2
.translate(box
.width
/2, box
.height
/2);
1549 aff2
.scale(scale
, scale
);
1550 aff2
.translate(-box
.width
/2, -box
.height
/2);
1551 aff
.preConcatenate(aff2
);
1553 // for each area, measure its area and its perimeter
1554 for (Iterator it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
1556 Map
.Entry entry
= (Map
.Entry
)it
.next();
1557 Object ob_area
= entry
.getValue();
1558 long lid
= ((Long
)entry
.getKey()).longValue();
1559 if (UNLOADED
== ob_area
) ob_area
= loadLayer(lid
);
1560 Area area2
= ((Area
)ob_area
).createTransformedArea(aff
);
1561 // paint the area, filling mode
1562 g
.setColor(Color
.white
);
1565 // count white pixels
1566 for (int i
=0; i
<pixels
.length
; i
++) {
1567 if (255 == (pixels
[i
]&0xff)) n_pix
++;
1568 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1571 // new ImagePlus("lid=" + lid, bi).show();
1574 double thickness
= layer_set
.getLayer(lid
).getThickness();
1575 volume
+= n_pix
* thickness
;
1576 // reset board (filling all, to make sure there are no rounding surprises)
1577 g
.setColor(Color
.black
);
1578 g
.fillRect(0, 0, w
, h
);
1579 // now measure length of perimeter
1580 ArrayList al_paths
= getPaths(lid
);
1582 for (Iterator ipath
= al_paths
.iterator(); ipath
.hasNext(); ) {
1583 ArrayList path
= (ArrayList
)ipath
.next();
1584 Point p2
= (Point
)path
.get(0);
1585 for (int i
=path
.size()-1; i
>-1; i
--) {
1586 Point p1
= (Point
)path
.get(i
);
1587 length
+= p1
.distance(p2
);
1591 surface
+= length
* thickness
;
1600 // remove pretentious after-comma digits on return:
1601 return new StringBuffer("Volume: ").append(IJ
.d2s(volume
, 2)).append(" (cubic pixels)\nLateral surface: ").append(IJ
.d2s(surface
, 2)).append(" (square pixels)\n").toString();
1604 /** @param area is expected in world coordinates. */
1605 public boolean intersects(final Area area
, final double z_first
, final double z_last
) {
1606 for (Iterator
<Map
.Entry
> it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
1607 Map
.Entry entry
= it
.next();
1608 Layer layer
= layer_set
.getLayer(((Long
)entry
.getKey()).longValue());
1609 if (layer
.getZ() >= z_first
&& layer
.getZ() <= z_last
) {
1610 Area a
= ((Area
)entry
.getValue()).createTransformedArea(this.at
);
1612 Rectangle r
= a
.getBounds();
1613 if (0 != r
.width
&& 0 != r
.height
) return true;
1619 /** 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. */
1620 static public void exportAsLabels(final java
.util
.List
<Displayable
> list
, 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
) {
1621 // survive everything:
1622 if (null == list
|| 0 == list
.size()) {
1623 Utils
.log("Null or empty list.");
1626 if (scale
< 0 || scale
> 1) {
1627 Utils
.log("Improper scale value. Must be 0 < scale <= 1");
1631 // Current AmiraMeshEncoder supports ByteProcessor only: 256 labels max, including background at zero.
1632 if (as_amira_labels
&& list
.size() > 255) {
1633 Utils
.log("Saving ONLY first 255 AreaLists!\nDiscarded:");
1634 StringBuffer sb
= new StringBuffer();
1635 for (final Displayable d
: list
.subList(256, list
.size())) {
1636 sb
.append(" ").append(d
.getProject().getShortMeaningfulTitle(d
)).append('\n');
1638 Utils
.log(sb
.toString());
1639 ArrayList
<Displayable
> li
= new ArrayList
<Displayable
>(list
);
1641 list
.addAll(li
.subList(0, 256));
1646 String ext
= as_amira_labels ?
".am" : ".tif";
1647 File f
= Utils
.chooseFile("labels", ext
);
1648 if (null == f
) return;
1649 path
= f
.getAbsolutePath().replace('\\','/');
1652 LayerSet layer_set
= list
.get(0).getLayerSet();
1653 if (first_layer
> last_layer
) {
1654 int tmp
= first_layer
;
1655 first_layer
= last_layer
;
1657 if (first_layer
< 0) first_layer
= 0;
1658 if (last_layer
>= layer_set
.size()) last_layer
= layer_set
.size()-1;
1660 // Create image according to roi and scale
1662 Rectangle broi
= null;
1664 width
= (int)(layer_set
.getLayerWidth() * scale
);
1665 height
= (int)(layer_set
.getLayerHeight() * scale
);
1667 broi
= roi
.getBounds();
1668 width
= (int)(broi
.width
* scale
);
1669 height
= (int)(broi
.height
* scale
);
1672 // Compute highest label value, which affects of course the stack image type
1673 TreeSet
<Integer
> label_values
= new TreeSet
<Integer
>();
1674 for (final Displayable d
: list
) {
1675 String label
= d
.getProperty("label");
1676 if (null != label
) label_values
.add(Integer
.parseInt(label
));
1680 if (label_values
.size() > 0) {
1681 lowest
= label_values
.first();
1682 highest
= label_values
.last();
1684 int n_non_labeled
= list
.size() - label_values
.size();
1685 int max_label_value
= highest
+ n_non_labeled
;
1687 final ImageStack stack
= new ImageStack(width
, height
);
1689 int type
= ImagePlus
.GRAY8
;
1690 if (max_label_value
> 255) { // 0 is background, and 255 different arealists
1691 type
= ImagePlus
.GRAY16
;
1692 if (max_label_value
> 65535) { // 0 is background, and 65535 different arealists
1693 type
= ImagePlus
.GRAY32
;
1696 Calibration cal
= layer_set
.getCalibration();
1698 String amira_params
= null;
1699 if (as_amira_labels
) {
1700 final StringBuffer sb
= new StringBuffer("CoordType \"uniform\"\nMaterials {\nExterior {\n Id 0,\nColor 0 0 0\n}\n");
1701 final float[] c
= new float[3];
1703 for (final Displayable d
: list
) {
1704 value
++; // 0 is background
1705 d
.getColor().getRGBColorComponents(c
);
1706 String s
= d
.getProject().getShortMeaningfulTitle(d
);
1707 s
= s
.replace('-', '_').replaceAll(" #", " id");
1708 sb
.append(Utils
.makeValidIdentifier(s
)).append(" {\n")
1709 .append("Id ").append(value
).append(",\n")
1710 .append("Color ").append(c
[0]).append(' ').append(c
[1]).append(' ').append(c
[2]).append("\n}\n");
1713 amira_params
= sb
.toString();
1717 final float len
= last_layer
- first_layer
+ 1;
1720 final HashMap
<Displayable
,Integer
> labels
= new HashMap
<Displayable
,Integer
>();
1721 for (final Displayable d
: list
) {
1722 if (visible_only
&& !d
.isVisible()) continue;
1723 String slabel
= d
.getProperty("label");
1725 if (null != slabel
) {
1726 label
= Integer
.parseInt(slabel
);
1728 label
= (++highest
); // 0 is background
1730 labels
.put(d
, label
);
1733 for (Layer la
: layer_set
.getLayers().subList(first_layer
, last_layer
+1)) {
1734 Utils
.showProgress(count
/len
);
1736 ImageProcessor ip
= Utils
.createProcessor(type
, width
, height
);
1737 if (!(ip
instanceof ByteProcessor
)) {
1738 ip
.setMinAndMax(lowest
, highest
);
1740 // paint here all arealist that paint to the layer 'la'
1741 for (final Displayable d
: list
) {
1742 if (visible_only
&& !d
.isVisible()) continue;
1743 ip
.setValue(labels
.get(d
));
1744 AreaList ali
= (AreaList
)d
;
1745 Area area
= ali
.getArea(la
);
1747 Utils
.log2("Layer " + la
+ " id: " + d
.getId() + " area is " + area
);
1750 // Transform: the scale and the roi
1751 AffineTransform aff
= new AffineTransform();
1752 // reverse order of transformations:
1753 /* 3 - To scale: */ if (1 != scale
) aff
.scale(scale
, scale
);
1754 /* 2 - To roi coordinates: */ if (null != broi
) aff
.translate(-broi
.x
, -broi
.y
);
1755 /* 1 - To world coordinates: */ aff
.concatenate(ali
.at
);
1756 ShapeRoi sroi
= new ShapeRoi(aff
.createTransformedShape(area
));
1758 ip
.fill(sroi
.getMask());
1760 stack
.addSlice(la
.getZ() * cal
.pixelWidth
+ "", ip
);
1762 Utils
.showProgress(1);
1763 // Save via file dialog:
1764 ImagePlus imp
= new ImagePlus("Labels", stack
);
1765 if (as_amira_labels
) imp
.setProperty("Info", amira_params
);
1766 imp
.setCalibration(layer_set
.getCalibrationCopy());
1768 if (as_amira_labels
) {
1769 AmiraMeshEncoder ame
= new AmiraMeshEncoder(path
);
1771 Utils
.log("Could not write to file " + path
);
1774 if (!ame
.write(imp
)) {
1775 Utils
.log("Error in writing Amira file!");
1779 new FileSaver(imp
).saveAsTiff(path
);
1784 public ResultsTable
measure(ResultsTable rt
) {
1785 if (0 == ht_areas
.size()) return rt
;
1786 if (null == rt
) rt
= Utils
.createResultsTable("AreaList results", new String
[]{"id", "volume", "name-id"});
1787 rt
.incrementCounter();
1788 rt
.addLabel("units", "cubic " + layer_set
.getCalibration().getUnit());
1789 rt
.addValue(0, this.id
);
1790 rt
.addValue(1, measureVolume());
1791 rt
.addValue(2, getNameId());
1795 public double measureVolume() {
1796 if (0 == ht_areas
.size()) return 0;
1797 Rectangle box
= getBoundingBox(null);
1799 while (!getProject().getLoader().releaseToFit(2 * (long)(scale
* (box
.width
* box
.height
)) + 1000000)) { // factor of 2, because a mask will be involved
1804 final int w
= (int)Math
.ceil(box
.width
* scale
);
1805 final int h
= (int)Math
.ceil(box
.height
* scale
);
1806 BufferedImage bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1807 Graphics2D g
= bi
.createGraphics();
1808 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1809 byte[] pixels
= ((DataBufferByte
)bi
.getRaster().getDataBuffer()).getData(); // buffer.getData();
1811 // prepare suitable transform
1812 AffineTransform aff
= (AffineTransform
)this.at
.clone();
1813 AffineTransform aff2
= new AffineTransform();
1814 // A - remove translation
1815 aff2
.translate(-box
.x
, -box
.y
);
1816 aff
.preConcatenate(aff2
);
1818 if (1.0f
!= scale
) {
1819 aff2
.setToIdentity();
1820 aff2
.translate(box
.width
/2, box
.height
/2);
1821 aff2
.scale(scale
, scale
);
1822 aff2
.translate(-box
.width
/2, -box
.height
/2);
1823 aff
.preConcatenate(aff2
);
1825 Calibration cal
= layer_set
.getCalibration();
1826 double pixelWidth
= cal
.pixelWidth
;
1827 double pixelHeight
= cal
.pixelHeight
;
1828 // for each area, measure its area and its perimeter
1829 for (Iterator it
= ht_areas
.entrySet().iterator(); it
.hasNext(); ) {
1831 Map
.Entry entry
= (Map
.Entry
)it
.next();
1832 Object ob_area
= entry
.getValue();
1833 long lid
= ((Long
)entry
.getKey()).longValue();
1834 if (UNLOADED
== ob_area
) ob_area
= loadLayer(lid
);
1835 Area area2
= ((Area
)ob_area
).createTransformedArea(aff
);
1836 // paint the area, filling mode
1837 g
.setColor(Color
.white
);
1840 // count white pixels
1841 for (int i
=0; i
<pixels
.length
; i
++) {
1842 if (255 == (pixels
[i
]&0xff)) n_pix
++;
1843 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1845 double thickness
= layer_set
.getLayer(lid
).getThickness();
1846 volume
+= n_pix
* thickness
* pixelWidth
* pixelHeight
* pixelWidth
; // the last one is NOT pixelDepth because layer thickness and Z are in pixels
1847 // reset board (filling all, to make sure there are no rounding surprises)
1848 g
.setColor(Color
.black
);
1849 g
.fillRect(0, 0, w
, h
);
1856 return volume
/= (scale
* scale
); // above, calibration is fixed while computing. Scale only corrects for the 2D plane.
1860 Class
getInternalDataPackageClass() {
1861 return DPAreaList
.class;
1865 Object
getDataPackage() {
1866 // The width,height,links,transform and list of areas
1867 return new DPAreaList(this);
1870 static private final class DPAreaList
extends Displayable
.DataPackage
{
1871 final protected HashMap ht
;
1872 DPAreaList(final AreaList ali
) {
1874 this.ht
= new HashMap();
1875 for (final Object entry
: ali
.ht_areas
.entrySet()) {
1876 Map
.Entry e
= (Map
.Entry
)entry
;
1877 Object area
= e
.getValue();
1878 if (area
.getClass() == Area
.class) area
= new Area((Area
)area
);
1879 this.ht
.put(e
.getKey(), area
);
1882 final boolean to2(final Displayable d
) {
1884 final AreaList ali
= (AreaList
)d
;
1885 ali
.ht_areas
.clear();
1886 for (final Object entry
: ht
.entrySet()) {
1887 final Map
.Entry e
= (Map
.Entry
)entry
;
1888 Object area
= e
.getValue();
1889 if (area
.getClass() == Area
.class) area
= new Area((Area
)area
);
1890 ali
.ht_areas
.put(e
.getKey(), area
);