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