Major cleanup of Utils class.
[trakem2.git] / ini / trakem2 / display / Patch.java
blobcde854282545205110ea285daf368b33ca42121a
1 /**
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.
21 **/
23 package ini.trakem2.display;
26 import ij.ImagePlus;
27 import ij.gui.GenericDialog;
28 import ij.gui.Roi;
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;
65 import java.util.Map;
66 import java.util.HashMap;
67 import java.util.ArrayList;
68 import java.util.HashSet;
69 import java.util.Collection;
70 import java.io.File;
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();
104 checkMinMax();
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);
110 addToDatabase();
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);
116 this.type = type;
117 this.min = min;
118 this.max = max;
119 if (0 == o_width) o_width = (int)width;
120 if (0 == o_height) o_height = (int)height;
121 checkMinMax();
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);
127 // cache path:
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);
141 hasmin = true;
142 } else if (key.equals("max")) {
143 this.max = Double.parseDouble(data);
144 hasmax = true;
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.
156 try {
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);
160 o_width = dim.width;
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;
167 IJError.print(e);
171 if (hasmin && hasmax) {
172 checkMinMax();
173 } else {
174 // standard, from the image, to be defined when first painted
175 min = max = -1;
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);
188 return imp;
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);
194 return ip;
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();
215 updateBucket();
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:
231 if (isStack()) {
232 for (Patch p : getStackPatches()) {
233 if (null == p.original_path) original_path = p.project.getLoader().getAbsolutePath(p);
235 } else {
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);
240 if (null == path) {
241 Utils.log2("setImageFile returned null!");
242 return null; // something went wrong
244 // 2 - update properties and mipmaps
245 if (isStack()) {
246 for (Patch p : getStackPatches()) {
247 p.readProps(new_imp);
248 project.getLoader().generateMipMaps(p); // sequentially
249 project.getLoader().decacheAWT(p.id);
251 } else {
252 readProps(new_imp);
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;
263 switch (type) {
264 case ImagePlus.GRAY8:
265 case ImagePlus.COLOR_RGB:
266 case ImagePlus.COLOR_256:
267 if (this.min < 0) this.min = 0;
268 break;
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) {
277 this.min = min;
278 this.max = 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) {
291 min = ip.getMin();
292 max = ip.getMax();
293 } else {
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() {
301 return type;
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() {
317 return channels;
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
327 Image awt = null;
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) {
333 // full transparency
334 awt = ip.createImage(); //imp.getImage();
335 // pixels array will be shared using ij138j and above
336 } else {
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];
343 int p;
344 for (int i=pixels.length -1; i>-1; i--) {
345 p = pixels[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);
356 } else {
357 awt = ip.createImage();
360 //Utils.log2("ip's min, max: " + ip.getMin() + ", " + ip.getMax());
362 this.channels = c;
364 return awt;
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;
388 // reuse if possible
389 if (awt instanceof BufferedImage) bi = (BufferedImage)awt;
390 else {
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;
398 // extract pixels
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
402 int p;
403 for (int i=0; i<pixels.length; i++) {
404 p = pixels[i];
405 pixels[i] = (((int)(((p&0xff0000)>>16) * cr))<<16)
406 + (((int)(((p&0xff00)>>8) * cg))<<8)
407 + (int) ((p&0xff) * cb);
409 // replace pixels
410 bi.setRGB(0, 0, bi.getWidth(), bi.getHeight(), pixels, 0, 1);
411 return bi;
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
421 // m10, m11
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);
430 if (null == 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;
445 if (alpha != 1.0f) {
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.
453 if (alpha != 1.0f) {
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
467 // m10, m11
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
475 if (null == image) {
476 image = project.getLoader().getCachedClosestBelowImage(this, sc); // below, not equal
477 boolean thread = false;
478 if (null == image) {
479 // fetch the proper image, nothing is cached
480 if (sc <= 0.5001) {
481 // load the mipmap
482 image = project.getLoader().fetchImage(this, sc);
483 } else {
484 // load a smaller mipmap, and then load the larger one and repaint on load.
485 image = project.getLoader().fetchImage(this, 0.25);
486 thread = true;
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!
489 } else {
490 // painting a smaller image, will need to repaint with the proper one
491 thread = true;
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);
499 if (null == image) {
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;
514 if (alpha != 1.0f) {
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;
535 if (alpha != 1.0f) {
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.
543 if (alpha != 1.0f) {
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);
559 gd.showDialog();
560 if (gd.wasCanceled()) return false;
561 boolean delete_empty_layers = gd.getNextBoolean();
562 // gather all
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.");
571 return false;
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);
578 return false;
580 p.unlink();
581 p.removeLinkedPropertiesFromOrigins();
582 //no need//it.remove();
583 al.add(p.layer);
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();
590 if (la.isEmpty()) {
591 project.getLayerTree().remove(la, false);
592 Display.close(la);
596 Search.remove(this);
597 return true;
598 } else {
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)
600 unlink();
601 removeLinkedPropertiesFromOrigins();
602 Search.remove(this);
603 return true;
604 } else {
605 Utils.showMessage("Patch: can't remove! The image is linked and thus supports other data).");
606 return false;
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;
619 return false;
622 /** Retuns a virtual ImagePlus with a virtual stack if necessary. */
623 public PatchStack makePatchStack() {
624 // are we a stack?
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
629 if (ht.size() > 1) {
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()];
635 int i = 0;
636 for (Double d : z) {
637 patch[i] = ht.get(d);
638 if (patch[i].id == this.id) currentSlice = i+1;
639 i++;
641 } else {
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>();
655 for (Double d : z) {
656 p.add(ht.get(d));
658 return p;
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>();
688 list1.add(this);
689 final ArrayList<Patch> list2 = new ArrayList<Patch>();
690 while (list1.size() > 0) {
691 list2.clear();
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);
700 list2.add(pa);
706 list1.clear();
707 list1.addAll(list2);
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";
714 String path = null;
715 String path2 = null;
716 //Utils.log2("#########\np id=" + id + " any is " + any);
717 if (null != 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");
728 rel_path = path2;
729 int i_slash = rel_path.lastIndexOf(java.io.File.separatorChar);
730 if (i_slash > 0) {
731 i_slash = rel_path.lastIndexOf(java.io.File.separatorChar, i_slash -1);
732 if (-1 != i_slash) {
733 rel_path = rel_path.substring(i_slash+1);
736 } else {
737 //Utils.log2("Setting rel_path to " + path2);
738 rel_path = 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;
745 if (null == path2) {
746 //Utils.log2("ERROR: No path for Patch id=" + id + " and title: " + title);
747 rel_path = title; // at least some clue for recovery
748 } else {
749 rel_path = path2;
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");
777 if (null != ct) {
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) {
787 int pow = 1;
788 switch (type) {
789 case ImagePlus.GRAY16: pow = 2; break; // TODO problems with unsigned short most likely
790 case ImagePlus.GRAY32: pow = 4; break;
791 default: return 255;
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;
799 // The Patch itself:
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;
823 copy.visible = true;
824 copy.channels = this.channels;
825 copy.min = this.min;
826 copy.max = this.max;
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));
831 return copy;
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()) {
841 case 0:
842 if (!project.getLoader().isSnapPaintable(this.id)) {
843 paintAsBox(g);
844 } else {
845 paint(g, mag, false, this.channels, layer);
847 return;
848 case 1:
849 paintAsBox(g);
850 return;
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();
860 if (len < 2) return;
861 final Patch[] pa = new Patch[len];
862 al.toArray(pa);
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;
867 pa[i].link(pa[j]);
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];
878 return iArray[0];
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);
884 if(null != iArray) {
885 iArray[0] = ia[0];
886 iArray[1] = ia[1];
887 iArray[2] = ia[2];
888 return iArray;
890 return ia;
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);
905 try {
906 pg.grabPixels();
907 } catch (InterruptedException ie) {
908 return pvalue;
910 switch (type) {
911 case ImagePlus.COLOR_256:
912 final PixelGrabber pg2 = new PixelGrabber(img,x2,y2,1,1,false);
913 try {
914 pg2.grabPixels();
915 } catch (InterruptedException ie) {
916 return pvalue;
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
926 break;
927 case ImagePlus.GRAY8:
928 pvalue[0] = pvalue[0]&0xff;
929 break;
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
933 if (mag <= 0.5) {
934 // mipmap was an 8-bit image, so expand
935 pvalue[0] = (int)(min + pvalue[0] * ( (max - min) / 256 ));
937 break;
940 return pvalue;
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) {
966 if (isStack()) {
967 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
968 getStackPatchesNR(ht);
969 for (Patch pa : ht.values()) {
970 pa.alpha = alpha;
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);
988 return false;
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);
994 return false;
996 // 3 - update path in loader, and cache imp for each stack slice id
997 if (isStack()) {
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);
1003 } else {
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);
1011 return true;
1014 /** For reconstruction purposes, overwrites the present CoordinateTransform, if any, with the given one. */
1015 public void setCoordinateTransformSilently(final CoordinateTransform ct) {
1016 this.ct = 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) {
1022 if (isLinked()) {
1023 Utils.log("Cannot set coordinate transform: patch is linked!");
1024 return;
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");
1035 this.ct = ct;
1036 updateInDatabase("ict_transform");
1038 if (null == this.ct) {
1039 width = o_width;
1040 height = o_height;
1041 updateBucket();
1042 return;
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
1053 updateBucket();
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
1063 * containing both.
1065 public final void appendCoordinateTransform(final CoordinateTransform ct) {
1066 if (null == this.ct)
1067 setCoordinateTransform(ct);
1068 else {
1069 final CoordinateTransformList ctl;
1070 if (this.ct instanceof CoordinateTransformList)
1071 ctl = (CoordinateTransformList)this.ct;
1072 else {
1073 ctl = new CoordinateTransformList();
1074 ctl.add(this.ct);
1076 ctl.add(ct);
1077 setCoordinateTransform(ctl);
1082 * Get the bounding rectangle of the transformed image relative to the
1083 * original image.
1085 * TODO
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.
1091 * @return
1093 public final Rectangle getCoordinateTransformBoundingBox() {
1094 if (null==ct)
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);
1118 outside.fill();
1120 outside = (ByteProcessor) mapping.createMappedImageInterpolated( outside );
1122 ByteProcessor mask = project.getLoader().fetchImageMask(this);
1123 if (null != mask)
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;
1153 this.mask = mask;
1154 this.outside = outside;
1155 this.box = box;
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:
1178 try {
1179 has_alpha = new File(project.getLoader().getAlphaPath(this)).exists();
1180 } catch (Exception e) {
1181 IJError.print(e);
1183 alpha_path_checked = true;
1184 return has_alpha;
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 {
1193 if (null == bp) {
1194 if (hasMask()) {
1195 if (project.getLoader().removeAlphaMask(this)) {
1196 alpha_path_checked = false;
1199 return;
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()) {
1217 case KeyEvent.VK_C:
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;
1224 if (null != ct) {
1225 list = new CoordinateTransformList();
1226 list.add(this.ct);
1228 if (0 == mod) { //SHIFT is down
1229 AffineModel2D am = new AffineModel2D();
1230 am.set(this.at);
1231 if (null == list) list = new CoordinateTransformList();
1232 list.add(am);
1234 ImageProcessor ip;
1235 if (null != list) {
1236 TransformMesh mesh = new TransformMesh(list, 32, o_width, o_height);
1237 TransformMeshMapping mapping = new TransformMeshMapping(mesh);
1238 ip = mapping.createMappedImageInterpolated(getImageProcessor());
1239 } else {
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);
1249 ke.consume();
1250 break;
1251 case KeyEvent.VK_F:
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 {
1256 startedWorking();
1257 ByteProcessor mask = project.getLoader().fetchImageMask(Patch.this);
1258 boolean is_new = false;
1259 if (null == mask) {
1260 mask = new ByteProcessor(o_width, o_height);
1261 mask.setValue(255);
1262 mask.fill();
1263 is_new = true;
1265 try {
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()));
1270 if (M.isEmpty(a)) {
1271 Utils.log("ROI does not intersect the active image!");
1272 return;
1275 if (null != ct) {
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);
1282 if (is_new) {
1283 rmask.setColor(Toolbar.getForegroundColor());
1284 } else {
1285 rmask.setValue(255);
1287 ShapeRoi sroi = new ShapeRoi(a);
1288 rmask.setRoi(sroi);
1289 rmask.fill(sroi.getMask());
1291 ByteProcessor inv_mask = (ByteProcessor) mapping.createInverseMappedImageInterpolated(rmask);
1293 if (is_new) {
1294 mask = inv_mask;
1295 // done!
1296 } else {
1297 // Blend
1298 rmask = null;
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));
1307 } else {
1308 ShapeRoi sroi = new ShapeRoi(a);
1309 mask.setRoi(sroi);
1310 mask.setColor(Toolbar.getForegroundColor());
1311 mask.fill(sroi.getMask());
1313 } catch (NoninvertibleTransformException nite) { IJError.print(nite); }
1314 setAlphaMask(mask);
1315 updateMipmaps();
1316 Display.repaint();
1317 } catch (Exception e) {
1318 IJError.print(e);
1319 } finally {
1320 finishedWorking();
1321 }}}, project);
1323 // capturing:
1324 ke.consume();
1325 break;
1329 @Override
1330 Class getInternalDataPackageClass() {
1331 return DPPatch.class;
1334 @Override
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) {
1344 super(patch);
1345 this.min = patch.min;
1346 this.max = patch.max;
1347 this.ct = null == ct ? null : patch.ct.clone();
1348 // channels is visualization
1349 // path is absolute
1350 // type is dependent on path, so absolute
1351 // o_width, o_height idem
1353 final boolean to2(final Displayable d) {
1354 super.to1(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));
1359 mipmaps = true;
1361 p.min = min;
1362 p.max = max;
1363 p.ct = null == ct ? null : (CoordinateTransform) ct.clone();
1365 if (mipmaps) {
1366 Utils.log2("Update mipmaps in a background task");
1367 ArrayList al = new ArrayList();
1368 al.add(p);
1369 p.project.getLoader().generateMipMaps(al, true);
1371 return true;