3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 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
.GenericDialog
;
29 import ij
.gui
.ShapeRoi
;
30 import ij
.gui
.Toolbar
;
31 import ij
.process
.ByteProcessor
;
32 import ij
.process
.ImageProcessor
;
33 import ini
.trakem2
.Project
;
34 import ini
.trakem2
.imaging
.PatchStack
;
35 import ini
.trakem2
.utils
.M
;
36 import ini
.trakem2
.utils
.Utils
;
37 import ini
.trakem2
.utils
.IJError
;
38 import ini
.trakem2
.utils
.Search
;
39 import ini
.trakem2
.utils
.Worker
;
40 import ini
.trakem2
.utils
.Bureaucrat
;
41 import ini
.trakem2
.persistence
.Loader
;
42 import ini
.trakem2
.vector
.VectorString3D
;
44 import java
.awt
.Dimension
;
45 import java
.awt
.Rectangle
;
46 import java
.awt
.Event
;
47 import java
.awt
.Image
;
48 import java
.awt
.Color
;
49 import java
.awt
.Composite
;
50 import java
.awt
.AlphaComposite
;
51 import java
.awt
.Toolkit
;
52 import java
.awt
.Graphics2D
;
53 import java
.awt
.image
.BufferedImage
;
54 import java
.awt
.image
.MemoryImageSource
;
55 import java
.awt
.image
.DirectColorModel
;
56 import java
.awt
.geom
.AffineTransform
;
57 import java
.awt
.geom
.Area
;
58 import java
.awt
.geom
.Point2D
;
59 import java
.awt
.Polygon
;
60 import java
.awt
.geom
.PathIterator
;
61 import java
.awt
.geom
.NoninvertibleTransformException
;
62 import java
.awt
.image
.PixelGrabber
;
63 import java
.awt
.event
.KeyEvent
;
64 import java
.util
.Iterator
;
66 import java
.util
.HashMap
;
67 import java
.util
.ArrayList
;
68 import java
.util
.HashSet
;
69 import java
.util
.Collection
;
72 import mpicbg
.models
.AffineModel2D
;
73 import mpicbg
.trakem2
.transform
.CoordinateTransform
;
74 import mpicbg
.trakem2
.transform
.TransformMesh
;
75 import mpicbg
.trakem2
.transform
.CoordinateTransformList
;
76 import mpicbg
.trakem2
.transform
.TransformMeshMapping
;
78 public final class Patch
extends Displayable
{
80 private int type
= -1; // unknown
81 /** The channels that the currently existing awt image has ready for painting. */
82 private int channels
= 0xffffffff;
84 /** To generate contrasted images non-destructively. */
85 private double min
= 0;
86 private double max
= 255;
88 private int o_width
= 0, o_height
= 0;
90 /** 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. */
91 private String current_path
= null;
92 /** To be read from XML, or set when the file ImagePlus has been updated and the current_path points to something else. */
93 private String original_path
= null;
95 /** The CoordinateTransform that transfers image data to mipmap image data. The AffineTransform is then applied to the mipmap image data. */
96 private CoordinateTransform ct
= null;
98 /** Construct a Patch from an image. */
99 public Patch(Project project
, String title
, double x
, double y
, ImagePlus imp
) {
100 super(project
, title
, x
, y
);
101 this.type
= imp
.getType();
102 this.min
= imp
.getProcessor().getMin();
103 this.max
= imp
.getProcessor().getMax();
105 this.o_width
= imp
.getWidth();
106 this.o_height
= imp
.getHeight();
107 this.width
= (int)o_width
;
108 this.height
= (int)o_height
;
109 project
.getLoader().cache(this, imp
);
113 /** Reconstruct a Patch from the database. The ImagePlus will be loaded when necessary. */
114 public Patch(Project project
, long id
, String title
, double width
, double height
, int type
, boolean locked
, double min
, double max
, AffineTransform at
) {
115 super(project
, id
, title
, locked
, at
, width
, height
);
119 if (0 == o_width
) o_width
= (int)width
;
120 if (0 == o_height
) o_height
= (int)height
;
124 /** Reconstruct from an XML entry. */
125 public Patch(Project project
, long id
, HashMap ht_attributes
, HashMap ht_links
) {
126 super(project
, id
, ht_attributes
, ht_links
);
128 project
.getLoader().addedPatchFrom((String
)ht_attributes
.get("file_path"), this);
129 boolean hasmin
= false;
130 boolean hasmax
= false;
131 // parse specific fields
132 final Iterator it
= ht_attributes
.entrySet().iterator();
133 while (it
.hasNext()) {
134 final Map
.Entry entry
= (Map
.Entry
)it
.next();
135 final String key
= (String
)entry
.getKey();
136 final String data
= (String
)entry
.getValue();
137 if (key
.equals("type")) {
138 this.type
= Integer
.parseInt(data
);
139 } else if (key
.equals("min")) {
140 this.min
= Double
.parseDouble(data
);
142 } else if (key
.equals("max")) {
143 this.max
= Double
.parseDouble(data
);
145 } else if (key
.equals("original_path")) {
146 this.original_path
= data
;
147 } else if (key
.equals("o_width")) {
148 this.o_width
= Integer
.parseInt(data
);
149 } else if (key
.equals("o_height")) {
150 this.o_height
= Integer
.parseInt(data
);
154 if (0 == o_width
|| 0 == o_height
) {
155 // The original image width and height are unknown.
157 Utils
.log2("Restoring original width/height from file for id=" + id
);
158 // Use BioFormats to read the dimensions out of the original file's header
159 final Dimension dim
= project
.getLoader().getDimensions(this);
161 o_height
= dim
.height
;
162 } catch (Exception e
) {
163 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.");
164 // So set them to whatever is somewhat survivable for the moment
165 o_width
= (int)width
;
166 o_height
= (int)height
;
171 if (hasmin
&& hasmax
) {
174 // standard, from the image, to be defined when first painted
177 //Utils.log2("new Patch from XML, min and max: " + min + "," + max);
180 /** The original width of the pixels in the source image file. */
181 public int getOWidth() { return o_width
; }
182 /** The original height of the pixels in the source image file. */
183 public int getOHeight() { return o_height
; }
185 /** 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. */
186 public ImagePlus
getImagePlus() {
187 final ImagePlus imp
= this.project
.getLoader().fetchImagePlus(this);
191 /** 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. */
192 public ImageProcessor
getImageProcessor() {
193 final ImageProcessor ip
= this.project
.getLoader().fetchImageProcessor(this);
197 /** Recreate mipmaps and flush away any cached ones.
198 * This method is essentially the same as patch.getProject().getLoader().update(patch);
199 * which in turn it's the same as the following two calls:
200 * patch.getProject().getLoader().generateMipMaps(patch);
201 * patch.getProject().getLoader().decacheAWT(patch.getId());
203 * If you want to update lots of Patch instances in parallel, consider also
204 * project.getLoader().generateMipMaps(ArrayList patches, boolean overwrite);
206 public boolean updateMipmaps() {
207 return project
.getLoader().update(this);
210 private void readProps(final ImagePlus new_imp
) {
211 this.type
= new_imp
.getType();
212 if (new_imp
.getWidth() != (int)this.width
|| new_imp
.getHeight() != this.height
) {
213 this.width
= new_imp
.getWidth();
214 this.height
= new_imp
.getHeight();
217 ImageProcessor ip
= new_imp
.getProcessor();
218 this.min
= ip
.getMin();
219 this.max
= ip
.getMax();
222 /** Set a new ImagePlus for this Patch.
223 * The original path and image remain untouched. Any later image is deleted and replaced by the new one.
225 synchronized public String
set(final ImagePlus new_imp
) {
226 if (null == new_imp
) return null;
227 // flag to mean: this Patch has never been set to any image except the original
228 // The intention is never to remove the mipmaps of original images
229 boolean first_time
= null == original_path
;
230 // 0 - set original_path to the current path if there is no original_path recorded:
232 for (Patch p
: getStackPatches()) {
233 if (null == p
.original_path
) original_path
= p
.project
.getLoader().getAbsolutePath(p
);
236 if (null == original_path
) original_path
= project
.getLoader().getAbsolutePath(this);
238 // 1 - tell the loader to store the image somewhere, unless the image has a path already
239 final String path
= project
.getLoader().setImageFile(this, new_imp
);
241 Utils
.log2("setImageFile returned null!");
242 return null; // something went wrong
244 // 2 - update properties and mipmaps
246 for (Patch p
: getStackPatches()) {
247 p
.readProps(new_imp
);
248 project
.getLoader().generateMipMaps(p
); // sequentially
249 project
.getLoader().decacheAWT(p
.id
);
253 project
.getLoader().generateMipMaps(this);
254 project
.getLoader().decacheAWT(this.id
);
256 Display
.repaint(layer
, this, 5);
257 return project
.getLoader().getAbsolutePath(this);
260 /** Boundary checks on min and max, given the image type. */
261 private void checkMinMax() {
262 if (-1 == this.type
) return;
264 case ImagePlus
.GRAY8
:
265 case ImagePlus
.COLOR_RGB
:
266 case ImagePlus
.COLOR_256
:
267 if (this.min
< 0) this.min
= 0;
270 final double max_max
= Patch
.getMaxMax(this.type
);
271 if (this.max
> max_max
) this.max
= max_max
;
272 // still this.max could be -1, in which case putMinAndMax will fix it to the ImageProcessor's values
275 /** 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. */
276 public void setMinAndMax(double min
, double max
) {
279 updateInDatabase("min_and_max");
280 Utils
.log2("Patch.setMinAndMax: min,max " + min
+ "," + max
);
283 public double getMin() { return min
; }
284 public double getMax() { return max
; }
286 /** Needs a non-null ImagePlus with a non-null ImageProcessor in it. This method is meant to be called only mmediately after the ImagePlus is loaded. */
287 public void putMinAndMax(final ImagePlus imp
) throws Exception
{
288 ImageProcessor ip
= imp
.getProcessor();
289 // adjust lack of values
290 if (-1 == min
|| -1 == max
) {
294 ip
.setMinAndMax(min
, max
);
296 //Utils.log2("Patch.putMinAndMax: min,max " + min + "," + max);
299 /** Returns the ImagePlus type of this Patch. */
300 public int getType() {
304 public Image
createImage(ImagePlus imp
) {
305 return adjustChannels(channels
, true, imp
);
308 public Image
createImage() {
309 return adjustChannels(channels
, true, null);
312 private Image
adjustChannels(int c
) {
313 return adjustChannels(c
, false, null);
316 public int getChannelAlphas() {
320 /** @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 />
321 * For non-color images, a standard image is returned regardless of the @param c
323 private Image
adjustChannels(final int c
, final boolean force
, ImagePlus imp
) {
324 if (null == imp
) imp
= project
.getLoader().fetchImagePlus(this);
325 ImageProcessor ip
= imp
.getProcessor();
326 if (null == ip
) return null; // fixing synch problems when deleting a Patch
328 if (ImagePlus
.COLOR_RGB
== type
) {
329 if (imp
.getType() != type
) {
330 ip
= Utils
.convertTo(ip
, type
, false); // all other types need not be converted, since there are no alphas anyway
332 if ((c
&0x00ffffff) == 0x00ffffff && !force
) {
334 awt
= ip
.createImage(); //imp.getImage();
335 // pixels array will be shared using ij138j and above
337 // modified from ij.process.ColorProcessor.createImage() by Wayne Rasband
338 int[] pixels
= (int[])ip
.getPixels();
339 float cr
= ((c
&0xff0000)>>16) / 255.0f
;
340 float cg
= ((c
&0xff00)>>8) / 255.0f
;
341 float cb
= (c
&0xff) / 255.0f
;
342 int[] pix
= new int[pixels
.length
];
344 for (int i
=pixels
.length
-1; i
>-1; i
--) {
346 pix
[i
] = (((int)(((p
&0xff0000)>>16) * cr
))<<16)
347 + (((int)(((p
&0xff00)>>8) * cg
))<<8)
348 + (int) ((p
&0xff) * cb
);
350 int w
= imp
.getWidth();
351 MemoryImageSource source
= new MemoryImageSource(w
, imp
.getHeight(), DCM
, pix
, 0, w
);
352 source
.setAnimated(true);
353 source
.setFullBufferUpdates(true);
354 awt
= Toolkit
.getDefaultToolkit().createImage(source
);
357 awt
= ip
.createImage();
360 //Utils.log2("ip's min, max: " + ip.getMin() + ", " + ip.getMax());
367 static final public DirectColorModel DCM
= new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
369 /** Just throws the cached image away if the alpha of the channels has changed. */
370 private final void checkChannels(int channels
, double magnification
) {
371 if (this.channels
!= channels
&& (ImagePlus
.COLOR_RGB
== this.type
|| ImagePlus
.COLOR_256
== this.type
)) {
372 final int old_channels
= this.channels
;
373 this.channels
= channels
; // before, so if any gets recreated it's done right
374 project
.getLoader().adjustChannels(this, old_channels
);
378 /** Takes an image and scales its channels according to the values packed in this.channels.
379 * This method is intended for fixing RGB images which are loaded from jpegs (the mipmaps), and which
380 * have then the full colorization of the original image present in their pixels array.
381 * Otherwise the channel opacity scaling makes no sense.
382 * If 0xffffffff == this.channels the awt is returned as is.
383 * If the awt is null returns null.
385 public final Image
adjustChannels(final Image awt
) {
386 if (0xffffffff == this.channels
|| null == awt
) return awt
;
387 BufferedImage bi
= null;
389 if (awt
instanceof BufferedImage
) bi
= (BufferedImage
)awt
;
391 bi
= new BufferedImage(awt
.getWidth(null), awt
.getHeight(null), BufferedImage
.TYPE_INT_ARGB
);
392 bi
.getGraphics().drawImage(awt
, 0, 0, null);
394 // extract channel values
395 final float cr
= ((channels
&0xff0000)>>16) / 255.0f
;
396 final float cg
= ((channels
&0xff00)>>8 ) / 255.0f
;
397 final float cb
= ( channels
&0xff ) / 255.0f
;
399 Utils
.log2("w, h: " + bi
.getWidth() + ", " + bi
.getHeight());
400 final int[] pixels
= bi
.getRGB(0, 0, bi
.getWidth(), bi
.getHeight(), null, 0, 1);
401 // scale them according to channel opacities
403 for (int i
=0; i
<pixels
.length
; i
++) {
405 pixels
[i
] = (((int)(((p
&0xff0000)>>16) * cr
))<<16)
406 + (((int)(((p
&0xff00)>>8) * cg
))<<8)
407 + (int) ((p
&0xff) * cb
);
410 bi
.setRGB(0, 0, bi
.getWidth(), bi
.getHeight(), pixels
, 0, 1);
414 public void paint(Graphics2D g
, double magnification
, boolean active
, int channels
, Layer active_layer
) {
416 AffineTransform atp
= this.at
;
418 checkChannels(channels
, magnification
);
420 // Consider all possible scaling components: m00, m01
422 double sc
= magnification
* Math
.max(Math
.abs(at
.getScaleX()),
423 Math
.max(Math
.abs(at
.getScaleY()),
424 Math
.max(Math
.abs(at
.getShearX()),
425 Math
.abs(at
.getShearY()))));
426 if (sc
< 0) sc
= magnification
;
427 final Image image
= project
.getLoader().fetchImage(this, sc
);
428 //Utils.log2("Patch " + id + " painted image " + image);
431 //Utils.log2("Patch.paint: null image, returning");
432 return; // TEMPORARY from lazy repaints after closing a Project
435 // fix dimensions: may be smaller or bigger mipmap than the image itself
436 final int iw
= image
.getWidth(null);
437 final int ih
= image
.getHeight(null);
438 if (iw
!= this.width
|| ih
!= this.height
) {
439 atp
= (AffineTransform
)atp
.clone();
440 atp
.scale(this.width
/ iw
, this.height
/ ih
);
443 //arrange transparency
444 Composite original_composite
= null;
446 original_composite
= g
.getComposite();
447 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
450 g
.drawImage(image
, atp
, null);
452 //Transparency: fix composite back to original.
454 g
.setComposite(original_composite
);
459 /** Paint first whatever is available, then request that the proper image be loaded and painted. */
460 public void prePaint(final Graphics2D g
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
) {
462 AffineTransform atp
= this.at
;
464 checkChannels(channels
, magnification
);
466 // Consider all possible scaling components: m00, m01
468 double sc
= magnification
* Math
.max(Math
.abs(at
.getScaleX()),
469 Math
.max(Math
.abs(at
.getScaleY()),
470 Math
.max(Math
.abs(at
.getShearX()),
471 Math
.abs(at
.getShearY()))));
472 if (sc
< 0) sc
= magnification
;
474 Image image
= project
.getLoader().getCachedClosestAboveImage(this, sc
); // above or equal
476 image
= project
.getLoader().getCachedClosestBelowImage(this, sc
); // below, not equal
477 boolean thread
= false;
479 // fetch the proper image, nothing is cached
482 image
= project
.getLoader().fetchImage(this, sc
);
484 // load a smaller mipmap, and then load the larger one and repaint on load.
485 image
= project
.getLoader().fetchImage(this, 0.25);
488 // TODO to be non-blocking, this should paint a black square with a "loading..." legend in it or something, then fire a later repaint thread like below. So don't wait!
490 // painting a smaller image, will need to repaint with the proper one
493 if (thread
&& !Loader
.NOT_FOUND
.equals(image
)) {
494 // use the lower resolution image, but ask to repaint it on load
495 Loader
.preload(this, sc
, true);
500 Utils
.log2("Patch.paint: null image, returning");
501 return; // TEMPORARY from lazy repaints after closing a Project
504 // fix dimensions: may be smaller or bigger mipmap than the image itself
505 final int iw
= image
.getWidth(null);
506 final int ih
= image
.getHeight(null);
507 if (iw
!= this.width
|| ih
!= this.height
) {
508 atp
= (AffineTransform
)atp
.clone();
509 atp
.scale(this.width
/ iw
, this.height
/ ih
);
512 //arrange transparency
513 Composite original_composite
= null;
515 original_composite
= g
.getComposite();
516 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
519 g
.drawImage(image
, atp
, null);
521 //Transparency: fix composite back to original.
522 if (null != original_composite
) {
523 g
.setComposite(original_composite
);
527 /** A method to paint, simply (to a flat image for example); no magnification or srcRect are considered. */
528 public void paint(Graphics2D g
) {
529 if (!this.visible
) return;
531 Image image
= project
.getLoader().fetchImage(this); // TODO: could read the scale parameter of the graphics object and call for the properly sized mipmap accordingly.
533 //arrange transparency
534 Composite original_composite
= null;
536 original_composite
= g
.getComposite();
537 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
540 g
.drawImage(image
, this.at
, null);
542 //Transparency: fix composite back to original.
544 g
.setComposite(original_composite
);
548 public boolean isDeletable() {
549 return 0 == width
&& 0 == height
;
552 /** Remove only if linked to other Patches or to noone. */
553 public boolean remove(boolean check
) {
554 if (check
&& !Utils
.check("Really remove " + this.toString() + " ?")) return false;
555 if (isStack()) { // this Patch is part of a stack
556 GenericDialog gd
= new GenericDialog("Stack!");
557 gd
.addMessage("Really delete the entire stack?");
558 gd
.addCheckbox("Delete layers if empty", true);
560 if (gd
.wasCanceled()) return false;
561 boolean delete_empty_layers
= gd
.getNextBoolean();
563 HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
564 getStackPatchesNR(ht
);
565 Utils
.log("Stack patches: " + ht
.size());
566 ArrayList al
= new ArrayList();
567 for (Iterator it
= ht
.values().iterator(); it
.hasNext(); ) {
568 Patch p
= (Patch
)it
.next();
569 if (!p
.isOnlyLinkedTo(this.getClass())) {
570 Utils
.showMessage("At least one slice of the stack (z=" + p
.getLayer().getZ() + ") is supporting other data.\nCan't delete.");
574 for (Iterator it
= ht
.values().iterator(); it
.hasNext(); ) {
575 Patch p
= (Patch
)it
.next();
576 if (!p
.layer
.remove(p
) || !p
.removeFromDatabase()) {
577 Utils
.showMessage("Can't delete Patch " + p
);
581 p
.removeLinkedPropertiesFromOrigins();
582 //no need//it.remove();
584 if (p
.layer
.isEmpty()) Display
.close(p
.layer
);
585 else Display
.repaint(p
.layer
);
587 if (delete_empty_layers
) {
588 for (Iterator it
= al
.iterator(); it
.hasNext(); ) {
589 Layer la
= (Layer
)it
.next();
591 project
.getLayerTree().remove(la
, false);
599 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)
601 removeLinkedPropertiesFromOrigins();
605 Utils
.showMessage("Patch: can't remove! The image is linked and thus supports other data).");
611 /** Returns true if this Patch holds direct links to at least one other image in a different layer. Doesn't check for total overlap. */
612 public boolean isStack() {
613 if (null == hs_linked
|| hs_linked
.isEmpty()) return false;
614 final Iterator it
= hs_linked
.iterator();
615 while (it
.hasNext()) {
616 Displayable d
= (Displayable
)it
.next();
617 if (d
instanceof Patch
&& d
.layer
.getId() != this.layer
.getId()) return true;
622 /** Retuns a virtual ImagePlus with a virtual stack if necessary. */
623 public PatchStack
makePatchStack() {
625 HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
626 getStackPatchesNR(ht
);
627 Patch
[] patch
= null;
628 int currentSlice
= 1; // from 1 to n, as in ImageStack
630 // a stack. Order by layer Z
631 ArrayList
<Double
> z
= new ArrayList
<Double
>();
632 z
.addAll(ht
.keySet());
633 java
.util
.Collections
.sort(z
);
634 patch
= new Patch
[z
.size()];
637 patch
[i
] = ht
.get(d
);
638 if (patch
[i
].id
== this.id
) currentSlice
= i
+1;
642 patch
= new Patch
[]{ this };
644 return new PatchStack(patch
, currentSlice
);
647 public ArrayList
<Patch
> getStackPatches() {
648 HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
649 getStackPatchesNR(ht
);
650 Utils
.log2("Found patches: " + ht
.size());
651 ArrayList
<Double
> z
= new ArrayList
<Double
>();
652 z
.addAll(ht
.keySet());
653 java
.util
.Collections
.sort(z
);
654 ArrayList
<Patch
> p
= new ArrayList
<Patch
>();
661 /** Collect linked Patch instances that do not lay in this layer. Recursive over linked Patch instances that lay in different layers. */ // This method returns a usable stack because Patch objects are only linked to other Patch objects when inserted together as stack. So the slices are all consecutive in space and have the same thickness. Yes this is rather convoluted, stacks should be full-grade citizens
662 private void getStackPatches(HashMap
<Double
,Patch
> ht
) {
663 if (ht
.containsKey(this)) return;
664 ht
.put(new Double(layer
.getZ()), this);
665 if (null != hs_linked
&& hs_linked
.size() > 0) {
667 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
668 Displayable ob = (Displayable)it.next();
669 if (ob instanceof Patch && !ob.layer.equals(this.layer)) {
670 ((Patch)ob).getStackPatches(ht);
674 // avoid stack overflow (with as little as 114 layers ... !!!)
675 Displayable
[] d
= new Displayable
[hs_linked
.size()];
676 hs_linked
.toArray(d
);
677 for (int i
=0; i
<d
.length
; i
++) {
678 if (d
[i
] instanceof Patch
&& d
[i
].layer
.equals(this.layer
)) {
679 ((Patch
)d
[i
]).getStackPatches(ht
);
685 /** Non-recursive version to avoid stack overflows with "excessive" recursion (I hate java). */
686 private void getStackPatchesNR(final HashMap
<Double
,Patch
> ht
) {
687 final ArrayList
<Patch
> list1
= new ArrayList
<Patch
>();
689 final ArrayList
<Patch
> list2
= new ArrayList
<Patch
>();
690 while (list1
.size() > 0) {
692 for (Patch p
: list1
) {
693 if (null != p
.hs_linked
) {
694 for (Iterator it
= p
.hs_linked
.iterator(); it
.hasNext(); ) {
695 Object ln
= it
.next();
696 if (ln
instanceof Patch
) {
697 Patch pa
= (Patch
)ln
;
698 if (!ht
.containsValue(pa
)) {
699 ht
.put(pa
.layer
.getZ(), pa
);
711 /** Opens and closes the tag and exports data. The image is saved in the directory provided in @param any as a String. */
712 public void exportXML(StringBuffer sb_body
, String indent
, Object any
) { // TODO the Loader should handle the saving of images, not this class.
713 String in
= indent
+ "\t";
716 //Utils.log2("#########\np id=" + id + " any is " + any);
718 path
= any
+ title
; // ah yes, automatic toString() .. it's like the ONLY smart logic at the object level built into java.
719 // save image without overwritting, and add proper extension (.zip)
720 path2
= project
.getLoader().exportImage(this, path
, false);
721 //Utils.log2("p id=" + id + " path2: " + path2);
722 // path2 will be null if the file exists already
724 sb_body
.append(indent
).append("<t2_patch\n");
725 String rel_path
= null;
726 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
727 //Utils.log2("p id=" + id + " path==path2");
729 int i_slash
= rel_path
.lastIndexOf(java
.io
.File
.separatorChar
);
731 i_slash
= rel_path
.lastIndexOf(java
.io
.File
.separatorChar
, i_slash
-1);
733 rel_path
= rel_path
.substring(i_slash
+1);
737 //Utils.log2("Setting rel_path to " + path2);
740 // For FSLoader projects, saving a second time will save images as null unless calling it
741 if (null == rel_path
) {
742 //Utils.log2("path2 was null");
743 Object ob
= project
.getLoader().getPath(this);
744 path2
= null == ob ?
null : (String
)ob
;
746 //Utils.log2("ERROR: No path for Patch id=" + id + " and title: " + title);
747 rel_path
= title
; // at least some clue for recovery
753 //Utils.log("Patch path is: " + rel_path);
755 super.exportXML(sb_body
, in
, any
);
756 String
[] RGB
= Utils
.getHexRGBColor(color
);
757 int type
= this.type
;
758 if (-1 == this.type
) {
759 Utils
.log2("Retrieving type for p = " + this);
760 ImagePlus imp
= project
.getLoader().fetchImagePlus(this);
761 if (null != imp
) type
= imp
.getType();
763 sb_body
.append(in
).append("type=\"").append(type
/*null == any ? ImagePlus.GRAY8 : type*/).append("\"\n")
764 .append(in
).append("file_path=\"").append(rel_path
).append("\"\n")
765 .append(in
).append("style=\"fill-opacity:").append(alpha
).append(";stroke:#").append(RGB
[0]).append(RGB
[1]).append(RGB
[2]).append(";\"\n")
766 .append(in
).append("o_width=\"").append(o_width
).append("\"\n")
767 .append(in
).append("o_height=\"").append(o_height
).append("\"\n")
769 if (null != original_path
) {
770 sb_body
.append(in
).append("original_path=\"").append(original_path
).append("\"\n");
772 if (0 != min
) sb_body
.append(in
).append("min=\"").append(min
).append("\"\n");
773 if (max
!= Patch
.getMaxMax(type
)) sb_body
.append(in
).append("max=\"").append(max
).append("\"\n");
775 sb_body
.append(indent
).append(">\n");
778 sb_body
.append(ct
.toXML(in
)).append('\n');
781 super.restXML(sb_body
, in
, any
);
783 sb_body
.append(indent
).append("</t2_patch>\n");
786 static private final double getMaxMax(final int type
) {
789 case ImagePlus
.GRAY16
: pow
= 2; break; // TODO problems with unsigned short most likely
790 case ImagePlus
.GRAY32
: pow
= 4; break;
793 return Math
.pow(256, pow
) - 1;
796 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
797 String type
= "t2_patch";
798 if (hs
.contains(type
)) return;
800 sb_header
.append(indent
).append("<!ELEMENT t2_patch (").append(Displayable
.commonDTDChildren()).append(",ict_transform,ict_transform_list)>\n");
801 Displayable
.exportDTD(type
, sb_header
, hs
, indent
);
802 sb_header
.append(indent
).append(TAG_ATTR1
).append(type
).append(" file_path").append(TAG_ATTR2
)
803 .append(indent
).append(TAG_ATTR1
).append(type
).append(" original_path").append(TAG_ATTR2
)
804 .append(indent
).append(TAG_ATTR1
).append(type
).append(" type").append(TAG_ATTR2
)
805 .append(indent
).append(TAG_ATTR1
).append(type
).append(" ct").append(TAG_ATTR2
)
806 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_width").append(TAG_ATTR2
)
807 .append(indent
).append(TAG_ATTR1
).append(type
).append(" o_height").append(TAG_ATTR2
)
809 // The InvertibleCoordinateTransform and a list of:
810 sb_header
.append(indent
).append("<!ELEMENT ict_transform EMPTY>\n");
811 sb_header
.append(indent
).append(TAG_ATTR1
).append("ict_transform class").append(TAG_ATTR2
)
812 .append(indent
).append(TAG_ATTR1
).append("ict_transform data").append(TAG_ATTR2
);
813 sb_header
.append(indent
).append("<!ELEMENT ict_transform_list (ict_transform)>\n");
817 /** Performs a copy of this object, without the links, unlocked and visible, except for the image which is NOT duplicated. If the project is NOT the same as this instance's project, then the id of this instance gets assigned as well to the returned clone. */
818 public Displayable
clone(final Project pr
, final boolean copy_id
) {
819 final long nid
= copy_id ?
this.id
: pr
.getLoader().getNextId();
820 final Patch copy
= new Patch(pr
, nid
, null != title ? title
.toString() : null, width
, height
, type
, false, min
, max
, (AffineTransform
)at
.clone());
821 copy
.color
= new Color(color
.getRed(), color
.getGreen(), color
.getBlue());
822 copy
.alpha
= this.alpha
;
824 copy
.channels
= this.channels
;
827 copy
.ct
= null == ct ?
null : this.ct
.clone();
828 copy
.addToDatabase();
829 pr
.getLoader().addedPatchFrom(this.project
.getLoader().getAbsolutePath(this), copy
);
830 copy
.setAlphaMask(this.project
.getLoader().fetchImageMask(this));
834 /** Override to cancel. */
835 public void linkPatches() {
836 Utils
.log2("Patch class can't link other patches using Displayble.linkPatches()");
839 public void paintSnapshot(final Graphics2D g
, final double mag
) {
840 switch (layer
.getParent().getSnapshotsMode()) {
842 if (!project
.getLoader().isSnapPaintable(this.id
)) {
845 paint(g
, mag
, false, this.channels
, layer
);
851 default: return; // case 2: // disabled, no paint
855 static protected void crosslink(final ArrayList patches
, final boolean overlapping_only
) {
856 if (null == patches
) return;
857 final ArrayList
<Patch
> al
= new ArrayList
<Patch
>();
858 for (Object ob
: patches
) if (ob
instanceof Patch
) al
.add((Patch
)ob
); // ...
859 final int len
= al
.size();
861 final Patch
[] pa
= new Patch
[len
];
863 // linking is reciprocal: need only call link() on one member of the pair
864 for (int i
=0; i
<pa
.length
; i
++) {
865 for (int j
=i
+1; j
<pa
.length
; j
++) {
866 if (overlapping_only
&& !pa
[i
].intersects(pa
[j
])) continue;
872 /** 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.*/
873 public int getPixel(double mag
, final int x
, final int y
) {
874 final int[] iArray
= getPixel(x
, y
, mag
);
875 if (ImagePlus
.COLOR_RGB
== this.type
) {
876 return (iArray
[0]<<16) + (iArray
[1]<<8) + iArray
[2];
881 /** 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.*/
882 public int[] getPixel(double mag
, final int x
, final int y
, final int[] iArray
) {
883 final int[] ia
= getPixel(x
, y
, mag
);
893 /** Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method. */
894 public int[] getPixel(final int x
, final int y
, final double mag
) {
895 if (project
.getLoader().isUnloadable(this)) return new int[4];
896 final Image img
= project
.getLoader().fetchImage(this, mag
);
897 if (Loader
.isSignalImage(img
)) return new int[4];
898 final int w
= img
.getWidth(null);
899 final double scale
= w
/ width
;
900 final Point2D
.Double pd
= inverseTransformPoint(x
, y
);
901 final int x2
= (int)(pd
.x
* scale
);
902 final int y2
= (int)(pd
.y
* scale
);
903 final int[] pvalue
= new int[4];
904 final PixelGrabber pg
= new PixelGrabber(img
, x2
, y2
, 1, 1, pvalue
, 0, w
);
907 } catch (InterruptedException ie
) {
911 case ImagePlus
.COLOR_256
:
912 final PixelGrabber pg2
= new PixelGrabber(img
,x2
,y2
,1,1,false);
915 } catch (InterruptedException ie
) {
918 final byte[] pix8
= (byte[])pg2
.getPixels();
919 pvalue
[3] = null != pix8 ? pix8
[0]&0xff : 0;
920 // fall through to get RGB values
921 case ImagePlus
.COLOR_RGB
:
922 final int c
= pvalue
[0];
923 pvalue
[0] = (c
&0xff0000)>>16; // R
924 pvalue
[1] = (c
&0xff00)>>8; // G
925 pvalue
[2] = c
&0xff; // B
927 case ImagePlus
.GRAY8
:
928 pvalue
[0] = pvalue
[0]&0xff;
930 default: // all others: GRAY16, GRAY32
931 pvalue
[0] = pvalue
[0]&0xff;
932 // correct range: from 8-bit of the mipmap to 16 or 32 bit
934 // mipmap was an 8-bit image, so expand
935 pvalue
[0] = (int)(min
+ pvalue
[0] * ( (max
- min
) / 256 ));
943 /** 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. */
944 public final String
getFilePath() {
945 if (null != current_path
) return current_path
;
946 return project
.getLoader().getAbsolutePath(this);
949 /** Returns the absolute path to the image file, as read by the OS. */
950 public final String
getImageFilePath() {
951 return project
.getLoader().getAbsoluteFilePath(this);
954 /** 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. */
955 public final String
getCurrentPath() { return current_path
; }
957 /** Cache a proper, good, known path to the image wrapped by this Patch. */
958 public final void cacheCurrentPath(final String path
) {
959 this.current_path
= path
;
962 /** 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. */
963 synchronized public String
getOriginalPath() { return original_path
; }
965 protected void setAlpha(float alpha
, boolean update
) {
967 HashMap
<Double
,Patch
> ht
= new HashMap
<Double
,Patch
>();
968 getStackPatchesNR(ht
);
969 for (Patch pa
: ht
.values()) {
971 pa
.updateInDatabase("alpha");
972 Display
.repaint(pa
.layer
, pa
, 5);
974 Display3D
.setTransparency(this, alpha
);
975 } else super.setAlpha(alpha
, update
);
978 public void debug() {
979 Utils
.log2("Patch id=" + id
+ "\n\toriginal_path=" + original_path
+ "\n\tcurrent_path=" + current_path
);
982 /** Revert the ImagePlus to the one stored in original_path, if any; will revert all linked patches if this is part of a stack. */
983 synchronized public boolean revert() {
984 if (null == original_path
) return false; // nothing to revert to
985 // 1 - check that original_path exists
986 if (!new File(original_path
).exists()) {
987 Utils
.log("CANNOT revert: Original file path does not exist: " + original_path
+ " for patch " + getTitle() + " #" + id
);
990 // 2 - check that the original can be loaded
991 final ImagePlus imp
= project
.getLoader().fetchOriginal(this);
992 if (null == imp
|| null == set(imp
)) {
993 Utils
.log("CANNOT REVERT: original image at path " + original_path
+ " fails to load, for patch " + getType() + " #" + id
);
996 // 3 - update path in loader, and cache imp for each stack slice id
998 for (Patch p
: getStackPatches()) {
999 p
.project
.getLoader().addedPatchFrom(p
.original_path
, p
);
1000 p
.project
.getLoader().cacheImagePlus(p
.id
, imp
);
1001 p
.project
.getLoader().generateMipMaps(p
);
1004 project
.getLoader().addedPatchFrom(original_path
, this);
1005 project
.getLoader().cacheImagePlus(id
, imp
);
1006 project
.getLoader().generateMipMaps(this);
1008 // 4 - update screens
1009 Display
.repaint(layer
, this, 0);
1010 Utils
.showStatus("Reverted patch " + getTitle(), false);
1014 /** For reconstruction purposes, overwrites the present CoordinateTransform, if any, with the given one. */
1015 public void setCoordinateTransformSilently(final CoordinateTransform ct
) {
1019 /** Set a CoordinateTransform to this Patch.
1020 * 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. */
1021 public final void setCoordinateTransform(final CoordinateTransform ct
) {
1023 Utils
.log("Cannot set coordinate transform: patch is linked!");
1027 if (null != this.ct
) {
1028 // restore image without the transform
1029 final TransformMesh mesh
= new TransformMesh(this.ct
, 32, o_width
, o_height
);
1030 final Rectangle box
= mesh
.getBoundingBox();
1031 this.at
.translate(-box
.x
, -box
.y
);
1032 updateInDatabase("transform+dimensions");
1036 updateInDatabase("ict_transform");
1038 if (null == this.ct
) {
1045 // Adjust the AffineTransform to correct for bounding box displacement
1047 final TransformMesh mesh
= new TransformMesh(this.ct
, 32, o_width
, o_height
);
1048 final Rectangle box
= mesh
.getBoundingBox();
1049 this.at
.translate(box
.x
, box
.y
);
1050 this.width
= box
.width
;
1051 this.height
= box
.height
;
1052 updateInDatabase("transform+dimensions"); // the AffineTransform
1055 // Updating the mipmaps will call createTransformedImage below if ct is not null
1056 /* DISABLED */ //updateMipmaps();
1060 * Append a {@link CoordinateTransform} to the current
1061 * {@link CoordinateTransformList}. If there is no transform yet, it just
1062 * sets it. If there is only one transform, it replaces it by a list
1065 public final void appendCoordinateTransform(final CoordinateTransform ct
) {
1066 if (null == this.ct
)
1067 setCoordinateTransform(ct
);
1069 final CoordinateTransformList ctl
;
1070 if (this.ct
instanceof CoordinateTransformList
)
1071 ctl
= (CoordinateTransformList
)this.ct
;
1073 ctl
= new CoordinateTransformList();
1077 setCoordinateTransform(ctl
);
1082 * Get the bounding rectangle of the transformed image relative to the
1086 * Currently, this is done in a very expensive way. The
1087 * {@linkplain TransformMesh} is built and its bounding rectangle is
1088 * returned. Think about just storing this rectangle in the
1089 * {@linkplain Patch} instance.
1093 public final Rectangle
getCoordinateTransformBoundingBox() {
1095 return new Rectangle(0,0,o_width
,o_height
);
1096 final TransformMesh mesh
= new TransformMesh(this.ct
, 32, o_width
, o_height
);
1097 return mesh
.getBoundingBox();
1100 public final CoordinateTransform
getCoordinateTransform() { return ct
; }
1102 public final Patch
.PatchImage
createCoordinateTransformedImage() {
1103 if (null == ct
) return null;
1105 project
.getLoader().releaseToFit(o_width
, o_height
, type
, 5);
1107 final ImageProcessor source
= getImageProcessor();
1109 //Utils.log2("source image dimensions: " + source.getWidth() + ", " + source.getHeight());
1111 final TransformMesh mesh
= new TransformMesh(ct
, 32, o_width
, o_height
);
1112 final TransformMeshMapping mapping
= new TransformMeshMapping( mesh
);
1114 ImageProcessor target
= mapping
.createMappedImageInterpolated( source
);
1116 ByteProcessor outside
= new ByteProcessor( source
.getWidth(), source
.getHeight() );
1117 outside
.setValue(255);
1120 outside
= (ByteProcessor
) mapping
.createMappedImageInterpolated( outside
);
1122 ByteProcessor mask
= project
.getLoader().fetchImageMask(this);
1124 mask
= (ByteProcessor
) mapping
.createMappedImageInterpolated( mask
);
1126 // Set all non-white pixels to zero
1127 final byte[] pix
= (byte[])outside
.getPixels();
1128 for (int i
=0; i
<pix
.length
; i
++)
1129 if ((pix
[i
]&0xff) != 255) pix
[i
] = 0;
1131 final Rectangle box
= mesh
.getBoundingBox();
1133 //Utils.log2("New image dimensions: " + target.getWidth() + ", " + target.getHeight());
1134 //Utils.log2("box: " + box);
1136 return new PatchImage( target
, mask
, outside
, box
, true );
1139 public final class PatchImage
{
1140 /** The image, coordinate-transformed if null != ct. */
1141 final public ImageProcessor target
;
1142 /** The alpha mask, coordinate-transformed if null != ct. */
1143 final public ByteProcessor mask
;
1144 /** The outside mask, coordinate-transformed if null != ct. */
1145 final public ByteProcessor outside
;
1146 /** The bounding box of the image relative to the original, with x,y as the displacement relative to the pixels of the original image. */
1147 final public Rectangle box
;
1148 /** Whether the image was generated with a CoordinateTransform or not. */
1149 final public boolean coordinate_transformed
;
1151 private PatchImage( ImageProcessor target
, ByteProcessor mask
, ByteProcessor outside
, Rectangle box
, boolean coordinate_transformed
) {
1152 this.target
= target
;
1154 this.outside
= outside
;
1156 this.coordinate_transformed
= coordinate_transformed
;
1160 /** 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). */
1161 public Patch
.PatchImage
createTransformedImage() {
1162 final Patch
.PatchImage pi
= createCoordinateTransformedImage();
1163 if (null != pi
) return pi
;
1164 // else, a new one with the untransformed, original image (a duplicate):
1165 project
.getLoader().releaseToFit(o_width
, o_height
, type
, 3);
1166 final ImageProcessor ip
= getImageProcessor();
1167 if (null == ip
) return null;
1168 return new PatchImage(ip
.duplicate(), project
.getLoader().fetchImageMask(this), null, new Rectangle(0, 0, o_width
, o_height
), false);
1171 private boolean has_alpha
= false;
1172 private boolean alpha_path_checked
= false;
1174 /** Caching system to avoid repeated checks. No automatic memoization ... snif */
1175 private final boolean hasMask() {
1176 if (alpha_path_checked
) return has_alpha
;
1177 // else, see if the path exists:
1179 has_alpha
= new File(project
.getLoader().getAlphaPath(this)).exists();
1180 } catch (Exception e
) {
1183 alpha_path_checked
= true;
1187 public boolean hasAlphaChannel() {
1188 return null != ct
|| hasMask();
1191 /** Must call updateMipmaps() afterwards. Set it to null to remove it. */
1192 public void setAlphaMask(ByteProcessor bp
) throws IllegalArgumentException
{
1195 if (project
.getLoader().removeAlphaMask(this)) {
1196 alpha_path_checked
= false;
1202 if (o_width
!= bp
.getWidth() || o_height
!= bp
.getHeight()) {
1203 throw new IllegalArgumentException("Need a mask of identical dimensions as the original image.");
1205 project
.getLoader().storeAlphaMask(this, bp
);
1206 alpha_path_checked
= false;
1209 public void keyPressed(KeyEvent ke
) {
1210 Object source
= ke
.getSource();
1211 if (! (source
instanceof DisplayCanvas
)) return;
1212 DisplayCanvas dc
= (DisplayCanvas
)source
;
1213 final Layer la
= dc
.getDisplay().getLayer();
1214 final Roi roi
= dc
.getFakeImagePlus().getRoi();
1216 switch (ke
.getKeyCode()) {
1218 // copy into ImageJ clipboard
1219 int mod
= ke
.getModifiers();
1221 // Ignoring masks: outside is already black, and ImageJ cannot handle alpha masks.
1222 if (0 == mod
|| (0 == (mod ^ Event
.SHIFT_MASK
))) {
1223 CoordinateTransformList list
= null;
1225 list
= new CoordinateTransformList();
1228 if (0 == mod
) { //SHIFT is down
1229 AffineModel2D am
= new AffineModel2D();
1231 if (null == list
) list
= new CoordinateTransformList();
1236 TransformMesh mesh
= new TransformMesh(list
, 32, o_width
, o_height
);
1237 TransformMeshMapping mapping
= new TransformMeshMapping(mesh
);
1238 ip
= mapping
.createMappedImageInterpolated(getImageProcessor());
1240 ip
= getImageProcessor();
1242 new ImagePlus(this.title
, ip
).copy(false);
1243 } else if (0 == (mod ^
(Event
.SHIFT_MASK
| Event
.ALT_MASK
))) {
1244 // On shift down (and no other flags!):
1245 // Place the source image, untransformed, into clipboard:
1246 ImagePlus imp
= getImagePlus();
1247 if (null != imp
) imp
.copy(false);
1252 // fill mask with current ROI using
1253 Utils
.log2("VK_F: roi is " + roi
);
1254 if (null != roi
&& M
.isAreaROI(roi
)) {
1255 Bureaucrat
.createAndStart(new Worker("Filling image mask") { public void run() { try {
1257 ByteProcessor mask
= project
.getLoader().fetchImageMask(Patch
.this);
1258 boolean is_new
= false;
1260 mask
= new ByteProcessor(o_width
, o_height
);
1266 // a roi local to the image bounding box
1267 final Area a
= new Area(new Rectangle(0, 0, (int)width
, (int)height
));
1268 a
.intersect(M
.getArea(roi
).createTransformedArea(Patch
.this.at
.createInverse()));
1271 Utils
.log("ROI does not intersect the active image!");
1276 // inverse the coordinate transform
1277 final TransformMesh mesh
= new TransformMesh(ct
, 32, o_width
, o_height
);
1278 final TransformMeshMapping mapping
= new TransformMeshMapping( mesh
);
1280 ByteProcessor rmask
= new ByteProcessor((int)width
, (int)height
);
1283 rmask
.setColor(Toolbar
.getForegroundColor());
1285 rmask
.setValue(255);
1287 ShapeRoi sroi
= new ShapeRoi(a
);
1289 rmask
.fill(sroi
.getMask());
1291 ByteProcessor inv_mask
= (ByteProcessor
) mapping
.createInverseMappedImageInterpolated(rmask
);
1299 inv_mask
.setMinAndMax(255, 255);
1300 final byte[] b1
= (byte[]) mask
.getPixels();
1301 final byte[] b2
= (byte[]) inv_mask
.getPixels();
1302 final int color
= mask
.getBestIndex(Toolbar
.getForegroundColor());
1303 for (int i
=0; i
<b1
.length
; i
++) {
1304 b1
[i
] = (byte) ((int)( (b2
[i
] & 0xff) / 255.0f
) * (color
- (b1
[i
] & 0xff) ) + (b1
[i
] & 0xff));
1308 ShapeRoi sroi
= new ShapeRoi(a
);
1310 mask
.setColor(Toolbar
.getForegroundColor());
1311 mask
.fill(sroi
.getMask());
1313 } catch (NoninvertibleTransformException nite
) { IJError
.print(nite
); }
1317 } catch (Exception e
) {
1330 Class
getInternalDataPackageClass() {
1331 return DPPatch
.class;
1335 Object
getDataPackage() {
1336 return new DPPatch(this);
1339 static private final class DPPatch
extends Displayable
.DataPackage
{
1340 final double min
, max
;
1341 CoordinateTransform ct
= null;
1343 DPPatch(final Patch patch
) {
1345 this.min
= patch
.min
;
1346 this.max
= patch
.max
;
1347 this.ct
= null == ct ?
null : patch
.ct
.clone();
1348 // channels is visualization
1350 // type is dependent on path, so absolute
1351 // o_width, o_height idem
1353 final boolean to2(final Displayable d
) {
1355 final Patch p
= (Patch
) d
;
1356 boolean mipmaps
= false;
1357 if (p
.min
!= min
|| p
.max
!= max
|| p
.ct
!= ct
|| (p
.ct
== ct
&& ct
instanceof CoordinateTransformList
)) {
1358 Utils
.log2("mipmaps is true! " + (p
.min
!= min
) + " " + (p
.max
!= max
) + " " + (p
.ct
!= ct
) + " " + (p
.ct
== ct
&& ct
instanceof CoordinateTransformList
));
1363 p
.ct
= null == ct ?
null : (CoordinateTransform
) ct
.clone();
1366 Utils
.log2("Update mipmaps in a background task");
1367 ArrayList al
= new ArrayList();
1369 p
.project
.getLoader().generateMipMaps(al
, true);