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
;
28 import ij
.gui
.GenericDialog
;
30 import ij
.gui
.ShapeRoi
;
31 import ij
.io
.FileOpener
;
32 import ij
.io
.TiffDecoder
;
33 import ij
.io
.TiffEncoder
;
34 import ij
.plugin
.WandToolOptions
;
35 import ij
.plugin
.filter
.ThresholdToSelection
;
36 import ij
.process
.ByteProcessor
;
37 import ij
.process
.ColorProcessor
;
38 import ij
.process
.FloatProcessor
;
39 import ij
.process
.ImageProcessor
;
40 import ij
.process
.ShortProcessor
;
41 import ini
.trakem2
.Project
;
42 import ini
.trakem2
.imaging
.PatchStack
;
43 import ini
.trakem2
.imaging
.filters
.FilterEditor
;
44 import ini
.trakem2
.imaging
.filters
.IFilter
;
45 import ini
.trakem2
.io
.CoordinateTransformXML
;
46 import ini
.trakem2
.io
.ImageSaver
;
47 import ini
.trakem2
.persistence
.FSLoader
;
48 import ini
.trakem2
.persistence
.Loader
;
49 import ini
.trakem2
.persistence
.XMLOptions
;
50 import ini
.trakem2
.utils
.Bureaucrat
;
51 import ini
.trakem2
.utils
.IJError
;
52 import ini
.trakem2
.utils
.M
;
53 import ini
.trakem2
.utils
.ProjectToolbar
;
54 import ini
.trakem2
.utils
.Search
;
55 import ini
.trakem2
.utils
.Utils
;
56 import ini
.trakem2
.utils
.Worker
;
58 import java
.awt
.Color
;
59 import java
.awt
.Composite
;
60 import java
.awt
.Dimension
;
61 import java
.awt
.Event
;
62 import java
.awt
.Graphics2D
;
63 import java
.awt
.Image
;
64 import java
.awt
.Polygon
;
65 import java
.awt
.Rectangle
;
66 import java
.awt
.Toolkit
;
67 import java
.awt
.event
.KeyEvent
;
68 import java
.awt
.event
.MouseEvent
;
69 import java
.awt
.geom
.AffineTransform
;
70 import java
.awt
.geom
.Area
;
71 import java
.awt
.geom
.NoninvertibleTransformException
;
72 import java
.awt
.geom
.Path2D
;
73 import java
.awt
.geom
.Point2D
;
74 import java
.awt
.image
.BufferedImage
;
75 import java
.awt
.image
.DirectColorModel
;
76 import java
.awt
.image
.MemoryImageSource
;
77 import java
.awt
.image
.PixelGrabber
;
78 import java
.io
.BufferedReader
;
79 import java
.io
.ByteArrayOutputStream
;
81 import java
.io
.FileInputStream
;
82 import java
.io
.FileReader
;
83 import java
.io
.IOException
;
84 import java
.io
.RandomAccessFile
;
85 import java
.io
.Reader
;
86 import java
.util
.ArrayList
;
87 import java
.util
.Collection
;
88 import java
.util
.HashMap
;
89 import java
.util
.HashSet
;
90 import java
.util
.Iterator
;
91 import java
.util
.List
;
93 import java
.util
.TreeMap
;
94 import java
.util
.concurrent
.Future
;
95 import java
.util
.zip
.ZipEntry
;
96 import java
.util
.zip
.ZipInputStream
;
97 import java
.util
.zip
.ZipOutputStream
;
99 import mpicbg
.imglib
.container
.shapelist
.ShapeList
;
100 import mpicbg
.imglib
.image
.display
.imagej
.ImageJFunctions
;
101 import mpicbg
.imglib
.type
.numeric
.integer
.UnsignedByteType
;
102 import mpicbg
.models
.CoordinateTransformMesh
;
103 import mpicbg
.models
.NoninvertibleModelException
;
104 import mpicbg
.trakem2
.transform
.AffineModel2D
;
105 import mpicbg
.trakem2
.transform
.CoordinateTransform
;
106 import mpicbg
.trakem2
.transform
.CoordinateTransformList
;
107 import mpicbg
.trakem2
.transform
.TransformMesh
;
108 import mpicbg
.trakem2
.transform
.TransformMeshMapping
;
109 import mpicbg
.trakem2
.transform
.TransformMeshMappingWithMasks
.ImageProcessorWithMasks
;
111 public final class Patch
extends Displayable
implements ImageData
{
113 final static private double SQRT2
= Math
.sqrt(2.0);
114 private int type
= -1; // unknown
115 private boolean false_color
= false; // such as ImageProcessor.isColorLut
116 /** The channels that the currently existing awt image has ready for painting. */
117 private int channels
= 0xffffffff;
119 /** To generate contrasted images non-destructively. */
120 private double min
= 0;
121 private double max
= 255;
123 private int o_width
= 0, o_height
= 0;
125 /** To be set after the first successful query on whether this file exists, from the Loader, via the setCurrentPath method. This works as a good path cache to avoid excessive calls to File.exists(), which shows up as a huge performance drag. */
126 private String current_path
= null;
127 /** To be read from XML, or set when the file ImagePlus has been updated and the current_path points to something else. */
128 private String original_path
= null;
130 /** A set of filters to apply to the ImageProcessor after it is loaded. */
131 private IFilter
[] filters
;
133 /** A unique ID for the {@link CoordinateTransform}; 0 means there isn't one. */
134 private long ct_id
= 0;
136 /** A unique ID for the alpha mask; 0 means there isn't one.
137 * The alpha mask is not the outside mask as potentially generated by a {@link CoordinateTransform}.
138 * The alpha mask determines transparencies inside the width,height domain of the image. */
139 private long alpha_mask_id
= 0;
141 protected int meshResolution
= project
.getProperty("mesh_resolution", 32);
142 public int getMeshResolution(){ return meshResolution
; }
145 * Change the resolution of meshes used to render patches transformed by a
146 * {@link CoordinateTransform}. The method has to update bounding box
147 * offsets introduced by the {@link CoordinateTransform} because the
148 * bounding box has been calculated using the mesh.
150 * @param meshResolution
152 public void setMeshResolution( final int meshResolution
)
154 if ( !hasCoordinateTransform() )
155 this.meshResolution
= meshResolution
;
158 Rectangle box
= this.getCoordinateTransformBoundingBox();
159 this.at
.translate( -box
.x
, -box
.y
);
160 this.meshResolution
= meshResolution
;
161 box
= this.getCoordinateTransformBoundingBox();
162 this.at
.translate( box
.x
, box
.y
);
165 updateInDatabase("transform+dimensions"); // the AffineTransform
170 /** Create a new Patch and register the associated {@param filepath}
171 * with the project's loader.
173 * This method is intended for scripting, to avoid having to create a new Patch
174 * and then call {@link Loader#addedPatchFrom(String, Patch)}, which is easy to forget.
176 * @return the new Patch.
177 * @throws Exception if the image cannot be loaded from the {@param filepath}, or it's an unsupported type such as a composite image or a hyperstack. */
178 static public final Patch
createPatch(final Project project
, final String filepath
) throws Exception
{
179 final ImagePlus imp
= project
.getLoader().openImagePlus(filepath
);
180 if (null == imp
) throw new Exception("Cannot create Patch: the image cannot be opened from filepath " + filepath
);
181 if (imp
.isComposite()) throw new Exception("Cannot create Patch: composite images are not supported. Convert them to RGB first.");
182 if (imp
.isHyperStack()) throw new Exception("Cannot create Patch: hyperstacks are not supported.");
183 final Patch p
= new Patch(project
, new File(filepath
).getName(), 0, 0, imp
);
184 project
.getLoader().addedPatchFrom(filepath
, p
);
188 /** Construct a Patch from an image;
189 * most likely you will need to add the file path to the {@param imp}
190 * by calling {@link Loader#addedPatchFrom(String, Patch)}, as in this example:
192 * project.getLoader().addedPatchFrom("/path/to/file.png", thePatch); */
193 public Patch(final Project project
, final String title
, final double x
, final double y
, final ImagePlus imp
) {
194 super(project
, title
, x
, y
);
195 this.type
= imp
.getType();
196 // Color LUT in ImageJ is a nightmare of inconsistency. We set the COLOR_256 only for 8-bit images that are LUT images themselves; not for 16 or 32-bit images that may have a color LUT (which, by the way, ImageJ tiff encoder cannot save with the tif file.)
197 if (ImagePlus
.GRAY8
== this.type
&& imp
.getProcessor().isColorLut()) this.type
= ImagePlus
.COLOR_256
;
198 this.min
= imp
.getProcessor().getMin();
199 this.max
= imp
.getProcessor().getMax();
201 this.o_width
= imp
.getWidth();
202 this.o_height
= imp
.getHeight();
203 this.width
= (int)o_width
;
204 this.height
= (int)o_height
;
205 project
.getLoader().cache(this, imp
);
206 this.false_color
= imp
.getProcessor().isColorLut();
210 /** Reconstruct a Patch from the database. The ImagePlus will be loaded when necessary. */
211 public Patch(final Project project
, final long id
, final String title
,
212 final float width
, final float height
,
213 final int o_width
, final int o_height
,
214 final int type
, final boolean locked
, final double min
, final double max
, final AffineTransform at
) {
215 super(project
, id
, title
, locked
, at
, width
, height
);
220 this.height
= height
;
221 this.o_width
= o_width
;
222 this.o_height
= o_height
;
226 /** Create a new Patch defining all necessary parameters; it is the responsibility
227 * of the caller to ensure that the parameters are in agreement with the image
228 * contained in the {@param file_path}. */
229 public Patch(final Project project
, final String title
,
230 final float width
, final float height
,
231 final int o_width
, final int o_height
,
232 final int type
, final float alpha
,
233 final Color color
, final boolean locked
,
234 final double min
, final double max
,
235 final AffineTransform at
,
236 final String file_path
) {
237 this(project
, project
.getLoader().getNextId(), title
, width
, height
, o_width
, o_height
, type
, locked
, min
, max
, at
);
238 this.alpha
= Math
.max(0, Math
.min(alpha
, 1.0f
));
239 this.color
= null == color ? Color
.yellow
: color
;
240 project
.getLoader().addedPatchFrom(file_path
, this);
243 /** Reconstruct from an XML entry. */
244 public Patch(final Project project
, final long id
, final HashMap
<String
,String
> ht_attributes
, final HashMap
<Displayable
,String
> ht_links
) {
245 super(project
, id
, ht_attributes
, ht_links
);
247 project
.getLoader().addedPatchFrom(ht_attributes
.get("file_path"), this);
248 boolean hasmin
= false;
249 boolean hasmax
= false;
250 // parse specific fields
252 if (null != (data
= ht_attributes
.get("type"))) this.type
= Integer
.parseInt(data
);
253 if (null != (data
= ht_attributes
.get("false_color"))) this.false_color
= Boolean
.parseBoolean(data
);
254 if (null != (data
= ht_attributes
.get("min"))) {
255 this.min
= Double
.parseDouble(data
);
258 if (null != (data
= ht_attributes
.get("max"))) {
259 this.max
= Double
.parseDouble(data
);
262 if (null != (data
= ht_attributes
.get("o_width"))) this.o_width
= Integer
.parseInt(data
);
263 if (null != (data
= ht_attributes
.get("o_height"))) this.o_height
= Integer
.parseInt(data
);
264 if (null != (data
= ht_attributes
.get("pps"))) {
265 if (FSLoader
.isRelativePath(data
)) data
= project
.getLoader().getParentFolder() + data
;
266 project
.getLoader().setPreprocessorScriptPathSilently(this, data
);
268 if (null != (data
= ht_attributes
.get("original_path"))) this.original_path
= data
;
269 if (null != (data
= ht_attributes
.get("mres"))) this.meshResolution
= Integer
.parseInt(data
);
270 if (null != (data
= ht_attributes
.get("ct_id"))) this.ct_id
= Long
.parseLong(data
);
271 if (null != (data
= ht_attributes
.get("alpha_mask_id"))) this.alpha_mask_id
= Long
.parseLong(data
);
273 if (0 == o_width
|| 0 == o_height
) {
274 // The original image width and height are unknown.
276 Utils
.log2("Restoring original width/height from file for id=" + id
);
277 // Use BioFormats to read the dimensions out of the original file's header
278 final Dimension dim
= project
.getLoader().getDimensions(this);
280 o_height
= dim
.height
;
281 } catch (final Exception e
) {
282 Utils
.log("Could not read source data width/height for patch " + this +"\n --> To fix it, close the project and add o_width=\"XXX\" o_height=\"YYY\"\n to patch entry with oid=\"" + id
+ "\",\n where o_width,o_height are the image dimensions as defined in the image file.");
283 // So set them to whatever is somewhat survivable for the moment
284 o_width
= (int)width
;
285 o_height
= (int)height
;
290 if (hasmin
&& hasmax
) {
293 if (ImagePlus
.GRAY8
== type
|| ImagePlus
.COLOR_RGB
== type
|| ImagePlus
.COLOR_256
== type
) {
298 final ImageProcessor ip
= getImageProcessor();
300 // Some values, to survive:
302 max
= Patch
.getMaxMax(this.type
);
303 Utils
.log("WARNING could not restore min and max from image file for Patch #" + this.id
+ ", and they are not present in the XML file.");
305 ip
.resetMinAndMax(); // finds automatically reasonable values
306 setMinAndMax(ip
.getMin(), ip
.getMax());
312 /** The original width of the pixels in the source image file. */
313 public int getOWidth() { return o_width
; }
314 /** The original height of the pixels in the source image file. */
315 public int getOHeight() { return o_height
; }
317 /** Fetches the ImagePlus from the cache; <b>be warned</b>: the returned ImagePlus may have been flushed, removed and then recreated if the program had memory needs that required flushing part of the cache; use @getImageProcessor to get the pixels guaranteed not to be ever null. */
318 public ImagePlus
getImagePlus() {
319 return this.project
.getLoader().fetchImagePlus(this);
322 /** Fetches the ImageProcessor from the cache, which will never be flushed or its pixels set to null. If you keep many of these, you may end running out of memory: I advise you to call this method everytime you need the processor. */
323 public ImageProcessor
getImageProcessor() {
324 return this.project
.getLoader().fetchImageProcessor(this);
327 /** Recreate mipmaps and flush away any cached ones.
328 * This method is essentially the same as patch.getProject().getLoader().update(patch);
329 * which in turn it's the same as the following two calls:
330 * patch.getProject().getLoader().generateMipMaps(patch);
331 * patch.getProject().getLoader().decacheAWT(patch.getId());
333 * If you want to update lots of Patch instances in parallel, consider also
334 * project.getLoader().generateMipMaps(ArrayList patches, boolean overwrite);
336 public Future
<Boolean
> updateMipMaps() {
337 return project
.getLoader().regenerateMipMaps(this);
340 /** Update type, original dimensions and min,max from the ImagePlus.
341 * This is automatically done after a preprocessor script has modified the image. */
342 public void updatePixelProperties(final ImagePlus imp
) {
346 /** Update type, original dimensions and min,max from the given ImagePlus. */
347 private void readProps(final ImagePlus imp
) {
348 this.type
= imp
.getType();
349 this.false_color
= imp
.getProcessor().isColorLut();
350 if (imp
.getWidth() != (int)this.o_width
|| imp
.getHeight() != this.o_height
) {
351 this.o_width
= imp
.getWidth();
352 this.o_height
= imp
.getHeight();
353 this.width
= o_width
;
354 this.height
= o_height
;
357 final ImageProcessor ip
= imp
.getProcessor();
358 this.min
= ip
.getMin();
359 this.max
= ip
.getMax();
360 final HashSet
<String
> keys
= new HashSet
<String
>();
362 keys
.add("dimensions");
363 keys
.add("min_and_max");
364 updateInDatabase(keys
);
365 //updateInDatabase(new HashSet<String>(Arrays.asList(new String[]{"type", "dimensions", "min_and_max"})));
368 /** Set a new ImagePlus for this Patch.
369 * The original path and image remain untouched. Any later image is deleted and replaced by the new one.
371 public String
set(final ImagePlus new_imp
) {
372 synchronized (this) {
373 if (null == new_imp
) return null;
374 // 0 - set original_path to the current path if there is no original_path recorded:
376 for (final Patch p
: getStackPatches()) {
377 if (null == p
.original_path
) original_path
= p
.project
.getLoader().getAbsolutePath(p
);
380 if (null == original_path
) original_path
= project
.getLoader().getAbsolutePath(this);
382 // 1 - tell the loader to store the image somewhere, unless the image has a path already
383 final String path
= project
.getLoader().setImageFile(this, new_imp
);
385 Utils
.log2("setImageFile returned null!");
386 return null; // something went wrong
388 // 2 - update properties and mipmaps
390 for (final Patch p
: getStackPatches()) {
391 p
.readProps(new_imp
);
392 project
.getLoader().regenerateMipMaps(p
);
396 project
.getLoader().regenerateMipMaps(this);
399 Display
.repaint(layer
, this, 5);
400 return project
.getLoader().getAbsolutePath(this);
403 /** Boundary checks on min and max, given the image type. */
404 private void checkMinMax() {
405 if (-1 == this.type
) {
406 Utils
.log("ERROR -1 == type for patch " + this);
409 final double max_max
= Patch
.getMaxMax(this.type
);
410 if (-1 == min
&& -1 == max
) {
415 case ImagePlus
.GRAY8
:
416 case ImagePlus
.COLOR_RGB
:
417 case ImagePlus
.COLOR_256
:
420 Utils
.log("WARNING set min to 0 for patch " + this + " of type " + type
);
424 if (this.max
> max_max
) {
426 Utils
.log("WARNING fixed max larger than maximum max for type " + type
);
428 if (this.min
> this.max
) {
430 Utils
.log("WARNING fixed min larger than max for patch " + this);
434 /** The min and max values are stored with the Patch, so that the image can be flushed away but the non-destructive contrast settings preserved. */
435 public void setMinAndMax(final double min
, final double max
) {
439 updateInDatabase("min_and_max");
440 Utils
.log2("Patch.setMinAndMax: min,max " + min
+ "," + max
);
443 public double getMin() { return min
; }
444 public double getMax() { return max
; }
446 /** Returns the ImagePlus type of this Patch. */
447 public int getType() {
451 public Image
createImage(final ImagePlus imp
) {
452 return adjustChannels(channels
, true, imp
);
455 public Image
createImage() {
456 return adjustChannels(channels
, true, null);
459 public int getChannelAlphas() {
463 /** @param c contains the current Display 'channels' value (the transparencies of each channel). This method creates a new color image in which each channel (R, G, B) has the corresponding alpha (in fact, opacity) specified in the 'c'. This alpha is independent of the alpha of the whole Patch. The method updates the Loader cache with the newly created image. The argument 'imp' is optional: if null, it will be retrieved from the loader.<br />
464 * For non-color images, a standard image is returned regardless of the @param c
466 private Image
adjustChannels(final int c
, final boolean force
, ImagePlus imp
) {
467 if (null == imp
) imp
= project
.getLoader().fetchImagePlus(this);
468 ImageProcessor ip
= imp
.getProcessor();
469 if (null == ip
) return null; // fixing synch problems when deleting a Patch
471 if (ImagePlus
.COLOR_RGB
== type
) {
472 if (imp
.getType() != type
) {
473 ip
= Utils
.convertTo(ip
, type
, false); // all other types need not be converted, since there are no alphas anyway
475 if ((c
&0x00ffffff) == 0x00ffffff && !force
) {
477 awt
= ip
.createImage(); //imp.getImage();
478 // pixels array will be shared using ij138j and above
480 // modified from ij.process.ColorProcessor.createImage() by Wayne Rasband
481 final int[] pixels
= (int[])ip
.getPixels();
482 final float cr
= ((c
&0xff0000)>>16) / 255.0f
;
483 final float cg
= ((c
&0xff00)>>8) / 255.0f
;
484 final float cb
= (c
&0xff) / 255.0f
;
485 final int[] pix
= new int[pixels
.length
];
487 for (int i
=pixels
.length
-1; i
>-1; i
--) {
489 pix
[i
] = (((int)(((p
&0xff0000)>>16) * cr
))<<16)
490 + (((int)(((p
&0xff00)>>8) * cg
))<<8)
491 + (int) ((p
&0xff) * cb
);
493 final int w
= imp
.getWidth();
494 final MemoryImageSource source
= new MemoryImageSource(w
, imp
.getHeight(), DCM
, pix
, 0, w
);
495 source
.setAnimated(true);
496 source
.setFullBufferUpdates(true);
497 awt
= Toolkit
.getDefaultToolkit().createImage(source
);
500 awt
= ip
.createImage();
503 //Utils.log2("ip's min, max: " + ip.getMin() + ", " + ip.getMax());
510 static final public DirectColorModel DCM
= new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
512 /** Just throws the cached image away if the alpha of the channels has changed. */
513 private final void checkChannels(final int channels
, final double magnification
) {
514 if (this.channels
!= channels
&& (ImagePlus
.COLOR_RGB
== this.type
|| ImagePlus
.COLOR_256
== this.type
)) {
515 final int old_channels
= this.channels
;
516 this.channels
= channels
; // before, so if any gets recreated it's done right
517 project
.getLoader().adjustChannels(this, old_channels
);
521 /** Takes an image and scales its channels according to the values packed in this.channels.
522 * This method is intended for fixing RGB images which are loaded from jpegs (the mipmaps), and which
523 * have then the full colorization of the original image present in their pixels array.
524 * Otherwise the channel opacity scaling makes no sense.
525 * If 0xffffffff == this.channels the awt is returned as is.
526 * If the awt is null returns null.
528 public final Image
adjustChannels(final Image awt
) {
529 if (0xffffffff == this.channels
|| null == awt
) return awt
;
530 BufferedImage bi
= null;
532 if (awt
instanceof BufferedImage
) bi
= (BufferedImage
)awt
;
534 bi
= new BufferedImage(awt
.getWidth(null), awt
.getHeight(null), BufferedImage
.TYPE_INT_ARGB
);
535 bi
.getGraphics().drawImage(awt
, 0, 0, null);
537 // extract channel values
538 final float cr
= ((channels
&0xff0000)>>16) / 255.0f
;
539 final float cg
= ((channels
&0xff00)>>8 ) / 255.0f
;
540 final float cb
= ( channels
&0xff ) / 255.0f
;
542 Utils
.log2("w, h: " + bi
.getWidth() + ", " + bi
.getHeight());
543 final int[] pixels
= bi
.getRGB(0, 0, bi
.getWidth(), bi
.getHeight(), null, 0, 1);
544 // scale them according to channel opacities
546 for (int i
=0; i
<pixels
.length
; i
++) {
548 pixels
[i
] = (((int)(((p
&0xff0000)>>16) * cr
))<<16)
549 + (((int)(((p
&0xff00)>>8) * cg
))<<8)
550 + (int) ((p
&0xff) * cb
);
553 bi
.setRGB(0, 0, bi
.getWidth(), bi
.getHeight(), pixels
, 0, 1);
558 public void paintOffscreen(final Graphics2D g
, final Rectangle srcRect
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
, final List
<Layer
> layers
) {
559 paint(g
, fetchImage(magnification
, channels
, true), srcRect
);
563 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
> _ignored
) {
564 paint(g
, fetchImage(magnification
, channels
, false), srcRect
);
567 private final MipMapImage
fetchImage(final double magnification
, final int channels
, final boolean wait_for_image
) {
568 checkChannels(channels
, magnification
);
570 // Consider all possible scaling components: m00, m01
572 double sc
= magnification
* Math
.max(Math
.abs(at
.getScaleX()),
573 Math
.max(Math
.abs(at
.getScaleY()),
574 Math
.max(Math
.abs(at
.getShearX()),
575 Math
.abs(at
.getShearY()))));
576 if (sc
< 0) sc
= magnification
;
577 return wait_for_image ?
578 project
.getLoader().fetchDataImage(this, sc
)
579 : project
.getLoader().fetchImage(this, sc
);
582 private void paint( final Graphics2D g
, final Image image
, final Rectangle srcRect
)
585 * infer scale: this scales the numbers of pixels according to patch
586 * size which might not be the exact scale the image was sampled at
588 final int iw
= image
.getWidth(null);
589 final int ih
= image
.getHeight(null);
590 paint( g
, new MipMapImage( image
, this.width
/ iw
, this.height
/ ih
), srcRect
);
593 private void paint(final Graphics2D g
, final MipMapImage mipMap
, final Rectangle srcRect
) {
595 final AffineTransform atp
= new AffineTransform();
598 * Compensate for AWT considering coordinates at pixel corners
599 * and TrakEM2 and mpicbg considering them at pixel centers.
601 atp
.translate( 0.5, 0.5 );
603 atp
.concatenate( this.at
);
605 atp
.scale( mipMap
.scaleX
, mipMap
.scaleY
);
608 * Compensate MipMap pixel access for AWT considering coordinates at
609 * pixel corners and TrakEM2 and mpicbg considering them at pixel
612 if (Loader
.GAUSSIAN
== project
.getMipMapsMode()) {
613 atp
.translate( -0.5, -0.5 );
616 atp
.translate( -0.5 / mipMap
.scaleX
, -0.5 / mipMap
.scaleY
);
619 paintMipMap(g
, mipMap
, atp
, srcRect
);
622 /** Paint first whatever is available, then request that the proper image be loaded and painted. */
624 public void prePaint(final Graphics2D g
, final Rectangle srcRect
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
, final List
<Layer
> _ignored
) {
626 final AffineTransform atp
= new AffineTransform();
629 * Compensate for AWT considering coordinates at pixel corners
630 * and TrakEM2 and mpicbg considering them at pixel centers.
632 atp
.translate( 0.5, 0.5 );
634 atp
.concatenate( this.at
);
636 checkChannels(channels
, magnification
);
638 // Consider all possible scaling components: m00, m01
640 double sc
= magnification
* Math
.max(Math
.abs(at
.getScaleX()),
641 Math
.max(Math
.abs(at
.getScaleY()),
642 Math
.max(Math
.abs(at
.getShearX()),
643 Math
.abs(at
.getShearY()))));
644 if (sc
< 0) sc
= magnification
;
646 MipMapImage mipMap
= project
.getLoader().getCachedClosestAboveImage(this, sc
); // above or equal
647 if (null == mipMap
) {
648 mipMap
= project
.getLoader().getCachedClosestBelowImage(this, sc
); // below, not equal
649 if (null == mipMap
) {
650 // fetch the smallest image possible
651 //image = project.getLoader().fetchAWTImage(this, Loader.getHighestMipMapLevel(this));
652 // fetch an image 1/4 of the necessary size
653 mipMap
= project
.getLoader().fetchImage(this, sc
/4);
655 // painting a smaller image, will need to repaint with the proper one
656 if (!Loader
.isSignalImage( mipMap
.image
) ) {
657 // use the lower resolution image, but ask to repaint it on load
658 Loader
.preload(this, sc
, true);
662 atp
.scale( mipMap
.scaleX
, mipMap
.scaleY
);
665 * Compensate MipMap pixel access for AWT considering coordinates at
666 * pixel corners and TrakEM2 and mpicbg considering them at pixel
669 if (Loader
.GAUSSIAN
== project
.getMipMapsMode()) {
670 atp
.translate( -0.5, -0.5 );
673 atp
.translate( -0.5 / mipMap
.scaleX
, -0.5 / mipMap
.scaleY
);
676 paintMipMap(g
, mipMap
, atp
, srcRect
);
679 private final void paintMipMap(final Graphics2D g
, final MipMapImage mipMap
,
680 final AffineTransform atp
, final Rectangle srcRect
)
682 final Composite original_composite
= g
.getComposite();
683 // Fail gracefully for graphics cards that don't support custom composites, like ATI cards:
685 g
.setComposite( getComposite(getCompositeMode()) );
686 g
.drawImage( mipMap
.image
, atp
, null );
687 } catch (final Throwable t
) {
688 g
.setComposite(original_composite
);
689 Utils
.log(new StringBuilder("Cannot paint Patch with composite type ").append(compositeModes
[getCompositeMode()]).append("\nReason:\n").append(t
.toString()).toString());
690 g
.drawImage( mipMap
.image
, atp
, null );
692 g
.setComposite( original_composite
);
696 public boolean isDeletable() {
697 return 0 == width
&& 0 == height
;
700 /** Remove only if linked to other Patches or to noone. */
702 public boolean remove(final boolean check
) {
703 if (check
&& !Utils
.check("Really remove " + this.toString() + " ?")) return false;
704 if (isStack()) { // this Patch is part of a stack
705 final GenericDialog gd
= new GenericDialog("Stack!");
706 gd
.addMessage("Really delete the entire stack?");
707 gd
.addCheckbox("Delete layers if empty", true);
709 if (gd
.wasCanceled()) return false;
710 final boolean delete_empty_layers
= gd
.getNextBoolean();
712 final HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
713 getStackPatchesNR(ht
);
714 Utils
.log2("Removing stack patches: " + ht
.size());
715 for (final Patch p
: ht
.values()) {
716 if (!p
.isOnlyLinkedTo(this.getClass())) {
717 Utils
.showMessage("At least one slice of the stack (z=" + p
.getLayer().getZ() + ") is supporting other data.\nCan't delete.");
721 final ArrayList
<Layer
> layers_to_remove
= new ArrayList
<Layer
>();
722 for (final Patch p
: ht
.values()) {
723 if (!p
.layer
.remove(p
) || !p
.removeFromDatabase()) {
724 Utils
.showMessage("Can't delete Patch " + p
);
728 p
.removeLinkedPropertiesFromOrigins();
729 //no need//it.remove();
730 layers_to_remove
.add(p
.layer
);
731 if (p
.layer
.isEmpty()) Display
.close(p
.layer
);
732 else Display
.repaint(p
.layer
);
734 if (delete_empty_layers
) {
735 for (final Layer la
: layers_to_remove
) {
737 project
.getLayerTree().remove(la
, false);
745 if (isOnlyLinkedTo(Patch
.class, this.layer
) && layer
.remove(this) && removeFromDatabase()) { // don't alow to remove linked patches (unless only linked to other patches in the same layer)
747 removeLinkedPropertiesFromOrigins();
751 Utils
.showMessage("Patch: can't remove! The image is linked and thus supports other data).");
757 /** Returns true if this Patch holds direct links to at least one other image in a different layer. Doesn't check for total overlap. */
758 public final boolean isStack() {
759 if (null == hs_linked
|| hs_linked
.isEmpty()) return false;
760 for (final Displayable d
: hs_linked
) {
761 if (d
.getClass() == Patch
.class && d
.layer
.getId() != this.layer
.getId()) return true;
766 /** Retuns a virtual ImagePlus with a virtual stack if necessary. */
767 public PatchStack
makePatchStack() {
769 final TreeMap
<Double
,Patch
> ht
= new TreeMap
<Double
,Patch
>();
770 getStackPatchesNR(ht
);
772 int currentSlice
= 1; // from 1 to n, as in ImageStack
774 patch
= new Patch
[ht
.size()];
776 for (final Patch p
: ht
.values()) { // sorted by z
778 if (p
.id
== this.id
) currentSlice
= i
+1;
782 patch
= new Patch
[]{ this };
784 return new PatchStack(patch
, currentSlice
);
787 public ArrayList
<Patch
> getStackPatches() {
788 final TreeMap
<Double
,Patch
> ht
= new TreeMap
<Double
,Patch
>();
789 getStackPatchesNR(ht
);
790 return new ArrayList
<Patch
>(ht
.values()); // sorted by z
793 /** Non-recursive version to avoid stack overflows with "excessive" recursion (I hate java). */
794 private void getStackPatchesNR(final Map
<Double
,Patch
> ht
) {
795 final ArrayList
<Patch
> list1
= new ArrayList
<Patch
>();
797 final ArrayList
<Patch
> list2
= new ArrayList
<Patch
>();
798 while (list1
.size() > 0) {
800 for (final Patch p
: list1
) {
801 if (null != p
.hs_linked
) {
802 for (final Iterator
<?
> it
= p
.hs_linked
.iterator(); it
.hasNext(); ) {
803 final Object ln
= it
.next();
804 if (ln
.getClass() == Patch
.class) {
805 final Patch pa
= (Patch
)ln
;
806 if (!ht
.containsValue(pa
)) {
807 ht
.put(pa
.layer
.getZ(), pa
);
819 /** Opens and closes the tag and exports data. The image is saved in the directory provided in @param any as a String. */
821 public void exportXML(final StringBuilder sb_body
, final String indent
, final XMLOptions options
) { // TODO the Loader should handle the saving of images, not this class.
822 final String in
= indent
+ "\t";
825 if (options
.export_images
) {
826 path
= options
.patches_dir
+ title
;
827 // save image without overwriting, and add proper extension (.zip)
828 path2
= project
.getLoader().exportImage(this, path
, false);
829 // path2 will be null if the file exists already
831 sb_body
.append(indent
).append("<t2_patch\n");
832 String rel_path
= null;
833 if (null != path
&& path
.equals(path2
)) { // this happens when a DB project is exported. It may be a different path when it's a FS loader
834 //Utils.log2("p id=" + id + " path==path2");
836 int i_slash
= rel_path
.lastIndexOf('/'); // TrakEM2 uses paths that always have '/' and never '\', so using java.io.File.separatorChar would be an error.
838 i_slash
= rel_path
.lastIndexOf('/', i_slash
-1);
840 rel_path
= rel_path
.substring(i_slash
+1);
844 //Utils.log2("Setting rel_path to " + path2);
847 // For FSLoader projects, saving a second time will save images as null unless calling it
848 if (null == rel_path
) {
849 //Utils.log2("path2 was null");
850 final Object ob
= project
.getLoader().getPath(this);
851 path2
= null == ob ?
null : (String
)ob
;
853 //Utils.log2("ERROR: No path for Patch id=" + id + " and title: " + title);
854 rel_path
= title
; // at least some clue for recovery
860 //Utils.log("Patch path is: " + rel_path);
862 super.exportXML(sb_body
, in
, options
);
863 final String
[] RGB
= Utils
.getHexRGBColor(color
);
864 int type
= this.type
;
865 if (-1 == this.type
) {
866 Utils
.log2("Retrieving type for p = " + this);
867 final ImagePlus imp
= project
.getLoader().fetchImagePlus(this);
868 if (null != imp
) type
= imp
.getType();
870 sb_body
.append(in
).append("type=\"").append(type
/*null == any ? ImagePlus.GRAY8 : type*/).append("\"\n")
871 .append(in
).append("file_path=\"").append(rel_path
).append("\"\n")
872 .append(in
).append("style=\"fill-opacity:").append(alpha
).append(";stroke:#").append(RGB
[0]).append(RGB
[1]).append(RGB
[2]).append(";\"\n")
873 .append(in
).append("o_width=\"").append(o_width
).append("\"\n")
874 .append(in
).append("o_height=\"").append(o_height
).append("\"\n")
876 if (null != original_path
) {
877 sb_body
.append(in
).append("original_path=\"").append(original_path
).append("\"\n");
879 sb_body
.append(in
).append("min=\"").append(min
).append("\"\n");
880 sb_body
.append(in
).append("max=\"").append(max
).append("\"\n");
882 final String pps
= getPreprocessorScriptPath();
883 if (null != pps
) sb_body
.append(in
).append("pps=\"").append(project
.getLoader().makeRelativePath(pps
)).append("\"\n");
885 sb_body
.append(in
).append("mres=\"").append(meshResolution
).append("\"\n");
887 if (hasCoordinateTransform()) {
888 sb_body
.append(in
).append("ct_id=\"").append(ct_id
).append("\"\n");
891 if (hasAlphaMask()) {
892 sb_body
.append(in
).append("alpha_mask_id=\"").append(alpha_mask_id
).append("\"\n");
895 sb_body
.append(indent
).append(">\n");
897 if (hasCoordinateTransform()) {
898 if (options
.include_coordinate_transform
) {
899 // Write an XML entry for the CoordinateTransform
900 char[] ct_chars
= null;
902 ct_chars
= readCoordinateTransformFile();
903 } catch (final Exception e
) {
906 if (null != ct_chars
) {
907 sb_body
.append(ct_chars
).append('\n');
909 Utils
.log("ERROR: could not write the CoordinateTransform to the XML file!");
914 if (null != filters
&& filters
.length
> 0) {
915 for (final IFilter f
: filters
) sb_body
.append(f
.toXML(in
)); // specify their own line termination
918 super.restXML(sb_body
, in
, options
);
920 sb_body
.append(indent
).append("</t2_patch>\n");
923 static private final double getMaxMax(final int type
) {
926 case ImagePlus
.GRAY16
: pow
= 2; break; // TODO problems with unsigned short most likely
927 case ImagePlus
.GRAY32
: pow
= 4; break;
930 return Math
.pow(256, pow
) - 1;
933 static public void exportDTD(final StringBuilder sb_header
, final HashSet
<String
> hs
, final String indent
) {
934 final String type
= "t2_patch";
935 if (hs
.contains(type
)) return;
936 // TrakEM2's XML is validated in a non-conventional way, so no need to specify the arguments for each filter
937 sb_header
.append(indent
).append("<!ELEMENT t2_filter EMPTY>\n");
939 sb_header
.append(indent
).append("<!ELEMENT t2_patch (").append(Displayable
.commonDTDChildren()).append(",ict_transform,ict_transform_list,t2_filter)>\n");
940 Displayable
.exportDTD(type
, sb_header
, hs
, indent
);
941 sb_header
.append(indent
).append(TAG_ATTR1
).append(type
).append(" file_path").append(TAG_ATTR2
)
942 .append(indent
).append(TAG_ATTR1
).append(type
).append(" original_path").append(TAG_ATTR2
)
943 .append(indent
).append(TAG_ATTR1
).append(type
).append(" type").append(TAG_ATTR2
)
944 .append(indent
).append(TAG_ATTR1
).append(type
).append(" false_color").append(TAG_ATTR2
)
945 .append(indent
).append(TAG_ATTR1
).append(type
).append(" ct").append(TAG_ATTR2
)
946 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_width").append(TAG_ATTR2
)
947 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_height").append(TAG_ATTR2
)
948 .append(indent
).append(TAG_ATTR1
).append(type
).append(" min").append(TAG_ATTR2
)
949 .append(indent
).append(TAG_ATTR1
).append(type
).append(" max").append(TAG_ATTR2
)
950 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_width").append(TAG_ATTR2
)
951 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_height").append(TAG_ATTR2
)
952 .append(indent
).append(TAG_ATTR1
).append(type
).append(" pps").append(TAG_ATTR2
) // preprocessor script
953 .append(indent
).append(TAG_ATTR1
).append(type
).append(" mres").append(TAG_ATTR2
)
954 .append(indent
).append(TAG_ATTR1
).append(type
).append(" ct_id").append(TAG_ATTR2
)
955 .append(indent
).append(TAG_ATTR1
).append(type
).append(" alpha_mask_id").append(TAG_ATTR2
)
959 /** Performs a copy of this object, without the links, unlocked and visible, except for the image which is NOT duplicated. */
961 public Displayable
clone(final Project pr
, final boolean copy_id
) {
962 final long nid
= copy_id ?
this.id
: pr
.getLoader().getNextId();
963 final Patch copy
= new Patch(pr
, nid
, null != title ? title
.toString() : null, width
, height
, o_width
, o_height
, type
, false, min
, max
, (AffineTransform
)at
.clone());
964 copy
.false_color
= this.false_color
;
965 copy
.color
= new Color(color
.getRed(), color
.getGreen(), color
.getBlue());
966 copy
.alpha
= this.alpha
;
968 copy
.channels
= this.channels
;
971 copy
.ct_id
= this.ct_id
;
972 copy
.alpha_mask_id
= this.alpha_mask_id
;
974 if (!copy_id
|| pr
!= this.project
) {
976 if (0 != copy
.alpha_mask_id
978 this.createAlphaMaskFilePath(this.alpha_mask_id
),
979 copy
.createAlphaMaskFilePath(copy
.alpha_mask_id
))) {
980 Utils
.log("ERROR: could not copy alpha mask file for patch #" + this.id
);
982 } catch (final IOException ioe
) {
984 Utils
.log("ERROR: could not copy alpha mask file for patch #" + this.id
);
989 this.createCTFilePath(this.ct_id
),
990 copy
.createCTFilePath(copy
.ct_id
))) {
991 Utils
.log("ERROR: could not copy coordinate transform file for patch #" + this.id
);
993 } catch (final IOException ioe
) {
995 Utils
.log("ERROR: could not copy coordinate transform file for patch #" + this.id
);
998 copy
.addToDatabase();
999 pr
.getLoader().addedPatchFrom(this.project
.getLoader().getAbsolutePath(this), copy
);
1001 // Copy preprocessor scripts
1002 final String pspath
= this.project
.getLoader().getPreprocessorScriptPath(this);
1003 if (null != pspath
) pr
.getLoader().setPreprocessorScriptPathSilently(copy
, pspath
);
1005 // Copy image filters
1006 if (null != filters
) {
1007 copy
.filters
= FilterEditor
.duplicate(this.filters
);
1013 static public final class TransformProperties
{
1014 final public Rectangle bounds
;
1015 final public AffineTransform at
;
1016 final public CoordinateTransform ct
;
1017 final public int meshResolution
;
1018 final public int o_width
, o_height
;
1019 final public Area area
;
1021 public TransformProperties(final Patch p
) {
1022 this.at
= new AffineTransform(p
.at
);
1023 this.ct
= p
.getCoordinateTransform();
1024 this.meshResolution
= p
.getMeshResolution();
1025 this.bounds
= p
.getBoundingBox(null);
1026 this.o_width
= p
.o_width
;
1027 this.o_height
= p
.o_height
;
1028 this.area
= p
.getArea();
1032 public Patch
.TransformProperties
getTransformPropertiesCopy() {
1033 return new Patch
.TransformProperties(this);
1037 /** Override to cancel. */
1039 public boolean linkPatches() {
1040 Utils
.log2("Patch class can't link other patches using Displayable.linkPatches()");
1045 public void paintSnapshot(final Graphics2D g
, final Layer layer
, final List
<Layer
> layers
, final Rectangle srcRect
, final double mag
) {
1046 switch (layer
.getParent().getSnapshotsMode()) {
1048 if (!project
.getLoader().isSnapPaintable(this.id
)) {
1051 paint(g
, srcRect
, mag
, false, this.channels
, layer
, layers
);
1057 default: return; // case 2: // disabled, no paint
1061 static protected void crosslink(final Collection
<Displayable
> patches
, final boolean overlapping_only
) {
1062 if (null == patches
) return;
1063 final ArrayList
<Patch
> al
= new ArrayList
<Patch
>();
1064 for (final Object ob
: patches
) if (ob
instanceof Patch
) al
.add((Patch
)ob
); // ...
1065 final int len
= al
.size();
1066 if (len
< 2) return;
1067 final Patch
[] pa
= new Patch
[len
];
1069 // linking is reciprocal: need only call link() on one member of the pair
1070 for (int i
=0; i
<pa
.length
; i
++) {
1071 for (int j
=i
+1; j
<pa
.length
; j
++) {
1072 if (overlapping_only
&& !pa
[i
].intersects(pa
[j
])) continue;
1078 /** Magnification-dependent counterpart to ImageProcessor.getPixel(x, y). Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method.*/
1079 public int getPixel(final double mag
, final int x
, final int y
) {
1080 final int[] iArray
= getPixel(x
, y
, mag
);
1081 if (ImagePlus
.COLOR_RGB
== this.type
) {
1082 return (iArray
[0]<<16) + (iArray
[1]<<8) + iArray
[2];
1087 /** Magnification-dependent counterpart to ImageProcessor.getPixel(x, y, iArray). Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method.*/
1088 public int[] getPixel(final double mag
, final int x
, final int y
, final int[] iArray
) {
1089 final int[] ia
= getPixel(x
, y
, mag
);
1090 if(null != iArray
) {
1099 /** Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method. */
1100 public int[] getPixel(final int x
, final int y
, final double mag
) {
1101 if (project
.getLoader().isUnloadable(this)) return new int[4];
1102 final MipMapImage mipMap
= project
.getLoader().fetchImage(this, mag
);
1103 if (Loader
.isSignalImage(mipMap
.image
)) return new int[4];
1104 final int w
= mipMap
.image
.getWidth(null);
1105 final Point2D
.Double pd
= inverseTransformPoint(x
, y
);
1106 final int x2
= (int)(pd
.x
/ mipMap
.scaleX
);
1107 final int y2
= (int)(pd
.y
/ mipMap
.scaleY
);
1108 final int[] pvalue
= new int[4];
1109 final PixelGrabber pg
= new PixelGrabber( mipMap
.image
, x2
, y2
, 1, 1, pvalue
, 0, w
);
1112 } catch (final InterruptedException ie
) {
1116 approximateTransferPixel(pvalue
);
1121 /** Transfer an 8-bit or RGB pixel to this image color space, interpolating;
1122 * the pvalue is modified in place.
1123 * For float images (GRAY32), the float value is packed into bits in pvalue[0],
1124 * and can be recovered with Float.intBitsToFloat(pvalue[0]). */
1125 protected void approximateTransferPixel(final int[] pvalue
) {
1127 case ImagePlus
.COLOR_256
: // mipmaps use RGB images internally, so I can't compute the index in the LUT
1128 case ImagePlus
.COLOR_RGB
:
1129 final int c
= pvalue
[0];
1130 pvalue
[0] = (c
&0xff0000)>>16; // R
1131 pvalue
[1] = (c
&0xff00)>>8; // G
1132 pvalue
[2] = c
&0xff; // B
1134 case ImagePlus
.GRAY8
:
1135 pvalue
[0] = pvalue
[0]&0xff;
1137 case ImagePlus
.GRAY16
:
1138 pvalue
[0] = pvalue
[0]&0xff;
1139 // correct range: from 8-bit of the mipmap to 16 bit
1140 pvalue
[0] = (int)(min
+ pvalue
[0] * ( (max
- min
) / 256 ));
1142 case ImagePlus
.GRAY32
:
1143 pvalue
[0] = pvalue
[0]&0xff;
1144 // correct range: from 8-bit of the mipmap to 32 bit
1145 // ... and encode, so that it will be decoded with Float.intBitsToFloat
1146 pvalue
[0] = Float
.floatToIntBits((float)(min
+ pvalue
[0] * ( (max
- min
) / 256 )));
1151 /** If this patch is part of a stack, the file path will contain the slice number attached to it, in the form -----#slice=10 for slice number 10. */
1152 public final String
getFilePath() {
1153 if (null != current_path
) return current_path
;
1154 return project
.getLoader().getAbsolutePath(this);
1157 /** Returns the absolute path to the image file, as read by the OS. */
1158 public final String
getImageFilePath() {
1159 return project
.getLoader().getImageFilePath(this);
1162 /** Returns the value of the field current_path, which may be null. If not null, the value may contain the slice info in it if it's part of a stack. */
1163 public final String
getCurrentPath() { return current_path
; }
1165 /** Cache a proper, good, known path to the image wrapped by this Patch. */
1166 public final void cacheCurrentPath(final String path
) {
1167 this.current_path
= path
;
1170 /** Returns the value of the field original_path, which may be null. If not null, the value may contain the slice info in it if it's part of a stack. */
1171 synchronized public String
getOriginalPath() { return original_path
; }
1174 protected void setAlpha(final float alpha
, final boolean update
) {
1176 final HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
1177 getStackPatchesNR(ht
);
1178 for (final Patch pa
: ht
.values()) {
1180 pa
.updateInDatabase("alpha");
1181 Display
.repaint(pa
.layer
, pa
, 5);
1183 Display3D
.setTransparency(this, alpha
);
1184 } else super.setAlpha(alpha
, update
);
1187 public void debug() {
1188 Utils
.log2("Patch id=" + id
+ "\n\toriginal_path=" + original_path
+ "\n\tcurrent_path=" + current_path
);
1191 /** Revert the ImagePlus to the one stored in original_path, if any; will revert all linked patches if this is part of a stack. */
1192 public boolean revert() {
1193 synchronized (this) {
1194 if (null == original_path
) return false; // nothing to revert to
1195 // 1 - check that original_path exists
1196 if (!new File(original_path
).exists()) {
1197 Utils
.log("CANNOT revert: Original file path does not exist: " + original_path
+ " for patch " + getTitle() + " #" + id
);
1200 // 2 - check that the original can be loaded
1201 final ImagePlus imp
= project
.getLoader().fetchOriginal(this);
1202 if (null == imp
|| null == set(imp
)) {
1203 Utils
.log("CANNOT REVERT: original image at path " + original_path
+ " fails to load, for patch " + getType() + " #" + id
);
1206 // 3 - update path in loader, and cache imp for each stack slice id
1208 for (final Patch p
: getStackPatches()) {
1209 p
.project
.getLoader().addedPatchFrom(p
.original_path
, p
);
1210 p
.project
.getLoader().cacheImagePlus(p
.id
, imp
);
1211 p
.project
.getLoader().regenerateMipMaps(p
);
1214 project
.getLoader().addedPatchFrom(original_path
, this);
1215 project
.getLoader().cacheImagePlus(id
, imp
);
1216 project
.getLoader().regenerateMipMaps(this);
1218 // 4 - update screens
1220 Display
.repaint(layer
, this, 0);
1221 Utils
.showStatus("Reverted patch " + getTitle(), false);
1225 /** For reconstruction purposes, overwrites the present {@link CoordinateTransform}, if any, with the given one.
1226 * This method has been repurposed to write the {@link CoordinateTransform} to disk and set a new {@link #ct_id}
1227 * that points to it. */
1228 public void setCoordinateTransformSilently(final CoordinateTransform ct
) {
1230 if (0 == this.ct_id
) {
1231 // Old XML, lacks a ct_id attribute; will get a new ct_id
1232 setNewCoordinateTransform(ct
);
1234 // New XML with ct_id attribute
1235 writeNewCoordinateTransform(ct
, this.ct_id
);
1237 } catch (final Exception e
) {
1242 /** Set a CoordinateTransform to this Patch.
1243 * The resulting image of applying the coordinate transform does not need to be rectangular: an alpha mask will take care of the borders. You should call updateMipMaps() afterwards to update the mipmap images used for painting this Patch to the screen. */
1244 public final void setCoordinateTransform(final CoordinateTransform ct
) {
1246 Utils
.log("Cannot set coordinate transform: patch is linked!");
1250 CoordinateTransform this_ct
= hasCoordinateTransform() ?
getCoordinateTransform() : null;
1252 if (null != this_ct
) {
1253 // restore image without the transform
1254 final TransformMesh mesh
= new TransformMesh(this_ct
, meshResolution
, o_width
, o_height
);
1255 final Rectangle box
= mesh
.getBoundingBox();
1256 this.at
.translate(-box
.x
, -box
.y
);
1257 updateInDatabase("transform+dimensions");
1261 setNewCoordinateTransform(ct
);
1262 } catch (final Exception e
) {
1263 throw new RuntimeException(e
);
1267 updateInDatabase("ict_transform");
1269 if (null == this_ct
) {
1276 // Adjust the AffineTransform to correct for bounding box displacement
1278 final TransformMesh mesh
= new TransformMesh(this_ct
, meshResolution
, o_width
, o_height
);
1279 final Rectangle box
= mesh
.getBoundingBox();
1280 this.at
.translate(box
.x
, box
.y
);
1282 height
= box
.height
;
1283 updateInDatabase("transform+dimensions"); // the AffineTransform
1286 // Updating the mipmaps will call createTransformedImage below if ct is not null
1287 /* DISABLED */ //updateMipMaps();
1291 * Append a {@link CoordinateTransform} to the current
1292 * {@link CoordinateTransformList}. If there is no transform yet, it just
1293 * sets it. If there is only one transform, it replaces it by a list
1294 * containing both, the existing first.
1296 @SuppressWarnings("unchecked")
1297 public final void appendCoordinateTransform(final CoordinateTransform ct
) {
1298 if (!hasCoordinateTransform())
1299 setCoordinateTransform(ct
);
1301 final CoordinateTransformList
< CoordinateTransform
> ctl
;
1302 final CoordinateTransform this_ct
= getCoordinateTransform();
1303 if (this_ct
instanceof CoordinateTransformList
<?
>)
1304 ctl
= (CoordinateTransformList
< CoordinateTransform
>)this_ct
.copy();
1306 ctl
= new CoordinateTransformList
< CoordinateTransform
>();
1310 setCoordinateTransform(ctl
);
1316 * Pre-append a {@link CoordinateTransform} to the current
1317 * {@link CoordinateTransformList}. If there is no transform yet, it just
1318 * sets it. If there is only one transform, it replaces it by a list
1319 * containing both, the new one first.
1321 @SuppressWarnings("unchecked")
1322 public final void preAppendCoordinateTransform(final CoordinateTransform ct
) {
1323 if (!hasCoordinateTransform())
1324 setCoordinateTransform(ct
);
1326 final CoordinateTransformList
< CoordinateTransform
> ctl
;
1327 if (ct
instanceof CoordinateTransformList
<?
>)
1328 ctl
= (CoordinateTransformList
< CoordinateTransform
>)ct
.copy();
1330 ctl
= new CoordinateTransformList
< CoordinateTransform
>();
1333 ctl
.add(getCoordinateTransform());
1334 setCoordinateTransform(ctl
);
1339 * Get the bounding rectangle of the transformed image relative to the
1343 * Currently, this is done in a very expensive way. The
1344 * {@linkplain TransformMesh} is built and its bounding rectangle is
1345 * returned. Think about just storing this rectangle in the
1346 * {@linkplain Patch} instance.
1350 public final Rectangle
getCoordinateTransformBoundingBox() {
1351 if (!hasCoordinateTransform())
1352 return new Rectangle(0,0,o_width
,o_height
);
1353 return Patch
.getCoordinateTransformBoundingBox(this, getCoordinateTransform());
1357 * Allow reusing a {@link CoordinateTransform} that was already loaded from a file.
1363 protected static final Rectangle
getCoordinateTransformBoundingBox(final Patch p
, final CoordinateTransform ct
) {
1364 if (!p
.hasCoordinateTransform())
1365 return new Rectangle(0,0,p
.o_width
,p
.o_height
);
1366 final TransformMesh mesh
= new TransformMesh(ct
, p
.meshResolution
, p
.o_width
, p
.o_height
);
1367 return mesh
.getBoundingBox();
1370 /** Obtain a copy of the {@link CoordinateTransform} that transfers image data to mipmap image data.
1371 * @return A copy of the {@link CoordinateTransform}, or null if none.
1372 * @see #setCoordinateTransform(CoordinateTransform) */
1373 public final CoordinateTransform
getCoordinateTransform() { return getCT(); }
1376 * Create a {@link CoordinateTransform} that incorporates both the
1377 * {@link CoordinateTransform} of this {@link Patch} (if present) and its
1378 * {@link AffineTransform}. The returned {@link CoordinateTransform} directly
1379 * transfers the {@link Patch} into world coordinates. An image can be rendered
1380 * e.g. using {@link mpicbg.ij.TransformMeshMapping} with an
1381 * {@link mpicbg.models.TransformMesh}. Note that you may prefer to use
1382 * {@link mpicbg.models.TransformMesh} which does not perform auto-boxing as
1383 * opposed to {@link TransformMesh} in the mpicbg.trakem2 package.
1387 final public CoordinateTransform
getFullCoordinateTransform()
1389 final CoordinateTransform ctp
= getCoordinateTransform();
1392 final AffineModel2D affine
= new AffineModel2D();
1398 final Rectangle box
= getCoordinateTransformBoundingBox();
1399 final AffineTransform at2
= new AffineTransform( at
);
1400 at2
.translate( -box
.x
, -box
.y
);
1401 final AffineModel2D affine
= new AffineModel2D();
1404 final CoordinateTransformList
< CoordinateTransform
> ctl
= new CoordinateTransformList
< CoordinateTransform
>();
1413 public final Patch
.PatchImage
createCoordinateTransformedImage() {
1414 if (!hasCoordinateTransform()) return null;
1416 final CoordinateTransform ct
= getCoordinateTransform();
1418 final ImageProcessor source
= getImageProcessor();
1420 if (null == source
) return null; // some error occurred
1422 //Utils.log2("source image dimensions: " + source.getWidth() + ", " + source.getHeight());
1424 final TransformMesh mesh
= new TransformMesh(ct
, meshResolution
, o_width
, o_height
);
1425 final Rectangle box
= mesh
.getBoundingBox();
1427 /* We can calculate the exact size of the image to be rendered, so let's do it */
1428 // project.getLoader().releaseToFit(o_width, o_height, type, 5);
1430 2 * o_width
* o_height
// outside and mask source
1431 + 2 * box
.width
* box
.height
// outside and mask target
1432 + 5 * o_width
* o_height
// image source
1433 + 5 * box
.width
* box
.height
; // image target
1434 project
.getLoader().releaseToFit( b
);
1436 final TransformMeshMapping mapping
= new TransformMeshMapping( mesh
);
1438 final ImageProcessorWithMasks target
= mapping
.createMappedMaskedImageInterpolated( source
, getAlphaMask() );
1441 target
.ip
.setColorModel(source
.getColorModel());
1443 // // Set all non-white pixels to zero
1444 // final byte[] pix = (byte[])target.outside.getPixels();
1445 // for (int i=0; i<pix.length; i++)
1446 // if ((pix[i]&0xff) != 255) pix[i] = 0;
1448 //Utils.log2("New image dimensions: " + target.getWidth() + ", " + target.getHeight());
1449 //Utils.log2("box: " + box);
1451 return new PatchImage( target
.ip
, ( ByteProcessor
)target
.mask
, target
.outside
, box
, true );
1454 static final public class PatchImage
{
1455 /** The image, coordinate-transformed if null != ct. */
1456 final public ImageProcessor target
;
1457 /** The alpha mask, coordinate-transformed if null != ct. */
1458 final public ByteProcessor mask
;
1459 /** The outside mask, coordinate-transformed if null != ct. */
1460 final public ByteProcessor outside
;
1461 /** The bounding box of the image relative to the original, with x,y as the displacement relative to the pixels of the original image. */
1462 final public Rectangle box
;
1463 /** Whether the image was generated with a CoordinateTransform or not. */
1464 final public boolean coordinate_transformed
;
1466 private PatchImage( final ImageProcessor target
, final ByteProcessor mask
, final ByteProcessor outside
, final Rectangle box
, final boolean coordinate_transformed
) {
1467 this.target
= target
;
1469 this.outside
= outside
;
1471 this.coordinate_transformed
= coordinate_transformed
;
1475 * <p>Get the mask. This is either:</p>
1477 * <li>null for a non-transformed patch without a mask,</li>
1478 * <li>the mask of a non-transformed patch,</li>
1479 * <li>the transformed mask of a transformed patch (including outside
1481 * <li>or the outside mask of a transformed patch without a mask,</li>
1486 final public ByteProcessor
getMask()
1488 return mask
== null ? outside
== null ?
null : outside
: mask
;
1491 final public Image
createImage(final double min
, final double max
) {
1492 final ImageProcessor ip
= target
;
1493 ip
.setMinAndMax(min
, max
);
1494 ByteProcessor alpha_mask
= mask
; // can be null;
1495 final ByteProcessor outside_mask
= outside
; // can be null
1496 if (null == alpha_mask
) {
1497 alpha_mask
= outside_mask
;
1499 if (null != alpha_mask
) {
1500 return ImageSaver
.createARGBImagePre(
1501 Loader
.embedAlphaPre((int[])ip
.convertToRGB().getPixels(),
1502 (byte[])alpha_mask
.getPixels(),
1503 null == outside_mask ?
null : (byte[])outside_mask
.getPixels()),
1504 ip
.getWidth(), ip
.getHeight());
1506 return ip
.createImage();
1511 /** Returns a PatchImage object containing the bottom-of-transformation-stack image and alpha mask, if any (except the AffineTransform, which is used for direct hw-accel screen rendering). */
1512 public Patch
.PatchImage
createTransformedImage() {
1513 final Patch
.PatchImage pi
= createCoordinateTransformedImage();
1514 if (null != pi
) return pi
;
1515 // else, a new one with the untransformed, original image (a duplicate):
1516 final ImageProcessor ip
= getImageProcessor();
1517 if (null == ip
) return null;
1518 project
.getLoader().releaseToFit(o_width
, o_height
, type
, 3);
1519 final ImageProcessor copy
= ip
.duplicate();
1520 copy
.setColorModel(ip
.getColorModel()); // one would expect "duplicate" to do this but it doesn't!
1521 return new PatchImage(copy
, getAlphaMask(), null, new Rectangle(0, 0, o_width
, o_height
), false);
1526 * Whether there is an alpha mask for the pixel data.
1528 public final boolean hasAlphaMask() {
1529 return 0 != alpha_mask_id
;
1532 public long getAlphaMaskId() {
1533 return alpha_mask_id
;
1537 * @return The absolute file path to the file specifying the image that
1538 * represents the alpha mask, or null if none.
1540 public String
getAlphaMaskFilePath() {
1541 return hasAlphaMask() ?
createAlphaMaskFilePath(this.alpha_mask_id
) : null;
1545 * Whether there is an alpha mask or there is an outside mask caused by a {@link CoordinateTransform}.
1547 public boolean hasAlphaChannel() {
1548 return hasCoordinateTransform() || hasAlphaMask();
1551 /** Must call updateMipMaps() afterwards. Set it to null to remove it.
1552 * @return true if the alpha mask file was written successfully. */
1553 public synchronized boolean setAlphaMask(final ByteProcessor bp
) throws IllegalArgumentException
{
1559 // Check that the alpha mask represented by argument bp
1560 // has the appropriate dimensions:
1561 if (o_width
!= bp
.getWidth() || o_height
!= bp
.getHeight()) {
1562 throw new IllegalArgumentException("Need a mask of identical dimensions as the original image.");
1565 final long amID
= project
.getLoader().getNextBlobId();
1566 if (writeAlphaMask(bp
, amID
)) {
1567 this.alpha_mask_id
= amID
;
1570 Utils
.log("Could NOT write the alpha mask file for patch #" + id
);
1577 * Return a new {@link ByteProcessor} representing the alpha mask, if any, over the pixel data.
1578 * @return null if there isn't one, or if the mask image could not be loaded.*/
1579 public synchronized ByteProcessor
getAlphaMask() {
1580 if (0 == alpha_mask_id
) return null;
1582 final String path
= createAlphaMaskFilePath(alpha_mask_id
);
1584 // Expects a zip file containing one single TIFF file entry
1585 ZipInputStream zis
= null;
1587 zis
= new ZipInputStream(new FileInputStream(path
));
1588 final ZipEntry ze
= zis
.getNextEntry(); // prepares the entry for reading
1589 // Assume the first entry is the mask
1590 final ImageProcessor mask
= new FileOpener(new TiffDecoder(zis
, ze
.getName()).getTiffInfo()[0]).open(false).getProcessor();
1591 if (mask
.getWidth() != o_width
|| mask
.getHeight() != o_height
) {
1592 Utils
.log2("Mask has improper dimensions: " + mask
.getWidth() + " x " + mask
.getHeight() + " for patch #" + this.id
+ " which is of " + o_width
+ " x " + o_height
);
1595 return (ByteProcessor
) (mask
.getClass() == ByteProcessor
.class ? mask
: mask
.convertToByte(false));
1596 } catch (final Throwable t
) {
1597 Utils
.log2("Could not load alpha mask for patch #" + this.id
+ " from file " + path
);
1601 try { if (null != zis
) zis
.close(); } catch (final Exception e
) { IJError
.print(e
); }
1605 private final String
createAlphaMaskFilePath(final long amID
) {
1606 final FSLoader l
= (FSLoader
)project
.getLoader();
1607 return l
.getMasksFolder() + FSLoader
.createIdPath(Long
.toString(amID
), Long
.toString(this.id
), ".zip");
1610 private synchronized final boolean writeAlphaMask(final ByteProcessor bp
, final long amID
) {
1611 RandomAccessFile ra
= null;
1613 final File f
= new File(createAlphaMaskFilePath(amID
));
1615 ra
= new RandomAccessFile(f
, "rw");
1617 final ByteArrayOutputStream ba
= new ByteArrayOutputStream(bp
.getWidth() * bp
.getHeight());
1618 final ZipOutputStream zos
= new ZipOutputStream(ba
);
1619 final ImagePlus imp
= new ImagePlus("mask.tif", bp
); // ImageJ looks for ".tif" extension in the ZipEntry
1620 zos
.putNextEntry(new ZipEntry(imp
.getTitle()));
1621 final TiffEncoder te
= new TiffEncoder(imp
.getFileInfo());
1624 ra
.write((byte[])ImageSaver
.Bbuf
.get(ba
), 0, ba
.size());
1626 } catch (final Throwable e
) {
1629 try { if (null != ra
) ra
.close(); } catch (final Throwable t
) { IJError
.print(t
); }
1636 * @return True if {@link #alpha_mask_id} {@code == 0} or if the file is found, or false if not found.
1638 public boolean checkAlphaMaskFile() {
1639 if (0 == this.alpha_mask_id
) return true; // means there isn't an alpha mask
1640 return new File(createAlphaMaskFilePath(this.alpha_mask_id
)).exists();
1645 public boolean paintsWithFalseColor() {
1650 public void keyPressed(final KeyEvent ke
) {
1651 final Object source
= ke
.getSource();
1652 if (! (source
instanceof DisplayCanvas
)) return;
1653 final DisplayCanvas dc
= (DisplayCanvas
)source
;
1654 final Roi roi
= dc
.getFakeImagePlus().getRoi();
1656 final int mod
= ke
.getModifiers();
1658 switch (ke
.getKeyCode()) {
1660 // copy into ImageJ clipboard
1661 // Ignoring masks: outside is already black, and ImageJ cannot handle alpha masks.
1662 if (0 == (mod ^
(Event
.SHIFT_MASK
| Event
.ALT_MASK
))) {
1663 // Place the source image, untransformed, into clipboard:
1664 final ImagePlus imp
= getImagePlus();
1665 if (null != imp
) imp
.copy(false);
1666 } else if (0 == mod
|| (0 == (mod ^ Event
.SHIFT_MASK
))) {
1667 CoordinateTransformList
<CoordinateTransform
> list
= null;
1668 if (hasCoordinateTransform()) {
1669 list
= new CoordinateTransformList
<CoordinateTransform
>();
1670 list
.add(getCoordinateTransform());
1672 if (0 == mod
) { //SHIFT is not down
1673 final AffineModel2D am
= new AffineModel2D();
1675 if (null == list
) list
= new CoordinateTransformList
<CoordinateTransform
>();
1680 final TransformMesh mesh
= new TransformMesh(list
, meshResolution
, o_width
, o_height
);
1681 final TransformMeshMapping mapping
= new TransformMeshMapping(mesh
);
1682 ip
= mapping
.createMappedImageInterpolated(getImageProcessor());
1684 ip
= getImageProcessor();
1686 new ImagePlus(this.title
, ip
).copy(false);
1691 // fill mask with current ROI using
1692 if (null != roi
&& M
.isAreaROI(roi
)) {
1693 Bureaucrat
.createAndStart(new Worker
.Task("Filling image mask") {
1695 public void exec() {
1696 getLayerSet().addDataEditStep(Patch
.this);
1698 addAlphaMask(roi
, ProjectToolbar
.getForegroundColorValue());
1699 } else if (0 == (mod ^ Event
.SHIFT_MASK
)) {
1700 // shift is down: fill outside
1702 final Area localRoi
= M
.areaInInts(M
.getArea(roi
)).createTransformedArea(at
.createInverse());
1703 final Area invLocalRoi
= new Area(new Rectangle(0, 0, getOWidth() , getOHeight()));
1704 invLocalRoi
.subtract(localRoi
);
1705 addAlphaMaskLocal(invLocalRoi
, ProjectToolbar
.getForegroundColorValue());
1706 } catch (final NoninvertibleTransformException e
) {
1711 getLayerSet().addDataEditStep(Patch
.this);
1712 try { updateMipMaps().get(); } catch (final Throwable t
) { IJError
.print(t
); } // wait
1721 super.keyPressed(ke
);
1727 Class
<?
> getInternalDataPackageClass() {
1728 return DPPatch
.class;
1732 Object
getDataPackage() {
1733 return new DPPatch(this);
1736 static private final class DPPatch
extends Displayable
.DataPackage
{
1737 final double min
, max
;
1738 final long ct_id
, alpha_mask_id
;
1739 final IFilter
[] filters
;
1740 final boolean false_color
;
1743 DPPatch(final Patch patch
) {
1745 this.min
= patch
.min
;
1746 this.max
= patch
.max
;
1747 this.ct_id
= patch
.ct_id
;
1748 this.alpha_mask_id
= patch
.alpha_mask_id
;
1749 this.filters
= null == patch
.filters ?
null : FilterEditor
.duplicate(patch
.filters
);
1750 this.false_color
= patch
.false_color
;
1751 // channels is visualization
1753 // type is dependent on path, so absolute
1754 // o_width, o_height idem
1757 final boolean to2(final Displayable d
) {
1759 final Patch p
= (Patch
) d
;
1760 boolean mipmaps
= false;
1761 if (p
.min
!= min
|| p
.max
!= max
|| p
.ct_id
!= ct_id
|| p
.alpha_mask_id
!= alpha_mask_id
) {
1765 if (null != filters
&& null == p
.filters
) mipmaps
= true;
1766 else if (null == filters
&& null != p
.filters
) mipmaps
= true;
1767 else if (null != filters
&& null != p
.filters
) {
1768 if (filters
.length
!= p
.filters
.length
) mipmaps
= true;
1770 for (int i
=0; i
<filters
.length
; ++i
) {
1771 if (filters
[i
].equals(p
.filters
[i
])) continue;
1781 p
.alpha_mask_id
= alpha_mask_id
;
1782 p
.filters
= null == filters ?
null : FilterEditor
.duplicate(filters
);
1783 p
.false_color
= false_color
;
1786 p
.project
.getLoader().regenerateMipMaps(p
);
1792 /** Considers the alpha mask. */
1794 public boolean contains(final double x_p
, final double y_p
) {
1795 if (!hasAlphaChannel()) return super.contains(x_p
, y_p
);
1796 // else, get pixel from image
1797 if (project
.getLoader().isUnloadable(this)) return super.contains(x_p
, y_p
);
1798 final MipMapImage mipMap
= project
.getLoader().fetchImage(this, 0.12499); // TODO ideally, would ask for image within 256x256 dimensions, but that would need knowing the screen image dimensions beforehand, or computing it from the CoordinateTransform, which may be very costly.
1799 if (Loader
.isSignalImage(mipMap
.image
)) return super.contains(x_p
, y_p
);
1800 final int w
= mipMap
.image
.getWidth(null);
1801 final Point2D
.Double pd
= inverseTransformPoint(x_p
, y_p
);
1802 final int x2
= (int)(pd
.x
/ mipMap
.scaleX
);
1803 final int y2
= (int)(pd
.y
/ mipMap
.scaleY
);
1804 final int[] pvalue
= new int[1];
1805 final PixelGrabber pg
= new PixelGrabber(mipMap
.image
, x2
, y2
, 1, 1, pvalue
, 0, w
);
1808 } catch (final InterruptedException ie
) {
1809 return super.contains(x_p
, y_p
);
1811 // Not true if alpha value is zero
1812 return 0 != (pvalue
[0] & 0xff000000);
1815 /** After setting a preprocessor script, it is advisable that you call updateMipMaps() immediately. */
1816 public void setPreprocessorScriptPath(final String path
) {
1817 final String old_path
= project
.getLoader().getPreprocessorScriptPath(this);
1819 if (null == path
&& null == old_path
) return;
1821 project
.getLoader().setPreprocessorScriptPath(this, path
);
1823 if (null != old_path
|| null != path
) {
1824 // Update dimensions
1825 ImagePlus imp
= getImagePlus(); // transformed by the new preprocessor script, if any
1826 final int w
= imp
.getWidth();
1827 final int h
= imp
.getHeight();
1829 if (w
!= this.o_width
|| h
!= this.o_height
) {
1830 // replace source ImagePlus o_width,o_height
1831 final int old_o_width
= this.o_width
;
1832 final int old_o_height
= this.o_height
;
1836 // scale width,height
1837 final double old_width
= this.width
;
1838 final double old_height
= this.height
;
1839 this.width
*= ((double)this.o_width
) / old_o_width
;
1840 this.height
*= ((double)this.o_height
) / old_o_height
;
1842 // translate Patch to preserve the center
1843 final AffineTransform aff
= new AffineTransform();
1844 aff
.translate((old_width
- this.width
) / 2, (old_height
- this.height
) / 2);
1845 updateInDatabase("dimensions");
1846 preTransform(aff
, false);
1851 /** Add the given roi, in world coords, to the alpha mask, using the given fill value. */
1852 public void addAlphaMask(final Roi roi
, final int value
) {
1853 if (null == roi
|| !M
.isAreaROI(roi
)) return;
1854 addAlphaMask(M
.areaInInts(M
.getArea(roi
)), value
);
1857 /** Add the given area, in world coords, to the alpha mask, using the given fill value. */
1858 public void addAlphaMask(final Area aw
, final int value
) {
1860 addAlphaMaskLocal(aw
.createTransformedArea(Patch
.this.at
.createInverse()), value
);
1861 } catch (final NoninvertibleTransformException nite
) { IJError
.print(nite
); }
1864 /** Add the given area, in local coordinates, to the alpha mask, using the given fill value. */
1865 public void addAlphaMaskLocal(final Area aLocal
, int value
) {
1866 if (value
< 0) value
= 0;
1867 if (value
> 255) value
= 255;
1869 CoordinateTransform ct
= null;
1870 if (hasCoordinateTransform() && null == (ct
= getCT())) {
1874 // When the area is larger than the image, sometimes the area fails to be set at all
1875 // Also, intersection accelerates calls to contains(x,y) for complex polygons
1876 final Area a
= new Area(new Rectangle(0, 0, (int)(width
+1), (int)(height
+1)));
1877 a
.intersect(aLocal
);
1881 Utils
.log("ROI does not intersect the active image!");
1885 ByteProcessor mask
= getAlphaMask();
1887 // Use imglib to bypass all the problems with ShapeROI
1888 // Create a Shape image with background and the Area on it with 'value'
1889 final int background
= (null != mask
&& 255 == value
) ?
0 : 255;
1890 final ShapeList
<UnsignedByteType
> shapeList
= new ShapeList
<UnsignedByteType
>(new int[]{(int)width
, (int)height
, 1}, new UnsignedByteType(background
));
1891 shapeList
.addShape(a
, new UnsignedByteType(value
), new int[]{0});
1892 final mpicbg
.imglib
.image
.Image
<UnsignedByteType
> shapeListImage
= new mpicbg
.imglib
.image
.Image
<UnsignedByteType
>(shapeList
, shapeList
.getBackground(), "mask");
1894 ByteProcessor rmask
= (ByteProcessor
) ImageJFunctions
.copyToImagePlus(shapeListImage
, ImagePlus
.GRAY8
).getProcessor();
1896 if (hasCoordinateTransform()) {
1897 // inverse the coordinate transform
1898 final TransformMesh mesh
= new TransformMesh(ct
, meshResolution
, o_width
, o_height
);
1899 final TransformMeshMapping mapping
= new TransformMeshMapping( mesh
);
1900 rmask
= (ByteProcessor
) mapping
.createInverseMappedImageInterpolated(rmask
);
1904 // There wasn't a mask, hence just set it
1907 final byte[] b1
= (byte[]) mask
.getPixels();
1908 final byte[] b2
= (byte[]) rmask
.getPixels();
1909 // Whatever is not background in the new mask gets set on the old mask
1910 for (int i
=0; i
<b1
.length
; i
++) {
1911 if (background
== (b2
[i
]&0xff)) continue; // background pixel in new mask
1912 b1
[i
] = b2
[i
]; // replace old pixel with new pixel
1918 public String
getPreprocessorScriptPath() {
1919 return project
.getLoader().getPreprocessorScriptPath(this);
1922 public boolean isPreprocessed() {
1923 return null != getPreprocessorScriptPath() || null != filters
;
1926 /** Returns an Area in world coords representing the inside of this Patch. The fully alpha pixels are considered outside. */
1928 public Area
getArea() {
1929 CoordinateTransform ct
= null;
1930 if (hasAlphaMask()) {
1931 // Read the mask as a ROI for the 0 pixels only and apply the AffineTransform to it:
1932 ImageProcessor alpha_mask
= getAlphaMask();
1933 if (null == alpha_mask
) {
1934 Utils
.log2("Could not retrieve alpha mask for " + this);
1936 if (hasCoordinateTransform()) {
1937 // must transform it
1938 ct
= getCoordinateTransform();
1939 final TransformMesh mesh
= new TransformMesh(ct
, meshResolution
, o_width
, o_height
);
1940 final TransformMeshMapping mapping
= new TransformMeshMapping( mesh
);
1941 alpha_mask
= mapping
.createMappedImage( alpha_mask
); // Without interpolation
1942 // Keep in mind the affine of the Patch already contains the translation specified by the mesh bounds.
1944 // Threshold all non-zero areas of the mask:
1945 alpha_mask
.setThreshold(1, 255, ImageProcessor
.NO_LUT_UPDATE
);
1946 final ImagePlus imp
= new ImagePlus("", alpha_mask
);
1947 final ThresholdToSelection tts
= new ThresholdToSelection(); // TODO replace by our much faster method that scans by line, in AmiraImporter
1949 tts
.run(alpha_mask
);
1950 final Roi roi
= imp
.getRoi();
1952 // All pixels in the alpha mask have a value of zero
1955 return M
.getArea(roi
).createTransformedArea(this.at
);
1958 // No alpha mask, or error in retrieving it:
1959 final int[] x
= new int[o_width
+ o_width
+ o_height
+ o_height
];
1960 final int[] y
= new int[x
.length
];
1963 for (int i
=0; i
<=o_width
; i
++, next
++) { // len: o_width + 1
1968 for (int i
=1; i
<=o_height
; i
++, next
++) { // len: o_height
1973 for (int i
=o_width
-1; i
>-1; i
--, next
++) { // len: o_width
1978 for (int i
=o_height
-1; i
>0; i
--, next
++) { // len: o_height -1
1983 if (hasCoordinateTransform() && null == ct
) ct
= getCoordinateTransform();
1985 final CoordinateTransformList
<CoordinateTransform
> t
= new CoordinateTransformList
<CoordinateTransform
>();
1987 final TransformMesh mesh
= new TransformMesh(ct
, meshResolution
, o_width
, o_height
);
1988 final Rectangle box
= mesh
.getBoundingBox();
1989 final AffineTransform aff
= new AffineTransform(this.at
);
1990 // Must correct for the inverse of the mesh translation, because the affine also includes the translation.
1991 aff
.translate(-box
.x
, -box
.y
);
1992 final AffineModel2D affm
= new AffineModel2D();
1998 * WORKS FINE, but for points that fall outside the mesh, they don't get transformed!
1999 // Do it like Patch does it to generate the mipmap, with a mesh (and all the imprecisions of a mesh):
2000 final CoordinateTransformList t = new CoordinateTransformList();
2001 final TransformMesh mesh = new TransformMesh(this.ct, meshResolution, o_width, o_height);
2002 final AffineTransform aff = new AffineTransform(this.at);
2004 final AffineModel2D affm = new AffineModel2D();
2009 final float[] f
= new float[]{x
[0], y
[0]};
2011 final Path2D
.Float path
= new Path2D
.Float(Path2D
.Float
.WIND_EVEN_ODD
, x
.length
+1);
2012 path
.moveTo(f
[0], f
[1]);
2014 for (int i
=1; i
<x
.length
; i
++) {
2018 path
.lineTo(f
[0], f
[1]);
2020 path
.closePath(); // line to last call to moveTo
2022 return new Area(path
);
2024 return new Area(new Polygon(x
, y
, x
.length
)).createTransformedArea(this.at
);
2028 /** Defaults to setMinAndMax = true. */
2029 static public ImageProcessor
makeFlatImage(final int type
, final Layer layer
, final Rectangle srcRect
, final double scale
, final Collection
<Patch
> patches
, final Color background
) {
2030 return makeFlatImage(type
, layer
, srcRect
, scale
, patches
, background
, true);
2033 /** Creates an ImageProcessor of the specified type.
2034 * @param type Any of ImagePlus.GRAY_8, GRAY_16, GRAY_32 or COLOR_RGB.
2035 * @param srcRect the box in world coordinates to make an image out of.
2036 * @param scale may be up to 1.0.
2037 * @param patches The list of patches to paint. The first gets painted first (at the bottom).
2038 * @param background The color with which to paint the outsides where no image paints into.
2039 * @param setMinAndMax defines whether the min and max of each Patch is set before pasting the Patch.
2041 * For exporting while blending the display ranges (min,max) and respecting alpha masks, {@see ExportUnsignedShort}.
2043 static public ImageProcessor
makeFlatImage(final int type
, final Layer layer
, final Rectangle srcRect
, final double scale
, final Collection
<Patch
> patches
, final Color background
, final boolean setMinAndMax
) {
2045 final ImageProcessor ip
;
2048 W
= (int)(srcRect
.width
* scale
);
2049 H
= (int)(srcRect
.height
* scale
);
2055 case ImagePlus
.GRAY8
:
2056 ip
= new ByteProcessor(W
, H
);
2058 case ImagePlus
.GRAY16
:
2059 ip
= new ShortProcessor(W
, H
);
2061 case ImagePlus
.GRAY32
:
2062 ip
= new FloatProcessor(W
, H
);
2064 case ImagePlus
.COLOR_RGB
:
2065 ip
= new ColorProcessor(W
, H
);
2068 Utils
.logAll("Cannot create an image of type " + type
+ ".\nSupported types: 8-bit, 16-bit, 32-bit and RGB.");
2072 // Fill with background
2073 if (null != background
&& Color
.black
!= background
) {
2074 ip
.setColor(background
);
2078 AffineModel2D sc
= null;
2081 sc
= new AffineModel2D();
2082 sc
.set( ( float )scale
, 0, 0, ( float )scale
, 0, 0 );
2084 for ( final Patch p
: patches
)
2086 // TODO patches seem to come in in inverse order---find out why
2088 // A list to represent all the transformations that the Patch image has to go through to reach the scaled srcRect image
2089 final CoordinateTransformList
< CoordinateTransform
> list
= new CoordinateTransformList
< CoordinateTransform
>();
2091 final AffineTransform at
= new AffineTransform();
2092 at
.translate( -srcRect
.x
, -srcRect
.y
);
2093 at
.concatenate( p
.getAffineTransform() );
2095 // 1. The coordinate tranform of the Patch, if any
2096 if (p
.hasCoordinateTransform()) {
2097 final CoordinateTransform ct
= p
.getCoordinateTransform();
2099 // Remove the translation in the patch_affine that the ct added to it
2100 final Rectangle box
= Patch
.getCoordinateTransformBoundingBox(p
, ct
);
2101 at
.translate( -box
.x
, -box
.y
);
2104 // 2. The affine transform of the Patch
2105 final AffineModel2D patch_affine
= new AffineModel2D();
2106 patch_affine
.set( at
);
2107 list
.add( patch_affine
);
2109 // 3. The desired scaling
2110 if (null != sc
) patch_affine
.preConcatenate( sc
);
2112 final CoordinateTransformMesh mesh
= new CoordinateTransformMesh( list
, p
.meshResolution
, p
.getOWidth(), p
.getOHeight() );
2114 final mpicbg
.ij
.TransformMeshMapping
<CoordinateTransformMesh
> mapping
= new mpicbg
.ij
.TransformMeshMapping
<CoordinateTransformMesh
>( mesh
);
2116 // 4. Convert the patch to the required type
2117 ImageProcessor pi
= p
.getImageProcessor();
2119 pi
= pi
.duplicate();
2120 pi
.setMinAndMax(p
.min
, p
.max
);
2124 case ImagePlus
.GRAY8
:
2125 pi
= pi
.convertToByte( true );
2127 case ImagePlus
.GRAY16
:
2128 pi
= pi
.convertToShort( true );
2130 case ImagePlus
.GRAY32
:
2131 pi
= pi
.convertToFloat();
2133 default: // ImagePlus.COLOR_RGB and COLOR_256
2134 pi
= pi
.convertToRGB();
2138 /* TODO for taking into account independent min/max setting for each patch,
2139 * we will need a mapping with an `intensity transfer function' to be implemented.
2140 * --> EXISTS already as mpicbg/trakem2/transform/ExportUnsignedShort.java
2142 mapping
.mapInterpolated( pi
, ip
);
2148 /** Make the border have an alpha of zero. */
2149 public boolean maskBorder(final int size
) {
2150 return maskBorder(size
, size
, size
, size
);
2152 /** Make the border have an alpha of zero. */
2153 public boolean maskBorder(final int left
, final int top
, final int right
, final int bottom
) {
2154 final int w
= o_width
- right
- left
;
2155 final int h
= o_height
- top
- bottom
;
2156 if (w
< 0 || h
< 0 || left
> o_width
|| top
> o_height
) {
2157 Utils
.log("Cannot cut border for patch " + this + " : border off image bounds.");
2161 ByteProcessor bp
= getAlphaMask();
2163 bp
= new ByteProcessor(o_width
, o_height
);
2164 bp
.setRoi(new Roi(left
, top
, w
, h
));
2168 // make borders black
2170 for (final Roi r
: new Roi
[]{new Roi(0, 0, o_width
, top
),
2171 new Roi(0, top
, left
, o_height
- top
- bottom
),
2172 new Roi(0, o_height
- bottom
, o_width
, bottom
),
2173 new Roi(o_width
- right
, top
, right
, o_height
- top
- bottom
)}) {
2179 } catch (final Exception e
) {
2186 /** Use this instead of getAreaAt which calls getArea which is ... dog slow for something like buckets. */
2188 protected Area
getAreaForBucket(final Layer l
) {
2189 return new Area(getPerimeter());
2193 protected boolean isRoughlyInside(final Layer l
, final Rectangle r
) {
2194 return l
== this.layer
&& r
.intersects(getBoundingBox());
2198 public boolean intersects(final Displayable d
) {
2199 if (hasAlphaChannel()) {
2200 // First try a cheap operation
2201 if (!getBoundingBox().intersects(d
.getBoundingBox())) {
2204 // If bounding boxes overlap, test with precision
2205 return M
.intersects(getArea(), d
.getAreaAt(this.layer
));
2207 return super.intersects(d
);
2211 * Append an array of {@link IFilter} to the array of existing {@link IFilter}.
2212 * @param fs The array of {@link IFilter} to use for this Patch.
2213 * @see #setFilters(Filter[]), {@link #getFilters()}
2215 public void appendFilters(final IFilter
[] fs
) {
2216 if (null == filters
|| 0 == filters
.length
) {
2220 if (null == fs
) return;
2221 final IFilter
[] c
= new IFilter
[filters
.length
+ fs
.length
];
2222 for (int i
=0; i
<filters
.length
; ++i
) c
[i
] = filters
[i
];
2223 for (int i
=filters
.length
; i
<c
.length
; ++i
) c
[i
] = fs
[i
-filters
.length
];
2228 * Set an array of @{link {@link IFilter}, which are applied in order to the {@link ImageProcessor}
2229 * after the preprocessor script is applied but before the rest of TrakEM2 sees the image.
2230 * @param fs The array of {@link IFilter} to use for this Patch. Can be null.
2231 * @see #appendFilters(Filter[]), {@link #getFilters()}
2233 public void setFilters(final IFilter
[] fs
) {
2239 * @return The array of {@link IFilter} of this {@link Patch}.
2240 * @see #appendFilters(Filter[]), {@link #setFilters(IFilter[])}
2242 public IFilter
[] getFilters() {
2246 public boolean hasCoordinateTransform() {
2250 /** A value of 0 indicates that there isn't one. */
2251 public long getCoordinateTransformId() {
2256 * @return The absolute file path to the file specifying the {@link CoordinateTransform}, or null if none.
2258 public String
getCoordinateTransformFilePath() {
2259 return hasCoordinateTransform() ?
createCTFilePath(this.ct_id
) : null;
2262 private final String
createCTFilePath(final long ctID
) {
2263 final FSLoader l
= (FSLoader
)project
.getLoader();
2264 return l
.getCoordinateTransformsFolder()
2265 + FSLoader
.createIdPath(Long
.toString(ctID
), Long
.toString(this.id
), ".ct");
2268 /** Obtains a {@link CoordinateTransform}.
2269 * This method is meant to be used only when {@link #hasCoordinateTransform()} returns true.
2271 * @return The {@link CoordinateTransform} from file, or null if there isn't one.
2272 * @throws {@link RuntimeException} wrapping the actual error in loading the file.
2274 private final CoordinateTransform
getCT() {
2276 return fetchCoordinateTransform();
2277 } catch (final Exception e
) {
2279 throw new RuntimeException(e
);
2284 * Read in the {@link CoordinateTransform} from a file whose name is crafted
2285 * from the {@link #ct_id} and this {@link Patch}'s {@link #id}.
2287 * @return A new instance of the {@link CoordinateTransform} of this {@link Patch}, or null if none.
2288 * @throws {@link Exception} if the file could not be found or parsed or read.
2290 synchronized public CoordinateTransform
fetchCoordinateTransform() throws Exception
{
2291 return hasCoordinateTransform() ?
2292 CoordinateTransformXML
.parse(createCTFilePath(this.ct_id
))
2296 /** Will throw an {@link Exception} if the file can't be read or is not there. */
2297 synchronized private char[] readCoordinateTransformFile() throws Exception
{
2298 final File f
= new File(createCTFilePath(this.ct_id
));
2299 final char[] c
= new char[(int)f
.length()];
2300 Reader reader
= null;
2302 reader
= new BufferedReader(new FileReader(f
), 32768); // TODO make this larger
2304 while (s
< c
.length
) {
2305 final int r
= reader
.read(c
, s
, c
.length
- s
);
2306 if (-1 == r
) break; // done
2311 if (null != reader
) reader
.close();
2316 * Writes the {@link CoordinateTransform} {@param t} to the trakem2.transforms/ directory, using the unique {@link #ct_id}
2317 * and this {@link Patch}'s {@link #id} to generate a file path for it.
2319 * @return true if it was written successfully.
2320 * @throws {@link Exception} if the new file could not be written.
2322 synchronized protected boolean setNewCoordinateTransform(final CoordinateTransform ct
) throws Exception
{
2323 // If the new CoordinateTransform is null, set the id to 0
2329 final long ctID
= project
.getLoader().getNextBlobId();
2330 // Write the ct to file, which may throw an exception
2331 if (writeNewCoordinateTransform(ct
, ctID
)) {
2336 Utils
.log("Could NOT write the CoordinateTransform file for patch #" + id
);
2343 * @param ctID The id
2344 * @see #setNewCoordinateTransform(CoordinateTransform) */
2345 synchronized private boolean writeNewCoordinateTransform(final CoordinateTransform ct
, final long ctID
) throws Exception
{
2346 RandomAccessFile ra
= null;
2348 final File f
= new File(createCTFilePath(ctID
));
2350 ra
= new RandomAccessFile(f
, "rw");
2351 ra
.write(ct
.toXML("\t\t\t\t").getBytes());
2354 if (null != ra
) try { ra
.close(); } catch (final Exception e
) { IJError
.print(e
); }
2360 * @return True if {@link #ct_id} {@code == 0} or if the file is found, or false if not found.
2362 public boolean checkCoordinateTransformFile() {
2363 if (0 == this.ct_id
) return true; // means there isn't a CoordinateTransform
2364 return new File(createCTFilePath(this.ct_id
)).exists();
2368 * Transfer a world coordinate (in pixels, uncalibrated) to the coordinate space of the original image.
2369 * The world coordinate is first transferred to this {@link Patch} space by inverting the {@link AffineTransform}
2370 * and then, if there is a {@link CoordinateTransform}, that is inverted as well to reach the coordinate space of the original image.
2374 * @return A {@code double[]} array with the x,y values.
2375 * @throws NoninvertibleTransformException
2376 * @throws NoninvertibleModelException
2378 public double[] toPixelCoordinate(final double world_x
, final double world_y
) throws NoninvertibleTransformException
{
2379 return Patch
.toPixelCoordinate(world_x
, world_y
, this.at
, hasCoordinateTransform() ?
getCoordinateTransform() : null, this.meshResolution
, this.o_width
, this.o_height
);
2383 * @see Patch#toPixelCoordinate(double, double)
2384 * @param world_x The X of the world coordinate (in pixels, uncalibrated)
2385 * @param world_y The Y of the world coordinate (in pixels, uncalibrated)
2386 * @param aff The {@link AffineTransform} of the {@link Patch}.
2387 * @param ct The {@link CoordinateTransform} of the {@link Patch}, if any (can be null).
2388 * @param meshResolution The precision demanded for approximating a transform with a {@link TransformMesh}.
2389 * @param o_width The width of the image underlying the {@link Patch}.
2390 * @param o_height The height of the image underlying the {@link Patch}.
2391 * @return A {@code double[]} array with the x,y values.
2392 * @throws NoninvertibleTransformException
2393 * @throws NoninvertibleModelException
2395 static public final double[] toPixelCoordinate(final double world_x
, final double world_y
,
2396 final AffineTransform aff
, final CoordinateTransform ct
,
2397 final int meshResolution
, final int o_width
, final int o_height
) throws NoninvertibleTransformException
{
2398 // Inverse the affine
2399 final double[] d
= new double[]{world_x
, world_y
};
2400 aff
.inverseTransform(d
, 0, d
, 0, 1);
2401 // Inverse the coordinate transform
2403 final float[] f
= new float[]{(float)d
[0], (float)d
[1]};
2404 final mpicbg
.models
.InvertibleCoordinateTransform t
=
2405 mpicbg
.models
.InvertibleCoordinateTransform
.class.isAssignableFrom(ct
.getClass()) ?
2406 (mpicbg
.models
.InvertibleCoordinateTransform
) ct
2407 : new mpicbg
.trakem2
.transform
.TransformMesh(ct
, meshResolution
, o_width
, o_height
);
2408 try { t
.applyInverseInPlace(f
); } catch ( final NoninvertibleModelException e
) {}
2417 * Return the local affine transformation for a passed location in world
2418 * coordinates. This affine transform is either the global affine
2419 * transform of the patch or the combined affine transform of the local
2420 * affine transform in the transform mesh and its global affine transform.
2426 public AffineTransform
getLocalAffine( final double wx
, final double wy
)
2428 final AffineTransform affine
= new AffineTransform( at
);
2429 if ( hasCoordinateTransform() )
2431 final CoordinateTransform ct
= getCoordinateTransform();
2432 final double[] w
= new double[]{ wx
, wy
};
2435 at
.inverseTransform( w
, 0, w
, 0, 1 );
2437 catch ( final NoninvertibleTransformException e
) {}
2438 final TransformMesh mesh
= new TransformMesh( ct
, meshResolution
, o_width
, o_height
);
2439 final mpicbg
.models
.AffineModel2D triangle
= mesh
.closestTargetAffine( new float[]{ ( float )w
[ 0 ], ( float )w
[ 1 ] } );
2440 affine
.concatenate( triangle
.createAffine() );
2445 public double getLocalScale( final double wx
, final double wy
)
2447 final AffineTransform affine
= getLocalAffine( wx
, wy
);
2448 final double a
= affine
.getScaleX();
2449 final double b
= affine
.getShearX();
2450 final double c
= affine
.getShearY();
2451 final double d
= affine
.getScaleY();
2453 final double l1x
= a
+ b
;
2454 final double l1y
= c
+ d
;
2455 final double l2x
= a
- b
;
2456 final double l2y
= c
- d
;
2458 final double l1
= Math
.sqrt( l1x
* l1x
+ l1y
* l1y
) / SQRT2
;
2459 final double l2
= Math
.sqrt( l2x
* l2x
+ l2y
* l2y
) / SQRT2
;
2461 return ( l1
+ l2
) / 2.0;
2465 public void mousePressed(final MouseEvent me
, final Layer la
, final int x_p
, final int y_p
, final double mag
) {
2466 final int tool
= ProjectToolbar
.getToolId();
2467 final DisplayCanvas canvas
= (DisplayCanvas
)me
.getSource();
2468 if (ProjectToolbar
.WAND
== tool
) {
2469 if (null == canvas
) return;
2470 Bureaucrat
.createAndStart(new Worker
.Task("Magic Wand ROI") {
2472 public void exec() {
2473 final PatchImage pai
= createTransformedImage();
2474 pai
.target
.setMinAndMax(min
, max
);
2475 final ImagePlus patchImp
= new ImagePlus("", pai
.target
.convertToByte(true));
2476 final float[] fp
= new float[2];
2480 at
.createInverse().transform(fp
, 0, fp
, 0, 1);
2481 } catch (final NoninvertibleTransformException e
) {
2485 final int npoints
= IJ
.doWand(patchImp
, (int)fp
[0], (int)fp
[1], WandToolOptions
.getTolerance(), WandToolOptions
.getMode());
2487 System
.out
.println("npoints " + npoints
);
2488 final Roi roi
= patchImp
.getRoi();
2490 final Area aroi
= M
.getArea(roi
);
2492 canvas
.getFakeImagePlus().setRoi(new ShapeRoi(aroi
));