Fix error in duplicating a Patch: the CT and alpha mask couldn't be found,
[trakem2.git] / ini / trakem2 / display / Patch.java
blob61d03525d07ea24dd40947e33bef45dc008cac8e
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.persistence.FSLoader;
47 import ini.trakem2.persistence.Loader;
48 import ini.trakem2.persistence.XMLOptions;
49 import ini.trakem2.utils.Bureaucrat;
50 import ini.trakem2.utils.IJError;
51 import ini.trakem2.utils.M;
52 import ini.trakem2.utils.ProjectToolbar;
53 import ini.trakem2.utils.Search;
54 import ini.trakem2.utils.Utils;
55 import ini.trakem2.utils.Worker;
57 import java.awt.Color;
58 import java.awt.Composite;
59 import java.awt.Dimension;
60 import java.awt.Event;
61 import java.awt.Graphics2D;
62 import java.awt.Image;
63 import java.awt.Polygon;
64 import java.awt.Rectangle;
65 import java.awt.Toolkit;
66 import java.awt.event.KeyEvent;
67 import java.awt.event.MouseEvent;
68 import java.awt.geom.AffineTransform;
69 import java.awt.geom.Area;
70 import java.awt.geom.NoninvertibleTransformException;
71 import java.awt.geom.Path2D;
72 import java.awt.geom.Point2D;
73 import java.awt.image.BufferedImage;
74 import java.awt.image.DirectColorModel;
75 import java.awt.image.MemoryImageSource;
76 import java.awt.image.PixelGrabber;
77 import java.io.BufferedOutputStream;
78 import java.io.BufferedReader;
79 import java.io.DataOutputStream;
80 import java.io.File;
81 import java.io.FileInputStream;
82 import java.io.FileOutputStream;
83 import java.io.FileReader;
84 import java.io.IOException;
85 import java.io.PrintWriter;
86 import java.io.Reader;
87 import java.util.ArrayList;
88 import java.util.Collection;
89 import java.util.HashMap;
90 import java.util.HashSet;
91 import java.util.Iterator;
92 import java.util.List;
93 import java.util.Map;
94 import java.util.TreeMap;
95 import java.util.concurrent.Future;
96 import java.util.zip.ZipEntry;
97 import java.util.zip.ZipInputStream;
98 import java.util.zip.ZipOutputStream;
100 import mpicbg.imglib.container.shapelist.ShapeList;
101 import mpicbg.imglib.image.display.imagej.ImageJFunctions;
102 import mpicbg.imglib.type.numeric.integer.UnsignedByteType;
103 import mpicbg.models.CoordinateTransformMesh;
104 import mpicbg.models.NoninvertibleModelException;
105 import mpicbg.trakem2.transform.AffineModel2D;
106 import mpicbg.trakem2.transform.CoordinateTransform;
107 import mpicbg.trakem2.transform.CoordinateTransformList;
108 import mpicbg.trakem2.transform.TransformMesh;
109 import mpicbg.trakem2.transform.TransformMeshMapping;
110 import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks;
112 public final class Patch extends Displayable implements ImageData {
114 final static private double SQRT2 = Math.sqrt(2.0);
115 private int type = -1; // unknown
116 private boolean false_color = false; // such as ImageProcessor.isColorLut
117 /** The channels that the currently existing awt image has ready for painting. */
118 private int channels = 0xffffffff;
120 /** To generate contrasted images non-destructively. */
121 private double min = 0;
122 private double max = 255;
124 private int o_width = 0, o_height = 0;
126 /** 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. */
127 private String current_path = null;
128 /** To be read from XML, or set when the file ImagePlus has been updated and the current_path points to something else. */
129 private String original_path = null;
131 /** A set of filters to apply to the ImageProcessor after it is loaded. */
132 private IFilter[] filters;
134 /** A unique ID for the {@link CoordinateTransform}; 0 means there isn't one. */
135 private long ct_id = 0;
137 /** A unique ID for the alpha mask; 0 means there isn't one.
138 * The alpha mask is not the outside mask as potentially generated by a {@link CoordinateTransform}.
139 * The alpha mask determines transparencies inside the width,height domain of the image. */
140 private long alpha_mask_id = 0;
142 protected int meshResolution = project.getProperty("mesh_resolution", 32);
143 public int getMeshResolution(){ return meshResolution; }
146 * Change the resolution of meshes used to render patches transformed by a
147 * {@link CoordinateTransform}. The method has to update bounding box
148 * offsets introduced by the {@link CoordinateTransform} because the
149 * bounding box has been calculated using the mesh.
151 * @param meshResolution
153 public void setMeshResolution( final int meshResolution )
155 if ( !hasCoordinateTransform() )
156 this.meshResolution = meshResolution;
157 else
159 Rectangle box = this.getCoordinateTransformBoundingBox();
160 this.at.translate( -box.x, -box.y );
161 this.meshResolution = meshResolution;
162 box = this.getCoordinateTransformBoundingBox();
163 this.at.translate( box.x, box.y );
164 width = box.width;
165 height = box.height;
166 updateInDatabase("transform+dimensions"); // the AffineTransform
167 updateBucket();
171 /** Create a new Patch and register the associated {@param filepath}
172 * with the project's loader.
174 * This method is intended for scripting, to avoid having to create a new Patch
175 * and then call {@link Loader#addedPatchFrom(String, Patch)}, which is easy to forget.
177 * @return the new Patch.
178 * @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. */
179 static public final Patch createPatch(final Project project, final String filepath) throws Exception {
180 ImagePlus imp = project.getLoader().openImagePlus(filepath);
181 if (null == imp) throw new Exception("Cannot create Patch: the image cannot be opened from filepath " + filepath);
182 if (imp.isComposite()) throw new Exception("Cannot create Patch: composite images are not supported. Convert them to RGB first.");
183 if (imp.isHyperStack()) throw new Exception("Cannot create Patch: hyperstacks are not supported.");
184 Patch p = new Patch(project, new File(filepath).getName(), 0, 0, imp);
185 project.getLoader().addedPatchFrom(filepath, p);
186 return p;
189 /** Construct a Patch from an image;
190 * most likely you will need to add the file path to the {@param imp}
191 * by calling {@link Loader#addedPatchFrom(String, Patch)}, as in this example:
193 * project.getLoader().addedPatchFrom("/path/to/file.png", thePatch); */
194 public Patch(Project project, String title, double x, double y, ImagePlus imp) {
195 super(project, title, x, y);
196 this.type = imp.getType();
197 // 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.)
198 if (ImagePlus.GRAY8 == this.type && imp.getProcessor().isColorLut()) this.type = ImagePlus.COLOR_256;
199 this.min = imp.getProcessor().getMin();
200 this.max = imp.getProcessor().getMax();
201 checkMinMax();
202 this.o_width = imp.getWidth();
203 this.o_height = imp.getHeight();
204 this.width = (int)o_width;
205 this.height = (int)o_height;
206 project.getLoader().cache(this, imp);
207 this.false_color = imp.getProcessor().isColorLut();
208 addToDatabase();
211 /** Reconstruct a Patch from the database. The ImagePlus will be loaded when necessary. */
212 public Patch(Project project, long id, String title,
213 float width, float height,
214 int o_width, int o_height,
215 int type, boolean locked, double min, double max, AffineTransform at) {
216 super(project, id, title, locked, at, width, height);
217 this.type = type;
218 this.min = min;
219 this.max = max;
220 this.width = width;
221 this.height = height;
222 this.o_width = o_width;
223 this.o_height = o_height;
224 checkMinMax();
227 /** Create a new Patch defining all necessary parameters; it is the responsibility
228 * of the caller to ensure that the parameters are in agreement with the image
229 * contained in the {@param file_path}. */
230 public Patch(Project project, String title,
231 float width, float height,
232 int o_width, int o_height,
233 int type, float alpha,
234 Color color, boolean locked,
235 double min, double max,
236 AffineTransform at,
237 String file_path) {
238 this(project, project.getLoader().getNextId(), title, width, height, o_width, o_height, type, locked, min, max, at);
239 this.alpha = Math.max(0, Math.min(alpha, 1.0f));
240 this.color = null == color ? Color.yellow : color;
241 project.getLoader().addedPatchFrom(file_path, this);
244 /** Reconstruct from an XML entry. */
245 public Patch(Project project, long id, HashMap<String,String> ht_attributes, HashMap<Displayable,String> ht_links) {
246 super(project, id, ht_attributes, ht_links);
247 // cache path:
248 project.getLoader().addedPatchFrom(ht_attributes.get("file_path"), this);
249 boolean hasmin = false;
250 boolean hasmax = false;
251 // parse specific fields
252 String data;
253 if (null != (data = ht_attributes.get("type"))) this.type = Integer.parseInt(data);
254 if (null != (data = ht_attributes.get("false_color"))) this.false_color = Boolean.parseBoolean(data);
255 if (null != (data = ht_attributes.get("min"))) {
256 this.min = Double.parseDouble(data);
257 hasmin = true;
259 if (null != (data = ht_attributes.get("max"))) {
260 this.max = Double.parseDouble(data);
261 hasmax = true;
263 if (null != (data = ht_attributes.get("o_width"))) this.o_width = Integer.parseInt(data);
264 if (null != (data = ht_attributes.get("o_height"))) this.o_height = Integer.parseInt(data);
265 if (null != (data = ht_attributes.get("pps"))) {
266 if (FSLoader.isRelativePath(data)) data = project.getLoader().getParentFolder() + data;
267 project.getLoader().setPreprocessorScriptPathSilently(this, data);
269 if (null != (data = ht_attributes.get("original_path"))) this.original_path = data;
270 if (null != (data = ht_attributes.get("mres"))) this.meshResolution = Integer.parseInt(data);
271 if (null != (data = ht_attributes.get("ct_id"))) this.ct_id = Long.parseLong(data);
272 if (null != (data = ht_attributes.get("alpha_mask_id"))) this.alpha_mask_id = Long.parseLong(data);
274 if (0 == o_width || 0 == o_height) {
275 // The original image width and height are unknown.
276 try {
277 Utils.log2("Restoring original width/height from file for id=" + id);
278 // Use BioFormats to read the dimensions out of the original file's header
279 final Dimension dim = project.getLoader().getDimensions(this);
280 o_width = dim.width;
281 o_height = dim.height;
282 } catch (Exception e) {
283 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.");
284 // So set them to whatever is somewhat survivable for the moment
285 o_width = (int)width;
286 o_height = (int)height;
287 IJError.print(e);
291 if (hasmin && hasmax) {
292 checkMinMax();
293 } else {
294 if (ImagePlus.GRAY8 == type || ImagePlus.COLOR_RGB == type || ImagePlus.COLOR_256 == type) {
295 min = 0;
296 max = 255;
297 } else {
298 // Re-read:
299 final ImageProcessor ip = getImageProcessor();
300 if (null == ip) {
301 // Some values, to survive:
302 min = 0;
303 max = Patch.getMaxMax(this.type);
304 Utils.log("WARNING could not restore min and max from image file for Patch #" + this.id + ", and they are not present in the XML file.");
305 } else {
306 ip.resetMinAndMax(); // finds automatically reasonable values
307 setMinAndMax(ip.getMin(), ip.getMax());
313 /** The original width of the pixels in the source image file. */
314 public int getOWidth() { return o_width; }
315 /** The original height of the pixels in the source image file. */
316 public int getOHeight() { return o_height; }
318 /** 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. */
319 public ImagePlus getImagePlus() {
320 return this.project.getLoader().fetchImagePlus(this);
323 /** 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. */
324 public ImageProcessor getImageProcessor() {
325 return this.project.getLoader().fetchImageProcessor(this);
328 /** Recreate mipmaps and flush away any cached ones.
329 * This method is essentially the same as patch.getProject().getLoader().update(patch);
330 * which in turn it's the same as the following two calls:
331 * patch.getProject().getLoader().generateMipMaps(patch);
332 * patch.getProject().getLoader().decacheAWT(patch.getId());
334 * If you want to update lots of Patch instances in parallel, consider also
335 * project.getLoader().generateMipMaps(ArrayList patches, boolean overwrite);
337 public Future<Boolean> updateMipMaps() {
338 return project.getLoader().regenerateMipMaps(this);
341 /** Update type, original dimensions and min,max from the ImagePlus.
342 * This is automatically done after a preprocessor script has modified the image. */
343 public void updatePixelProperties(final ImagePlus imp) {
344 readProps(imp);
347 /** Update type, original dimensions and min,max from the given ImagePlus. */
348 private void readProps(final ImagePlus imp) {
349 this.type = imp.getType();
350 this.false_color = imp.getProcessor().isColorLut();
351 if (imp.getWidth() != (int)this.o_width || imp.getHeight() != this.o_height) {
352 this.o_width = imp.getWidth();
353 this.o_height = imp.getHeight();
354 this.width = o_width;
355 this.height = o_height;
356 updateBucket();
358 ImageProcessor ip = imp.getProcessor();
359 this.min = ip.getMin();
360 this.max = ip.getMax();
361 final HashSet<String> keys = new HashSet<String>();
362 keys.add("type");
363 keys.add("dimensions");
364 keys.add("min_and_max");
365 updateInDatabase(keys);
366 //updateInDatabase(new HashSet<String>(Arrays.asList(new String[]{"type", "dimensions", "min_and_max"})));
369 /** Set a new ImagePlus for this Patch.
370 * The original path and image remain untouched. Any later image is deleted and replaced by the new one.
372 public String set(final ImagePlus new_imp) {
373 synchronized (this) {
374 if (null == new_imp) return null;
375 // 0 - set original_path to the current path if there is no original_path recorded:
376 if (isStack()) {
377 for (Patch p : getStackPatches()) {
378 if (null == p.original_path) original_path = p.project.getLoader().getAbsolutePath(p);
380 } else {
381 if (null == original_path) original_path = project.getLoader().getAbsolutePath(this);
383 // 1 - tell the loader to store the image somewhere, unless the image has a path already
384 final String path = project.getLoader().setImageFile(this, new_imp);
385 if (null == path) {
386 Utils.log2("setImageFile returned null!");
387 return null; // something went wrong
389 // 2 - update properties and mipmaps
390 if (isStack()) {
391 for (Patch p : getStackPatches()) {
392 p.readProps(new_imp);
393 project.getLoader().regenerateMipMaps(p);
395 } else {
396 readProps(new_imp);
397 project.getLoader().regenerateMipMaps(this);
400 Display.repaint(layer, this, 5);
401 return project.getLoader().getAbsolutePath(this);
404 /** Boundary checks on min and max, given the image type. */
405 private void checkMinMax() {
406 if (-1 == this.type) {
407 Utils.log("ERROR -1 == type for patch " + this);
408 return;
410 final double max_max = Patch.getMaxMax(this.type);
411 if (-1 == min && -1 == max) {
412 this.min = 0;
413 this.max = max_max;
415 switch (type) {
416 case ImagePlus.GRAY8:
417 case ImagePlus.COLOR_RGB:
418 case ImagePlus.COLOR_256:
419 if (this.min < 0) {
420 this.min = 0;
421 Utils.log("WARNING set min to 0 for patch " + this + " of type " + type);
423 break;
425 if (this.max > max_max) {
426 this.max = max_max;
427 Utils.log("WARNING fixed max larger than maximum max for type " + type);
429 if (this.min > this.max) {
430 this.min = this.max;
431 Utils.log("WARNING fixed min larger than max for patch " + this);
435 /** 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. */
436 public void setMinAndMax(double min, double max) {
437 this.min = min;
438 this.max = max;
439 checkMinMax();
440 updateInDatabase("min_and_max");
441 Utils.log2("Patch.setMinAndMax: min,max " + min + "," + max);
444 public double getMin() { return min; }
445 public double getMax() { return max; }
447 /** Returns the ImagePlus type of this Patch. */
448 public int getType() {
449 return type;
452 public Image createImage(ImagePlus imp) {
453 return adjustChannels(channels, true, imp);
456 public Image createImage() {
457 return adjustChannels(channels, true, null);
460 public int getChannelAlphas() {
461 return channels;
464 /** @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 />
465 * For non-color images, a standard image is returned regardless of the @param c
467 private Image adjustChannels(final int c, final boolean force, ImagePlus imp) {
468 if (null == imp) imp = project.getLoader().fetchImagePlus(this);
469 ImageProcessor ip = imp.getProcessor();
470 if (null == ip) return null; // fixing synch problems when deleting a Patch
471 Image awt = null;
472 if (ImagePlus.COLOR_RGB == type) {
473 if (imp.getType() != type ) {
474 ip = Utils.convertTo(ip, type, false); // all other types need not be converted, since there are no alphas anyway
476 if ((c&0x00ffffff) == 0x00ffffff && !force) {
477 // full transparency
478 awt = ip.createImage(); //imp.getImage();
479 // pixels array will be shared using ij138j and above
480 } else {
481 // modified from ij.process.ColorProcessor.createImage() by Wayne Rasband
482 int[] pixels = (int[])ip.getPixels();
483 float cr = ((c&0xff0000)>>16) / 255.0f;
484 float cg = ((c&0xff00)>>8) / 255.0f;
485 float cb = (c&0xff) / 255.0f;
486 int[] pix = new int[pixels.length];
487 int p;
488 for (int i=pixels.length -1; i>-1; i--) {
489 p = pixels[i];
490 pix[i] = (((int)(((p&0xff0000)>>16) * cr))<<16)
491 + (((int)(((p&0xff00)>>8) * cg))<<8)
492 + (int) ((p&0xff) * cb);
494 int w = imp.getWidth();
495 MemoryImageSource source = new MemoryImageSource(w, imp.getHeight(), DCM, pix, 0, w);
496 source.setAnimated(true);
497 source.setFullBufferUpdates(true);
498 awt = Toolkit.getDefaultToolkit().createImage(source);
500 } else {
501 awt = ip.createImage();
504 //Utils.log2("ip's min, max: " + ip.getMin() + ", " + ip.getMax());
506 this.channels = c;
508 return awt;
511 static final public DirectColorModel DCM = new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
513 /** Just throws the cached image away if the alpha of the channels has changed. */
514 private final void checkChannels(int channels, double magnification) {
515 if (this.channels != channels && (ImagePlus.COLOR_RGB == this.type || ImagePlus.COLOR_256 == this.type)) {
516 final int old_channels = this.channels;
517 this.channels = channels; // before, so if any gets recreated it's done right
518 project.getLoader().adjustChannels(this, old_channels);
522 /** Takes an image and scales its channels according to the values packed in this.channels.
523 * This method is intended for fixing RGB images which are loaded from jpegs (the mipmaps), and which
524 * have then the full colorization of the original image present in their pixels array.
525 * Otherwise the channel opacity scaling makes no sense.
526 * If 0xffffffff == this.channels the awt is returned as is.
527 * If the awt is null returns null.
529 public final Image adjustChannels(final Image awt) {
530 if (0xffffffff == this.channels || null == awt) return awt;
531 BufferedImage bi = null;
532 // reuse if possible
533 if (awt instanceof BufferedImage) bi = (BufferedImage)awt;
534 else {
535 bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), BufferedImage.TYPE_INT_ARGB);
536 bi.getGraphics().drawImage(awt, 0, 0, null);
538 // extract channel values
539 final float cr = ((channels&0xff0000)>>16) / 255.0f;
540 final float cg = ((channels&0xff00)>>8 ) / 255.0f;
541 final float cb = ( channels&0xff ) / 255.0f;
542 // extract pixels
543 Utils.log2("w, h: " + bi.getWidth() + ", " + bi.getHeight());
544 final int[] pixels = bi.getRGB(0, 0, bi.getWidth(), bi.getHeight(), null, 0, 1);
545 // scale them according to channel opacities
546 int p;
547 for (int i=0; i<pixels.length; i++) {
548 p = pixels[i];
549 pixels[i] = (((int)(((p&0xff0000)>>16) * cr))<<16)
550 + (((int)(((p&0xff00)>>8) * cg))<<8)
551 + (int) ((p&0xff) * cb);
553 // replace pixels
554 bi.setRGB(0, 0, bi.getWidth(), bi.getHeight(), pixels, 0, 1);
555 return bi;
558 public void paintOffscreen(Graphics2D g, Rectangle srcRect, double magnification, boolean active, int channels, Layer active_layer) {
559 paint(g, fetchImage(magnification, channels, true), srcRect);
562 @Override
563 public void paint(Graphics2D g, Rectangle srcRect, double magnification, boolean active, int channels, Layer active_layer, 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 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 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 (Throwable t) {
688 Utils.log(new StringBuilder("Cannot paint Patch with composite type ").append(compositeModes[getCompositeMode()]).append("\nReason:\n").append(t.toString()).toString());
689 g.drawImage( mipMap.image, atp, null );
691 g.setComposite( original_composite );
694 public boolean isDeletable() {
695 return 0 == width && 0 == height;
698 /** Remove only if linked to other Patches or to noone. */
699 public boolean remove(boolean check) {
700 if (check && !Utils.check("Really remove " + this.toString() + " ?")) return false;
701 if (isStack()) { // this Patch is part of a stack
702 GenericDialog gd = new GenericDialog("Stack!");
703 gd.addMessage("Really delete the entire stack?");
704 gd.addCheckbox("Delete layers if empty", true);
705 gd.showDialog();
706 if (gd.wasCanceled()) return false;
707 boolean delete_empty_layers = gd.getNextBoolean();
708 // gather all
709 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
710 getStackPatchesNR(ht);
711 Utils.log2("Removing stack patches: " + ht.size());
712 for (final Patch p : ht.values()) {
713 if (!p.isOnlyLinkedTo(this.getClass())) {
714 Utils.showMessage("At least one slice of the stack (z=" + p.getLayer().getZ() + ") is supporting other data.\nCan't delete.");
715 return false;
718 ArrayList<Layer> layers_to_remove = new ArrayList<Layer>();
719 for (final Patch p : ht.values()) {
720 if (!p.layer.remove(p) || !p.removeFromDatabase()) {
721 Utils.showMessage("Can't delete Patch " + p);
722 return false;
724 p.unlink();
725 p.removeLinkedPropertiesFromOrigins();
726 //no need//it.remove();
727 layers_to_remove.add(p.layer);
728 if (p.layer.isEmpty()) Display.close(p.layer);
729 else Display.repaint(p.layer);
731 if (delete_empty_layers) {
732 for (final Layer la : layers_to_remove) {
733 if (la.isEmpty()) {
734 project.getLayerTree().remove(la, false);
735 Display.close(la);
739 Search.remove(this);
740 return true;
741 } else {
742 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)
743 unlink();
744 removeLinkedPropertiesFromOrigins();
745 Search.remove(this);
746 return true;
747 } else {
748 Utils.showMessage("Patch: can't remove! The image is linked and thus supports other data).");
749 return false;
754 /** Returns true if this Patch holds direct links to at least one other image in a different layer. Doesn't check for total overlap. */
755 public final boolean isStack() {
756 if (null == hs_linked || hs_linked.isEmpty()) return false;
757 for (final Displayable d : hs_linked) {
758 if (d.getClass() == Patch.class && d.layer.getId() != this.layer.getId()) return true;
760 return false;
763 /** Retuns a virtual ImagePlus with a virtual stack if necessary. */
764 public PatchStack makePatchStack() {
765 // are we a stack?
766 final TreeMap<Double,Patch> ht = new TreeMap<Double,Patch>();
767 getStackPatchesNR(ht);
768 final Patch[] patch;
769 int currentSlice = 1; // from 1 to n, as in ImageStack
770 if (ht.size() > 1) {
771 patch = new Patch[ht.size()];
772 int i = 0;
773 for (final Patch p : ht.values()) { // sorted by z
774 patch[i] = p;
775 if (p.id == this.id) currentSlice = i+1;
776 i++;
778 } else {
779 patch = new Patch[]{ this };
781 return new PatchStack(patch, currentSlice);
784 public ArrayList<Patch> getStackPatches() {
785 final TreeMap<Double,Patch> ht = new TreeMap<Double,Patch>();
786 getStackPatchesNR(ht);
787 return new ArrayList<Patch>(ht.values()); // sorted by z
790 /** Non-recursive version to avoid stack overflows with "excessive" recursion (I hate java). */
791 private void getStackPatchesNR(final Map<Double,Patch> ht) {
792 final ArrayList<Patch> list1 = new ArrayList<Patch>();
793 list1.add(this);
794 final ArrayList<Patch> list2 = new ArrayList<Patch>();
795 while (list1.size() > 0) {
796 list2.clear();
797 for (Patch p : list1) {
798 if (null != p.hs_linked) {
799 for (Iterator<?> it = p.hs_linked.iterator(); it.hasNext(); ) {
800 Object ln = it.next();
801 if (ln.getClass() == Patch.class) {
802 Patch pa = (Patch)ln;
803 if (!ht.containsValue(pa)) {
804 ht.put(pa.layer.getZ(), pa);
805 list2.add(pa);
811 list1.clear();
812 list1.addAll(list2);
816 /** Opens and closes the tag and exports data. The image is saved in the directory provided in @param any as a String. */
817 @Override
818 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.
819 String in = indent + "\t";
820 String path = null;
821 String path2 = null;
822 if (options.export_images) {
823 path = options.patches_dir + title;
824 // save image without overwriting, and add proper extension (.zip)
825 path2 = project.getLoader().exportImage(this, path, false);
826 // path2 will be null if the file exists already
828 sb_body.append(indent).append("<t2_patch\n");
829 String rel_path = null;
830 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
831 //Utils.log2("p id=" + id + " path==path2");
832 rel_path = path2;
833 int i_slash = rel_path.lastIndexOf('/'); // TrakEM2 uses paths that always have '/' and never '\', so using java.io.File.separatorChar would be an error.
834 if (i_slash > 0) {
835 i_slash = rel_path.lastIndexOf('/', i_slash -1);
836 if (-1 != i_slash) {
837 rel_path = rel_path.substring(i_slash+1);
840 } else {
841 //Utils.log2("Setting rel_path to " + path2);
842 rel_path = path2;
844 // For FSLoader projects, saving a second time will save images as null unless calling it
845 if (null == rel_path) {
846 //Utils.log2("path2 was null");
847 Object ob = project.getLoader().getPath(this);
848 path2 = null == ob ? null : (String)ob;
849 if (null == path2) {
850 //Utils.log2("ERROR: No path for Patch id=" + id + " and title: " + title);
851 rel_path = title; // at least some clue for recovery
852 } else {
853 rel_path = path2;
857 //Utils.log("Patch path is: " + rel_path);
859 super.exportXML(sb_body, in, options);
860 String[] RGB = Utils.getHexRGBColor(color);
861 int type = this.type;
862 if (-1 == this.type) {
863 Utils.log2("Retrieving type for p = " + this);
864 ImagePlus imp = project.getLoader().fetchImagePlus(this);
865 if (null != imp) type = imp.getType();
867 sb_body.append(in).append("type=\"").append(type /*null == any ? ImagePlus.GRAY8 : type*/).append("\"\n")
868 .append(in).append("file_path=\"").append(rel_path).append("\"\n")
869 .append(in).append("style=\"fill-opacity:").append(alpha).append(";stroke:#").append(RGB[0]).append(RGB[1]).append(RGB[2]).append(";\"\n")
870 .append(in).append("o_width=\"").append(o_width).append("\"\n")
871 .append(in).append("o_height=\"").append(o_height).append("\"\n")
873 if (null != original_path) {
874 sb_body.append(in).append("original_path=\"").append(original_path).append("\"\n");
876 sb_body.append(in).append("min=\"").append(min).append("\"\n");
877 sb_body.append(in).append("max=\"").append(max).append("\"\n");
879 String pps = getPreprocessorScriptPath();
880 if (null != pps) sb_body.append(in).append("pps=\"").append(project.getLoader().makeRelativePath(pps)).append("\"\n");
882 sb_body.append(in).append("mres=\"").append(meshResolution).append("\"\n");
884 if (hasCoordinateTransform()) {
885 sb_body.append(in).append("ct_id=\"").append(ct_id).append("\"\n");
888 if (hasAlphaMask()) {
889 sb_body.append(in).append("alpha_mask_id=\"").append(alpha_mask_id).append("\"\n");
892 sb_body.append(indent).append(">\n");
894 if (hasCoordinateTransform()) {
895 if (options.include_coordinate_transform) {
896 // Write an XML entry for the CoordinateTransform
897 char[] ct_chars = null;
898 try {
899 ct_chars = readCoordinateTransformFile();
900 } catch (Exception e) {
901 IJError.print(e);
903 if (null != ct_chars) {
904 sb_body.append(ct_chars).append('\n');
905 } else {
906 Utils.log("ERROR: could not write the CoordinateTransform to the XML file!");
911 if (null != filters && filters.length > 0) {
912 for (IFilter f : filters) sb_body.append(f.toXML(in)); // specify their own line termination
915 super.restXML(sb_body, in, options);
917 sb_body.append(indent).append("</t2_patch>\n");
920 static private final double getMaxMax(final int type) {
921 int pow = 1;
922 switch (type) {
923 case ImagePlus.GRAY16: pow = 2; break; // TODO problems with unsigned short most likely
924 case ImagePlus.GRAY32: pow = 4; break;
925 default: return 255;
927 return Math.pow(256, pow) - 1;
930 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
931 final String type = "t2_patch";
932 if (hs.contains(type)) return;
933 // TrakEM2's XML is validated in a non-conventional way, so no need to specify the arguments for each filter
934 sb_header.append(indent).append("<!ELEMENT t2_filter EMPTY>\n");
935 // The Patch itself:
936 sb_header.append(indent).append("<!ELEMENT t2_patch (").append(Displayable.commonDTDChildren()).append(",ict_transform,ict_transform_list,t2_filter)>\n");
937 Displayable.exportDTD(type, sb_header, hs, indent);
938 sb_header.append(indent).append(TAG_ATTR1).append(type).append(" file_path").append(TAG_ATTR2)
939 .append(indent).append(TAG_ATTR1).append(type).append(" original_path").append(TAG_ATTR2)
940 .append(indent).append(TAG_ATTR1).append(type).append(" type").append(TAG_ATTR2)
941 .append(indent).append(TAG_ATTR1).append(type).append(" false_color").append(TAG_ATTR2)
942 .append(indent).append(TAG_ATTR1).append(type).append(" ct").append(TAG_ATTR2)
943 .append(indent).append(TAG_ATTR1).append(type).append(" o_width").append(TAG_ATTR2)
944 .append(indent).append(TAG_ATTR1).append(type).append(" o_height").append(TAG_ATTR2)
945 .append(indent).append(TAG_ATTR1).append(type).append(" min").append(TAG_ATTR2)
946 .append(indent).append(TAG_ATTR1).append(type).append(" max").append(TAG_ATTR2)
947 .append(indent).append(TAG_ATTR1).append(type).append(" o_width").append(TAG_ATTR2)
948 .append(indent).append(TAG_ATTR1).append(type).append(" o_height").append(TAG_ATTR2)
949 .append(indent).append(TAG_ATTR1).append(type).append(" pps").append(TAG_ATTR2) // preprocessor script
950 .append(indent).append(TAG_ATTR1).append(type).append(" mres").append(TAG_ATTR2)
951 .append(indent).append(TAG_ATTR1).append(type).append(" ct_id").append(TAG_ATTR2)
952 .append(indent).append(TAG_ATTR1).append(type).append(" alpha_mask_id").append(TAG_ATTR2)
956 /** Performs a copy of this object, without the links, unlocked and visible, except for the image which is NOT duplicated. */
957 public Displayable clone(final Project pr, final boolean copy_id) {
958 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
959 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());
960 copy.false_color = this.false_color;
961 copy.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
962 copy.alpha = this.alpha;
963 copy.visible = true;
964 copy.channels = this.channels;
965 copy.min = this.min;
966 copy.max = this.max;
967 copy.ct_id = this.ct_id;
968 copy.alpha_mask_id = this.alpha_mask_id;
969 // Copy the files
970 if (!copy_id || pr != this.project) {
971 try {
972 if (0 != copy.alpha_mask_id
973 && !Utils.safeCopy(
974 this.createAlphaMaskFilePath(this.alpha_mask_id),
975 copy.createAlphaMaskFilePath(copy.alpha_mask_id))) {
976 Utils.log("ERROR: could not copy alpha mask file for patch #" + this.id);
978 } catch (IOException ioe) {
979 IJError.print(ioe);
980 Utils.log("ERROR: could not copy alpha mask file for patch #" + this.id);
982 try {
983 if (0 != copy.ct_id
984 && !Utils.safeCopy(
985 this.createCTFilePath(this.ct_id),
986 copy.createCTFilePath(copy.ct_id))) {
987 Utils.log("ERROR: could not copy coordinate transform file for patch #" + this.id);
989 } catch (IOException ioe) {
990 IJError.print(ioe);
991 Utils.log("ERROR: could not copy coordinate transform file for patch #" + this.id);
994 copy.addToDatabase();
995 pr.getLoader().addedPatchFrom(this.project.getLoader().getAbsolutePath(this), copy);
997 // Copy preprocessor scripts
998 String pspath = this.project.getLoader().getPreprocessorScriptPath(this);
999 if (null != pspath) pr.getLoader().setPreprocessorScriptPathSilently(copy, pspath);
1001 return copy;
1004 static public final class TransformProperties {
1005 final public Rectangle bounds;
1006 final public AffineTransform at;
1007 final public CoordinateTransform ct;
1008 final public int meshResolution;
1009 final public int o_width, o_height;
1010 final public Area area;
1012 public TransformProperties(final Patch p) {
1013 this.at = new AffineTransform(p.at);
1014 this.ct = p.getCoordinateTransform();
1015 this.meshResolution = p.getMeshResolution();
1016 this.bounds = p.getBoundingBox(null);
1017 this.o_width = p.o_width;
1018 this.o_height = p.o_height;
1019 this.area = p.getArea();
1023 public Patch.TransformProperties getTransformPropertiesCopy() {
1024 return new Patch.TransformProperties(this);
1028 /** Override to cancel. */
1029 public boolean linkPatches() {
1030 Utils.log2("Patch class can't link other patches using Displayable.linkPatches()");
1031 return false;
1034 @Override
1035 public void paintSnapshot(final Graphics2D g, final Layer layer, final List<Layer> layers, final Rectangle srcRect, final double mag) {
1036 switch (layer.getParent().getSnapshotsMode()) {
1037 case 0:
1038 if (!project.getLoader().isSnapPaintable(this.id)) {
1039 paintAsBox(g);
1040 } else {
1041 paint(g, srcRect, mag, false, this.channels, layer, layers);
1043 return;
1044 case 1:
1045 paintAsBox(g);
1046 return;
1047 default: return; // case 2: // disabled, no paint
1051 static protected void crosslink(final Collection<Displayable> patches, final boolean overlapping_only) {
1052 if (null == patches) return;
1053 final ArrayList<Patch> al = new ArrayList<Patch>();
1054 for (Object ob : patches) if (ob instanceof Patch) al.add((Patch)ob); // ...
1055 final int len = al.size();
1056 if (len < 2) return;
1057 final Patch[] pa = new Patch[len];
1058 al.toArray(pa);
1059 // linking is reciprocal: need only call link() on one member of the pair
1060 for (int i=0; i<pa.length; i++) {
1061 for (int j=i+1; j<pa.length; j++) {
1062 if (overlapping_only && !pa[i].intersects(pa[j])) continue;
1063 pa[i].link(pa[j]);
1068 /** 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.*/
1069 public int getPixel(double mag, final int x, final int y) {
1070 final int[] iArray = getPixel(x, y, mag);
1071 if (ImagePlus.COLOR_RGB == this.type) {
1072 return (iArray[0]<<16) + (iArray[1]<<8) + iArray[2];
1074 return iArray[0];
1077 /** 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.*/
1078 public int[] getPixel(double mag, final int x, final int y, final int[] iArray) {
1079 final int[] ia = getPixel(x, y, mag);
1080 if(null != iArray) {
1081 iArray[0] = ia[0];
1082 iArray[1] = ia[1];
1083 iArray[2] = ia[2];
1084 return iArray;
1086 return ia;
1089 /** Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method. */
1090 public int[] getPixel(final int x, final int y, final double mag) {
1091 if (project.getLoader().isUnloadable(this)) return new int[4];
1092 final MipMapImage mipMap = project.getLoader().fetchImage(this, mag);
1093 if (Loader.isSignalImage(mipMap.image)) return new int[4];
1094 final int w = mipMap.image.getWidth(null);
1095 final Point2D.Double pd = inverseTransformPoint(x, y);
1096 final int x2 = (int)(pd.x / mipMap.scaleX);
1097 final int y2 = (int)(pd.y / mipMap.scaleY);
1098 final int[] pvalue = new int[4];
1099 final PixelGrabber pg = new PixelGrabber( mipMap.image, x2, y2, 1, 1, pvalue, 0, w);
1100 try {
1101 pg.grabPixels();
1102 } catch (InterruptedException ie) {
1103 return pvalue;
1106 approximateTransferPixel(pvalue);
1108 return pvalue;
1111 /** Transfer an 8-bit or RGB pixel to this image color space, interpolating;
1112 * the pvalue is modified in place.
1113 * For float images (GRAY32), the float value is packed into bits in pvalue[0],
1114 * and can be recovered with Float.intBitsToFloat(pvalue[0]). */
1115 protected void approximateTransferPixel(final int[] pvalue) {
1116 switch (type) {
1117 case ImagePlus.COLOR_256: // mipmaps use RGB images internally, so I can't compute the index in the LUT
1118 case ImagePlus.COLOR_RGB:
1119 final int c = pvalue[0];
1120 pvalue[0] = (c&0xff0000)>>16; // R
1121 pvalue[1] = (c&0xff00)>>8; // G
1122 pvalue[2] = c&0xff; // B
1123 break;
1124 case ImagePlus.GRAY8:
1125 pvalue[0] = pvalue[0]&0xff;
1126 break;
1127 case ImagePlus.GRAY16:
1128 pvalue[0] = pvalue[0]&0xff;
1129 // correct range: from 8-bit of the mipmap to 16 bit
1130 pvalue[0] = (int)(min + pvalue[0] * ( (max - min) / 256 ));
1131 break;
1132 case ImagePlus.GRAY32:
1133 pvalue[0] = pvalue[0]&0xff;
1134 // correct range: from 8-bit of the mipmap to 32 bit
1135 // ... and encode, so that it will be decoded with Float.intBitsToFloat
1136 pvalue[0] = Float.floatToIntBits((float)(min + pvalue[0] * ( (max - min) / 256 )));
1137 break;
1141 /** 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. */
1142 public final String getFilePath() {
1143 if (null != current_path) return current_path;
1144 return project.getLoader().getAbsolutePath(this);
1147 /** Returns the absolute path to the image file, as read by the OS. */
1148 public final String getImageFilePath() {
1149 return project.getLoader().getImageFilePath(this);
1152 /** 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. */
1153 public final String getCurrentPath() { return current_path; }
1155 /** Cache a proper, good, known path to the image wrapped by this Patch. */
1156 public final void cacheCurrentPath(final String path) {
1157 this.current_path = path;
1160 /** 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. */
1161 synchronized public String getOriginalPath() { return original_path; }
1163 protected void setAlpha(float alpha, boolean update) {
1164 if (isStack()) {
1165 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
1166 getStackPatchesNR(ht);
1167 for (Patch pa : ht.values()) {
1168 pa.alpha = alpha;
1169 pa.updateInDatabase("alpha");
1170 Display.repaint(pa.layer, pa, 5);
1172 Display3D.setTransparency(this, alpha);
1173 } else super.setAlpha(alpha, update);
1176 public void debug() {
1177 Utils.log2("Patch id=" + id + "\n\toriginal_path=" + original_path + "\n\tcurrent_path=" + current_path);
1180 /** Revert the ImagePlus to the one stored in original_path, if any; will revert all linked patches if this is part of a stack. */
1181 public boolean revert() {
1182 synchronized (this) {
1183 if (null == original_path) return false; // nothing to revert to
1184 // 1 - check that original_path exists
1185 if (!new File(original_path).exists()) {
1186 Utils.log("CANNOT revert: Original file path does not exist: " + original_path + " for patch " + getTitle() + " #" + id);
1187 return false;
1189 // 2 - check that the original can be loaded
1190 final ImagePlus imp = project.getLoader().fetchOriginal(this);
1191 if (null == imp || null == set(imp)) {
1192 Utils.log("CANNOT REVERT: original image at path " + original_path + " fails to load, for patch " + getType() + " #" + id);
1193 return false;
1195 // 3 - update path in loader, and cache imp for each stack slice id
1196 if (isStack()) {
1197 for (Patch p : getStackPatches()) {
1198 p.project.getLoader().addedPatchFrom(p.original_path, p);
1199 p.project.getLoader().cacheImagePlus(p.id, imp);
1200 p.project.getLoader().regenerateMipMaps(p);
1202 } else {
1203 project.getLoader().addedPatchFrom(original_path, this);
1204 project.getLoader().cacheImagePlus(id, imp);
1205 project.getLoader().regenerateMipMaps(this);
1207 // 4 - update screens
1209 Display.repaint(layer, this, 0);
1210 Utils.showStatus("Reverted patch " + getTitle(), false);
1211 return true;
1214 /** For reconstruction purposes, overwrites the present {@link CoordinateTransform}, if any, with the given one.
1215 * This method has been repurposed to write the {@link CoordinateTransform} to disk and set a new {@link #ct_id}
1216 * that points to it. */
1217 public void setCoordinateTransformSilently(final CoordinateTransform ct) {
1218 try {
1219 if (0 == this.ct_id) {
1220 // Old XML, lacks a ct_id attribute; will get a new ct_id
1221 setNewCoordinateTransform(ct);
1222 } else {
1223 // New XML with ct_id attribute
1224 writeNewCoordinateTransform(ct, this.ct_id);
1226 } catch (Exception e) {
1227 IJError.print(e);
1231 /** Set a CoordinateTransform to this Patch.
1232 * 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. */
1233 public final void setCoordinateTransform(final CoordinateTransform ct) {
1234 if (isLinked()) {
1235 Utils.log("Cannot set coordinate transform: patch is linked!");
1236 return;
1239 CoordinateTransform this_ct = hasCoordinateTransform() ? getCoordinateTransform() : null;
1241 if (null != this_ct) {
1242 // restore image without the transform
1243 final TransformMesh mesh = new TransformMesh(this_ct, meshResolution, o_width, o_height);
1244 final Rectangle box = mesh.getBoundingBox();
1245 this.at.translate(-box.x, -box.y);
1246 updateInDatabase("transform+dimensions");
1249 try {
1250 setNewCoordinateTransform(ct);
1251 } catch (Exception e) {
1252 throw new RuntimeException(e);
1254 this_ct = ct;
1256 updateInDatabase("ict_transform");
1258 if (null == this_ct) {
1259 width = o_width;
1260 height = o_height;
1261 updateBucket();
1262 return;
1265 // Adjust the AffineTransform to correct for bounding box displacement
1267 final TransformMesh mesh = new TransformMesh(this_ct, meshResolution, o_width, o_height);
1268 final Rectangle box = mesh.getBoundingBox();
1269 this.at.translate(box.x, box.y);
1270 width = box.width;
1271 height = box.height;
1272 updateInDatabase("transform+dimensions"); // the AffineTransform
1273 updateBucket();
1275 // Updating the mipmaps will call createTransformedImage below if ct is not null
1276 /* DISABLED */ //updateMipMaps();
1280 * Append a {@link CoordinateTransform} to the current
1281 * {@link CoordinateTransformList}. If there is no transform yet, it just
1282 * sets it. If there is only one transform, it replaces it by a list
1283 * containing both, the existing first.
1285 @SuppressWarnings("unchecked")
1286 public final void appendCoordinateTransform(final CoordinateTransform ct) {
1287 if (!hasCoordinateTransform())
1288 setCoordinateTransform(ct);
1289 else {
1290 final CoordinateTransformList< CoordinateTransform > ctl;
1291 final CoordinateTransform this_ct = getCoordinateTransform();
1292 if (this_ct instanceof CoordinateTransformList<?>)
1293 ctl = (CoordinateTransformList< CoordinateTransform >)this_ct.copy();
1294 else {
1295 ctl = new CoordinateTransformList< CoordinateTransform >();
1296 ctl.add(this_ct);
1298 ctl.add(ct);
1299 setCoordinateTransform(ctl);
1305 * Pre-append a {@link CoordinateTransform} to the current
1306 * {@link CoordinateTransformList}. If there is no transform yet, it just
1307 * sets it. If there is only one transform, it replaces it by a list
1308 * containing both, the new one first.
1310 @SuppressWarnings("unchecked")
1311 public final void preAppendCoordinateTransform(final CoordinateTransform ct) {
1312 if (!hasCoordinateTransform())
1313 setCoordinateTransform(ct);
1314 else {
1315 final CoordinateTransformList< CoordinateTransform > ctl;
1316 if (ct instanceof CoordinateTransformList<?>)
1317 ctl = (CoordinateTransformList< CoordinateTransform >)ct.copy();
1318 else {
1319 ctl = new CoordinateTransformList< CoordinateTransform >();
1320 ctl.add(ct);
1322 ctl.add(getCoordinateTransform());
1323 setCoordinateTransform(ctl);
1328 * Get the bounding rectangle of the transformed image relative to the
1329 * original image.
1331 * TODO
1332 * Currently, this is done in a very expensive way. The
1333 * {@linkplain TransformMesh} is built and its bounding rectangle is
1334 * returned. Think about just storing this rectangle in the
1335 * {@linkplain Patch} instance.
1337 * @return
1339 public final Rectangle getCoordinateTransformBoundingBox() {
1340 if (!hasCoordinateTransform())
1341 return new Rectangle(0,0,o_width,o_height);
1342 return Patch.getCoordinateTransformBoundingBox(this, getCoordinateTransform());
1346 * Allow reusing a {@link CoordinateTransform} that was already loaded from a file.
1348 * @param p
1349 * @param ct
1350 * @return
1352 protected static final Rectangle getCoordinateTransformBoundingBox(final Patch p, final CoordinateTransform ct) {
1353 if (!p.hasCoordinateTransform())
1354 return new Rectangle(0,0,p.o_width,p.o_height);
1355 final TransformMesh mesh = new TransformMesh(ct, p.meshResolution, p.o_width, p.o_height);
1356 return mesh.getBoundingBox();
1359 /** Obtain a copy of the {@link CoordinateTransform} that transfers image data to mipmap image data.
1360 * @return A copy of the {@link CoordinateTransform}, or null if none.
1361 * @see #setCoordinateTransform(CoordinateTransform) */
1362 public final CoordinateTransform getCoordinateTransform() { return getCT(); }
1364 public final Patch.PatchImage createCoordinateTransformedImage() {
1365 if (!hasCoordinateTransform()) return null;
1367 final CoordinateTransform ct = getCoordinateTransform();
1369 final ImageProcessor source = getImageProcessor();
1371 if (null == source) return null; // some error occurred
1373 //Utils.log2("source image dimensions: " + source.getWidth() + ", " + source.getHeight());
1375 final TransformMesh mesh = new TransformMesh(ct, meshResolution, o_width, o_height);
1376 final Rectangle box = mesh.getBoundingBox();
1378 /* We can calculate the exact size of the image to be rendered, so let's do it */
1379 // project.getLoader().releaseToFit(o_width, o_height, type, 5);
1380 final long b =
1381 2 * o_width * o_height // outside and mask source
1382 + 2 * box.width * box.height // outside and mask target
1383 + 5 * o_width * o_height // image source
1384 + 5 * box.width * box.height; // image target
1385 project.getLoader().releaseToFit( b );
1387 final TransformMeshMapping mapping = new TransformMeshMapping( mesh );
1389 final ImageProcessorWithMasks target = mapping.createMappedMaskedImageInterpolated( source, getAlphaMask() );
1391 // Set the LUT
1392 target.ip.setColorModel(source.getColorModel());
1394 // // Set all non-white pixels to zero
1395 // final byte[] pix = (byte[])target.outside.getPixels();
1396 // for (int i=0; i<pix.length; i++)
1397 // if ((pix[i]&0xff) != 255) pix[i] = 0;
1399 //Utils.log2("New image dimensions: " + target.getWidth() + ", " + target.getHeight());
1400 //Utils.log2("box: " + box);
1402 return new PatchImage( target.ip, ( ByteProcessor )target.mask, target.outside, box, true );
1405 static final public class PatchImage {
1406 /** The image, coordinate-transformed if null != ct. */
1407 final public ImageProcessor target;
1408 /** The alpha mask, coordinate-transformed if null != ct. */
1409 final public ByteProcessor mask;
1410 /** The outside mask, coordinate-transformed if null != ct. */
1411 final public ByteProcessor outside;
1412 /** The bounding box of the image relative to the original, with x,y as the displacement relative to the pixels of the original image. */
1413 final public Rectangle box;
1414 /** Whether the image was generated with a CoordinateTransform or not. */
1415 final public boolean coordinate_transformed;
1417 private PatchImage( ImageProcessor target, ByteProcessor mask, ByteProcessor outside, Rectangle box, boolean coordinate_transformed ) {
1418 this.target = target;
1419 this.mask = mask;
1420 this.outside = outside;
1421 this.box = box;
1422 this.coordinate_transformed = coordinate_transformed;
1426 * <p>Get the mask. This is either:</p>
1427 * <ul>
1428 * <li>null for a non-transformed patch without a mask,</li>
1429 * <li>the mask of a non-transformed patch,</li>
1430 * <li>the transformed mask of a transformed patch (including outside
1431 * mask),</li>
1432 * <li>or the outside mask of a transformed patch without a mask,</li>
1433 * </ul>
1435 * @return
1437 final public ByteProcessor getMask()
1439 return mask == null ? outside == null ? null : outside : mask;
1443 /** 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). */
1444 public Patch.PatchImage createTransformedImage() {
1445 final Patch.PatchImage pi = createCoordinateTransformedImage();
1446 if (null != pi) return pi;
1447 // else, a new one with the untransformed, original image (a duplicate):
1448 final ImageProcessor ip = getImageProcessor();
1449 if (null == ip) return null;
1450 project.getLoader().releaseToFit(o_width, o_height, type, 3);
1451 final ImageProcessor copy = ip.duplicate();
1452 copy.setColorModel(ip.getColorModel()); // one would expect "duplicate" to do this but it doesn't!
1453 return new PatchImage(copy, getAlphaMask(), null, new Rectangle(0, 0, o_width, o_height), false);
1458 * Whether there is an alpha mask for the pixel data.
1460 public final boolean hasAlphaMask() {
1461 return 0 != alpha_mask_id;
1464 public long getAlphaMaskId() {
1465 return alpha_mask_id;
1469 * @return The absolute file path to the file specifying the image that
1470 * represents the alpha mask, or null if none.
1472 public String getAlphaMaskFilePath() {
1473 return hasAlphaMask() ? createAlphaMaskFilePath(this.alpha_mask_id) : null;
1477 * Whether there is an alpha mask or there is an outside mask caused by a {@link CoordinateTransform}.
1479 public boolean hasAlphaChannel() {
1480 return hasCoordinateTransform() || hasAlphaMask();
1483 /** Must call updateMipMaps() afterwards. Set it to null to remove it.
1484 * @return true if the alpha mask file was written successfully. */
1485 public synchronized boolean setAlphaMask(final ByteProcessor bp) throws IllegalArgumentException {
1486 if (null == bp) {
1487 alpha_mask_id = 0;
1488 return true;
1491 // Check that the alpha mask represented by argument bp
1492 // has the appropriate dimensions:
1493 if (o_width != bp.getWidth() || o_height != bp.getHeight()) {
1494 throw new IllegalArgumentException("Need a mask of identical dimensions as the original image.");
1497 final long amID = project.getLoader().getNextBlobId();
1498 if (writeAlphaMask(bp, amID)) {
1499 this.alpha_mask_id = amID;
1500 return true;
1501 } else {
1502 Utils.log("Could NOT write the alpha mask file for patch #" + id);
1505 return false;
1509 * Return a new {@link ByteProcessor} representing the alpha mask, if any, over the pixel data.
1510 * @return null if there isn't one, or if the mask image could not be loaded.*/
1511 public synchronized ByteProcessor getAlphaMask() {
1512 if (0 == alpha_mask_id) return null;
1514 final String path = createAlphaMaskFilePath(alpha_mask_id);
1516 // Expects a zip file containing one single TIFF file entry
1517 ZipInputStream zis = null;
1518 try {
1519 zis = new ZipInputStream(new FileInputStream(path));
1520 final ZipEntry ze = zis.getNextEntry(); // prepares the entry for reading
1521 // Assume the first entry is the mask
1522 final ImageProcessor mask = new FileOpener(new TiffDecoder(zis, ze.getName()).getTiffInfo()[0]).open(false).getProcessor();
1523 if (mask.getWidth() != o_width || mask.getHeight() != o_height) {
1524 Utils.log2("Mask has improper dimensions: " + mask.getWidth() + " x " + mask.getHeight() + " for patch #" + this.id + " which is of " + o_width + " x " + o_height);
1525 return null;
1527 return (ByteProcessor) (mask.getClass() == ByteProcessor.class ? mask : mask.convertToByte(false));
1528 } catch (Throwable t) {
1529 Utils.log2("Could not load alpha mask for patch #" + this.id + " from file " + path);
1530 IJError.print(t);
1531 return null;
1532 } finally {
1533 try { if (null != zis) zis.close(); } catch (Exception e) { IJError.print(e); }
1537 private final String createAlphaMaskFilePath(final long amID) {
1538 final FSLoader l = (FSLoader)project.getLoader();
1539 return l.getMasksFolder() + FSLoader.createIdPath(Long.toString(amID), Long.toString(this.id), ".zip");
1542 private synchronized final boolean writeAlphaMask(final ByteProcessor bp, final long amID) {
1543 DataOutputStream out = null;
1544 try {
1545 final File f = new File(createAlphaMaskFilePath(amID));
1546 Utils.ensure(f);
1547 //new FileSaver(new ImagePlus("mask", fp)).saveAsZip(path); -- doesn't sync!
1548 FileOutputStream fos = new FileOutputStream(f);
1549 ZipOutputStream zos = new ZipOutputStream(fos);
1550 out = new DataOutputStream(new BufferedOutputStream(zos, 32768));
1551 ImagePlus imp = new ImagePlus("mask.tif", bp); // ImageJ looks for ".tif" extension in the ZipEntry
1552 zos.putNextEntry(new ZipEntry(imp.getTitle()));
1553 TiffEncoder te = new TiffEncoder(imp.getFileInfo());
1554 te.write(out);
1555 out.flush();
1556 fos.getFD().sync();
1557 return true;
1558 } catch (Throwable e) {
1559 IJError.print(e);
1560 } finally {
1561 try { if (null != out) out.close(); } catch (Throwable t) { IJError.print(t); }
1563 return false;
1568 * @return True if {@link #alpha_mask_id} {@code == 0} or if the file is found, or false if not found.
1570 public boolean checkAlphaMaskFile() {
1571 if (0 == this.alpha_mask_id) return true; // means there isn't an alpha mask
1572 return new File(createAlphaMaskFilePath(this.alpha_mask_id)).exists();
1577 public boolean paintsWithFalseColor() {
1578 return false_color;
1581 public void keyPressed(final KeyEvent ke) {
1582 Object source = ke.getSource();
1583 if (! (source instanceof DisplayCanvas)) return;
1584 DisplayCanvas dc = (DisplayCanvas)source;
1585 final Roi roi = dc.getFakeImagePlus().getRoi();
1587 final int mod = ke.getModifiers();
1589 switch (ke.getKeyCode()) {
1590 case KeyEvent.VK_C:
1591 // copy into ImageJ clipboard
1592 // Ignoring masks: outside is already black, and ImageJ cannot handle alpha masks.
1593 if (0 == (mod ^ (Event.SHIFT_MASK | Event.ALT_MASK))) {
1594 // Place the source image, untransformed, into clipboard:
1595 ImagePlus imp = getImagePlus();
1596 if (null != imp) imp.copy(false);
1597 } else if (0 == mod || (0 == (mod ^ Event.SHIFT_MASK))) {
1598 CoordinateTransformList<CoordinateTransform> list = null;
1599 if (hasCoordinateTransform()) {
1600 list = new CoordinateTransformList<CoordinateTransform>();
1601 list.add(getCoordinateTransform());
1603 if (0 == mod) { //SHIFT is not down
1604 AffineModel2D am = new AffineModel2D();
1605 am.set(this.at);
1606 if (null == list) list = new CoordinateTransformList<CoordinateTransform>();
1607 list.add(am);
1609 ImageProcessor ip;
1610 if (null != list) {
1611 TransformMesh mesh = new TransformMesh(list, meshResolution, o_width, o_height);
1612 TransformMeshMapping mapping = new TransformMeshMapping(mesh);
1613 ip = mapping.createMappedImageInterpolated(getImageProcessor());
1614 } else {
1615 ip = getImageProcessor();
1617 new ImagePlus(this.title, ip).copy(false);
1619 ke.consume();
1620 break;
1621 case KeyEvent.VK_F:
1622 // fill mask with current ROI using
1623 if (null != roi && M.isAreaROI(roi)) {
1624 Bureaucrat.createAndStart(new Worker.Task("Filling image mask") {
1625 public void exec() {
1626 getLayerSet().addDataEditStep(Patch.this);
1627 if (0 == mod) {
1628 addAlphaMask(roi, ProjectToolbar.getForegroundColorValue());
1629 } else if (0 == (mod ^ Event.SHIFT_MASK)) {
1630 // shift is down: fill outside
1631 try {
1632 Area localRoi = M.areaInInts(M.getArea(roi)).createTransformedArea(at.createInverse());
1633 Area invLocalRoi = new Area(new Rectangle(0, 0, getOWidth() , getOHeight()));
1634 invLocalRoi.subtract(localRoi);
1635 addAlphaMaskLocal(invLocalRoi, ProjectToolbar.getForegroundColorValue());
1636 } catch (NoninvertibleTransformException e) {
1637 IJError.print(e);
1638 return;
1641 getLayerSet().addDataEditStep(Patch.this);
1642 try { updateMipMaps().get(); } catch (Throwable t) { IJError.print(t); } // wait
1643 Display.repaint();
1645 }, project);
1647 // capturing:
1648 ke.consume();
1649 break;
1650 default:
1651 super.keyPressed(ke);
1652 break;
1656 @Override
1657 Class<?> getInternalDataPackageClass() {
1658 return DPPatch.class;
1661 @Override
1662 Object getDataPackage() {
1663 return new DPPatch(this);
1666 static private final class DPPatch extends Displayable.DataPackage {
1667 final double min, max;
1668 final long ct_id, alpha_mask_id;
1669 final IFilter[] filters;
1670 final boolean false_color;
1673 DPPatch(final Patch patch) {
1674 super(patch);
1675 this.min = patch.min;
1676 this.max = patch.max;
1677 this.ct_id = patch.ct_id;
1678 this.alpha_mask_id = patch.alpha_mask_id;
1679 this.filters = null == patch.filters ? null : FilterEditor.duplicate(patch.filters);
1680 this.false_color = patch.false_color;
1681 // channels is visualization
1682 // path is absolute
1683 // type is dependent on path, so absolute
1684 // o_width, o_height idem
1686 final boolean to2(final Displayable d) {
1687 super.to1(d);
1688 final Patch p = (Patch) d;
1689 boolean mipmaps = false;
1690 if (p.min != min || p.max != max || p.ct_id != ct_id || p.alpha_mask_id != alpha_mask_id) {
1691 mipmaps = true;
1693 if (!mipmaps) {
1694 if (null != filters && null == p.filters) mipmaps = true;
1695 else if (null == filters && null != p.filters) mipmaps = true;
1696 else if (null != filters && null != p.filters) {
1697 if (filters.length != p.filters.length) mipmaps = true;
1698 else {
1699 for (int i=0; i<filters.length; ++i) {
1700 if (filters[i].equals(p.filters[i])) continue;
1701 mipmaps = false;
1702 break;
1707 p.min = min;
1708 p.max = max;
1709 p.ct_id = ct_id;
1710 p.alpha_mask_id = alpha_mask_id;
1711 p.filters = null == filters ? null : FilterEditor.duplicate(filters);
1712 p.false_color = false_color;
1714 if (mipmaps) {
1715 p.project.getLoader().regenerateMipMaps(p);
1717 return true;
1721 /** Considers the alpha mask. */
1722 @Override
1723 public boolean contains(final double x_p, final double y_p) {
1724 if (!hasAlphaChannel()) return super.contains(x_p, y_p);
1725 // else, get pixel from image
1726 if (project.getLoader().isUnloadable(this)) return super.contains(x_p, y_p);
1727 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.
1728 if (Loader.isSignalImage(mipMap.image)) return super.contains(x_p, y_p);
1729 final int w = mipMap.image.getWidth(null);
1730 final Point2D.Double pd = inverseTransformPoint(x_p, y_p);
1731 final int x2 = (int)(pd.x / mipMap.scaleX);
1732 final int y2 = (int)(pd.y / mipMap.scaleY);
1733 final int[] pvalue = new int[1];
1734 final PixelGrabber pg = new PixelGrabber(mipMap.image, x2, y2, 1, 1, pvalue, 0, w);
1735 try {
1736 pg.grabPixels();
1737 } catch (InterruptedException ie) {
1738 return super.contains(x_p, y_p);
1740 // Not true if alpha value is zero
1741 return 0 != (pvalue[0] & 0xff000000);
1744 /** After setting a preprocessor script, it is advisable that you call updateMipMaps() immediately. */
1745 public void setPreprocessorScriptPath(final String path) {
1746 final String old_path = project.getLoader().getPreprocessorScriptPath(this);
1748 if (null == path && null == old_path) return;
1750 project.getLoader().setPreprocessorScriptPath(this, path);
1752 if (null != old_path || null != path) {
1753 // Update dimensions
1754 ImagePlus imp = getImagePlus(); // transformed by the new preprocessor script, if any
1755 final int w = imp.getWidth();
1756 final int h = imp.getHeight();
1757 imp = null;
1758 if (w != this.o_width || h != this.o_height) {
1759 // replace source ImagePlus o_width,o_height
1760 int old_o_width = this.o_width;
1761 int old_o_height = this.o_height;
1762 this.o_width = w;
1763 this.o_height = h;
1765 // scale width,height
1766 double old_width = this.width;
1767 double old_height = this.height;
1768 this.width *= ((double)this.o_width) / old_o_width;
1769 this.height *= ((double)this.o_height) / old_o_height;
1771 // translate Patch to preserve the center
1772 AffineTransform aff = new AffineTransform();
1773 aff.translate((old_width - this.width) / 2, (old_height - this.height) / 2);
1774 updateInDatabase("dimensions");
1775 preTransform(aff, false);
1780 /** Add the given roi, in world coords, to the alpha mask, using the given fill value. */
1781 public void addAlphaMask(final Roi roi, int value) {
1782 if (null == roi || !M.isAreaROI(roi)) return;
1783 addAlphaMask(M.areaInInts(M.getArea(roi)), value);
1786 /** Add the given area, in world coords, to the alpha mask, using the given fill value. */
1787 public void addAlphaMask(final Area aw, int value) {
1788 try {
1789 addAlphaMaskLocal(aw.createTransformedArea(Patch.this.at.createInverse()), value);
1790 } catch (NoninvertibleTransformException nite) { IJError.print(nite); }
1793 /** Add the given area, in local coordinates, to the alpha mask, using the given fill value. */
1794 public void addAlphaMaskLocal(final Area aLocal, int value) {
1795 if (value < 0) value = 0;
1796 if (value > 255) value = 255;
1798 CoordinateTransform ct = null;
1799 if (hasCoordinateTransform() && null == (ct = getCT())) {
1800 return;
1803 // When the area is larger than the image, sometimes the area fails to be set at all
1804 // Also, intersection accelerates calls to contains(x,y) for complex polygons
1805 final Area a = new Area(new Rectangle(0, 0, (int)(width+1), (int)(height+1)));
1806 a.intersect(aLocal);
1809 if (M.isEmpty(a)) {
1810 Utils.log("ROI does not intersect the active image!");
1811 return;
1814 ByteProcessor mask = getAlphaMask();
1816 // Use imglib to bypass all the problems with ShapeROI
1817 // Create a Shape image with background and the Area on it with 'value'
1818 final int background = (null != mask && 255 == value) ? 0 : 255;
1819 final ShapeList<UnsignedByteType> shapeList = new ShapeList<UnsignedByteType>(new int[]{(int)width, (int)height, 1}, new UnsignedByteType(background));
1820 shapeList.addShape(a, new UnsignedByteType(value), new int[]{0});
1821 final mpicbg.imglib.image.Image<UnsignedByteType> shapeListImage = new mpicbg.imglib.image.Image<UnsignedByteType>(shapeList, shapeList.getBackground(), "mask");
1823 ByteProcessor rmask = (ByteProcessor) ImageJFunctions.copyToImagePlus(shapeListImage, ImagePlus.GRAY8).getProcessor();
1825 if (hasCoordinateTransform()) {
1826 // inverse the coordinate transform
1827 final TransformMesh mesh = new TransformMesh(ct, meshResolution, o_width, o_height);
1828 final TransformMeshMapping mapping = new TransformMeshMapping( mesh );
1829 rmask = (ByteProcessor) mapping.createInverseMappedImageInterpolated(rmask);
1832 if (null == mask) {
1833 // There wasn't a mask, hence just set it
1834 mask = rmask;
1835 } else {
1836 final byte[] b1 = (byte[]) mask.getPixels();
1837 final byte[] b2 = (byte[]) rmask.getPixels();
1838 // Whatever is not background in the new mask gets set on the old mask
1839 for (int i=0; i<b1.length; i++) {
1840 if (background == (b2[i]&0xff)) continue; // background pixel in new mask
1841 b1[i] = b2[i]; // replace old pixel with new pixel
1844 setAlphaMask(mask);
1847 public String getPreprocessorScriptPath() {
1848 return project.getLoader().getPreprocessorScriptPath(this);
1851 public boolean isPreprocessed() {
1852 return null != getPreprocessorScriptPath() || null != filters;
1855 /** Returns an Area in world coords representing the inside of this Patch. The fully alpha pixels are considered outside. */
1856 @Override
1857 public Area getArea() {
1858 CoordinateTransform ct = null;
1859 if (hasAlphaMask()) {
1860 // Read the mask as a ROI for the 0 pixels only and apply the AffineTransform to it:
1861 ImageProcessor alpha_mask = getAlphaMask();
1862 if (null == alpha_mask) {
1863 Utils.log2("Could not retrieve alpha mask for " + this);
1864 } else {
1865 if (hasCoordinateTransform()) {
1866 // must transform it
1867 ct = getCoordinateTransform();
1868 final TransformMesh mesh = new TransformMesh(ct, meshResolution, o_width, o_height);
1869 final TransformMeshMapping mapping = new TransformMeshMapping( mesh );
1870 alpha_mask = mapping.createMappedImage( alpha_mask ); // Without interpolation
1871 // Keep in mind the affine of the Patch already contains the translation specified by the mesh bounds.
1873 // Threshold all non-zero areas of the mask:
1874 alpha_mask.setThreshold(1, 255, ImageProcessor.NO_LUT_UPDATE);
1875 ImagePlus imp = new ImagePlus("", alpha_mask);
1876 ThresholdToSelection tts = new ThresholdToSelection(); // TODO replace by our much faster method that scans by line, in AmiraImporter
1877 tts.setup("", imp);
1878 tts.run(alpha_mask);
1879 Roi roi = imp.getRoi();
1880 if (null == roi) {
1881 // All pixels in the alpha mask have a value of zero
1882 return new Area();
1884 return M.getArea(roi).createTransformedArea(this.at);
1887 // No alpha mask, or error in retrieving it:
1888 final int[] x = new int[o_width + o_width + o_height + o_height];
1889 final int[] y = new int[x.length];
1890 int next = 0;
1891 // Top edge:
1892 for (int i=0; i<=o_width; i++, next++) { // len: o_width + 1
1893 x[next] = i;
1894 y[next] = 0;
1896 // Right edge:
1897 for (int i=1; i<=o_height; i++, next++) { // len: o_height
1898 x[next] = o_width;
1899 y[next] = i;
1901 // bottom edge:
1902 for (int i=o_width-1; i>-1; i--, next++) { // len: o_width
1903 x[next] = i;
1904 y[next] = o_height;
1906 // left edge:
1907 for (int i=o_height-1; i>0; i--, next++) { // len: o_height -1
1908 x[next] = 0;
1909 y[next] = i;
1912 if (hasCoordinateTransform() && null == ct) ct = getCoordinateTransform();
1913 if (null != ct) {
1914 final CoordinateTransformList<CoordinateTransform> t = new CoordinateTransformList<CoordinateTransform>();
1915 t.add(ct);
1916 final TransformMesh mesh = new TransformMesh(ct, meshResolution, o_width, o_height);
1917 final Rectangle box = mesh.getBoundingBox();
1918 final AffineTransform aff = new AffineTransform(this.at);
1919 // Must correct for the inverse of the mesh translation, because the affine also includes the translation.
1920 aff.translate(-box.x, -box.y);
1921 final AffineModel2D affm = new AffineModel2D();
1922 affm.set(aff);
1923 t.add(affm);
1927 * WORKS FINE, but for points that fall outside the mesh, they don't get transformed!
1928 // Do it like Patch does it to generate the mipmap, with a mesh (and all the imprecisions of a mesh):
1929 final CoordinateTransformList t = new CoordinateTransformList();
1930 final TransformMesh mesh = new TransformMesh(this.ct, meshResolution, o_width, o_height);
1931 final AffineTransform aff = new AffineTransform(this.at);
1932 t.add(mesh);
1933 final AffineModel2D affm = new AffineModel2D();
1934 affm.set(aff);
1935 t.add(affm);
1938 final float[] f = new float[]{x[0], y[0]};
1939 t.applyInPlace(f);
1940 final Path2D.Float path = new Path2D.Float(Path2D.Float.WIND_EVEN_ODD, x.length+1);
1941 path.moveTo(f[0], f[1]);
1943 for (int i=1; i<x.length; i++) {
1944 f[0] = x[i];
1945 f[1] = y[i];
1946 t.applyInPlace(f);
1947 path.lineTo(f[0], f[1]);
1949 path.closePath(); // line to last call to moveTo
1951 return new Area(path);
1952 } else {
1953 return new Area(new Polygon(x, y, x.length)).createTransformedArea(this.at);
1957 /** Defaults to setMinAndMax = true. */
1958 static public ImageProcessor makeFlatImage(final int type, final Layer layer, final Rectangle srcRect, final double scale, final Collection<Patch> patches, final Color background) {
1959 return makeFlatImage(type, layer, srcRect, scale, patches, background, true);
1962 /** Creates an ImageProcessor of the specified type.
1963 * @param type Any of ImagePlus.GRAY_8, GRAY_16, GRAY_32 or COLOR_RGB.
1964 * @param srcRect the box in world coordinates to make an image out of.
1965 * @param scale may be up to 1.0.
1966 * @param patches The list of patches to paint. The first gets painted first (at the bottom).
1967 * @param background The color with which to paint the outsides where no image paints into.
1968 * @param setMinAndMax defines whether the min and max of each Patch is set before pasting the Patch.
1970 * For exporting while blending the display ranges (min,max) and respecting alpha masks, {@see ExportUnsignedShort}.
1972 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) {
1974 final ImageProcessor ip;
1975 final int W, H;
1976 if (scale < 1) {
1977 W = (int)(srcRect.width * scale);
1978 H = (int)(srcRect.height * scale);
1979 } else {
1980 W = srcRect.width;
1981 H = srcRect.height;
1983 switch (type) {
1984 case ImagePlus.GRAY8:
1985 ip = new ByteProcessor(W, H);
1986 break;
1987 case ImagePlus.GRAY16:
1988 ip = new ShortProcessor(W, H);
1989 break;
1990 case ImagePlus.GRAY32:
1991 ip = new FloatProcessor(W, H);
1992 break;
1993 case ImagePlus.COLOR_RGB:
1994 ip = new ColorProcessor(W, H);
1995 break;
1996 default:
1997 Utils.logAll("Cannot create an image of type " + type + ".\nSupported types: 8-bit, 16-bit, 32-bit and RGB.");
1998 return null;
2001 // Fill with background
2002 if (null != background && Color.black != background) {
2003 ip.setColor(background);
2004 ip.fill();
2007 AffineModel2D sc = null;
2008 if ( scale < 1.0 )
2010 sc = new AffineModel2D();
2011 sc.set( ( float )scale, 0, 0, ( float )scale, 0, 0 );
2013 for ( final Patch p : patches )
2015 // TODO patches seem to come in in inverse order---find out why
2017 // A list to represent all the transformations that the Patch image has to go through to reach the scaled srcRect image
2018 final CoordinateTransformList< CoordinateTransform > list = new CoordinateTransformList< CoordinateTransform >();
2020 final AffineTransform at = new AffineTransform();
2021 at.translate( -srcRect.x, -srcRect.y );
2022 at.concatenate( p.getAffineTransform() );
2024 // 1. The coordinate tranform of the Patch, if any
2025 if (p.hasCoordinateTransform()) {
2026 CoordinateTransform ct = p.getCoordinateTransform();
2027 list.add(ct);
2028 // Remove the translation in the patch_affine that the ct added to it
2029 final Rectangle box = Patch.getCoordinateTransformBoundingBox(p, ct);
2030 at.translate( -box.x, -box.y );
2033 // 2. The affine transform of the Patch
2034 final AffineModel2D patch_affine = new AffineModel2D();
2035 patch_affine.set( at );
2036 list.add( patch_affine );
2038 // 3. The desired scaling
2039 if (null != sc) patch_affine.preConcatenate( sc );
2041 final CoordinateTransformMesh mesh = new CoordinateTransformMesh( list, p.meshResolution, p.getOWidth(), p.getOHeight() );
2043 mpicbg.ij.TransformMeshMapping<CoordinateTransformMesh> mapping = new mpicbg.ij.TransformMeshMapping<CoordinateTransformMesh>( mesh );
2045 // 4. Convert the patch to the required type
2046 ImageProcessor pi = p.getImageProcessor();
2047 if (setMinAndMax) {
2048 pi = pi.duplicate();
2049 pi.setMinAndMax(p.min, p.max);
2051 switch ( type )
2053 case ImagePlus.GRAY8:
2054 pi = pi.convertToByte( true );
2055 break;
2056 case ImagePlus.GRAY16:
2057 pi = pi.convertToShort( true );
2058 break;
2059 case ImagePlus.GRAY32:
2060 pi = pi.convertToFloat();
2061 break;
2062 default: // ImagePlus.COLOR_RGB and COLOR_256
2063 pi = pi.convertToRGB();
2064 break;
2067 /* TODO for taking into account independent min/max setting for each patch,
2068 * we will need a mapping with an `intensity transfer function' to be implemented.
2069 * --> EXISTS already as mpicbg/trakem2/transform/ExportUnsignedShort.java
2071 mapping.mapInterpolated( pi, ip );
2074 return ip;
2077 /** Make the border have an alpha of zero. */
2078 public boolean maskBorder(final int size) {
2079 return maskBorder(size, size, size, size);
2081 /** Make the border have an alpha of zero. */
2082 public boolean maskBorder(final int left, final int top, final int right, final int bottom) {
2083 int w = o_width - right - left;
2084 int h = o_height - top - bottom;
2085 if (w < 0 || h < 0 || left > o_width || top > o_height) {
2086 Utils.log("Cannot cut border for patch " + this + " : border off image bounds.");
2087 return false;
2089 try {
2090 ByteProcessor bp = getAlphaMask();
2091 if (null == bp) {
2092 bp = new ByteProcessor(o_width, o_height);
2093 bp.setRoi(new Roi(left, top, w, h));
2094 bp.setValue(255);
2095 bp.fill();
2096 } else {
2097 // make borders black
2098 bp.setValue(0);
2099 for (Roi r : new Roi[]{new Roi(0, 0, o_width, top),
2100 new Roi(0, top, left, o_height - top - bottom),
2101 new Roi(0, o_height - bottom, o_width, bottom),
2102 new Roi(o_width - right, top, right, o_height - top - bottom)}) {
2103 bp.setRoi(r);
2104 bp.fill();
2107 setAlphaMask(bp);
2108 } catch (Exception e) {
2109 IJError.print(e);
2110 return false;
2112 return true;
2115 /** Use this instead of getAreaAt which calls getArea which is ... dog slow for something like buckets. */
2116 @Override
2117 protected Area getAreaForBucket(final Layer l) {
2118 return new Area(getPerimeter());
2121 @Override
2122 protected boolean isRoughlyInside(final Layer l, final Rectangle r) {
2123 return l == this.layer && r.intersects(getBoundingBox());
2127 * Append an array of {@link IFilter} to the array of existing {@link IFilter}.
2128 * @param fs The array of {@link IFilter} to use for this Patch.
2129 * @see #setFilters(Filter[]), {@link #getFilters()}
2131 public void appendFilters(IFilter[] fs) {
2132 if (null == filters || 0 == filters.length) {
2133 filters = fs;
2134 return;
2136 if (null == fs) return;
2137 IFilter[] c = new IFilter[filters.length + fs.length];
2138 for (int i=0; i<filters.length; ++i) c[i] = filters[i];
2139 for (int i=filters.length; i<c.length; ++i) c[i] = fs[i-filters.length];
2140 this.filters = c;
2144 * Set an array of @{link {@link IFilter}, which are applied in order to the {@link ImageProcessor}
2145 * after the preprocessor script is applied but before the rest of TrakEM2 sees the image.
2146 * @param fs The array of {@link IFilter} to use for this Patch. Can be null.
2147 * @see #appendFilters(Filter[]), {@link #getFilters()}
2149 public void setFilters(IFilter[] fs) {
2150 this.filters = fs;
2155 * @return The array of {@link IFilter} of this {@link Patch}.
2156 * @see #appendFilters(Filter[]), {@link #setFilters(IFilter[])}
2158 public IFilter[] getFilters() {
2159 return filters;
2162 public boolean hasCoordinateTransform() {
2163 return 0 != ct_id;
2166 /** A value of 0 indicates that there isn't one. */
2167 public long getCoordinateTransformId() {
2168 return ct_id;
2172 * @return The absolute file path to the file specifying the {@link CoordinateTransform}, or null if none.
2174 public String getCoordinateTransformFilePath() {
2175 return hasCoordinateTransform() ? createCTFilePath(this.ct_id) : null;
2178 private final String createCTFilePath(final long ctID) {
2179 final FSLoader l = (FSLoader)project.getLoader();
2180 return l.getCoordinateTransformsFolder()
2181 + FSLoader.createIdPath(Long.toString(ctID), Long.toString(this.id), ".ct");
2184 /** Obtains a {@link CoordinateTransform}.
2185 * This method is meant to be used only when {@link #hasCoordinateTransform()} returns true.
2187 * @return The {@link CoordinateTransform} from file, or null if there isn't one.
2188 * @throws {@link RuntimeException} wrapping the actual error in loading the file.
2190 private final CoordinateTransform getCT() {
2191 try {
2192 return fetchCoordinateTransform();
2193 } catch (Exception e) {
2194 IJError.print(e);
2195 throw new RuntimeException(e);
2200 * Read in the {@link CoordinateTransform} from a file whose name is crafted
2201 * from the {@link #ct_id} and this {@link Patch}'s {@link #id}.
2203 * @return A new instance of the {@link CoordinateTransform} of this {@link Patch}, or null if none.
2204 * @throws {@link Exception} if the file could not be found or parsed or read.
2206 synchronized public CoordinateTransform fetchCoordinateTransform() throws Exception {
2207 return hasCoordinateTransform() ?
2208 CoordinateTransformXML.parse(createCTFilePath(this.ct_id))
2209 : null;
2212 /** Will throw an {@link Exception} if the file can't be read or is not there. */
2213 synchronized private char[] readCoordinateTransformFile() throws Exception {
2214 final File f = new File(createCTFilePath(this.ct_id));
2215 final char[] c = new char[(int)f.length()];
2216 Reader reader = null;
2217 try {
2218 reader = new BufferedReader(new FileReader(f), 32768); // TODO make this larger
2219 int s = 0;
2220 while (s < c.length) {
2221 int r = reader.read(c, s, c.length - s);
2222 if (-1 == r) break; // done
2223 s += r;
2225 return c;
2226 } finally {
2227 if (null != reader) reader.close();
2232 * Writes the {@link CoordinateTransform} {@param t} to the trakem2.transforms/ directory, using the unique {@link #ct_id}
2233 * and this {@link Patch}'s {@link #id} to generate a file path for it.
2235 * @return true if it was written successfully.
2236 * @throws {@link Exception} if the new file could not be written.
2238 synchronized protected boolean setNewCoordinateTransform(final CoordinateTransform ct) throws Exception {
2239 // If the new CoordinateTransform is null, set the id to 0
2240 if (null == ct) {
2241 this.ct_id = 0;
2242 return true;
2244 // Obtain a new ID
2245 final long ctID = project.getLoader().getNextBlobId();
2246 // Write the ct to file, which may throw an exception
2247 if (writeNewCoordinateTransform(ct, ctID)) {
2248 // Set the new ID
2249 this.ct_id = ctID;
2250 return true;
2251 } else {
2252 Utils.log("Could NOT write the CoordinateTransform file for patch #" + id);
2255 return false;
2258 /** @param ct
2259 * @param ctID The id
2260 * @see #setNewCoordinateTransform(CoordinateTransform) */
2261 synchronized private boolean writeNewCoordinateTransform(final CoordinateTransform ct, final long ctID) throws Exception {
2262 PrintWriter pw = null;
2263 try {
2264 final File f = new File(createCTFilePath(ctID));
2265 Utils.ensure(f);
2266 pw = new PrintWriter(new BufferedOutputStream(new FileOutputStream(f)));
2267 pw.write(ct.toXML("\t\t\t\t")); // so that "Save" will generate a pretty, formatted XML.
2268 pw.flush();
2269 return true;
2270 } finally {
2271 if (null != pw) try { pw.close(); } catch (Exception e) { IJError.print(e); }
2277 * @return True if {@link #ct_id} {@code == 0} or if the file is found, or false if not found.
2279 public boolean checkCoordinateTransformFile() {
2280 if (0 == this.ct_id) return true; // means there isn't a CoordinateTransform
2281 return new File(createCTFilePath(this.ct_id)).exists();
2285 * Transfer a world coordinate (in pixels, uncalibrated) to the coordinate space of the original image.
2286 * The world coordinate is first transferred to this {@link Patch} space by inverting the {@link AffineTransform}
2287 * and then, if there is a {@link CoordinateTransform}, that is inverted as well to reach the coordinate space of the original image.
2289 * @param world_x
2290 * @param world_y
2291 * @return A {@code double[]} array with the x,y values.
2292 * @throws NoninvertibleTransformException
2293 * @throws NoninvertibleModelException
2295 public double[] toPixelCoordinate(final double world_x, final double world_y) throws NoninvertibleTransformException {
2296 return Patch.toPixelCoordinate(world_x, world_y, this.at, hasCoordinateTransform() ? getCoordinateTransform() : null, this.meshResolution, this.o_width, this.o_height);
2300 * @see Patch#toPixelCoordinate(double, double)
2301 * @param world_x The X of the world coordinate (in pixels, uncalibrated)
2302 * @param world_y The Y of the world coordinate (in pixels, uncalibrated)
2303 * @param aff The {@link AffineTransform} of the {@link Patch}.
2304 * @param ct The {@link CoordinateTransform} of the {@link Patch}, if any (can be null).
2305 * @param meshResolution The precision demanded for approximating a transform with a {@link TransformMesh}.
2306 * @param o_width The width of the image underlying the {@link Patch}.
2307 * @param o_height The height of the image underlying the {@link Patch}.
2308 * @return A {@code double[]} array with the x,y values.
2309 * @throws NoninvertibleTransformException
2310 * @throws NoninvertibleModelException
2312 static public final double[] toPixelCoordinate(final double world_x, final double world_y,
2313 final AffineTransform aff, final CoordinateTransform ct,
2314 final int meshResolution, final int o_width, final int o_height) throws NoninvertibleTransformException {
2315 // Inverse the affine
2316 final double[] d = new double[]{world_x, world_y};
2317 aff.inverseTransform(d, 0, d, 0, 1);
2318 // Inverse the coordinate transform
2319 if (null != ct) {
2320 final float[] f = new float[]{(float)d[0], (float)d[1]};
2321 final mpicbg.models.InvertibleCoordinateTransform t =
2322 mpicbg.models.InvertibleCoordinateTransform.class.isAssignableFrom(ct.getClass()) ?
2323 (mpicbg.models.InvertibleCoordinateTransform) ct
2324 : new mpicbg.trakem2.transform.TransformMesh(ct, meshResolution, o_width, o_height);
2325 try { t.applyInverseInPlace(f); } catch ( NoninvertibleModelException e ) {}
2326 d[0] = f[0];
2327 d[1] = f[1];
2329 return d;
2334 * Return the local affine transformation for a passed location in world
2335 * coordinates. This affine transform is either the global affine
2336 * transform of the patch or the combined affine transform of the local
2337 * affine transform in the transform mesh and its global affine transform.
2339 * @param wx
2340 * @param wy
2341 * @return
2343 public AffineTransform getLocalAffine( final double wx, final double wy )
2345 final AffineTransform affine = new AffineTransform( at );
2346 if ( hasCoordinateTransform() )
2348 final CoordinateTransform ct = getCoordinateTransform();
2349 final double[] w = new double[]{ wx, wy };
2352 at.inverseTransform( w, 0, w, 0, 1 );
2354 catch ( NoninvertibleTransformException e ) {}
2355 final TransformMesh mesh = new TransformMesh( ct, meshResolution, o_width, o_height );
2356 final mpicbg.models.AffineModel2D triangle = mesh.closestTargetAffine( new float[]{ ( float )w[ 0 ], ( float )w[ 1 ] } );
2357 affine.concatenate( triangle.createAffine() );
2359 return affine;
2362 public double getLocalScale( final double wx, final double wy )
2364 final AffineTransform affine = getLocalAffine( wx, wy );
2365 final double a = affine.getScaleX();
2366 final double b = affine.getShearX();
2367 final double c = affine.getShearY();
2368 final double d = affine.getScaleY();
2370 final double l1x = a + b;
2371 final double l1y = c + d;
2372 final double l2x = a - b;
2373 final double l2y = c - d;
2375 final double l1 = Math.sqrt( l1x * l1x + l1y * l1y ) / SQRT2;
2376 final double l2 = Math.sqrt( l2x * l2x + l2y * l2y ) / SQRT2;
2378 return ( l1 + l2 ) / 2.0;
2381 @Override
2382 public void mousePressed(final MouseEvent me, final Layer la, final int x_p, final int y_p, final double mag) {
2383 final int tool = ProjectToolbar.getToolId();
2384 final DisplayCanvas canvas = (DisplayCanvas)me.getSource();
2385 if (ProjectToolbar.WAND == tool) {
2386 if (null == canvas) return;
2387 Bureaucrat.createAndStart(new Worker.Task("Magic Wand ROI") {
2388 @Override
2389 public void exec() {
2390 PatchImage pai = createTransformedImage();
2391 pai.target.setMinAndMax(min, max);
2392 final ImagePlus patchImp = new ImagePlus("", pai.target.convertToByte(true));
2393 final float[] fp = new float[2];
2394 fp[0] = x_p;
2395 fp[1] = y_p;
2396 try {
2397 at.createInverse().transform(fp, 0, fp, 0, 1);
2398 } catch (NoninvertibleTransformException e) {
2399 IJError.print(e);
2400 return;
2402 int npoints = IJ.doWand(patchImp, (int)fp[0], (int)fp[1], WandToolOptions.getTolerance(), WandToolOptions.getMode());
2403 if (npoints > 0) {
2404 System.out.println("npoints " + npoints);
2405 Roi roi = patchImp.getRoi();
2406 if (null != roi) {
2407 Area aroi = M.getArea(roi);
2408 aroi.transform(at);
2409 canvas.getFakeImagePlus().setRoi(new ShapeRoi(aroi));
2413 }, project);