Added automatic GUI to fix file paths.
[trakem2.git] / ini / trakem2 / persistence / Loader.java
bloba7aad4e49cb9467b53d1ab4f87496f55469b0e3c
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.persistence;
25 import ini.trakem2.utils.IJError;
27 import ij.IJ;
28 import ij.ImagePlus;
29 import ij.ImageStack;
30 import ij.WindowManager;
31 import ij.gui.GenericDialog;
32 import ij.gui.Roi;
33 import ij.gui.YesNoCancelDialog;
34 import ij.io.DirectoryChooser;
35 import ij.io.FileInfo;
36 import ij.io.FileSaver;
37 import ij.io.Opener;
38 import ij.io.OpenDialog;
39 import ij.io.TiffEncoder;
40 import ij.plugin.filter.PlugInFilter;
41 import ij.plugin.ContrastEnhancer;
42 import ij.process.ByteProcessor;
43 import ij.process.ImageProcessor;
44 import ij.process.FloatProcessor;
45 import ij.process.StackProcessor;
46 import ij.process.StackStatistics;
47 import ij.process.ImageStatistics;
48 import ij.process.ColorProcessor;
49 import ij.measure.Calibration;
50 import ij.measure.Measurements;
52 import ini.trakem2.Project;
53 import ini.trakem2.display.AreaList;
54 import ini.trakem2.display.Ball;
55 import ini.trakem2.display.DLabel;
56 import ini.trakem2.display.Display;
57 import ini.trakem2.display.Displayable;
58 import ini.trakem2.display.Layer;
59 import ini.trakem2.display.LayerSet;
60 import ini.trakem2.display.Patch;
61 import ini.trakem2.display.Pipe;
62 import ini.trakem2.display.Polyline;
63 import ini.trakem2.display.Profile;
64 import ini.trakem2.display.YesNoDialog;
65 import ini.trakem2.display.ZDisplayable;
66 import ini.trakem2.tree.*;
67 import ini.trakem2.utils.*;
68 import ini.trakem2.io.*;
69 import ini.trakem2.imaging.*;
70 import ini.trakem2.ControlWindow;
72 import javax.swing.tree.*;
73 import javax.swing.JPopupMenu;
74 import javax.swing.JMenuItem;
75 import java.awt.Color;
76 import java.awt.Component;
77 import java.awt.Checkbox;
78 import java.awt.Cursor;
79 import java.awt.Dimension;
80 import java.awt.Graphics;
81 import java.awt.Graphics2D;
82 import java.awt.Image;
83 import java.awt.Point;
84 import java.awt.Rectangle;
85 import java.awt.RenderingHints;
86 import java.awt.image.BufferedImage;
87 import java.awt.image.IndexColorModel;
88 import java.awt.geom.Area;
89 import java.awt.geom.AffineTransform;
90 import java.io.BufferedOutputStream;
91 import java.io.BufferedReader;
92 import java.io.ByteArrayInputStream;
93 import java.io.ByteArrayOutputStream;
94 import java.io.DataOutputStream;
95 import java.io.File;
96 import java.io.FileReader;
97 import java.io.FilenameFilter;
98 import java.io.IOException;
99 import java.io.InputStream;
100 import java.io.ObjectInputStream;
101 import java.io.ObjectOutputStream;
102 import java.io.FileInputStream;
103 import java.io.FileOutputStream;
104 import java.io.OutputStreamWriter;
105 import java.util.List;
106 import java.util.ArrayList;
107 import java.util.Arrays;
108 import java.util.Hashtable;
109 import java.util.Map;
110 import java.util.HashSet;
111 import java.util.Set;
112 import java.util.Collections;
113 import java.util.Collection;
114 import java.util.Iterator;
115 import java.util.LinkedList;
116 import java.util.IdentityHashMap;
117 import java.util.HashMap;
118 import java.util.Vector;
119 import java.util.LinkedHashSet;
120 import java.util.zip.ZipEntry;
121 import java.util.zip.ZipInputStream;
122 import java.util.zip.ZipOutputStream;
123 import java.util.zip.Inflater;
124 import java.util.zip.Deflater;
126 import javax.swing.JMenu;
128 import mpi.fruitfly.math.datastructures.FloatArray2D;
129 import mpi.fruitfly.registration.ImageFilter;
130 import mpi.fruitfly.registration.PhaseCorrelation2D;
131 import mpi.fruitfly.registration.Feature;
132 import mpi.fruitfly.general.MultiThreading;
134 import java.util.concurrent.atomic.AtomicInteger;
135 import java.lang.reflect.Field;
136 import java.lang.reflect.Method;
138 import loci.formats.ChannelSeparator;
139 import loci.formats.FormatException;
140 import loci.formats.IFormatReader;
142 /** Handle all data-related issues with a virtualization engine, including load/unload and saving, saving as and overwriting. */
143 abstract public class Loader {
145 // Only one thread at a time is to use the connection and cache
146 protected final Object db_lock = new Object();
147 private boolean db_busy = false;
149 protected Opener opener = new Opener();
151 /** The cache is shared, and can be flagged to do massive flushing. */
152 private boolean massive_mode = false;
154 /** Keep track of whether there are any unsaved changes.*/
155 protected boolean changes = false;
158 static public final int ERROR_PATH_NOT_FOUND = Integer.MAX_VALUE;
160 /** Whether incremental garbage collection is enabled. */
162 static protected final boolean Xincgc = isXincgcSet();
164 static protected final boolean isXincgcSet() {
165 String[] args = IJ.getInstance().getArgs();
166 for (int i=0; i<args.length; i++) {
167 if ("-Xingc".equals(args[i])) return true;
169 return false;
173 static public final IndexColorModel GRAY_LUT = makeGrayLut();
175 static public final IndexColorModel makeGrayLut() {
176 final byte[] r = new byte[256];
177 final byte[] g = new byte[256];
178 final byte[] b = new byte[256];
179 for (int i=0; i<256; i++) {
180 r[i]=(byte)i;
181 g[i]=(byte)i;
182 b[i]=(byte)i;
184 return new IndexColorModel(8, 256, r, g, b);
188 protected final Set<Patch> hs_unloadable = Collections.synchronizedSet(new HashSet<Patch>());
190 static public final BufferedImage NOT_FOUND = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
191 static {
192 Graphics2D g = NOT_FOUND.createGraphics();
193 g.setColor(Color.white);
194 g.drawRect(1, 1, 8, 8);
195 g.drawLine(3, 3, 7, 7);
196 g.drawLine(7, 3, 3, 7);
199 static public final BufferedImage REGENERATING = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
200 static {
201 Graphics2D g = REGENERATING.createGraphics();
202 g.setColor(Color.white);
203 g.setFont(new java.awt.Font("SansSerif", java.awt.Font.PLAIN, 8));
204 g.drawString("R", 1, 9);
207 /** Returns true if the awt is a signaling image like NOT_FOUND or REGENERATING. */
208 static public boolean isSignalImage(final Image awt) {
209 return REGENERATING == awt || NOT_FOUND == awt;
212 // the cache: shared, for there is only one JVM! (we could open a second one, and store images there, and transfer them through sockets)
215 // What I need is not provided: a LinkedHashMap with a method to do 'removeFirst' or remove(0) !!! To call my_map.entrySet().iterator() to delete the the first element of a LinkedHashMap is just too much calling for an operation that has to be blazing fast. So I create a double list setup with arrays. The variables are not static because each loader could be connected to a different database, and each database has its own set of unique ids. Memory from other loaders is free by the releaseOthers(double) method.
216 transient protected CacheImagePlus imps = new CacheImagePlus(50);
217 transient protected CacheImageMipMaps mawts = new CacheImageMipMaps(50);
219 static transient protected Vector<Loader> v_loaders = null; // Vector: synchronized
221 protected Loader() {
222 // register
223 if (null == v_loaders) v_loaders = new Vector<Loader>();
224 v_loaders.add(this);
225 if (!ControlWindow.isGUIEnabled()) {
226 opener.setSilentMode(true);
229 // debug: report cache status every ten seconds
231 final Loader lo = this;
232 new Thread() {
233 public void run() {
234 setPriority(Thread.NORM_PRIORITY);
235 while (true) {
236 try { Thread.sleep(1000); } catch (InterruptedException ie) {}
237 synchronized(db_lock) {
238 lock();
239 //if (!v_loaders.contains(lo)) {
240 // unlock();
241 // break;
242 //} // TODO BROKEN: not registered!
243 Utils.log2("CACHE: \n\timps: " + imps.size() + "\n\tmawts: " + mawts.size());
244 mawts.debug();
245 unlock();
249 }.start();
252 Utils.log2("MAX_MEMORY: " + max_memory);
255 /** When the loader has completed its initialization, it should return true on this method. */
256 abstract public boolean isReady();
258 /** To be called within a synchronized(db_lock) */
259 protected final void lock() {
260 //Utils.printCaller(this, 7);
261 while (db_busy) { try { db_lock.wait(); } catch (InterruptedException ie) {} }
262 db_busy = true;
265 /** To be called within a synchronized(db_lock) */
266 protected final void unlock() {
267 //Utils.printCaller(this, 7);
268 if (db_busy) {
269 db_busy = false;
270 db_lock.notifyAll();
274 /** Release all memory and unregister itself. Child Loader classes should call this method in their destroy() methods. */
275 synchronized public void destroy() {
276 if (null != IJ.getInstance() && IJ.getInstance().quitting()) {
277 return; // no need to do anything else
279 Utils.showStatus("Releasing all memory ...", false);
280 destroyCache();
281 Project p = Project.findProject(this);
282 if (null != v_loaders) {
283 v_loaders.remove(this); // sync issues when deleting two loaders consecutively
284 if (0 == v_loaders.size()) v_loaders = null;
288 /**Retrieve next id from a sequence for a new DBObject to be added.*/
289 abstract public long getNextId();
291 /** Ask for the user to provide a template XML file to extract a root TemplateThing. */
292 public TemplateThing askForXMLTemplate(Project project) {
293 // ask for an .xml file or a .dtd file
294 //fd.setFilenameFilter(new XMLFileFilter(XMLFileFilter.BOTH));
295 String user = System.getProperty("user.name");
296 OpenDialog od = new OpenDialog("Select XML Template",
297 OpenDialog.getDefaultDirectory(),
298 null);
299 String file = od.getFileName();
300 if (null == file || file.toLowerCase().startsWith("null")) return null;
301 // if there is a path, read it out if possible
302 String path = od.getDirectory() + "/" + file;
303 TemplateThing[] roots = DTDParser.extractTemplate(path);
304 if (null == roots || roots.length < 1) return null;
305 if (roots.length > 1) {
306 Utils.showMessage("Found more than one root.\nUsing first root only.");
308 return roots[0];
311 private int temp_snapshots_mode = 0;
313 public void startLargeUpdate() {
314 LayerSet ls = Project.findProject(this).getRootLayerSet();
315 temp_snapshots_mode = ls.getSnapshotsMode();
316 if (2 != temp_snapshots_mode) ls.setSnapshotsMode(2); // disable repainting snapshots
319 public void commitLargeUpdate() {
320 Project.findProject(this).getRootLayerSet().setSnapshotsMode(temp_snapshots_mode);
323 public void rollback() {
324 Project.findProject(this).getRootLayerSet().setSnapshotsMode(temp_snapshots_mode);
327 abstract public double[][][] fetchBezierArrays(long id);
329 abstract public ArrayList fetchPipePoints(long id);
331 abstract public ArrayList fetchBallPoints(long id);
333 abstract public Area fetchArea(long area_list_id, long layer_id);
335 /* GENERIC, from DBObject calls */
336 abstract public boolean addToDatabase(DBObject ob);
338 abstract public boolean updateInDatabase(DBObject ob, String key);
340 abstract public boolean removeFromDatabase(DBObject ob);
342 /* Reflection would be the best way to do all above; when it's about and 'id', one only would have to check whether the field in question is a BIGINT and the object given a DBObject, and call getId(). Such an approach demands, though, perfect matching of column names with class field names. */
344 // for cache flushing
345 public boolean getMassiveMode() {
346 return massive_mode;
349 // for cache flushing
350 public final void setMassiveMode(boolean m) {
351 massive_mode = m;
352 //Utils.log2("massive mode is " + m + " for loader " + this);
355 /** Retrieves a zipped ImagePlus from the given InputStream. The stream is not closed and must be closed elsewhere. No error checking is done as to whether the stream actually contains a zipped tiff file. */
356 protected ImagePlus unzipTiff(InputStream i_stream, String title) {
357 ImagePlus imp;
358 try {
359 // Reading a zipped tiff file in the database
362 /* // works but not faster
363 byte[] bytes = null;
364 // new style: RAM only
365 ByteArrayOutputStream out = new ByteArrayOutputStream();
366 byte[] buf = new byte[4096];
367 int len;
368 int length = 0;
369 while (true) {
370 len = i_stream.read(buf);
371 if (len<0) break;
372 length += len;
373 out.write(buf, 0, len);
375 Inflater infl = new Inflater();
376 infl.setInput(out.toByteArray(), 0, length);
377 int buflen = length + length;
378 buf = new byte[buflen]; //short almost for sure
379 int offset = 0;
380 ArrayList al = new ArrayList();
381 while (true) {
382 len = infl.inflate(buf, offset, buf.length);
383 al.add(buf);
384 if (0 == infl.getRemaining()) break;
385 buf = new byte[length*2];
386 offset += len;
388 infl.end();
389 byte[][] b = new byte[al.size()][];
390 al.toArray(b);
391 int blength = buflen * (b.length -1) + len; // the last may be shorter
392 bytes = new byte[blength];
393 for (int i=0; i<b.length -1; i++) {
394 System.arraycopy(b[i], 0, bytes, i*buflen, buflen);
396 System.arraycopy(b[b.length-1], 0, bytes, buflen * (b.length-1), len);
400 //OLD, creates tmp file (archive style)
401 ZipInputStream zis = new ZipInputStream(i_stream);
402 ByteArrayOutputStream out = new ByteArrayOutputStream();
403 byte[] buf = new byte[4096]; //copying savagely from ImageJ's Opener.openZip()
404 ZipEntry entry = zis.getNextEntry(); // I suspect this is needed as an iterator
405 int len;
406 while (true) {
407 len = zis.read(buf);
408 if (len<0) break;
409 out.write(buf, 0, len);
411 zis.close();
412 byte[] bytes = out.toByteArray();
414 ij.IJ.redirectErrorMessages();
415 imp = opener.openTiff(new ByteArrayInputStream(bytes), title);
416 // NO! Database images may get preprocessed everytime one opends them. The preprocessor is only intended to be applied to files opened from the file system. //preProcess(imp);
418 //old
419 //ij.IJ.redirectErrorMessages();
420 //imp = new Opener().openTiff(i_stream, title);
421 } catch (Exception e) {
422 IJError.print(e);
423 return null;
425 return imp;
428 public void addCrossLink(long project_id, long id1, long id2) {}
430 /** Remove a link between two objects. Returns true always in this empty method. */
431 public boolean removeCrossLink(long id1, long id2) { return true; }
433 /** Add to the cache, or if already there, make it be the last (to be flushed the last). */
434 public void cache(final Displayable d, final ImagePlus imp) {
435 synchronized (db_lock) {
436 lock();
437 final long id = d.getId(); // each Displayable has a unique id for each database, not for different databases, that's why the cache is NOT shared.
438 if (Patch.class == d.getClass()) {
439 unlock();
440 cache((Patch)d, imp);
441 return;
442 } else {
443 Utils.log("Loader.cache: don't know how to cache: " + d);
445 unlock();
449 public void cache(final Patch p, final ImagePlus imp) {
450 if (null == imp || null == imp.getProcessor()) return;
451 synchronized (db_lock) {
452 lock();
453 final long id = p.getId();
454 final ImagePlus cached = imps.get(id);
455 if (null == cached
456 || cached != imp
457 || imp.getProcessor().getPixels() != cached.getProcessor().getPixels()
459 imps.put(id, imp);
460 } else {
461 imps.get(id); // send to the end
463 unlock();
467 /** Cache any ImagePlus, as long as a unique id is assigned to it there won't be problems; you can obtain a unique id from method getNextId() .*/
468 public void cacheImagePlus(long id, ImagePlus imp) {
469 synchronized (db_lock) {
470 lock();
471 imps.put(id, imp); // TODO this looks totally unnecessary
472 unlock();
476 public void decacheImagePlus(long id) {
477 synchronized (db_lock) {
478 lock();
479 ImagePlus imp = imps.remove(id);
480 flush(imp);
481 unlock();
485 public void decacheImagePlus(long[] id) {
486 synchronized (db_lock) {
487 lock();
488 for (int i=0; i<id.length; i++) {
489 ImagePlus imp = imps.remove(id[i]);
490 flush(imp);
492 unlock();
496 public void updateCache(final Displayable d, final String key) {
497 /* CRUFT FROM THE PAST, for LayerStack I think
498 if (key.startsWith("points=")) {
499 long lid = Long.parseLong(key.substring(7)); // for AreaList
500 decacheImagePlus(lid);
501 } else if (d instanceof ZDisplayable) {
502 // remove all layers in which the ZDisplayable paints to
503 ZDisplayable zd = (ZDisplayable)d;
504 for (Iterator it = zd.getLayerSet().getLayers().iterator(); it.hasNext(); ) {
505 Layer layer = (Layer)it.next();
506 if (zd.paintsAt(layer)) decacheImagePlus(layer.getId());
508 } else {
509 // remove the layer where the Displayable paints to
510 if (null == d.getLayer()) {
511 // Top Level LayerSet has no layer
512 return;
514 decacheImagePlus(d.getLayer().getId());
519 ///////////////////
521 static protected final Runtime RUNTIME = Runtime.getRuntime();
523 static public final long getCurrentMemory() {
524 // totalMemory() is the amount of current JVM heap allocation, whether it's being used or not. It may grow over time if -Xms < -Xmx
525 return RUNTIME.totalMemory() - RUNTIME.freeMemory(); }
527 static public final long getFreeMemory() {
528 // max_memory changes as some is reserved by image opening calls
529 return max_memory - getCurrentMemory(); }
531 /** Really available maximum memory, in bytes. */
532 static protected long max_memory = RUNTIME.maxMemory() - 128000000; // 128 M always free
533 //static protected long max_memory = (long)(IJ.maxMemory() - 128000000); // 128 M always free
535 /** Measure whether there are at least 'n_bytes' free. */
536 static final protected boolean enoughFreeMemory(final long n_bytes) {
537 long free = getFreeMemory();
538 if (free < n_bytes) {
539 return false; }
540 //if (Runtime.getRuntime().freeMemory() < n_bytes + MIN_FREE_BYTES) return false;
541 return n_bytes < max_memory - getCurrentMemory();
544 public final boolean releaseToFit(final int width, final int height, final int type, float factor) {
545 long bytes = width * height;
546 switch (type) {
547 case ImagePlus.GRAY32:
548 bytes *= 5; // 4 for the FloatProcessor, and 1 for the generated pixels8 to make an image
549 if (factor < 4) factor = 4; // for Open_MRC_Leginon ... TODO this is unnecessary in all other cases
550 break;
551 case ImagePlus.COLOR_RGB:
552 bytes *= 4;
553 break;
554 case ImagePlus.GRAY16:
555 bytes *= 3; // 2 for the ShortProcessor, and 1 for the pixels8
556 break;
557 default: // times 1
558 break;
560 return releaseToFit((long)(bytes*factor));
563 /** Release enough memory so that as many bytes as passed as argument can be loaded. */
564 public final boolean releaseToFit(final long bytes) {
565 if (bytes > max_memory) {
566 Utils.log("WARNING: Can't fit " + bytes + " bytes in memory.");
567 // Try anyway
568 releaseAll();
569 return false;
571 final boolean previous = massive_mode;
572 if (bytes > max_memory / 4) setMassiveMode(true);
573 if (enoughFreeMemory(bytes)) return true;
574 boolean result = true;
575 synchronized (db_lock) {
576 lock();
577 result = releaseToFit2(bytes);
578 unlock();
580 setMassiveMode(previous);
581 return result;
584 // non-locking version
585 protected final boolean releaseToFit2(long n_bytes) {
586 //if (enoughFreeMemory(n_bytes)) return true;
587 if (releaseMemory(0.5D, true, n_bytes) >= n_bytes) return true; // Java will free on its own if it has to
588 // else, wait for GC
589 int iterations = 30;
591 while (iterations > 0) {
592 if (0 == imps.size() && 0 == mawts.size()) {
593 // wait for GC ...
594 System.gc();
595 try { Thread.sleep(300); } catch (InterruptedException ie) {}
597 if (enoughFreeMemory(n_bytes)) return true;
598 iterations--;
600 return true;
603 /** This method tries to cope with the lack of real time garbage collection in java (that is, lack of predictable time for memory release). */
604 public final int runGC() {
605 //Utils.printCaller("runGC", 4);
606 final long initial = IJ.currentMemory();
607 long now = initial;
608 final int max = 7;
609 long sleep = 50; // initial value
610 int iterations = 0;
611 do {
612 //Runtime.getRuntime().runFinalization(); // enforce it
613 System.gc();
614 Thread.yield();
615 try { Thread.sleep(sleep); } catch (InterruptedException ie) {}
616 sleep += sleep; // incremental
617 now = IJ.currentMemory();
618 Utils.log("\titer " + iterations + " initial: " + initial + " now: " + now);
619 Utils.log2("\t mawts: " + mawts.size() + " imps: " + imps.size());
620 iterations++;
621 } while (now >= initial && iterations < max);
622 Utils.log2("finished runGC");
623 if (iterations >= 7) {
624 //Utils.printCaller(this, 10);
626 return iterations + 1;
629 static public final void runGCAll() {
630 Loader[] lo = new Loader[v_loaders.size()];
631 v_loaders.toArray(lo);
632 for (int i=0; i<lo.length; i++) {
633 lo[i].runGC();
637 static public void printCacheStatus() {
638 Loader[] lo = new Loader[v_loaders.size()];
639 v_loaders.toArray(lo);
640 for (int i=0; i<lo.length; i++) {
641 Utils.log2("Loader " + i + " : mawts: " + lo[i].mawts.size() + " imps: " + lo[i].imps.size());
645 /** The minimal number of memory bytes that should always be free. */
646 public static final long MIN_FREE_BYTES = max_memory > 1000000000 /*1 Gb*/ ? 150000000 /*150 Mb*/ : 50000000 /*50 Mb*/; // (long)(max_memory * 0.2f);
648 /** Remove up to half the ImagePlus cache of others (but their mawts first if needed) and then one single ImagePlus of this Loader's cache. */
649 protected final long releaseMemory() {
650 return releaseMemory(0.5D, true, MIN_FREE_BYTES);
653 private final long measureSize(final ImagePlus imp) {
654 if (null == imp) return 0;
655 final long size = imp.getWidth() * imp.getHeight();
656 switch (imp.getType()) {
657 case ImagePlus.GRAY16:
658 return size * 2 + 100;
659 case ImagePlus.GRAY32:
660 case ImagePlus.COLOR_RGB:
661 return size * 4 + 100; // some overhead, it's 16 but allowing for a lot more
662 case ImagePlus.GRAY8:
663 return size + 100;
664 case ImagePlus.COLOR_256:
665 return size + 868; // 100 + 3 * 256 (the LUT)
667 return 0;
670 /** Returns a lower-bound estimate: as if it was grayscale; plus some overhead. */
671 private final long measureSize(final Image img) {
672 if (null == img) return 0;
673 return img.getWidth(null) * img.getHeight(null) + 100;
676 public long releaseMemory(double percent, boolean from_all_projects) {
677 if (!from_all_projects) return releaseMemory(percent);
678 long mem = 0;
679 for (Loader loader : v_loaders) mem += loader.releaseMemory(percent);
680 return mem;
683 /** From 0 to 1. */
684 public long releaseMemory(double percent) {
685 if (percent <= 0) return 0;
686 if (percent > 1) percent = 1;
687 synchronized (db_lock) {
688 try {
689 lock();
690 return releaseMemory(percent, false, MIN_FREE_BYTES);
691 } catch (Throwable e) {
692 IJError.print(e);
693 } finally {
694 // gets called by the 'return' above and by any other sort of try{}catch interruption
695 unlock();
698 return 0;
701 /** Release as much of the cache as necessary to make at least min_free_bytes free.<br />
702 * The very last thing to remove is the stored awt.Image objects.<br />
703 * Removes one ImagePlus at a time if a == 0, else up to 0 &lt; a &lt;= 1.0 .<br />
704 * NOT locked, however calls must take care of that.<br />
706 protected final long releaseMemory(final double a, final boolean release_others, final long min_free_bytes) {
707 long released = 0;
708 try {
709 //while (!enoughFreeMemory(min_free_bytes)) {
710 while (released < min_free_bytes) {
711 if (enoughFreeMemory(min_free_bytes)) return released;
712 // release the cache of other loaders (up to 'a' of the ImagePlus cache of them if necessary)
713 if (massive_mode) {
714 // release others regardless of the 'release_others' boolean
715 released += releaseOthers(0.5D);
716 // reset
717 if (released >= min_free_bytes) return released;
718 // remove half of the imps
719 if (0 != imps.size()) {
720 for (int i=imps.size()/2; i>-1; i--) {
721 ImagePlus imp = imps.removeFirst();
722 released += measureSize(imp);
723 flush(imp);
725 Thread.yield();
726 if (released >= min_free_bytes) return released;
728 // finally, release snapshots
729 if (0 != mawts.size()) {
730 // release almost all snapshots (they're cheap to reload/recreate)
731 for (int i=(int)(mawts.size() * 0.25); i>-1; i--) {
732 Image mawt = mawts.removeFirst();
733 released += measureSize(mawt);
734 if (null != mawt) mawt.flush();
736 if (released >= min_free_bytes) return released;
738 } else {
739 if (release_others) {
740 released += releaseOthers(a);
741 if (released >= min_free_bytes) return released;
743 if (0 == imps.size()) {
744 // release half the cached awt images
745 if (0 != mawts.size()) {
746 for (int i=mawts.size()/3; i>-1; i--) {
747 Image im = mawts.removeFirst();
748 released += measureSize(im);
749 if (null != im) im.flush();
751 if (released >= min_free_bytes) return released;
754 // finally:
755 if (a > 0.0D && a <= 1.0D) {
756 // up to 'a' of the ImagePlus cache:
757 for (int i=(int)(imps.size() * a); i>-1; i--) {
758 ImagePlus imp = imps.removeFirst();
759 released += measureSize(imp);
760 flush(imp);
762 } else {
763 // just one:
764 ImagePlus imp = imps.removeFirst();
765 flush(imp);
769 // sanity check:
770 if (0 == imps.size() && 0 == mawts.size()) {
771 Utils.log2("Loader.releaseMemory: empty cache.");
772 // Remove any autotraces
773 Polyline.flushTraceCache(Project.findProject(this));
774 // in any case, can't release more:
775 mawts.gc();
776 return released;
779 } catch (Exception e) {
780 IJError.print(e);
782 return released;
785 /** Release memory from other loaders. */
786 private long releaseOthers(double a) {
787 if (null == v_loaders || 1 == v_loaders.size()) return 0;
788 if (a <= 0.0D || a > 1.0D) return 0;
789 final Iterator it = v_loaders.iterator();
790 long released = 0;
791 while (it.hasNext()) {
792 Loader loader = (Loader)it.next();
793 if (loader == this) continue;
794 else {
795 loader.setMassiveMode(false); // otherwise would loop back!
796 released += loader.releaseMemory(a, false, MIN_FREE_BYTES);
799 return released;
802 static public void releaseAllCaches() {
803 for (final Loader lo : new Vector<Loader>(v_loaders)) {
804 lo.releaseAll();
808 /** Empties the caches. */
809 public void releaseAll() {
810 synchronized (db_lock) {
811 lock();
812 try {
813 for (ImagePlus imp : imps.removeAll()) {
814 flush(imp);
816 mawts.removeAndFlushAll();
817 } catch (Exception e) {
818 IJError.print(e);
820 unlock();
824 private void destroyCache() {
825 synchronized (db_lock) {
826 try {
827 lock();
828 if (null != IJ.getInstance() && IJ.getInstance().quitting()) {
829 return;
831 if (null != imps) {
832 for (ImagePlus imp : imps.removeAll()) {
833 flush(imp);
835 imps = null;
837 if (null != mawts) {
838 mawts.removeAndFlushAll();
840 } catch (Exception e) {
841 unlock();
842 IJError.print(e);
843 } finally {
844 unlock();
849 /** Removes from the cache all awt images bond to the given id. */
850 public void decacheAWT(final long id) {
851 synchronized (db_lock) {
852 lock();
853 mawts.removeAndFlush(id); // where are my lisp macros! Wrapping any function in a synch/lock/unlock could be done crudely with reflection, but what a pain
854 unlock();
858 public void cacheOffscreen(final Layer layer, final Image awt) {
859 synchronized (db_lock) {
860 lock();
861 mawts.put(layer.getId(), awt, 0);
862 unlock();
866 /** Transform mag to nearest scale level that delivers an equally sized or larger image.<br />
867 * Requires 0 &lt; mag &lt;= 1.0<br />
868 * Returns -1 if the magnification is NaN or negative or zero.<br />
869 * As explanation:<br />
870 * mag = 1 / Math.pow(2, level) <br />
871 * so that 100% is 0, 50% is 1, 25% is 2, and so on, but represented in values between 0 and 1.
873 static public final int getMipMapLevel(final double mag, final double size) {
874 // check parameters
875 if (mag > 1) return 0; // there is no level for mag > 1, so use mag = 1
876 if (mag <= 0 || Double.isNaN(mag)) {
877 Utils.log2("ERROR: mag is " + mag);
878 return 0; // survive
881 final int level = (int)(0.0001 + Math.log(1/mag) / Math.log(2)); // compensating numerical instability: 1/0.25 should be 2 eaxctly
882 final int max_level = getHighestMipMapLevel(size);
884 if (max_level > 6) {
885 Utils.log2("ERROR max_level > 6: " + max_level + ", size: " + size);
888 return Math.min(level, max_level);
891 int level = 0;
892 double scale;
893 while (true) {
894 scale = 1 / Math.pow(2, level);
895 //Utils.log2("scale, mag, level: " + scale + ", " + mag + ", " + level);
896 if (Math.abs(scale - mag) < 0.00000001) { //if (scale == mag) { // floating-point typical behaviour
897 break;
898 } else if (scale < mag) {
899 // provide the previous one
900 level--;
901 break;
903 // else, continue search
904 level++;
906 return level;
910 public static final double maxDim(final Displayable d) {
911 return Math.max(d.getWidth(), d.getHeight());
914 /** Returns true if there is a cached awt image for the given mag and Patch id. */
915 public boolean isCached(final Patch p, final double mag) {
916 synchronized (db_lock) {
917 lock();
918 boolean b = mawts.contains(p.getId(), Loader.getMipMapLevel(mag, maxDim(p)));
919 unlock();
920 return b;
924 public Image getCached(final long id, final int level) {
925 Image awt = null;
926 synchronized (db_lock) {
927 lock();
928 awt = mawts.getClosestAbove(id, level);
929 unlock();
931 return awt;
934 /** Above or equal in size. */
935 public Image getCachedClosestAboveImage(final Patch p, final double mag) {
936 Image awt = null;
937 synchronized (db_lock) {
938 lock();
939 awt = mawts.getClosestAbove(p.getId(), Loader.getMipMapLevel(mag, maxDim(p)));
940 unlock();
942 return awt;
945 /** Below, not equal. */
946 public Image getCachedClosestBelowImage(final Patch p, final double mag) {
947 Image awt = null;
948 synchronized (db_lock) {
949 lock();
950 awt = mawts.getClosestBelow(p.getId(), Loader.getMipMapLevel(mag, maxDim(p)));
951 unlock();
953 return awt;
956 protected final class PatchLoadingLock extends Lock {
957 final String key;
958 PatchLoadingLock(final String key) { this.key = key; }
961 /** Table of dynamic locks, a single one per Patch if any. */
962 private final Hashtable<String,PatchLoadingLock> ht_plocks = new Hashtable<String,PatchLoadingLock>();
964 protected final PatchLoadingLock getOrMakePatchLoadingLock(final Patch p, final int level) {
965 final String key = new StringBuffer().append(p.getId()).append('.').append(level).toString();
966 PatchLoadingLock plock = ht_plocks.get(key);
967 if (null != plock) return plock;
968 plock = new PatchLoadingLock(key);
969 ht_plocks.put(key, plock);
970 return plock;
972 protected final void removePatchLoadingLock(final PatchLoadingLock pl) {
973 ht_plocks.remove(pl.key);
976 public Image fetchImage(Patch p) {
977 return fetchImage(p, 1.0);
980 /** Fetch a suitable awt.Image for the given mag(nification).
981 * If the mag is bigger than 1.0, it will return as if was 1.0.
982 * Will return Loader.NOT_FOUND if, err, not found (probably an Exception will print along).
984 public Image fetchImage(final Patch p, double mag) {
985 // Below, the complexity of the synchronized blocks is to provide sufficient granularity. Keep in mind that only one thread at at a time can access a synchronized block for the same object (in this case, the db_lock), and thus calling lock() and unlock() is not enough. One needs to break the statement in as many synch blocks as possible for maximizing the number of threads concurrently accessing different parts of this function.
987 if (mag > 1.0) mag = 1.0; // Don't want to create gigantic images!
988 int level = Loader.getMipMapLevel(mag, maxDim(p));
989 int max_level = Loader.getHighestMipMapLevel(p);
990 //Utils.log2("level=" + level + " max_level=" + max_level);
991 if (level > max_level) level = max_level;
993 // testing:
994 // if (level > 0) level--; // passing an image double the size, so it's like interpolating when doing nearest neighbor since the images are blurred with sigma 0.5
995 // SLOW, very slow ...
997 // find an equal or larger existing pyramid awt
998 final long id = p.getId();
999 PatchLoadingLock plock = null;
1001 synchronized (db_lock) {
1002 lock();
1003 try {
1004 if (null == mawts) {
1005 return NOT_FOUND; // when lazy repainting after closing a project, the awts is null
1007 if (level >= 0 && isMipMapsEnabled()) {
1008 // 1 - check if the exact level is cached
1009 final Image mawt = mawts.get(id, level);
1010 if (null != mawt) {
1011 //Utils.log2("returning cached exact mawt for level " + level);
1012 return mawt;
1015 releaseMemory();
1016 plock = getOrMakePatchLoadingLock(p, level);
1018 } catch (Exception e) {
1019 IJError.print(e);
1020 } finally {
1021 unlock();
1025 Image mawt = null;
1026 long n_bytes = 0;
1028 // 2 - check if the exact file is present for the desired level
1029 if (level >= 0 && isMipMapsEnabled()) {
1030 synchronized (plock) {
1031 plock.lock();
1033 synchronized (db_lock) {
1034 lock();
1035 mawt = mawts.get(id, level);
1036 unlock();
1038 if (null != mawt) {
1039 plock.unlock();
1040 return mawt; // was loaded by a different thread
1043 // going to load:
1045 synchronized (db_lock) {
1046 lock();
1047 n_bytes = estimateImageFileSize(p, level);
1048 max_memory -= n_bytes;
1049 unlock();
1051 releaseToFit(n_bytes * 6); // six times, for the jpeg decoder alloc/dealloc at least 2 copies, and with alpha even one more
1052 mawt = fetchMipMapAWT(p, level);
1054 synchronized (db_lock) {
1055 try {
1056 lock();
1057 max_memory += n_bytes;
1058 if (null != mawt) {
1059 if (REGENERATING != mawt) mawts.put(id, mawt, level);
1060 //Utils.log2("returning exact mawt from file for level " + level);
1061 Display.repaintSnapshot(p);
1062 return mawt;
1064 // 3 - else, load closest level to it but still giving a larger image
1065 final int lev = getClosestMipMapLevel(p, level); // finds the file for the returned level, otherwise returns zero
1066 //Utils.log2("closest mipmap level is " + lev);
1067 if (lev >= 0) {
1068 mawt = mawts.getClosestAbove(id, lev);
1069 boolean newly_cached = false;
1070 if (null == mawt) {
1071 // reload existing scaled file
1072 releaseToFit(n_bytes); // overshooting
1073 mawt = fetchMipMapAWT2(p, lev);
1074 if (null != mawt) {
1075 mawts.put(id, mawt, lev);
1076 newly_cached = true; // means: cached was false, now it is
1078 // else if null, the file did not exist or could not be regenerated or regeneration is off
1080 //Utils.log2("from getClosestMipMapLevel: mawt is " + mawt);
1081 if (null != mawt) {
1082 if (newly_cached) Display.repaintSnapshot(p);
1083 //Utils.log2("returning from getClosestMipMapAWT with level " + lev);
1084 return mawt;
1086 } else if (ERROR_PATH_NOT_FOUND == lev) {
1087 mawt = NOT_FOUND;
1089 } catch (Exception e) {
1090 IJError.print(e);
1091 } finally {
1092 removePatchLoadingLock(plock);
1093 unlock();
1094 plock.unlock();
1100 // level is zero or nonsensically lower than zero, or was not found
1101 //Utils.log2("not found!");
1103 synchronized (db_lock) {
1104 try {
1105 lock();
1107 // 4 - check if any suitable level is cached (whithout mipmaps, it may be the large image)
1108 mawt = mawts.getClosestAbove(id, level);
1109 if (null != mawt) {
1110 //Utils.log2("returning from getClosest with level " + level);
1111 return mawt;
1113 } catch (Exception e) {
1114 IJError.print(e);
1115 } finally {
1116 unlock();
1120 // 5 - else, fetch the (perhaps) transformed ImageProcessor and make an image from it of the proper size and quality
1122 if (hs_unloadable.contains(p)) return NOT_FOUND;
1124 synchronized (db_lock) {
1125 try {
1126 lock();
1127 releaseMemory();
1128 plock = getOrMakePatchLoadingLock(p, level);
1129 } catch (Exception e) {
1130 return NOT_FOUND;
1131 } finally {
1132 unlock();
1136 synchronized (plock) {
1137 try {
1138 plock.lock();
1140 // Check if a previous call made it while waiting:
1141 mawt = mawts.getClosestAbove(id, level);
1142 if (null != mawt) {
1143 synchronized (db_lock) {
1144 lock();
1145 removePatchLoadingLock(plock);
1146 unlock();
1148 return mawt;
1151 // Else, create the mawt:
1152 plock.unlock();
1153 Patch.PatchImage pai = p.createTransformedImage();
1154 plock.lock();
1155 final ImageProcessor ip = pai.target;
1156 ByteProcessor alpha_mask = pai.mask; // can be null;
1157 final ByteProcessor outside_mask = pai.outside; // can be null
1158 if (null == alpha_mask) {
1159 alpha_mask = outside_mask;
1161 pai = null;
1162 if (null != alpha_mask) {
1163 mawt = createARGBImage(ip.getWidth(), ip.getHeight(),
1164 embedAlpha((int[])ip.convertToRGB().getPixels(),
1165 (byte[])alpha_mask.getPixels(),
1166 null == outside_mask ? null : (byte[])outside_mask.getPixels()));
1167 } else {
1168 mawt = ip.createImage();
1170 } catch (Exception e) {
1171 Utils.log2("Could not create an image for Patch " + p);
1172 mawt = null;
1173 } finally {
1174 plock.unlock();
1178 synchronized (db_lock) {
1179 try {
1180 lock();
1181 if (null != mawt) {
1182 mawts.put(id, mawt, level);
1183 Display.repaintSnapshot(p);
1184 //Utils.log2("Created mawt from scratch.");
1185 return mawt;
1187 } catch (Exception e) {
1188 IJError.print(e);
1189 } finally {
1190 removePatchLoadingLock(plock);
1191 unlock();
1195 return NOT_FOUND;
1198 /** Returns null.*/
1199 public ByteProcessor fetchImageMask(final Patch p) {
1200 return null;
1203 public String getAlphaPath(final Patch p) {
1204 return null;
1207 /** Does nothing unless overriden. */
1208 public void storeAlphaMask(final Patch p, final ByteProcessor fp) {}
1210 /** Does nothing unless overriden. */
1211 public boolean removeAlphaMask(final Patch p) { return false; }
1213 /** Must be called within synchronized db_lock. */
1214 private final Image fetchMipMapAWT2(final Patch p, final int level) {
1215 final long size = estimateImageFileSize(p, level);
1216 max_memory -= size;
1217 unlock();
1218 Image mawt = fetchMipMapAWT(p, level);
1219 lock();
1220 max_memory += size;
1221 return mawt;
1224 /** Simply reads from the cache, does no reloading at all. If the ImagePlus is not found in the cache, it returns null and the burden is on the calling method to do reconstruct it if necessary. This is intended for the LayerStack. */
1225 public ImagePlus getCachedImagePlus(final long id) {
1226 synchronized(db_lock) {
1227 ImagePlus imp = null;
1228 lock();
1229 imp = imps.get(id);
1230 unlock();
1231 return imp;
1235 abstract public ImagePlus fetchImagePlus(Patch p);
1236 /** Returns null unless overriden. */
1237 public ImageProcessor fetchImageProcessor(Patch p) { return null; }
1239 abstract public Object[] fetchLabel(DLabel label);
1242 /**Returns the ImagePlus as a zipped InputStream of bytes; the InputStream has to be closed by whoever is calling this method. */
1243 protected InputStream createZippedStream(ImagePlus imp) throws Exception {
1244 FileInfo fi = imp.getFileInfo();
1245 Object info = imp.getProperty("Info");
1246 if (info != null && (info instanceof String)) {
1247 fi.info = (String)info;
1249 if (null == fi.description) {
1250 fi.description = new ij.io.FileSaver(imp).getDescriptionString();
1252 //see whether this is a stack or not
1253 /* //never the case in my program
1254 if (fi.nImages > 1) {
1255 IJ.log("saving a stack!");
1256 //virtual stacks would be supported? I don't think so because the FileSaver.saveAsTiffStack(String path) doesn't.
1257 if (fi.pixels == null && imp.getStack().isVirtual()) {
1258 //don't save it!
1259 IJ.showMessage("Virtual stacks not supported.");
1260 return false;
1262 //setup stack things as in FileSaver.saveAsTiffStack(String path)
1263 fi.sliceLabels = imp.getStack().getSliceLabels();
1266 TiffEncoder te = new TiffEncoder(fi);
1267 ByteArrayInputStream i_stream = null;
1268 ByteArrayOutputStream o_bytes = new ByteArrayOutputStream();
1269 DataOutputStream o_stream = null;
1270 try {
1271 /* // works, but not significantly faster and breaks older databases (can't read zipped images properly)
1272 byte[] bytes = null;
1273 // compress in RAM
1274 o_stream = new DataOutputStream(new BufferedOutputStream(o_bytes));
1275 te.write(o_stream);
1276 o_stream.flush();
1277 o_stream.close();
1278 Deflater defl = new Deflater();
1279 byte[] unzipped_bytes = o_bytes.toByteArray();
1280 defl.setInput(unzipped_bytes);
1281 defl.finish();
1282 bytes = new byte[unzipped_bytes.length]; // this length *should* be enough
1283 int length = defl.deflate(bytes);
1284 if (length < unzipped_bytes.length) {
1285 byte[] bytes2 = new byte[length];
1286 System.arraycopy(bytes, 0, bytes2, 0, length);
1287 bytes = bytes2;
1290 // old, creates temp file
1291 o_bytes = new ByteArrayOutputStream(); // clearing
1292 ZipOutputStream zos = new ZipOutputStream(o_bytes);
1293 o_stream = new DataOutputStream(new BufferedOutputStream(zos));
1294 zos.putNextEntry(new ZipEntry(imp.getTitle()));
1295 te.write(o_stream);
1296 o_stream.flush(); //this was missing and was 1) truncating the Path images and 2) preventing the snapshots (which are very small) to be saved at all!!
1297 o_stream.close(); // this should ensure the flush above anyway. This can work because closing a ByteArrayOutputStream has no effect.
1298 byte[] bytes = o_bytes.toByteArray();
1300 //Utils.showStatus("Zipping " + bytes.length + " bytes...", false);
1301 //Utils.debug("Zipped ImagePlus byte array size = " + bytes.length);
1302 i_stream = new ByteArrayInputStream(bytes);
1303 } catch (Exception e) {
1304 Utils.log("Loader: ImagePlus NOT zipped! Problems at writing the ImagePlus using the TiffEncoder.write(dos) :\n " + e);
1305 //attempt to cleanup:
1306 try {
1307 if (null != o_stream) o_stream.close();
1308 if (null != i_stream) i_stream.close();
1309 } catch (IOException ioe) {
1310 Utils.log("Loader: Attempt to clean up streams failed.");
1311 IJError.print(ioe);
1313 return null;
1315 return i_stream;
1319 /** A dialog to open a stack, making sure there is enough memory for it. */
1320 synchronized public ImagePlus openStack() {
1321 final OpenDialog od = new OpenDialog("Select stack", OpenDialog.getDefaultDirectory(), null);
1322 String file_name = od.getFileName();
1323 if (null == file_name || file_name.toLowerCase().startsWith("null")) return null;
1324 String dir = od.getDirectory().replace('\\', '/');
1325 if (!dir.endsWith("/")) dir += "/";
1327 File f = new File(dir + file_name);
1328 if (!f.exists()) {
1329 Utils.showMessage("File " + dir + file_name + " does not exist.");
1330 return null;
1332 // avoid opening trakem2 projects
1333 if (file_name.toLowerCase().endsWith(".xml")) {
1334 Utils.showMessage("Cannot import " + file_name + " as a stack.");
1335 return null;
1337 // estimate file size: assumes an uncompressed tif, or a zipped tif with an average compression ratio of 2.5
1338 long size = f.length() / 1024; // in megabytes
1339 if (file_name.length() -4 == file_name.toLowerCase().lastIndexOf(".zip")) {
1340 size = (long)(size * 2.5); // 2.5 is a reasonable compression ratio estimate based on my experience
1342 int max_iterations = 15;
1343 while (enoughFreeMemory(size)) {
1344 if (0 == max_iterations) {
1345 // leave it to the Opener class to throw an OutOfMemoryExceptionm if so.
1346 break;
1348 max_iterations--;
1349 releaseMemory();
1351 ImagePlus imp_stack = null;
1352 try {
1353 IJ.redirectErrorMessages();
1354 imp_stack = opener.openImage(f.getCanonicalPath());
1355 } catch (Exception e) {
1356 IJError.print(e);
1357 return null;
1359 if (null == imp_stack) {
1360 Utils.showMessage("Can't open the stack.");
1361 return null;
1362 } else if (1 == imp_stack.getStackSize()) {
1363 Utils.showMessage("Not a stack!");
1364 return null;
1366 return imp_stack; // the open... command
1369 public Bureaucrat importSequenceAsGrid(Layer layer) {
1370 return importSequenceAsGrid(layer, null);
1373 public Bureaucrat importSequenceAsGrid(final Layer layer, String dir) {
1374 return importSequenceAsGrid(layer, dir, null);
1377 /** Open one of the images to find out the dimensions, and get a good guess at what is the desirable scale for doing phase- and cross-correlations with about 512x512 images. */
1378 private int getCCScaleGuess(final File images_dir, final String[] all_images) {
1379 try {
1380 if (null != all_images && all_images.length > 0) {
1381 Utils.showStatus("Opening one image ... ", false);
1382 String sdir = images_dir.getAbsolutePath().replace('\\', '/');
1383 if (!sdir.endsWith("/")) sdir += "/";
1384 IJ.redirectErrorMessages();
1385 ImagePlus imp = opener.openImage(sdir + all_images[0]);
1386 if (null != imp) {
1387 int w = imp.getWidth();
1388 int h = imp.getHeight();
1389 flush(imp);
1390 imp = null;
1391 int cc_scale = (int)((512.0 / (w > h ? w : h)) * 100);
1392 if (cc_scale > 100) return 100;
1393 return cc_scale;
1396 } catch (Exception e) {
1397 Utils.log2("Could not get an estimate for the optimal scale.");
1399 return 25;
1402 /** Import a sequence of images as a grid, and put them in the layer. If the directory (@param dir) is null, it'll be asked for. The image_file_names can be null, and in any case it's only the names, not the paths. */
1403 public Bureaucrat importSequenceAsGrid(final Layer layer, String dir, final String[] image_file_names) {
1404 try {
1406 String[] all_images = null;
1407 String file = null; // first file
1408 File images_dir = null;
1410 if (null != dir && null != image_file_names) {
1411 all_images = image_file_names;
1412 images_dir = new File(dir);
1413 } else if (null == dir) {
1414 String[] dn = Utils.selectFile("Select first image");
1415 if (null == dn) return null;
1416 dir = dn[0];
1417 file = dn[1];
1418 images_dir = new File(dir);
1419 } else {
1420 images_dir = new File(dir);
1421 if (!(images_dir.exists() && images_dir.isDirectory())) {
1422 Utils.showMessage("Something went wrong:\n\tCan't find directory " + dir);
1423 return null;
1426 if (null == image_file_names) all_images = images_dir.list(new ini.trakem2.io.ImageFileFilter("", null));
1428 if (null == file && all_images.length > 0) {
1429 file = all_images[0];
1432 int n_max = all_images.length;
1434 String preprocessor = "";
1435 int n_rows = 0;
1436 int n_cols = 0;
1437 double bx = 0;
1438 double by = 0;
1439 double bt_overlap = 0;
1440 double lr_overlap = 0;
1441 boolean link_images = true;
1442 boolean stitch_tiles = true;
1443 boolean homogenize_contrast = true;
1445 // reasonable estimate
1446 n_rows = n_cols = (int)Math.floor(Math.sqrt(n_max));
1448 GenericDialog gd = new GenericDialog("Conventions");
1449 gd.addStringField("file_name_matches: ", "");
1450 gd.addNumericField("first_image: ", 1, 0);
1451 gd.addNumericField("last_image: ", n_max, 0);
1452 gd.addCheckbox("Reverse list order", false);
1453 gd.addNumericField("number_of_rows: ", n_rows, 0);
1454 gd.addNumericField("number_of_columns: ", n_cols, 0);
1455 gd.addMessage("The top left coordinate for the imported grid:");
1456 gd.addNumericField("base_x: ", 0, 3);
1457 gd.addNumericField("base_y: ", 0, 3);
1458 gd.addMessage("Amount of image overlap, in pixels");
1459 gd.addNumericField("bottom-top overlap: ", bt_overlap, 2); //as asked by Joachim Walter
1460 gd.addNumericField("left-right overlap: ", lr_overlap, 2);
1461 gd.addCheckbox("link images", link_images);
1462 gd.addStringField("preprocess with: ", preprocessor); // the name of a plugin to use for preprocessing the images before importing, which implements PlugInFilter
1463 gd.addCheckbox("use_cross-correlation", stitch_tiles);
1464 StitchingTEM.addStitchingRuleChoice(gd);
1465 gd.addSlider("tile_overlap (%): ", 1, 100, 10);
1466 gd.addSlider("cc_scale (%):", 1, 100, getCCScaleGuess(images_dir, all_images));
1467 gd.addCheckbox("homogenize_contrast", homogenize_contrast);
1468 final Component[] c = {
1469 (Component)gd.getSliders().get(gd.getSliders().size()-2),
1470 (Component)gd.getNumericFields().get(gd.getNumericFields().size()-2),
1471 (Component)gd.getSliders().get(gd.getSliders().size()-1),
1472 (Component)gd.getNumericFields().get(gd.getNumericFields().size()-1),
1473 (Component)gd.getChoices().get(gd.getChoices().size()-1)
1475 // enable the checkbox to control the slider and its associated numeric field:
1476 Utils.addEnablerListener((Checkbox)gd.getCheckboxes().get(gd.getCheckboxes().size()-2), c, null);
1477 //gd.addCheckbox("Apply non-linear deformation", false);
1479 gd.showDialog();
1481 if (gd.wasCanceled()) return null;
1483 final String regex = gd.getNextString();
1484 Utils.log2(new StringBuffer("using regex: ").append(regex).toString()); // avoid destroying backslashes
1485 int first = (int)gd.getNextNumber();
1486 if (first < 1) first = 1;
1487 int last = (int)gd.getNextNumber();
1488 if (last < 1) last = 1;
1489 if (last < first) {
1490 Utils.showMessage("Last is smaller that first!");
1491 return null;
1494 final boolean reverse_order = gd.getNextBoolean();
1496 n_rows = (int)gd.getNextNumber();
1497 n_cols = (int)gd.getNextNumber();
1498 bx = gd.getNextNumber();
1499 by = gd.getNextNumber();
1500 bt_overlap = gd.getNextNumber();
1501 lr_overlap = gd.getNextNumber();
1502 link_images = gd.getNextBoolean();
1503 preprocessor = gd.getNextString().replace(' ', '_'); // just in case
1504 stitch_tiles = gd.getNextBoolean();
1505 float cc_percent_overlap = (float)gd.getNextNumber() / 100f;
1506 float cc_scale = (float)gd.getNextNumber() / 100f;
1507 homogenize_contrast = gd.getNextBoolean();
1508 int stitching_rule = gd.getNextChoiceIndex();
1509 //boolean apply_non_linear_def = gd.getNextBoolean();
1511 // Ensure tiles overlap if using SIFT
1512 if (StitchingTEM.FREE_RULE == stitching_rule) {
1513 if (bt_overlap <= 0) bt_overlap = 1;
1514 if (lr_overlap <= 0) lr_overlap = 1;
1517 String[] file_names = null;
1518 if (null == image_file_names) {
1519 file_names = images_dir.list(new ini.trakem2.io.ImageFileFilter(regex, null));
1520 Arrays.sort(file_names); //assumes 001, 002, 003 ... that style, since it does binary sorting of strings
1521 if (reverse_order) {
1522 // flip in place
1523 for (int i=file_names.length/2; i>-1; i--) {
1524 String tmp = file_names[i];
1525 int j = file_names.length -1 -i;
1526 file_names[i] = file_names[j];
1527 file_names[j] = tmp;
1530 } else {
1531 file_names = all_images;
1534 if (0 == file_names.length) {
1535 Utils.showMessage("No images found.");
1536 return null;
1538 // check if the selected image is in the list. Otherwise, shift selected image to the first of the included ones.
1539 boolean found_first = false;
1540 for (int i=0; i<file_names.length; i++) {
1541 if (file.equals(file_names[i])) {
1542 found_first = true;
1543 break;
1546 if (!found_first) {
1547 file = file_names[0];
1548 Utils.log("Using " + file + " as the reference image for size.");
1550 // crop list
1551 if (last > file_names.length) last = file_names.length -1;
1552 if (first < 1) first = 1;
1553 if (1 != first || last != file_names.length) {
1554 Utils.log("Cropping list.");
1555 String[] file_names2 = new String[last - first + 1];
1556 System.arraycopy(file_names, first -1, file_names2, 0, file_names2.length);
1557 file_names = file_names2;
1559 // should be multiple of rows and cols
1560 if (file_names.length != n_rows * n_cols) {
1561 Utils.log2("n_images:" + file_names.length + " rows,cols : " + n_rows + "," + n_cols + " total=" + n_rows*n_cols);
1562 Utils.showMessage("rows * cols does not match with the number of selected images.");
1563 return null;
1565 // put in columns
1566 ArrayList cols = new ArrayList();
1567 for (int i=0; i<n_cols; i++) {
1568 String[] col = new String[n_rows];
1569 for (int j=0; j<n_rows; j++) {
1570 col[j] = file_names[j*n_cols + i];
1572 cols.add(col);
1575 return insertGrid(layer, dir, file, file_names.length, cols, bx, by, bt_overlap, lr_overlap, link_images, preprocessor, stitch_tiles, cc_percent_overlap, cc_scale, homogenize_contrast, stitching_rule/*, apply_non_linear_def*/);
1577 } catch (Exception e) {
1578 IJError.print(e);
1580 return null;
1583 private ImagePlus preprocess(String preprocessor, ImagePlus imp, String path) {
1584 if (null == imp) return null;
1585 try {
1586 startSetTempCurrentImage(imp);
1587 IJ.redirectErrorMessages();
1588 Object ob = IJ.runPlugIn(preprocessor, "[path=" + path + "]");
1589 ImagePlus pp_imp = WindowManager.getCurrentImage();
1590 if (null != pp_imp) {
1591 finishSetTempCurrentImage();
1592 return pp_imp;
1593 } else {
1594 // discard this image
1595 Utils.log("Ignoring " + imp.getTitle() + " from " + path + " since the preprocessor " + preprocessor + " returned null on it.");
1596 flush(imp);
1597 finishSetTempCurrentImage();
1598 return null;
1600 } catch (Exception e) {
1601 IJError.print(e);
1602 finishSetTempCurrentImage();
1603 Utils.log("Ignoring " + imp.getTitle() + " from " + path + " since the preprocessor " + preprocessor + " throwed an Exception on it.");
1604 flush(imp);
1606 return null;
1609 public Bureaucrat importGrid(Layer layer) {
1610 return importGrid(layer, null);
1613 /** Import a grid of images and put them in the layer. If the directory (@param dir) is null, it'll be asked for. */
1614 public Bureaucrat importGrid(Layer layer, String dir) {
1615 try {
1616 String file = null;
1617 if (null == dir) {
1618 String[] dn = Utils.selectFile("Select first image");
1619 if (null == dn) return null;
1620 dir = dn[0];
1621 file = dn[1];
1624 String convention = "cdd"; // char digit digit
1625 boolean chars_are_columns = true;
1626 // examine file name
1628 if (file.matches("\\A[a-zA-Z]\\d\\d.*")) { // one letter, 2 numbers
1629 //means:
1630 // \A - beggining of input
1631 // [a-zA-Z] - any letter upper or lower case
1632 // \d\d - two consecutive digits
1633 // .* - any row of chars
1634 ini_grid_convention = true;
1637 // ask for chars->rows, numbers->columns or viceversa
1638 GenericDialog gd = new GenericDialog("Conventions");
1639 gd.addStringField("file_name_contains:", "");
1640 gd.addNumericField("base_x: ", 0, 3);
1641 gd.addNumericField("base_y: ", 0, 3);
1642 gd.addMessage("Use: x(any), c(haracter), d(igit)");
1643 gd.addStringField("convention: ", convention);
1644 final String[] cr = new String[]{"columns", "rows"};
1645 gd.addChoice("characters are: ", cr, cr[0]);
1646 gd.addMessage("[File extension ignored]");
1648 gd.addNumericField("bottom-top overlap: ", 0, 3); //as asked by Joachim Walter
1649 gd.addNumericField("left-right overlap: ", 0, 3);
1650 gd.addCheckbox("link_images", false);
1651 gd.addStringField("Preprocess with: ", ""); // the name of a plugin to use for preprocessing the images before importing, which implements Preprocess
1652 gd.addCheckbox("use_cross-correlation", false);
1653 StitchingTEM.addStitchingRuleChoice(gd);
1654 gd.addSlider("tile_overlap (%): ", 1, 100, 10);
1655 gd.addSlider("cc_scale (%):", 1, 100, 25);
1656 gd.addCheckbox("homogenize_contrast", true);
1657 final Component[] c = {
1658 (Component)gd.getSliders().get(gd.getSliders().size()-2),
1659 (Component)gd.getNumericFields().get(gd.getNumericFields().size()-2),
1660 (Component)gd.getSliders().get(gd.getSliders().size()-1),
1661 (Component)gd.getNumericFields().get(gd.getNumericFields().size()-1),
1662 (Component)gd.getChoices().get(gd.getChoices().size()-1)
1664 // enable the checkbox to control the slider and its associated numeric field:
1665 Utils.addEnablerListener((Checkbox)gd.getCheckboxes().get(gd.getCheckboxes().size()-1), c, null);
1666 //gd.addCheckbox("Apply non-linear deformation", false);
1668 gd.showDialog();
1669 if (gd.wasCanceled()) {
1670 return null;
1672 //collect data
1673 String regex = gd.getNextString(); // filter away files not containing this tag
1674 // the base x,y of the whole grid
1675 double bx = gd.getNextNumber();
1676 double by = gd.getNextNumber();
1677 //if (!ini_grid_convention) {
1678 convention = gd.getNextString().toLowerCase();
1680 if (/*!ini_grid_convention && */ (null == convention || convention.equals("") || -1 == convention.indexOf('c') || -1 == convention.indexOf('d'))) { // TODO check that the convention has only 'cdx' chars and also that there is an island of 'c's and of 'd's only.
1681 Utils.showMessage("Convention '" + convention + "' needs both c(haracters) and d(igits), optionally 'x', and nothing else!");
1682 return null;
1684 chars_are_columns = (0 == gd.getNextChoiceIndex());
1685 double bt_overlap = gd.getNextNumber();
1686 double lr_overlap = gd.getNextNumber();
1687 boolean link_images = gd.getNextBoolean();
1688 String preprocessor = gd.getNextString();
1689 boolean stitch_tiles = gd.getNextBoolean();
1690 float cc_percent_overlap = (float)gd.getNextNumber() / 100f;
1691 float cc_scale = (float)gd.getNextNumber() / 100f;
1692 boolean homogenize_contrast = gd.getNextBoolean();
1693 int stitching_rule = gd.getNextChoiceIndex();
1694 //boolean apply_non_linear_def = gd.getNextBoolean();
1696 // Ensure tiles overlap if using SIFT
1697 if (StitchingTEM.FREE_RULE == stitching_rule) {
1698 if (bt_overlap <= 0) bt_overlap = 1;
1699 if (lr_overlap <= 0) lr_overlap = 1;
1702 //start magic
1703 //get ImageJ-openable files that comply with the convention
1704 File images_dir = new File(dir);
1705 if (!(images_dir.exists() && images_dir.isDirectory())) {
1706 Utils.showMessage("Something went wrong:\n\tCan't find directory " + dir);
1707 return null;
1709 String[] file_names = images_dir.list(new ImageFileFilter(regex, convention));
1710 if (null == file && file_names.length > 0) {
1711 // the 'selected' file
1712 file = file_names[0];
1714 Utils.showStatus("Adding " + file_names.length + " patches.", false);
1715 if (0 == file_names.length) {
1716 Utils.log("Zero files match the convention '" + convention + "'");
1717 return null;
1720 // How to: select all files, and order their names in a double array as they should be placed in the Display. Then place them, displacing by offset, and resizing if necessary.
1721 // gather image files:
1722 Montage montage = new Montage(convention, chars_are_columns);
1723 montage.addAll(file_names);
1724 ArrayList cols = montage.getCols(); // an array of Object[] arrays, of unequal length maybe, each containing a column of image file names
1725 return insertGrid(layer, dir, file, file_names.length, cols, bx, by, bt_overlap, lr_overlap, link_images, preprocessor, stitch_tiles, cc_percent_overlap, cc_scale, homogenize_contrast, stitching_rule/*, apply_non_linear_def*/);
1727 } catch (Exception e) {
1728 IJError.print(e);
1730 return null;
1734 * @param layer The Layer to inser the grid into
1735 * @param dir The base dir of the images to open
1736 * @param cols The list of columns, containing each an array of String file names in each column.
1737 * @param bx The top-left X coordinate of the grid to insert
1738 * @param by The top-left Y coordinate of the grid to insert
1739 * @param bt_overlap bottom-top overlap of the images
1740 * @param lr_overlap left-right overlap of the images
1741 * @param link_images Link images to their neighbors.
1742 * @param preproprecessor The name of a PluginFilter in ImageJ's plugin directory, to be called on every image prior to insertion.
1744 private Bureaucrat insertGrid(final Layer layer, final String dir_, final String first_image_name, final int n_images, final ArrayList cols, final double bx, final double by, final double bt_overlap, final double lr_overlap, final boolean link_images, final String preprocessor, final boolean stitch_tiles, final float cc_percent_overlap, final float cc_scale, final boolean homogenize_contrast, final int stitching_rule/*, final boolean apply_non_linear_def*/) {
1746 // create a Worker, then give it to the Bureaucrat
1748 Worker worker = new Worker("Inserting grid") {
1749 public void run() {
1750 startedWorking();
1752 try {
1753 String dir = dir_;
1754 ArrayList al = new ArrayList();
1755 setMassiveMode(true);//massive_mode = true;
1756 Utils.showProgress(0.0D);
1757 opener.setSilentMode(true); // less repaints on IJ status bar
1759 Utils.log2("Preprocessor plugin: " + preprocessor);
1760 boolean preprocess = null != preprocessor && preprocessor.length() > 0;
1761 if (preprocess) {
1762 // check the given plugin
1763 IJ.redirectErrorMessages();
1764 startSetTempCurrentImage(null);
1765 try {
1766 Object ob = IJ.runPlugIn(preprocessor, "");
1767 if (!(ob instanceof PlugInFilter)) {
1768 Utils.showMessageT("Plug in " + preprocessor + " is invalid: does not implement interface PlugInFilter");
1769 finishSetTempCurrentImage();
1770 return;
1772 finishSetTempCurrentImage();
1773 } catch (Exception e) {
1774 IJError.print(e);
1775 finishSetTempCurrentImage();
1776 Utils.showMessageT("Plug in " + preprocessor + " is invalid: ImageJ has trhown an exception when testing it with a null image.");
1777 return;
1781 /* If requested, ask for a text file containing the non-linear deformation coefficients
1782 * and obtain a NonLinearTransform object and coefficients to apply to all images. */
1784 // NOT READY YET
1785 final NonLinearTransform nlt = apply_non_linear_def ? askForNonLinearTransform() : null;
1786 final double[][] nlt_coeffs = null != nlt ? nlt.getCoefficients() : null;
1788 if (apply_non_linear_def && null == nlt) {
1789 finishedWorking();
1790 return;
1795 int x = 0;
1796 int y = 0;
1797 int largest_y = 0;
1798 ImagePlus img = null;
1799 // open the selected image, to use as reference for width and height
1800 if (!enoughFreeMemory(MIN_FREE_BYTES)) releaseMemory();
1801 dir = dir.replace('\\', '/'); // w1nd0wz safe
1802 if (!dir.endsWith("/")) dir += "/";
1803 String path = dir + first_image_name;
1804 IJ.redirectErrorMessages();
1805 ImagePlus first_img = opener.openImage(path);
1806 if (null == first_img) {
1807 Utils.log("Selected image to open first is null.");
1808 return;
1811 if (preprocess) first_img = preprocess(preprocessor, first_img, path);
1812 else preProcess(first_img); // the system wide, if any
1813 if (null == first_img) return;
1814 final int first_image_width = first_img.getWidth();
1815 final int first_image_height = first_img.getHeight();
1816 final int first_image_type = first_img.getType();
1817 // start
1818 final Patch[][] pall = new Patch[cols.size()][((String[])cols.get(0)).length];
1819 int width, height;
1820 int k = 0; //counter
1821 boolean auto_fix_all = false;
1822 boolean ignore_all = false;
1823 boolean resize = false;
1824 if (!ControlWindow.isGUIEnabled()) {
1825 // headless mode: autofix all
1826 auto_fix_all = true;
1827 resize = true;
1830 startLargeUpdate();
1831 for (int i=0; i<cols.size(); i++) {
1832 String[] rows = (String[])cols.get(i);
1833 if (i > 0) {
1834 x -= lr_overlap;
1836 for (int j=0; j<rows.length; j++) {
1837 if (this.quit) {
1838 Display.repaint(layer);
1839 rollback();
1840 return;
1842 if (j > 0) {
1843 y -= bt_overlap;
1845 // get file name
1846 String file_name = (String)rows[j];
1847 path = dir + file_name;
1848 if (null != first_img && file_name.equals(first_image_name)) {
1849 img = first_img;
1850 first_img = null; // release pointer
1851 } else {
1852 // open image
1853 //if (!enoughFreeMemory(MIN_FREE_BYTES)) releaseMemory(); // UNSAFE, doesn't wait for GC
1854 releaseToFit(first_image_width, first_image_height, first_image_type, 1.5f);
1855 try {
1856 IJ.redirectErrorMessages();
1857 img = opener.openImage(path);
1858 } catch (OutOfMemoryError oome) {
1859 printMemState();
1860 throw oome;
1862 // Preprocess ImagePlus
1863 if (preprocess) {
1864 img = preprocess(preprocessor, img, path);
1865 if (null == img) continue;
1866 } else {
1867 // use standard project wide , if any
1868 preProcess(img);
1871 if (null == img) {
1872 Utils.log("null image! skipping.");
1873 pall[i][j] = null;
1874 continue;
1877 width = img.getWidth();
1878 height = img.getHeight();
1879 int rw = width;
1880 int rh = height;
1881 if (width != first_image_width || height != first_image_height) {
1882 int new_width = first_image_width;
1883 int new_height = first_image_height;
1884 if (!auto_fix_all && !ignore_all) {
1885 GenericDialog gdr = new GenericDialog("Size mismatch!");
1886 gdr.addMessage("The size of " + file_name + " is " + width + " x " + height);
1887 gdr.addMessage("but the selected image was " + first_image_width + " x " + first_image_height);
1888 gdr.addMessage("Adjust to selected image dimensions?");
1889 gdr.addNumericField("width: ", (double)first_image_width, 0);
1890 gdr.addNumericField("height: ", (double)first_image_height, 0); // should not be editable ... or at least, explain in some way that the dimensions can be edited just for this image --> done below
1891 gdr.addMessage("[If dimensions are changed they will apply only to this image]");
1892 gdr.addMessage("");
1893 String[] au = new String[]{"fix all", "ignore all"};
1894 gdr.addChoice("Automate:", au, au[1]);
1895 gdr.addMessage("Cancel == NO OK = YES");
1896 gdr.showDialog();
1897 if (gdr.wasCanceled()) {
1898 resize = false;
1899 // do nothing: don't fix/resize
1901 resize = true;
1902 //catch values
1903 new_width = (int)gdr.getNextNumber();
1904 new_height = (int)gdr.getNextNumber();
1905 int iau = gdr.getNextChoiceIndex();
1906 if (new_width != first_image_width || new_height != first_image_height) {
1907 auto_fix_all = false;
1908 } else {
1909 auto_fix_all = (0 == iau);
1911 ignore_all = (1 == iau);
1912 if (ignore_all) resize = false;
1914 if (resize) {
1915 //resize Patch dimensions
1916 rw = first_image_width;
1917 rh = first_image_height;
1921 //add new Patch at base bx,by plus the x,y of the grid
1922 Patch patch = new Patch(layer.getProject(), img.getTitle(), bx + x, by + y, img); // will call back and cache the image
1923 if (width != rw || height != rh) patch.setDimensions(rw, rh, false);
1924 //if (null != nlt_coeffs) patch.setNonLinearCoeffs(nlt_coeffs);
1925 addedPatchFrom(path, patch);
1926 if (homogenize_contrast) setMipMapsRegeneration(false); // prevent it
1927 else generateMipMaps(patch);
1929 layer.add(patch, true); // after the above two lines! Otherwise it will paint fine, but throw exceptions on the way
1930 patch.updateInDatabase("tiff_snapshot"); // otherwise when reopening it has to fetch all ImagePlus and scale and zip them all! This method though creates the awt and the snap, thus filling up memory and slowing down, but it's worth it.
1931 pall[i][j] = patch;
1933 al.add(patch);
1934 if (ControlWindow.isGUIEnabled()) {
1935 layer.getParent().enlargeToFit(patch, LayerSet.NORTHWEST); // northwest to prevent screwing up Patch coordinates.
1937 y += img.getHeight();
1938 Utils.showProgress((double)k / n_images);
1939 k++;
1941 x += img.getWidth();
1942 if (largest_y < y) {
1943 largest_y = y;
1945 y = 0; //resetting!
1948 // build list
1949 final Patch[] pa = new Patch[al.size()];
1950 int f = 0;
1951 // list in row-first order
1952 for (int j=0; j<pall[0].length; j++) { // 'j' is row
1953 for (int i=0; i<pall.length; i++) { // 'i' is column
1954 pa[f++] = pall[i][j];
1957 // optimize repaints: all to background image
1958 Display.clearSelection(layer);
1960 // make the first one be top, and the rest under it in left-right and top-bottom order
1961 for (int j=0; j<pa.length; j++) {
1962 layer.moveBottom(pa[j]);
1965 // make picture
1966 //getFlatImage(layer, layer.getMinimalBoundingBox(Patch.class), 0.25, 1, ImagePlus.GRAY8, Patch.class, null, false).show();
1968 // optimize repaints: all to background image
1969 Display.clearSelection(layer);
1971 if (homogenize_contrast) {
1972 setTaskName("Enhancing contrast");
1973 // 0 - check that all images are of the same type
1974 int tmp_type = pa[0].getType();
1975 for (int e=1; e<pa.length; e++) {
1976 if (pa[e].getType() != tmp_type) {
1977 // can't continue
1978 tmp_type = Integer.MAX_VALUE;
1979 Utils.log("Can't homogenize histograms: images are not all of the same type.\nFirst offending image is: " + al.get(e));
1980 break;
1983 if (Integer.MAX_VALUE != tmp_type) { // checking on error flag
1984 // Set min and max for all images
1985 // 1 - fetch statistics for each image
1986 final ArrayList al_st = new ArrayList();
1987 final ArrayList al_p = new ArrayList(); // list of Patch ordered by stdDev ASC
1988 int type = -1;
1989 releaseMemory(); // need some to operate
1990 for (int i=0; i<pa.length; i++) {
1991 if (this.quit) {
1992 Display.repaint(layer);
1993 rollback();
1994 return;
1996 ImagePlus imp = fetchImagePlus(pa[i]);
1997 // speed-up trick: extract data from smaller image
1998 if (imp.getWidth() > 1024) {
1999 releaseToFit(1024, (int)((imp.getHeight() * 1024) / imp.getWidth()), imp.getType(), 1.1f);
2000 // cheap and fast nearest-point resizing
2001 imp = new ImagePlus(imp.getTitle(), imp.getProcessor().resize(1024));
2003 if (-1 == type) type = imp.getType();
2004 ImageStatistics i_st = imp.getStatistics();
2005 // order by stdDev, from small to big
2006 int q = 0;
2007 for (Iterator it = al_st.iterator(); it.hasNext(); ) {
2008 ImageStatistics st = (ImageStatistics)it.next();
2009 q++;
2010 if (st.stdDev > i_st.stdDev) break;
2012 if (q == al.size()) {
2013 al_st.add(i_st); // append at the end. WARNING if importing thousands of images, this is a potential source of out of memory errors. I could just recompute it when I needed it again below
2014 al_p.add(pa[i]);
2015 } else {
2016 al_st.add(q, i_st);
2017 al_p.add(q, pa[i]);
2020 final ArrayList al_p2 = (ArrayList)al_p.clone(); // shallow copy of the ordered list
2021 // 2 - discard the first and last 25% (TODO: a proper histogram clustering analysis and histogram examination should apply here)
2022 if (pa.length > 3) { // under 4 images, use them all
2023 int i=0;
2024 while (i <= pa.length * 0.25) {
2025 al_p.remove(i);
2026 i++;
2028 int count = i;
2029 i = pa.length -1 -count;
2030 while (i > (pa.length* 0.75) - count) {
2031 al_p.remove(i);
2032 i--;
2035 // 3 - compute common histogram for the middle 50% images
2036 final Patch[] p50 = new Patch[al_p.size()];
2037 al_p.toArray(p50);
2038 StackStatistics stats = new StackStatistics(new PatchStack(p50, 1));
2039 int n = 1;
2040 switch (type) {
2041 case ImagePlus.GRAY16:
2042 case ImagePlus.GRAY32:
2043 n = 2;
2044 break;
2046 // 4 - compute autoAdjust min and max values
2047 // extracting code from ij.plugin.frame.ContrastAdjuster, method autoAdjust
2048 int autoThreshold = 0;
2049 double min = 0;
2050 double max = 0;
2051 // once for 8-bit and color, twice for 16 and 32-bit (thus the 2501 autoThreshold value)
2052 int limit = stats.pixelCount/10;
2053 int[] histogram = stats.histogram;
2054 //if (autoThreshold<10) autoThreshold = 5000;
2055 //else autoThreshold /= 2;
2056 if (ImagePlus.GRAY16 == type || ImagePlus.GRAY32 == type) autoThreshold = 2500;
2057 else autoThreshold = 5000;
2058 int threshold = stats.pixelCount / autoThreshold;
2059 int i = -1;
2060 boolean found = false;
2061 int count;
2062 do {
2063 i++;
2064 count = histogram[i];
2065 if (count>limit) count = 0;
2066 found = count > threshold;
2067 } while (!found && i<255);
2068 int hmin = i;
2069 i = 256;
2070 do {
2071 i--;
2072 count = histogram[i];
2073 if (count > limit) count = 0;
2074 found = count > threshold;
2075 } while (!found && i>0);
2076 int hmax = i;
2077 if (hmax >= hmin) {
2078 min = stats.histMin + hmin*stats.binSize;
2079 max = stats.histMin + hmax*stats.binSize;
2080 if (min == max) {
2081 min = stats.min;
2082 max = stats.max;
2085 // 5 - compute common mean within min,max range
2086 double target_mean = getMeanOfRange(stats, min, max);
2087 Utils.log2("Loader min,max: " + min + ", " + max + ", target mean: " + target_mean);
2088 // 6 - apply to all
2089 for (i=al_p2.size()-1; i>-1; i--) {
2090 Patch p = (Patch)al_p2.get(i); // the order is different, thus getting it from the proper list
2091 double dm = target_mean - getMeanOfRange((ImageStatistics)al_st.get(i), min, max);
2092 p.setMinAndMax(min - dm, max - dm); // displacing in the opposite direction, makes sense, so that the range is drifted upwards and thus the target 256 range for an awt.Image will be closer to the ideal target_mean
2093 // OBSOLETE and wrong //p.putMinAndMax(fetchImagePlus(p));
2096 if (isMipMapsEnabled()) {
2097 setTaskName("Regenerating snapshots.");
2098 // recreate files
2099 Utils.log2("Generating mipmaps for " + al.size() + " patches.");
2100 Thread t = generateMipMaps(al, false);
2101 if (null != t) try { t.join(); } catch (InterruptedException ie) {}
2103 // 7 - flush away any existing awt images, so that they'll be recreated with the new min and max
2104 synchronized (db_lock) {
2105 lock();
2106 for (i=0; i<pa.length; i++) {
2107 mawts.removeAndFlush(pa[i].getId());
2108 Utils.log2(i + "removing mawt for " + pa[i].getId());
2110 unlock();
2112 setMipMapsRegeneration(true);
2113 Display.repaint(layer, new Rectangle(0, 0, (int)layer.getParent().getLayerWidth(), (int)layer.getParent().getLayerHeight()), 0);
2115 // make picture
2116 //getFlatImage(layer, layer.getMinimalBoundingBox(Patch.class), 0.25, 1, ImagePlus.GRAY8, Patch.class, null, false).show();
2120 if (stitch_tiles) {
2121 setTaskName("stitching tiles");
2122 // create undo
2123 layer.getParent().addTransformStep(new HashSet<Displayable>(layer.getDisplayables(Patch.class)));
2124 // wait until repainting operations have finished (otherwise, calling crop on an ImageProcessor fails with out of bounds exception sometimes)
2125 if (null != Display.getFront()) Display.getFront().getCanvas().waitForRepaint();
2126 Bureaucrat task = StitchingTEM.stitch(pa, cols.size(), cc_percent_overlap, cc_scale, bt_overlap, lr_overlap, true, stitching_rule);
2127 if (null != task) try { task.join(); } catch (Exception e) {}
2130 // link with images on top, bottom, left and right.
2131 if (link_images) {
2132 for (int i=0; i<pall.length; i++) { // 'i' is column
2133 for (int j=0; j<pall[0].length; j++) { // 'j' is row
2134 Patch p = pall[i][j];
2135 if (null == p) continue; // can happen if a slot is empty
2136 if (i>0 && null != pall[i-1][j]) p.link(pall[i-1][j]);
2137 if (i<pall.length -1 && null != pall[i+1][j]) p.link(pall[i+1][j]);
2138 if (j>0 && null != pall[i][j-1]) p.link(pall[i][j-1]);
2139 if (j<pall[0].length -1 && null != pall[i][j+1]) p.link(pall[i][j+1]);
2144 commitLargeUpdate();
2146 // resize LayerSet
2147 int new_width = x;
2148 int new_height = largest_y;
2149 layer.getParent().setMinimumDimensions(); //Math.abs(bx) + new_width, Math.abs(by) + new_height);
2150 // update indexes
2151 layer.updateInDatabase("stack_index"); // so its done once only
2152 // create panels in all Displays showing this layer
2153 /* // not needed anymore
2154 Iterator it = al.iterator();
2155 while (it.hasNext()) {
2156 Display.add(layer, (Displayable)it.next(), false); // don't set it active, don't want to reload the ImagePlus!
2159 // update Displays
2160 Display.update(layer);
2162 //reset Loader mode
2163 setMassiveMode(false);//massive_mode = false;
2165 layer.recreateBuckets();
2167 //debug:
2168 } catch (Throwable t) {
2169 IJError.print(t);
2170 rollback();
2171 setMassiveMode(false); //massive_mode = false;
2172 setMipMapsRegeneration(true);
2174 finishedWorking();
2176 }// end of run method
2179 // watcher thread
2180 return Bureaucrat.createAndStart(worker, layer.getProject());
2183 public Bureaucrat importImages(final Layer ref_layer) {
2184 return importImages(ref_layer, null, null, 0, 0);
2187 /** Import images from the given text file, which is expected to contain 4 columns:<br />
2188 * - column 1: image file path (if base_dir is not null, it will be prepended)<br />
2189 * - column 2: x coord<br />
2190 * - column 3: y coord<br />
2191 * - column 4: z coord (layer_thickness will be multiplied to it if not zero)<br />
2193 * Layers will be automatically created as needed inside the LayerSet to which the given ref_layer belongs.. <br />
2194 * The text file can contain comments that start with the # sign.<br />
2195 * Images will be imported in parallel, using as many cores as your machine has.<br />
2196 * The @param calibration transforms the read coordinates into pixel coordinates, including x,y,z, and layer thickness.
2198 public Bureaucrat importImages(Layer ref_layer, String abs_text_file_path_, String column_separator_, double layer_thickness_, double calibration_) {
2199 // check parameters: ask for good ones if necessary
2200 if (null == abs_text_file_path_) {
2201 String[] file = Utils.selectFile("Select text file");
2202 if (null == file) return null; // user canceled dialog
2203 abs_text_file_path_ = file[0] + file[1];
2205 if (null == ref_layer || null == column_separator_ || 0 == column_separator_.length() || Double.isNaN(layer_thickness_) || layer_thickness_ <= 0 || Double.isNaN(calibration_) || calibration_ <= 0) {
2206 GenericDialog gdd = new GenericDialog("Options");
2207 String[] separators = new String[]{"tab", "space", "coma (,)"};
2208 gdd.addMessage("Choose a layer to act as the zero for the Z coordinates:");
2209 Utils.addLayerChoice("Base layer", ref_layer, gdd);
2210 gdd.addChoice("Column separator: ", separators, separators[0]);
2211 gdd.addNumericField("Layer thickness: ", 60, 2); // default: 60 nm
2212 gdd.addNumericField("Calibration (data to pixels): ", 1, 2);
2213 gdd.showDialog();
2214 if (gdd.wasCanceled()) return null;
2215 layer_thickness_ = gdd.getNextNumber();
2216 if (layer_thickness_ < 0 || Double.isNaN(layer_thickness_)) {
2217 Utils.log("Improper layer thickness value.");
2218 return null;
2220 calibration_ = gdd.getNextNumber();
2221 if (0 == calibration_ || Double.isNaN(calibration_)) {
2222 Utils.log("Improper calibration value.");
2223 return null;
2225 ref_layer = ref_layer.getParent().getLayer(gdd.getNextChoiceIndex());
2226 column_separator_ = "\t";
2227 switch (gdd.getNextChoiceIndex()) {
2228 case 1:
2229 column_separator_ = " ";
2230 break;
2231 case 2:
2232 column_separator_ = ",";
2233 break;
2234 default:
2235 break;
2239 // make vars accessible from inner threads:
2240 final Layer base_layer = ref_layer;
2241 final String abs_text_file_path = abs_text_file_path_;
2242 final String column_separator = column_separator_;
2243 final double layer_thickness = layer_thickness_;
2244 final double calibration = calibration_;
2247 GenericDialog gd = new GenericDialog("Options");
2248 gd.addMessage("For all touched layers:");
2249 gd.addCheckbox("Homogenize histograms", false);
2250 gd.addCheckbox("Register tiles and layers", true);
2251 gd.addCheckbox("With overlapping tiles only", true); // TODO could also use near tiles, defining near as "within a radius of one image width from the center of the tile"
2252 final Component[] c_enable = {
2253 (Component)gd.getCheckboxes().get(2)
2255 Utils.addEnablerListener((Checkbox)gd.getCheckboxes().get(1), c_enable, null);
2256 //gd.addCheckbox("Apply non-linear deformation", false);
2257 gd.showDialog();
2258 if (gd.wasCanceled()) return null;
2259 final boolean homogenize_contrast = gd.getNextBoolean();
2260 final boolean register_tiles = gd.getNextBoolean();
2261 final boolean overlapping_only = gd.getNextBoolean();
2262 final int layer_subset = gd.getNextChoiceIndex();
2263 //final boolean apply_non_linear_def = gd.getNextBoolean();
2264 final Set touched_layers = Collections.synchronizedSet(new HashSet());
2265 gd = null;
2268 /* If requested, ask for a text file containing the non-linear deformation coefficients
2269 * and obtain a NonLinearTransform object and coefficients to apply to all images. */
2271 // NOT READY YET
2272 final NonLinearTransform nlt = apply_non_linear_def ? askForNonLinearTransform() : null;
2273 final double[][] nlt_coeffs = null != nlt ? nlt.getCoefficients() : null;
2275 if (apply_non_linear_def && null == nlt) {
2276 return null;
2281 final Worker worker = new Worker("Importing images") {
2282 public void run() {
2283 startedWorking();
2284 final Worker wo = this;
2285 try {
2286 // 1 - read text file
2287 final String[] lines = Utils.openTextFileLines(abs_text_file_path);
2288 if (null == lines || 0 == lines.length) {
2289 Utils.log2("No images to import from " + abs_text_file_path);
2290 finishedWorking();
2291 return;
2293 final String sep2 = column_separator + column_separator;
2294 // 2 - set a base dir path if necessary
2295 final String[] base_dir = new String[]{null, null}; // second item will work as flag if the dialog to ask for a directory is canceled in any of the threads.
2297 ///////// Multithreading ///////
2298 final AtomicInteger ai = new AtomicInteger(0);
2299 final Thread[] threads = MultiThreading.newThreads();
2301 final Lock lock = new Lock();
2302 final LayerSet layer_set = base_layer.getParent();
2303 final double z_zero = base_layer.getZ();
2304 final AtomicInteger n_imported = new AtomicInteger(0);
2306 for (int ithread = 0; ithread < threads.length; ++ithread) {
2307 threads[ithread] = new Thread() {
2308 public void run() {
2309 setPriority(Thread.NORM_PRIORITY);
2310 ///////////////////////////////
2312 // 3 - parse each line
2313 for (int i = ai.getAndIncrement(); i < lines.length; i = ai.getAndIncrement()) {
2314 if (wo.hasQuitted()) return;
2315 // process line
2316 String line = lines[i].replace('\\','/').trim(); // first thing is the backslash removal, before they get processed at all
2317 int ic = line.indexOf('#');
2318 if (-1 != ic) line = line.substring(0, ic); // remove comment at end of line if any
2319 if (0 == line.length() || '#' == line.charAt(0)) continue;
2320 // reduce line, so that separators are really unique
2321 while (-1 != line.indexOf(sep2)) {
2322 line = line.replaceAll(sep2, column_separator);
2324 String[] column = line.split(column_separator);
2325 if (column.length < 4) {
2326 Utils.log("Less than 4 columns: can't import from line " + i + " : " + line);
2327 continue;
2329 // obtain coordinates
2330 double x=0,
2331 y=0,
2332 z=0;
2333 try {
2334 x = Double.parseDouble(column[1].trim());
2335 y = Double.parseDouble(column[2].trim());
2336 z = Double.parseDouble(column[3].trim());
2337 } catch (NumberFormatException nfe) {
2338 Utils.log("Non-numeric value in a numeric column at line " + i + " : " + line);
2339 continue;
2341 x *= calibration;
2342 y *= calibration;
2343 z = z * calibration + z_zero;
2344 // obtain path
2345 String path = column[0].trim();
2346 if (0 == path.length()) continue;
2347 // check if path is relative
2348 if ((!IJ.isWindows() && '/' != path.charAt(0)) || (IJ.isWindows() && 1 != path.indexOf(":/"))) {
2349 synchronized (lock) {
2350 lock.lock();
2351 if ("QUIT".equals(base_dir[1])) {
2352 // dialog to ask for directory was quitted
2353 lock.unlock();
2354 finishedWorking();
2355 return;
2357 // path is relative.
2358 if (null == base_dir[0]) { // may not be null if another thread that got the lock first set it to non-null
2359 // Ask for source directory
2360 DirectoryChooser dc = new DirectoryChooser("Choose source directory");
2361 String dir = dc.getDirectory();
2362 if (null == dir) {
2363 // quit all threads
2364 base_dir[1] = "QUIT";
2365 lock.unlock();
2366 finishedWorking();
2367 return;
2369 // else, set the base dir
2370 base_dir[0] = dir.replace('\\', '/');
2371 if (!base_dir[0].endsWith("/")) base_dir[0] += "/";
2373 lock.unlock();
2376 if (null != base_dir[0]) path = base_dir[0] + path;
2377 File f = new File(path);
2378 if (!f.exists()) {
2379 Utils.log("No file found for path " + path);
2380 continue;
2382 synchronized (db_lock) {
2383 lock();
2384 releaseMemory(); //ensures a usable minimum is free
2385 unlock();
2387 /* */
2388 IJ.redirectErrorMessages();
2389 ImagePlus imp = opener.openImage(path);
2390 if (null == imp) {
2391 Utils.log("Ignoring unopenable image from " + path);
2392 continue;
2394 // add Patch and generate its mipmaps
2395 Patch patch = null;
2396 Layer layer = null;
2397 synchronized (lock) {
2398 try {
2399 lock.lock();
2400 layer = layer_set.getLayer(z, layer_thickness, true); // will create a new Layer if necessary
2401 touched_layers.add(layer);
2402 patch = new Patch(layer.getProject(), imp.getTitle(), x, y, imp);
2403 //if (null != nlt_coeffs) patch.setNonLinearCoeffs(nlt_coeffs);
2404 addedPatchFrom(path, patch);
2405 } catch (Exception e) {
2406 IJError.print(e);
2407 } finally {
2408 lock.unlock();
2411 if (null != patch) {
2412 if (!generateMipMaps(patch)) {
2413 Utils.log("Failed to generate mipmaps for " + patch);
2415 synchronized (lock) {
2416 try {
2417 lock.lock();
2418 layer.add(patch, true);
2419 } catch (Exception e) {
2420 IJError.print(e);
2421 } finally {
2422 lock.unlock();
2425 decacheImagePlus(patch.getId()); // no point in keeping it around
2428 wo.setTaskName("Imported " + (n_imported.getAndIncrement() + 1) + "/" + lines.length);
2431 /////////////////////////
2435 MultiThreading.startAndJoin(threads);
2436 /////////////////////////
2438 if (0 == n_imported.get()) {
2439 Utils.log("No images imported.");
2440 finishedWorking();
2441 return;
2444 base_layer.getParent().setMinimumDimensions();
2445 Display.repaint(base_layer.getParent());
2447 final Layer[] la = new Layer[touched_layers.size()];
2448 touched_layers.toArray(la);
2450 if (homogenize_contrast) {
2451 setTaskName("");
2452 // layer-wise (layer order is irrelevant):
2453 Thread t = homogenizeContrast(la); // multithreaded
2454 if (null != t) t.join();
2456 if (register_tiles) {
2457 wo.setTaskName("Registering tiles.");
2458 // sequential, from first to last layer
2459 Layer first = la[0];
2460 Layer last = la[0];
2461 // order touched layers by Z coord
2462 for (int i=1; i<la.length; i++) {
2463 if (la[i].getZ() < first.getZ()) first = la[i];
2464 if (la[i].getZ() > last.getZ()) last = la[i];
2466 LayerSet ls = base_layer.getParent();
2467 List<Layer> las = ls.getLayers().subList(ls.indexOf(first), ls.indexOf(last)+1);
2468 // decide if processing all or just the touched ones or what range
2469 if (ls.size() != las.size()) {
2470 GenericDialog gd = new GenericDialog("Layer Range");
2471 gd.addMessage("Apply registration to layers:");
2472 Utils.addLayerRangeChoices(first, last, gd);
2473 gd.showDialog();
2474 if (gd.wasCanceled()) {
2475 finishedWorking();
2476 return;
2478 las = ls.getLayers().subList(gd.getNextChoiceIndex(), gd.getNextChoiceIndex()+1);
2480 Layer[] zla = new Layer[las.size()];
2481 zla = las.toArray(zla);
2482 Thread t = Registration.registerTilesSIFT(zla, overlapping_only);
2483 if (null != t) t.join();
2486 recreateBuckets(la);
2488 } catch (Exception e) {
2489 IJError.print(e);
2491 finishedWorking();
2494 return Bureaucrat.createAndStart(worker, base_layer.getProject());
2497 public Bureaucrat importLabelsAsAreaLists(final Layer layer) {
2498 return importLabelsAsAreaLists(layer, null, 0, 0, 0.4f, false);
2501 /** If base_x or base_y are Double.MAX_VALUE, then those values are asked for in a GenericDialog. */
2502 public Bureaucrat importLabelsAsAreaLists(final Layer first_layer, final String path_, final double base_x_, final double base_y_, final float alpha_, final boolean add_background_) {
2503 Worker worker = new Worker("Import labels as arealists") {
2504 public void run() {
2505 startedWorking();
2506 try {
2507 String path = path_;
2508 if (null == path) {
2509 OpenDialog od = new OpenDialog("Select stack", "");
2510 String name = od.getFileName();
2511 if (null == name || 0 == name.length()) {
2512 return;
2514 String dir = od.getDirectory().replace('\\', '/');
2515 if (!dir.endsWith("/")) dir += "/";
2516 path = dir + name;
2518 if (path.toLowerCase().endsWith(".xml")) {
2519 Utils.log("Avoided opening a TrakEM2 project.");
2520 return;
2522 double base_x = base_x_;
2523 double base_y = base_y_;
2524 float alpha = alpha_;
2525 boolean add_background = add_background_;
2526 Layer layer = first_layer;
2527 if (Double.MAX_VALUE == base_x || Double.MAX_VALUE == base_y || alpha < 0 || alpha > 1) {
2528 GenericDialog gd = new GenericDialog("Base x, y");
2529 Utils.addLayerChoice("First layer:", first_layer, gd);
2530 gd.addNumericField("Base_X:", 0, 0);
2531 gd.addNumericField("Base_Y:", 0, 0);
2532 gd.addSlider("Alpha:", 0, 100, 40);
2533 gd.addCheckbox("Add background (zero)", false);
2534 gd.showDialog();
2535 if (gd.wasCanceled()) {
2536 return;
2538 layer = first_layer.getParent().getLayer(gd.getNextChoiceIndex());
2539 base_x = gd.getNextNumber();
2540 base_y = gd.getNextNumber();
2541 if (Double.isNaN(base_x) || Double.isNaN(base_y)) {
2542 Utils.log("Base x or y is NaN!");
2543 return;
2545 alpha = (float)(gd.getNextNumber() / 100);
2546 add_background = gd.getNextBoolean();
2548 releaseMemory();
2549 final ImagePlus imp = opener.openImage(path);
2550 if (null == imp) {
2551 Utils.log("Could not open image at " + path);
2552 return;
2554 Map<Float,AreaList> alis = AmiraImporter.extractAreaLists(imp, layer, base_x, base_y, alpha, add_background);
2555 if (!hasQuitted() && alis.size() > 0) {
2556 layer.getProject().getProjectTree().insertSegmentations(layer.getProject(), alis.values());
2558 } catch (Exception e) {
2559 IJError.print(e);
2560 } finally {
2561 finishedWorking();
2565 return Bureaucrat.createAndStart(worker, first_layer.getProject());
2568 public void recreateBuckets(final Collection<Layer> col) {
2569 final Layer[] lall = new Layer[col.size()];
2570 col.toArray(lall);
2571 recreateBuckets(lall);
2574 /** Recreate buckets for each Layer, one thread per layer, in as many threads as CPUs. */
2575 public void recreateBuckets(final Layer[] la) {
2576 final AtomicInteger ai = new AtomicInteger(0);
2577 final Thread[] threads = MultiThreading.newThreads();
2579 for (int ithread = 0; ithread < threads.length; ++ithread) {
2580 threads[ithread] = new Thread() {
2581 public void run() {
2582 setPriority(Thread.NORM_PRIORITY);
2583 for (int i = ai.getAndIncrement(); i < la.length; i = ai.getAndIncrement()) {
2584 la[i].recreateBuckets();
2589 MultiThreading.startAndJoin(threads);
2592 private double getMeanOfRange(ImageStatistics st, double min, double max) {
2593 if (min == max) return min;
2594 double mean = 0;
2595 int nn = 0;
2596 int first_bin = 0;
2597 int last_bin = st.nBins -1;
2598 for (int b=0; b<st.nBins; b++) {
2599 if (st.min + st.binSize * b > min) { first_bin = b; break; }
2601 for (int b=last_bin; b>first_bin; b--) {
2602 if (st.max - st.binSize * b <= max) { last_bin = b; break; }
2604 for (int h=first_bin; h<=last_bin; h++) {
2605 nn += st.histogram[h];
2606 mean += h * st.histogram[h];
2608 return mean /= nn;
2611 /** Used for the revert command. */
2612 abstract public ImagePlus fetchOriginal(Patch patch);
2614 /** Set massive mode if not much is cached of the new layer given for loading. */
2615 public void prepare(Layer layer) {
2616 /* // this piece of ancient code is doing more harm than good
2618 ArrayList al = layer.getDisplayables();
2619 long[] ids = new long[al.size()];
2620 int next = 0;
2621 Iterator it = al.iterator();
2622 while (it.hasNext()) {
2623 Object ob = it.next();
2624 if (ob instanceof Patch)
2625 ids[next++] = ((DBObject)ob).getId();
2628 int n_cached = 0;
2629 double area = 0;
2630 if (0 == next) return; // no need
2631 else if (n_cached > 0) { // make no assumptions on image compression, assume 8-bit though
2632 long estimate = (long)(((area / n_cached) * next * 8) / 1024.0D); // 'next' is total
2633 if (!enoughFreeMemory(estimate)) {
2634 setMassiveMode(true);//massive_mode = true;
2636 } else setMassiveMode(false); //massive_mode = true; // nothing loaded, so no clue, set it to load fast by flushing fast.
2641 public Bureaucrat makeFlatImage(final Layer[] layer, final Rectangle srcRect, final double scale, final int c_alphas, final int type, final boolean force_to_file, final boolean quality) {
2642 return makeFlatImage(layer, srcRect, scale, c_alphas, type, force_to_file, quality, Color.black);
2644 /** If the srcRect is null, makes a flat 8-bit or RGB image of the entire layer. Otherwise just of the srcRect. Checks first for enough memory and frees some if feasible. */
2645 public Bureaucrat makeFlatImage(final Layer[] layer, final Rectangle srcRect, final double scale, final int c_alphas, final int type, final boolean force_to_file, final boolean quality, final Color background) {
2646 if (null == layer || 0 == layer.length) {
2647 Utils.log2("makeFlatImage: null or empty list of layers to process.");
2648 return null;
2650 final Worker worker = new Worker("making flat images") { public void run() {
2651 try {
2653 startedWorking();
2655 Rectangle srcRect_ = srcRect;
2656 if (null == srcRect_) srcRect_ = layer[0].getParent().get2DBounds();
2658 ImagePlus imp = null;
2659 String target_dir = null;
2660 boolean choose_dir = force_to_file;
2661 // if not saving to a file:
2662 if (!force_to_file) {
2663 final long size = (long)Math.ceil((srcRect_.width * scale) * (srcRect_.height * scale) * ( ImagePlus.GRAY8 == type ? 1 : 4 ) * layer.length);
2664 if (size > IJ.maxMemory() * 0.9) {
2665 YesNoCancelDialog yn = new YesNoCancelDialog(IJ.getInstance(), "WARNING", "The resulting stack of flat images is too large to fit in memory.\nChoose a directory to save the slices as an image sequence?");
2666 if (yn.yesPressed()) {
2667 choose_dir = true;
2668 } else if (yn.cancelPressed()) {
2669 finishedWorking();
2670 return;
2671 } else {
2672 choose_dir = false; // your own risk
2676 if (choose_dir) {
2677 final DirectoryChooser dc = new DirectoryChooser("Target directory");
2678 target_dir = dc.getDirectory();
2679 if (null == target_dir || target_dir.toLowerCase().startsWith("null")) {
2680 finishedWorking();
2681 return;
2684 if (layer.length > 1) {
2685 // 1 - determine stack voxel depth (by choosing one, if there are layers with different thickness)
2686 double voxel_depth = 1;
2687 if (null != target_dir) { // otherwise, saving separately
2688 ArrayList al_thickness = new ArrayList();
2689 for (int i=0; i<layer.length; i++) {
2690 Double t = new Double(layer[i].getThickness());
2691 if (!al_thickness.contains(t)) al_thickness.add(t);
2693 if (1 == al_thickness.size()) { // trivial case
2694 voxel_depth = ((Double)al_thickness.get(0)).doubleValue();
2695 } else {
2696 String[] st = new String[al_thickness.size()];
2697 for (int i=0; i<st.length; i++) {
2698 st[i] = al_thickness.get(i).toString();
2700 GenericDialog gdd = new GenericDialog("Choose voxel depth");
2701 gdd.addChoice("voxel depth: ", st, st[0]);
2702 gdd.showDialog();
2703 if (gdd.wasCanceled()) {
2704 finishedWorking();
2705 return;
2707 voxel_depth = ((Double)al_thickness.get(gdd.getNextChoiceIndex())).doubleValue();
2711 // 2 - get all slices
2712 ImageStack stack = null;
2713 for (int i=0; i<layer.length; i++) {
2714 final ImagePlus slice = getFlatImage(layer[i], srcRect_, scale, c_alphas, type, Displayable.class, null, quality, background);
2715 if (null == slice) {
2716 Utils.log("Could not retrieve flat image for " + layer[i].toString());
2717 continue;
2719 if (null != target_dir) {
2720 saveToPath(slice, target_dir, layer[i].getPrintableTitle(), ".tif");
2721 } else {
2722 if (null == stack) stack = new ImageStack(slice.getWidth(), slice.getHeight());
2723 stack.addSlice(layer[i].getProject().findLayerThing(layer[i]).toString(), slice.getProcessor());
2726 if (null != stack) {
2727 imp = new ImagePlus("z=" + layer[0].getZ() + " to z=" + layer[layer.length-1].getZ(), stack);
2728 imp.setCalibration(layer[0].getParent().getCalibrationCopy());
2730 } else {
2731 imp = getFlatImage(layer[0], srcRect_, scale, c_alphas, type, Displayable.class, null, quality, background);
2732 if (null != target_dir) {
2733 saveToPath(imp, target_dir, layer[0].getPrintableTitle(), ".tif");
2734 imp = null; // to prevent showing it
2737 if (null != imp) imp.show();
2738 } catch (Throwable e) {
2739 IJError.print(e);
2741 finishedWorking();
2742 }}; // I miss my lisp macros, you have no idea
2743 return Bureaucrat.createAndStart(worker, layer[0].getProject());
2746 /** Will never overwrite, rather, add an underscore and ordinal to the file name. */
2747 private void saveToPath(final ImagePlus imp, final String dir, final String file_name, final String extension) {
2748 if (null == imp) {
2749 Utils.log2("Loader.saveToPath: can't save a null image.");
2750 return;
2752 // create a unique file name
2753 String path = dir + "/" + file_name;
2754 File file = new File(path + extension);
2755 int k = 1;
2756 while (file.exists()) {
2757 file = new File(path + "_" + k + ".tif");
2758 k++;
2760 try {
2761 new FileSaver(imp).saveAsTiff(file.getAbsolutePath());
2762 } catch (OutOfMemoryError oome) {
2763 Utils.log2("Not enough memory. Could not save image for " + file_name);
2764 IJError.print(oome);
2765 } catch (Exception e) {
2766 Utils.log2("Could not save image for " + file_name);
2767 IJError.print(e);
2771 public ImagePlus getFlatImage(final Layer layer, final Rectangle srcRect_, final double scale, final int c_alphas, final int type, final Class clazz, final boolean quality) {
2772 return getFlatImage(layer, srcRect_, scale, c_alphas, type, clazz, null, quality, Color.black);
2775 public ImagePlus getFlatImage(final Layer layer, final Rectangle srcRect_, final double scale, final int c_alphas, final int type, final Class clazz, ArrayList al_displ) {
2776 return getFlatImage(layer, srcRect_, scale, c_alphas, type, clazz, al_displ, false, Color.black);
2779 public ImagePlus getFlatImage(final Layer layer, final Rectangle srcRect_, final double scale, final int c_alphas, final int type, final Class clazz, ArrayList al_displ, boolean quality) {
2780 return getFlatImage(layer, srcRect_, scale, c_alphas, type, clazz, al_displ, quality, Color.black);
2783 /** Returns a screenshot of the given layer for the given magnification and srcRect. Returns null if the was not enough memory to create it.
2784 * @param al_displ The Displayable objects to paint. If null, all those matching Class clazz are included.
2786 * If the 'quality' flag is given, then the flat image is created at a scale of 1.0, and later scaled down using the Image.getScaledInstance method with the SCALE_AREA_AVERAGING flag.
2789 public ImagePlus getFlatImage(final Layer layer, final Rectangle srcRect_, final double scale, final int c_alphas, final int type, final Class clazz, ArrayList al_displ, boolean quality, final Color background) {
2790 final Image bi = getFlatAWTImage(layer, srcRect_, scale, c_alphas, type, clazz, al_displ, quality, background);
2791 final ImagePlus imp = new ImagePlus(layer.getPrintableTitle(), bi);
2792 imp.setCalibration(layer.getParent().getCalibrationCopy());
2793 bi.flush();
2794 return imp;
2797 public Image getFlatAWTImage(final Layer layer, final Rectangle srcRect_, final double scale, final int c_alphas, final int type, final Class clazz, ArrayList al_displ, boolean quality, final Color background) {
2799 try {
2800 // if quality is specified, then a larger image is generated:
2801 // - full size if no mipmaps
2802 // - double the size if mipmaps is enabled
2803 double scaleP = scale;
2804 if (quality) {
2805 if (isMipMapsEnabled()) {
2806 // just double the size
2807 scaleP = scale + scale;
2808 if (scaleP > 1.0) scaleP = 1.0;
2809 } else {
2810 // full
2811 scaleP = 1.0;
2815 // dimensions
2816 int x = 0;
2817 int y = 0;
2818 int w = 0;
2819 int h = 0;
2820 Rectangle srcRect = (null == srcRect_) ? null : (Rectangle)srcRect_.clone();
2821 if (null != srcRect) {
2822 x = srcRect.x;
2823 y = srcRect.y;
2824 w = srcRect.width;
2825 h = srcRect.height;
2826 } else {
2827 w = (int)Math.ceil(layer.getLayerWidth());
2828 h = (int)Math.ceil(layer.getLayerHeight());
2829 srcRect = new Rectangle(0, 0, w, h);
2831 Utils.log2("Loader.getFlatImage: using rectangle " + srcRect);
2832 // estimate image size
2833 final long n_bytes = (long)((w * h * scaleP * scaleP * (ImagePlus.GRAY8 == type ? 1.0 /*byte*/ : 4.0 /*int*/)));
2834 Utils.log2("Flat image estimated size in bytes: " + Long.toString(n_bytes) + " w,h : " + (int)Math.ceil(w * scaleP) + "," + (int)Math.ceil(h * scaleP) + (quality ? " (using 'quality' flag: scaling to " + scale + " is done later with proper area averaging)" : ""));
2836 if (!releaseToFit(n_bytes)) { // locks on it's own
2837 Utils.showMessage("Not enough free RAM for a flat image.");
2838 return null;
2840 // go
2841 synchronized (db_lock) {
2842 lock();
2843 releaseMemory(); // savage ...
2844 unlock();
2846 BufferedImage bi = null;
2847 switch (type) {
2848 case ImagePlus.GRAY8:
2849 bi = new BufferedImage((int)Math.ceil(w * scaleP), (int)Math.ceil(h * scaleP), BufferedImage.TYPE_BYTE_INDEXED, GRAY_LUT);
2850 break;
2851 case ImagePlus.COLOR_RGB:
2852 bi = new BufferedImage((int)Math.ceil(w * scaleP), (int)Math.ceil(h * scaleP), BufferedImage.TYPE_INT_ARGB);
2853 break;
2854 default:
2855 Utils.log2("Left bi,icm as null");
2856 break;
2858 final Graphics2D g2d = bi.createGraphics();
2860 g2d.setColor(background);
2861 g2d.fillRect(0, 0, bi.getWidth(), bi.getHeight());
2863 g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
2864 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // to smooth edges of the images
2865 g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
2866 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
2868 synchronized (db_lock) {
2869 lock();
2870 releaseMemory(); // savage ...
2871 unlock();
2874 ArrayList al_zdispl = null;
2875 if (null == al_displ) {
2876 al_displ = layer.getDisplayables(clazz);
2877 al_zdispl = layer.getParent().getZDisplayables(clazz);
2878 } else {
2879 // separate ZDisplayables into their own array
2880 al_displ = (ArrayList)al_displ.clone();
2881 //Utils.log2("al_displ size: " + al_displ.size());
2882 al_zdispl = new ArrayList();
2883 for (Iterator it = al_displ.iterator(); it.hasNext(); ) {
2884 Object ob = it.next();
2885 if (ob instanceof ZDisplayable) {
2886 it.remove();
2887 al_zdispl.add(ob);
2890 // order ZDisplayables by their stack order
2891 ArrayList al_zdispl2 = layer.getParent().getZDisplayables();
2892 for (Iterator it = al_zdispl2.iterator(); it.hasNext(); ) {
2893 Object ob = it.next();
2894 if (!al_zdispl.contains(ob)) it.remove();
2896 al_zdispl = al_zdispl2;
2899 // prepare the canvas for the srcRect and magnification
2900 final AffineTransform at_original = g2d.getTransform();
2901 final AffineTransform atc = new AffineTransform();
2902 atc.scale(scaleP, scaleP);
2903 atc.translate(-srcRect.x, -srcRect.y);
2904 at_original.preConcatenate(atc);
2905 g2d.setTransform(at_original);
2907 //Utils.log2("will paint: " + al_displ.size() + " displ and " + al_zdispl.size() + " zdispl");
2908 int total = al_displ.size() + al_zdispl.size();
2909 int count = 0;
2910 boolean zd_done = false;
2911 for(Iterator it = al_displ.iterator(); it.hasNext(); ) {
2912 Displayable d = (Displayable)it.next();
2913 //Utils.log2("d is: " + d);
2914 // paint the ZDisplayables before the first label, if any
2915 if (!zd_done && d instanceof DLabel) {
2916 zd_done = true;
2917 for (Iterator itz = al_zdispl.iterator(); itz.hasNext(); ) {
2918 ZDisplayable zd = (ZDisplayable)itz.next();
2919 if (!zd.isOutOfRepaintingClip(scaleP, srcRect, null)) {
2920 zd.paint(g2d, scaleP, false, c_alphas, layer);
2922 count++;
2923 //Utils.log2("Painted " + count + " of " + total);
2926 if (!d.isOutOfRepaintingClip(scaleP, srcRect, null)) {
2927 d.paint(g2d, scaleP, false, c_alphas, layer);
2928 //Utils.log("painted: " + d + "\n with: " + scaleP + ", " + c_alphas + ", " + layer);
2929 } else {
2930 //Utils.log2("out: " + d);
2932 count++;
2933 //Utils.log2("Painted " + count + " of " + total);
2935 if (!zd_done) {
2936 zd_done = true;
2937 for (Iterator itz = al_zdispl.iterator(); itz.hasNext(); ) {
2938 ZDisplayable zd = (ZDisplayable)itz.next();
2939 if (!zd.isOutOfRepaintingClip(scaleP, srcRect, null)) {
2940 zd.paint(g2d, scaleP, false, c_alphas, layer);
2942 count++;
2943 //Utils.log2("Painted " + count + " of " + total);
2946 // ensure enough memory is available for the processor and a new awt from it
2947 releaseToFit((long)(n_bytes*2.3)); // locks on its own
2949 try {
2950 if (quality) {
2951 // need to scale back down
2952 Image scaled = null;
2953 if (!isMipMapsEnabled() || scale >= 0.499) { // there are no proper mipmaps above 50%, so there's need for SCALE_AREA_AVERAGING.
2954 scaled = bi.getScaledInstance((int)(w * scale), (int)(h * scale), Image.SCALE_AREA_AVERAGING); // very slow, but best by far
2955 if (ImagePlus.GRAY8 == type) {
2956 // getScaledInstance generates RGB images for some reason.
2957 BufferedImage bi8 = new BufferedImage((int)(w * scale), (int)(h * scale), BufferedImage.TYPE_BYTE_GRAY);
2958 bi8.createGraphics().drawImage(scaled, 0, 0, null);
2959 scaled.flush();
2960 scaled = bi8;
2962 } else {
2963 // faster, but requires gaussian blurred images (such as the mipmaps)
2964 if (bi.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
2965 scaled = new BufferedImage((int)(w * scale), (int)(h * scale), bi.getType(), GRAY_LUT);
2966 } else {
2967 scaled = new BufferedImage((int)(w * scale), (int)(h * scale), bi.getType());
2969 Graphics2D gs = (Graphics2D)scaled.getGraphics();
2970 //gs.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
2971 gs.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
2972 gs.drawImage(bi, 0, 0, (int)(w * scale), (int)(h * scale), null);
2974 bi.flush();
2975 return scaled;
2976 } else {
2977 // else the image was made scaled down already, and of the proper type
2978 return bi;
2980 } catch (OutOfMemoryError oome) {
2981 Utils.log("Not enough memory to create the ImagePlus. Try scaling it down or not using the 'quality' flag.");
2984 } catch (Exception e) {
2985 IJError.print(e);
2988 return null;
2991 public Bureaucrat makePrescaledTiles(final Layer[] layer, final Class clazz, final Rectangle srcRect, double max_scale_, final int c_alphas, final int type) {
2992 return makePrescaledTiles(layer, clazz, srcRect, max_scale_, c_alphas, type, null);
2995 /** Generate 256x256 tiles, as many as necessary, to cover the given srcRect, starting at max_scale. Designed to be slow but memory-capable.
2997 * filename = z + "/" + row + "_" + column + "_" + s + ".jpg";
2999 * row and column run from 0 to n stepsize 1
3000 * that is, row = y / ( 256 * 2^s ) and column = x / ( 256 * 2^s )
3002 * z : z-level (slice)
3003 * x,y: the row and column
3004 * s: scale, which is 1 / (2^s), in integers: 0, 1, 2 ...
3006 *  var MAX_S = Math.floor( Math.log( MAX_Y + 1 ) / Math.LN2 ) - Math.floor( Math.log( Y_TILE_SIZE ) / Math.LN2 ) - 1;
3008 * The module should not be more than 5
3009 * At al levels, there should be an even number of rows and columns, except for the coarsest level.
3010 * The coarsest level should be at least 5x5 tiles.
3012 * Best results obtained when the srcRect approaches or is a square. Black space will pad the right and bottom edges when the srcRect is not exactly a square.
3013 * Only the area within the srcRect is ever included, even if actual data exists beyond.
3015 * Returns the watcher thread, for joining purposes, or null if the dialog is canceled or preconditions ar enot passed.
3017 public Bureaucrat makePrescaledTiles(final Layer[] layer, final Class clazz, final Rectangle srcRect, double max_scale_, final int c_alphas, final int type, String target_dir) {
3018 if (null == layer || 0 == layer.length) return null;
3019 // choose target directory
3020 if (null == target_dir) {
3021 DirectoryChooser dc = new DirectoryChooser("Choose target directory");
3022 target_dir = dc.getDirectory();
3023 if (null == target_dir) return null;
3025 target_dir = target_dir.replace('\\', '/'); // Windows fixing
3026 if (!target_dir.endsWith("/")) target_dir += "/";
3028 if (max_scale_ > 1) {
3029 Utils.log("Prescaled Tiles: using max scale of 1.0");
3030 max_scale_ = 1; // no point
3033 final String dir = target_dir;
3034 final double max_scale = max_scale_;
3035 final float jpeg_quality = ij.plugin.JpegWriter.getQuality() / 100.0f;
3036 Utils.log("Using jpeg quality: " + jpeg_quality);
3038 Worker worker = new Worker("Creating prescaled tiles") {
3039 private void cleanUp() {
3040 finishedWorking();
3042 public void run() {
3043 startedWorking();
3045 try {
3047 // project name
3048 String pname = layer[0].getProject().getTitle();
3050 // create 'z' directories if they don't exist: check and ask!
3052 // start with the highest scale level
3053 final int[] best = determineClosestPowerOfTwo(srcRect.width > srcRect.height ? srcRect.width : srcRect.height);
3054 final int edge_length = best[0];
3055 final int n_edge_tiles = edge_length / 256;
3056 Utils.log2("srcRect: " + srcRect);
3057 Utils.log2("edge_length, n_edge_tiles, best[1] " + best[0] + ", " + n_edge_tiles + ", " + best[1]);
3060 // thumbnail dimensions
3061 LayerSet ls = layer[0].getParent();
3062 double ratio = srcRect.width / (double)srcRect.height;
3063 double thumb_scale = 1.0;
3064 if (ratio >= 1) {
3065 // width is larger or equal than height
3066 thumb_scale = 192.0 / srcRect.width;
3067 } else {
3068 thumb_scale = 192.0 / srcRect.height;
3071 for (int iz=0; iz<layer.length; iz++) {
3072 if (this.quit) {
3073 cleanUp();
3074 return;
3077 // 1 - create a directory 'z' named as the layer's Z coordinate
3078 String tile_dir = dir + layer[iz].getParent().indexOf(layer[iz]);
3079 File fdir = new File(tile_dir);
3080 int tag = 1;
3081 // Ensure there is a usable directory:
3082 while (fdir.exists() && !fdir.isDirectory()) {
3083 fdir = new File(tile_dir + "_" + tag);
3085 if (!fdir.exists()) {
3086 fdir.mkdir();
3087 Utils.log("Created directory " + fdir);
3089 // if the directory exists already just reuse it, overwritting its files if so.
3090 final String tmp = fdir.getAbsolutePath().replace('\\','/');
3091 if (!tile_dir.equals(tmp)) Utils.log("\tWARNING: directory will not be in the standard location.");
3092 // debug:
3093 Utils.log2("tile_dir: " + tile_dir + "\ntmp: " + tmp);
3094 tile_dir = tmp;
3095 if (!tile_dir.endsWith("/")) tile_dir += "/";
3097 // 2 - create layer thumbnail, max 192x192
3098 ImagePlus thumb = getFlatImage(layer[iz], srcRect, thumb_scale, c_alphas, type, clazz, true);
3099 ImageSaver.saveAsJpeg(thumb.getProcessor(), tile_dir + "small.jpg", jpeg_quality, ImagePlus.COLOR_RGB != type);
3100 flush(thumb);
3101 thumb = null;
3103 // 3 - fill directory with tiles
3104 if (edge_length < 256) { // edge_length is the largest length of the 256x256 tile map that covers an area equal or larger than the desired srcRect (because all tiles have to be 256x256 in size)
3105 // create single tile per layer
3106 makeTile(layer[iz], srcRect, max_scale, c_alphas, type, clazz, jpeg_quality, tile_dir + "0_0_0.jpg");
3107 } else {
3108 // create piramid of tiles
3109 double scale = 1; //max_scale; // WARNING if scale is different than 1, it will FAIL to set the next scale properly.
3110 int scale_pow = 0;
3111 int n_et = n_edge_tiles; // cached for local modifications in the loop, works as loop controler
3112 while (n_et >= best[1]) { // best[1] is the minimal root found, i.e. 1,2,3,4,5 from hich then powers of two were taken to make up for the edge_length
3113 int tile_side = (int)(256/scale); // 0 < scale <= 1, so no precision lost
3114 for (int row=0; row<n_et; row++) {
3115 for (int col=0; col<n_et; col++) {
3116 final int i_tile = row * n_et + col;
3117 Utils.showProgress(i_tile / (double)(n_et * n_et));
3119 if (0 == i_tile % 100) {
3120 setMassiveMode(true);
3121 releaseMemory();
3124 if (this.quit) {
3125 cleanUp();
3126 return;
3128 Rectangle tile_src = new Rectangle(srcRect.x + tile_side*row,
3129 srcRect.y + tile_side*col,
3130 tile_side,
3131 tile_side); // in absolute coords, magnification later.
3132 // crop bounds
3133 if (tile_src.x + tile_src.width > srcRect.x + srcRect.width) tile_src.width = srcRect.x + srcRect.width - tile_src.x;
3134 if (tile_src.y + tile_src.height > srcRect.y + srcRect.height) tile_src.height = srcRect.y + srcRect.height - tile_src.y;
3135 // negative tile sizes will be made into black tiles
3136 // (negative dimensions occur for tiles beyond the edges of srcRect, since the grid of tiles has to be of equal number of rows and cols)
3137 makeTile(layer[iz], tile_src, scale, c_alphas, type, clazz, jpeg_quality, new StringBuffer(tile_dir).append(col).append('_').append(row).append('_').append(scale_pow).append(".jpg").toString()); // should be row_col_scale, but results in transposed tiles in googlebrains, so I inversed it.
3140 scale_pow++;
3141 scale = 1 / Math.pow(2, scale_pow); // works as magnification
3142 n_et /= 2;
3146 } catch (Exception e) {
3147 IJError.print(e);
3148 } finally {
3149 Utils.showProgress(1);
3151 cleanUp();
3152 finishedWorking();
3154 }// end of run method
3157 // watcher thread
3158 return Bureaucrat.createAndStart(worker, layer[0].getProject());
3161 /** Will overwrite if the file path exists. */
3162 private void makeTile(Layer layer, Rectangle srcRect, double mag, int c_alphas, int type, Class clazz, final float jpeg_quality, String file_path) throws Exception {
3163 ImagePlus imp = null;
3164 if (srcRect.width > 0 && srcRect.height > 0) {
3165 imp = getFlatImage(layer, srcRect, mag, c_alphas, type, clazz, null, true); // with quality
3166 } else {
3167 imp = new ImagePlus("", new ByteProcessor(256, 256)); // black tile
3169 // correct cropped tiles
3170 if (imp.getWidth() < 256 || imp.getHeight() < 256) {
3171 ImagePlus imp2 = new ImagePlus(imp.getTitle(), imp.getProcessor().createProcessor(256, 256));
3172 // ensure black background for color images
3173 if (imp2.getType() == ImagePlus.COLOR_RGB) {
3174 Roi roi = new Roi(0, 0, 256, 256);
3175 imp2.setRoi(roi);
3176 imp2.getProcessor().setValue(0); // black
3177 imp2.getProcessor().fill();
3179 imp2.getProcessor().insert(imp.getProcessor(), 0, 0);
3180 imp = imp2;
3182 // debug
3183 //Utils.log("would save: " + srcRect + " at " + file_path);
3184 ImageSaver.saveAsJpeg(imp.getProcessor(), file_path, jpeg_quality, ImagePlus.COLOR_RGB != type);
3187 /** Find the closest, but larger, power of 2 number for the given edge size; the base root may be any of {1,2,3,5}. */
3188 private int[] determineClosestPowerOfTwo(final int edge) {
3189 int[] starter = new int[]{1, 2, 3, 5}; // I love primer numbers
3190 int[] larger = new int[starter.length]; System.arraycopy(starter, 0, larger, 0, starter.length); // I hate java's obscene verbosity
3191 for (int i=0; i<larger.length; i++) {
3192 while (larger[i] < edge) {
3193 larger[i] *= 2;
3196 int min_larger = larger[0];
3197 int min_i = 0;
3198 for (int i=1; i<larger.length; i++) {
3199 if (larger[i] < min_larger) {
3200 min_i = i;
3201 min_larger = larger[i];
3204 // 'larger' is now larger or equal to 'edge', and will reduce to starter[min_i] tiles squared.
3205 return new int[]{min_larger, starter[min_i]};
3208 private String last_opened_path = null;
3210 /** Subclasses can override this method to register the URL of the imported image. */
3211 public void addedPatchFrom(String path, Patch patch) {}
3213 /** Import an image into the given layer, in a separate task thread. */
3214 public Bureaucrat importImage(final Layer layer, final double x, final double y, final String path) {
3215 Worker worker = new Worker("Importing image") {
3216 public void run() {
3217 startedWorking();
3218 try {
3219 ////
3220 if (null == layer) {
3221 Utils.log("Can't import. No layer found.");
3222 finishedWorking();
3223 return;
3225 Patch p = importImage(layer.getProject(), x, y, path);
3226 if (null != p) {
3227 layer.add(p);
3228 layer.getParent().enlargeToFit(p, LayerSet.NORTHWEST);
3230 ////
3231 } catch (Exception e) {
3232 IJError.print(e);
3234 finishedWorking();
3237 return Bureaucrat.createAndStart(worker, layer.getProject());
3240 public Patch importImage(Project project, double x, double y) {
3241 return importImage(project, x, y, null);
3243 /** Import a new image at the given coordinates; does not puts it into any layer, unless it's a stack -in which case importStack is called with the current front layer of the given project as target. If a path is not provided it will be asked for.*/
3244 public Patch importImage(Project project, double x, double y, String path) {
3245 if (null == path) {
3246 OpenDialog od = new OpenDialog("Import image", "");
3247 String name = od.getFileName();
3248 if (null == name || 0 == name.length()) return null;
3249 String dir = od.getDirectory().replace('\\', '/');
3250 if (!dir.endsWith("/")) dir += "/";
3251 path = dir + name;
3252 } else {
3253 path = path.replace('\\', '/');
3255 // avoid opening trakem2 projects
3256 if (path.toLowerCase().endsWith(".xml")) {
3257 Utils.showMessage("Cannot import " + path + " as a stack.");
3258 return null;
3260 releaseMemory(); // some: TODO this should read the header only, and figure out the dimensions to do a releaseToFit(n_bytes) call
3261 IJ.redirectErrorMessages();
3262 final ImagePlus imp = opener.openImage(path);
3263 if (null == imp) return null;
3264 preProcess(imp);
3265 if (imp.getNSlices() > 1) {
3266 // a stack!
3267 Layer layer = Display.getFrontLayer(project);
3268 if (null == layer) return null;
3269 importStack(layer, imp, true, path); // TODO: the x,y location is not set
3270 return null;
3272 if (0 == imp.getWidth() || 0 == imp.getHeight()) {
3273 Utils.showMessage("Can't import image of zero width or height.");
3274 flush(imp);
3275 return null;
3277 last_opened_path = path;
3278 Patch p = new Patch(project, imp.getTitle(), x, y, imp);
3279 addedPatchFrom(last_opened_path, p);
3280 if (isMipMapsEnabled()) generateMipMaps(p);
3281 return p;
3283 public Patch importNextImage(Project project, double x, double y) {
3284 if (null == last_opened_path) {
3285 return importImage(project, x, y);
3287 int i_slash = last_opened_path.lastIndexOf("/");
3288 String dir_name = last_opened_path.substring(0, i_slash);
3289 File dir = new File(dir_name);
3290 String last_file = last_opened_path.substring(i_slash + 1);
3291 String[] file_names = dir.list();
3292 String next_file = null;
3293 final String exts = "tiftiffjpgjpegpnggifzipdicombmppgm";
3294 for (int i=0; i<file_names.length; i++) {
3295 if (last_file.equals(file_names[i]) && i < file_names.length -1) {
3296 // loop until finding a suitable next
3297 for (int j=i+1; j<file_names.length; j++) {
3298 String ext = file_names[j].substring(file_names[j].lastIndexOf('.') + 1).toLowerCase();
3299 if (-1 != exts.indexOf(ext)) {
3300 next_file = file_names[j];
3301 break;
3304 break;
3307 if (null == next_file) {
3308 Utils.showMessage("No more files after " + last_file);
3309 return null;
3311 releaseMemory(); // some: TODO this should read the header only, and figure out the dimensions to do a releaseToFit(n_bytes) call
3312 IJ.redirectErrorMessages();
3313 ImagePlus imp = opener.openImage(dir_name, next_file);
3314 preProcess(imp);
3315 if (null == imp) return null;
3316 if (0 == imp.getWidth() || 0 == imp.getHeight()) {
3317 Utils.showMessage("Can't import image of zero width or height.");
3318 flush(imp);
3319 return null;
3321 last_opened_path = dir + "/" + next_file;
3322 Patch p = new Patch(project, imp.getTitle(), x, y, imp);
3323 addedPatchFrom(last_opened_path, p);
3324 if (isMipMapsEnabled()) generateMipMaps(p);
3325 return p;
3328 public Bureaucrat importStack(Layer first_layer, ImagePlus imp_stack_, boolean ask_for_data) {
3329 return importStack(first_layer, imp_stack_, ask_for_data, null);
3331 public Bureaucrat importStack(final Layer first_layer, final ImagePlus imp_stack_, final boolean ask_for_data, final String filepath_) {
3332 return importStack(first_layer, -1, -1, imp_stack_, ask_for_data, filepath_);
3334 /** Imports an image stack from a multitiff file and places each slice in the proper layer, creating new layers as it goes. If the given stack is null, popup a file dialog to choose one*/
3335 public Bureaucrat importStack(final Layer first_layer, final int x, final int y, final ImagePlus imp_stack_, final boolean ask_for_data, final String filepath_) {
3336 Utils.log2("Loader.importStack filepath: " + filepath_);
3337 if (null == first_layer) return null;
3339 Worker worker = new Worker("import stack") {
3340 public void run() {
3341 startedWorking();
3342 try {
3345 String filepath = filepath_;
3346 /* On drag and drop the stack is not null! */ //Utils.log2("imp_stack_ is " + imp_stack_);
3347 ImagePlus[] stks = null;
3348 if (null == imp_stack_) {
3349 stks = Utils.findOpenStacks();
3350 } else {
3351 stks = new ImagePlus[]{imp_stack_};
3353 ImagePlus imp_stack = null;
3354 // ask to open a stack if it's null
3355 if (null == stks) {
3356 imp_stack = openStack(); // choose one
3357 } else if (stks.length > 0) {
3358 // choose one from the list
3359 GenericDialog gd = new GenericDialog("Choose one");
3360 gd.addMessage("Choose a stack from the list or 'open...' to bring up a file chooser dialog:");
3361 String[] list = new String[stks.length +1];
3362 for (int i=0; i<list.length -1; i++) {
3363 list[i] = stks[i].getTitle();
3365 list[list.length-1] = "[ Open stack... ]";
3366 gd.addChoice("choose stack: ", list, list[0]);
3367 gd.showDialog();
3368 if (gd.wasCanceled()) {
3369 finishedWorking();
3370 return;
3372 int i_choice = gd.getNextChoiceIndex();
3373 if (list.length-1 == i_choice) { // the open... command
3374 imp_stack = first_layer.getProject().getLoader().openStack();
3375 } else {
3376 imp_stack = stks[i_choice];
3378 } else {
3379 imp_stack = imp_stack_;
3381 // check:
3382 if (null == imp_stack) {
3383 finishedWorking();
3384 return;
3387 final String props = (String)imp_stack.getProperty("Info");
3389 // check if it's amira labels stack to prevent missimports
3390 if (null != props && -1 != props.indexOf("Materials {")) {
3391 YesNoDialog yn = new YesNoDialog(IJ.getInstance(), "Warning", "You are importing a stack of Amira labels as a regular image stack. Continue anyway?");
3392 if (!yn.yesPressed()) {
3393 finishedWorking();
3394 return;
3398 String dir = imp_stack.getFileInfo().directory;
3399 double layer_width = first_layer.getLayerWidth();
3400 double layer_height= first_layer.getLayerHeight();
3401 double current_thickness = first_layer.getThickness();
3402 double thickness = current_thickness;
3403 boolean expand_layer_set = false;
3404 boolean lock_stack = false;
3405 int anchor = LayerSet.NORTHWEST; //default
3406 if (ask_for_data) {
3407 // ask for slice separation in pixels
3408 GenericDialog gd = new GenericDialog("Slice separation?");
3409 gd.addMessage("Please enter the slice thickness, in pixels");
3410 gd.addNumericField("slice_thickness: ", Math.abs(imp_stack.getCalibration().pixelDepth / imp_stack.getCalibration().pixelHeight), 3); // assuming pixelWidth == pixelHeight
3411 if (layer_width != imp_stack.getWidth() || layer_height != imp_stack.getHeight()) {
3412 gd.addCheckbox("Resize canvas to fit stack", true);
3413 gd.addChoice("Anchor: ", LayerSet.ANCHORS, LayerSet.ANCHORS[0]);
3415 gd.addCheckbox("Lock stack", false);
3416 gd.showDialog();
3417 if (gd.wasCanceled()) {
3418 if (null == stks) { // flush only if it was not open before
3419 flush(imp_stack);
3421 finishedWorking();
3422 return;
3424 if (layer_width != imp_stack.getWidth() || layer_height != imp_stack.getHeight()) {
3425 expand_layer_set = gd.getNextBoolean();
3426 anchor = gd.getNextChoiceIndex();
3428 lock_stack = gd.getNextBoolean();
3429 thickness = gd.getNextNumber();
3430 // check provided thickness with that of the first layer:
3431 if (thickness != current_thickness) {
3432 boolean adjust_thickness = false;
3433 if (!(1 == first_layer.getParent().size() && first_layer.isEmpty())) {
3434 YesNoCancelDialog yn = new YesNoCancelDialog(IJ.getInstance(), "Mismatch!", "The current layer's thickness is " + current_thickness + "\nwhich is " + (thickness < current_thickness ? "larger":"smaller") + " than\nthe desired " + thickness + " for each stack slice.\nAdjust current layer's thickness to " + thickness + " ?");
3435 if (yn.cancelPressed()) {
3436 if (null != imp_stack_) flush(imp_stack); // was opened new
3437 finishedWorking();
3438 return;
3439 } else if (yn.yesPressed()) {
3440 adjust_thickness = true;
3443 if (adjust_thickness) first_layer.setThickness(thickness);
3447 if (null == imp_stack.getStack()) {
3448 Utils.showMessage("Not a stack.");
3449 finishedWorking();
3450 return;
3453 // WARNING: there are fundamental issues with calibration, because the Layer thickness is disconnected from the Calibration pixelDepth
3455 // set LayerSet calibration if there is no calibration
3456 boolean calibrate = true;
3457 if (ask_for_data && first_layer.getParent().isCalibrated()) {
3458 if (!ControlWindow.isGUIEnabled()) {
3459 Utils.log2("Loader.importStack: overriding LayerSet calibration with that of the imported stack.");
3460 } else {
3461 YesNoDialog yn = new YesNoDialog("Calibration", "The layer set is already calibrated. Override with the stack calibration values?");
3462 if (!yn.yesPressed()) {
3463 calibrate = false;
3467 if (calibrate) {
3468 first_layer.getParent().setCalibration(imp_stack.getCalibration());
3471 if (layer_width < imp_stack.getWidth() || layer_height < imp_stack.getHeight()) {
3472 expand_layer_set = true;
3475 if (null == filepath) {
3476 // try to get it from the original FileInfo
3477 final FileInfo fi = imp_stack.getOriginalFileInfo();
3478 if (null != fi && null != fi.directory && null != fi.fileName) {
3479 filepath = fi.directory.replace('\\', '/');
3480 if (!filepath.endsWith("/")) filepath += '/';
3481 filepath += fi.fileName;
3483 Utils.log2("Getting filepath from FileInfo: " + filepath);
3484 // check that file exists, otherwise save a copy in the storage folder
3485 if (null == filepath || (!filepath.startsWith("http://") && !new File(filepath).exists())) {
3486 filepath = handlePathlessImage(imp_stack);
3488 } else {
3489 filepath = filepath.replace('\\', '/');
3492 // Place the first slice in the current layer, and then query the parent LayerSet for subsequent layers, and create them if not present.
3493 Patch last_patch = Loader.this.importStackAsPatches(first_layer.getProject(), first_layer, x, y, imp_stack, null != imp_stack_ && null != imp_stack_.getCanvas(), filepath);
3494 if (null != last_patch) last_patch.setLocked(lock_stack);
3496 if (expand_layer_set) {
3497 last_patch.getLayer().getParent().setMinimumDimensions();
3500 Utils.log2("props: " + props);
3502 // check if it's an amira stack, then ask to import labels
3503 if (null != props && -1 == props.indexOf("Materials {") && -1 != props.indexOf("CoordType")) {
3504 YesNoDialog yn = new YesNoDialog(IJ.getInstance(), "Amira Importer", "Import labels as well?");
3505 if (yn.yesPressed()) {
3506 // select labels
3507 Collection<AreaList> alis = AmiraImporter.importAmiraLabels(first_layer, last_patch.getX(), last_patch.getY(), imp_stack.getOriginalFileInfo().directory);
3508 if (null != alis) {
3509 // import all created AreaList as nodes in the ProjectTree under a new imported_segmentations node
3510 first_layer.getProject().getProjectTree().insertSegmentations(first_layer.getProject(), alis);
3511 // link them to the images
3512 for (final AreaList ali : alis) {
3513 ali.linkPatches();
3519 // it is safe not to flush the imp_stack, because all its resources are being used anyway (all the ImageProcessor), and it has no awt.Image. Unless it's being shown in ImageJ, and then it will be flushed on its own when the user closes its window.
3520 } catch (Exception e) {
3521 IJError.print(e);
3523 finishedWorking();
3526 return Bureaucrat.createAndStart(worker, first_layer.getProject());
3529 public String handlePathlessImage(ImagePlus imp) { return null; }
3531 protected Patch importStackAsPatches(final Project project, final Layer first_layer, final ImagePlus stack, final boolean as_copy, String filepath) {
3532 return importStackAsPatches(project, first_layer, Integer.MAX_VALUE, Integer.MAX_VALUE, stack, as_copy, filepath);
3534 abstract protected Patch importStackAsPatches(final Project project, final Layer first_layer, final int x, final int y, final ImagePlus stack, final boolean as_copy, String filepath);
3536 protected String export(Project project, File fxml) {
3537 return export(project, fxml, true);
3540 /** Exports the project and its images (optional); if export_images is true, it will be asked for confirmation anyway -beware: for FSLoader, images are not exported since it doesn't own them; only their path.*/
3541 protected String export(final Project project, final File fxml, boolean export_images) {
3542 releaseToFit(MIN_FREE_BYTES);
3543 String path = null;
3544 if (null == project || null == fxml) return null;
3545 try {
3546 if (export_images && !(this instanceof FSLoader)) {
3547 final YesNoCancelDialog yn = ini.trakem2.ControlWindow.makeYesNoCancelDialog("Export images?", "Export images as well?");
3548 if (yn.cancelPressed()) return null;
3549 if (yn.yesPressed()) export_images = true;
3550 else export_images = false; // 'no' option
3552 // 1 - get headers in DTD format
3553 StringBuffer sb_header = new StringBuffer("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<!DOCTYPE ").append(project.getDocType()).append(" [\n");
3555 final HashSet hs = new HashSet();
3556 project.exportDTD(sb_header, hs, "\t");
3558 sb_header.append("] >\n\n");
3560 // 2 - fill in the data
3561 String patches_dir = null;
3562 if (export_images) {
3563 patches_dir = makePatchesDir(fxml);
3565 java.io.Writer writer = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(fxml)), "8859_1");
3566 try {
3567 writer.write(sb_header.toString());
3568 sb_header = null;
3569 project.exportXML(writer, "", patches_dir);
3570 writer.flush(); // make sure all buffered chars are written
3571 setChanged(false);
3572 path = fxml.getAbsolutePath().replace('\\', '/');
3573 } catch (Exception e) {
3574 Utils.log("FAILED to save the file at " + fxml);
3575 path = null;
3576 } finally {
3577 writer.close();
3578 writer = null;
3581 // Remove the patches_dir if empty (can happen when doing a "save" on a FSLoader project if no new Patch have been created that have no path.
3582 if (export_images) {
3583 File fpd = new File(patches_dir);
3584 if (fpd.exists() && fpd.isDirectory()) {
3585 // check if it contains any files
3586 File[] ff = fpd.listFiles();
3587 boolean rm = true;
3588 for (int k=0; k<ff.length; k++) {
3589 if (!ff[k].isHidden()) {
3590 // one non-hidden file found.
3591 rm = false;
3592 break;
3595 if (rm) {
3596 try {
3597 fpd.delete();
3598 } catch (Exception e) {
3599 Utils.log2("Could not delete empty directory " + patches_dir);
3600 IJError.print(e);
3606 } catch (Exception e) {
3607 IJError.print(e);
3609 ControlWindow.updateTitle(project);
3610 return path;
3613 static public long countObjects(final LayerSet ls) {
3614 // estimate total number of bytes: large estimate is 500 bytes of xml text for each object
3615 int count = 1; // the given LayerSet itself
3616 for (Layer la : (ArrayList<Layer>)ls.getLayers()) {
3617 count += la.getNDisplayables();
3618 for (Object ls2 : la.getDisplayables(LayerSet.class)) { // can't cast ArrayList<Displayable> to ArrayList<LayerSet> ????
3619 count += countObjects((LayerSet)ls2);
3622 return count;
3625 /** Calls saveAs() unless overriden. Returns full path to the xml file. */
3626 public String save(Project project) { // yes the project is the same project pointer, which for some reason I never committed myself to place it in the Loader class as a field.
3627 String path = saveAs(project);
3628 if (null != path) setChanged(false);
3629 return path;
3632 /** Save the project under a different name by choosing from a dialog, and exporting all images (will popup a YesNoCancelDialog to confirm exporting images.) */
3633 public String saveAs(Project project) {
3634 return saveAs(project, null, true);
3637 /** Exports to an XML file chosen by the user in a dialog if @param xmlpath is null. Images exist already in the file system, so none are exported. Returns the full path to the xml file. */
3638 public String saveAs(Project project, String xmlpath, boolean export_images) {
3639 long size = countObjects(project.getRootLayerSet()) * 500;
3640 releaseToFit(size > MIN_FREE_BYTES ? size : MIN_FREE_BYTES);
3641 String default_dir = null;
3642 default_dir = getStorageFolder();
3643 // Select a file to export to
3644 File fxml = null == xmlpath ? Utils.chooseFile(default_dir, null, ".xml") : new File(xmlpath);
3645 if (null == fxml) return null;
3646 String path = export(project, fxml, export_images);
3647 if (null != path) setChanged(false);
3648 return path;
3651 /** Meant to be overriden -- as is, will call saveAs(project, path, export_images = getClass() != FSLoader.class ). */
3652 public String saveAs(String path, boolean overwrite) {
3653 if (null == path) return null;
3654 return export(Project.findProject(this), new File(path), this.getClass() != FSLoader.class);
3657 /** Parses the xml_path and returns the folder in the same directory that has the same name plus "_images". */
3658 public String extractRelativeFolderPath(final File fxml) {
3659 try {
3660 String patches_dir = fxml.getParent() + "/" + fxml.getName();
3661 if (patches_dir.toLowerCase().lastIndexOf(".xml") == patches_dir.length() - 4) {
3662 patches_dir = patches_dir.substring(0, patches_dir.lastIndexOf('.'));
3664 return patches_dir + "_images";
3665 } catch (Exception e) {
3666 IJError.print(e);
3667 return null;
3671 protected String makePatchesDir(final File fxml) {
3672 // Create a directory to store the images
3673 String patches_dir = extractRelativeFolderPath(fxml);
3674 if (null == patches_dir) return null;
3675 File dir = new File(patches_dir);
3676 String patches_dir2 = null;
3677 int i = 1;
3678 while (dir.exists()) {
3679 patches_dir2 = patches_dir + "_" + Integer.toString(i);
3680 dir = new File(patches_dir2);
3681 i++;
3683 if (null != patches_dir2) patches_dir = patches_dir2;
3684 if (null == patches_dir) return null;
3685 try {
3686 dir.mkdir();
3687 } catch (Exception e) {
3688 IJError.print(e);
3689 Utils.showMessage("Could not create a directory for the images.");
3690 return null;
3692 if (File.separatorChar != patches_dir.charAt(patches_dir.length() -1)) {
3693 patches_dir += "/";
3695 return patches_dir;
3698 public String exportImage(final Patch patch, final String path, final boolean overwrite) {
3699 return exportImage(patch, fetchImagePlus(patch), path, overwrite);
3702 /** Returns the path to the saved image, or null if not saved. */
3703 public String exportImage(final Patch patch, final ImagePlus imp, final String path, final boolean overwrite) {
3704 // if !overwrite, save only if not there already
3705 if (null == path || null == imp || (!overwrite && new File(path).exists())) return null;
3706 try {
3707 if (imp.getNSlices() > 1) new FileSaver(imp).saveAsTiffStack(path);
3708 else new FileSaver(imp).saveAsTiff(path);
3709 } catch (Exception e) {
3710 Utils.log("Could not save an image for Patch #" + patch.getId() + " at: " + path);
3711 IJError.print(e);
3712 return null;
3714 return path;
3717 /** Whether any changes need to be saved. */
3718 public boolean hasChanges() {
3719 return this.changes;
3722 public void setChanged(final boolean changed) {
3723 this.changes = changed;
3724 //Utils.printCaller(this, 7);
3727 /** Returns null unless overriden. This is intended for FSLoader projects. */
3728 public String getPath(final Patch patch) { return null; }
3730 /** Returns null unless overriden. This is intended for FSLoader projects. */
3731 public String getAbsolutePath(final Patch patch) { return null; }
3733 /** Returns null unless overriden. This is intended for FSLoader projects. */
3734 public String getAbsoluteFilePath(final Patch p) { return null; }
3736 /** Does nothing unless overriden. */
3737 public void setupMenuItems(final JMenu menu, final Project project) {}
3739 /** Test whether this Loader needs recurrent calls to a "save" of some sort, such as for the FSLoader. */
3740 public boolean isAsynchronous() {
3741 // in the future, DBLoader may also be asynchronous
3742 return this.getClass() == FSLoader.class;
3745 /** Throw away all awts and snaps that depend on this image, so that they will be recreated next time they are needed. */
3746 public void decache(final ImagePlus imp) {
3747 synchronized(db_lock) {
3748 lock();
3749 try {
3750 final long id = imps.getId(imp);
3751 Utils.log2("decaching " + id);
3752 if (Long.MIN_VALUE == id) return;
3753 mawts.removeAndFlush(id);
3754 } catch (Exception e) {
3755 IJError.print(e);
3756 } finally {
3757 unlock();
3762 static private Object temp_current_image_lock = new Object();
3763 static private boolean temp_in_use = false;
3764 static private ImagePlus previous_current_image = null;
3766 static public void startSetTempCurrentImage(final ImagePlus imp) {
3767 synchronized (temp_current_image_lock) {
3768 //Utils.log2("temp in use: " + temp_in_use);
3769 while (temp_in_use) { try { temp_current_image_lock.wait(); } catch (InterruptedException ie) {} }
3770 temp_in_use = true;
3771 previous_current_image = WindowManager.getCurrentImage();
3772 WindowManager.setTempCurrentImage(imp);
3776 /** This method MUST always be called after startSetTempCurrentImage(ImagePlus imp) has been called and the action on the image has finished. */
3777 static public void finishSetTempCurrentImage() {
3778 synchronized (temp_current_image_lock) {
3779 WindowManager.setTempCurrentImage(previous_current_image); // be nice
3780 temp_in_use = false;
3781 temp_current_image_lock.notifyAll();
3785 static public void setTempCurrentImage(final ImagePlus imp) {
3786 synchronized (temp_current_image_lock) {
3787 while (temp_in_use) { try { temp_current_image_lock.wait(); } catch (InterruptedException ie) {} }
3788 temp_in_use = true;
3789 WindowManager.setTempCurrentImage(imp);
3790 temp_in_use = false;
3791 temp_current_image_lock.notifyAll();
3795 protected String preprocessor = null;
3797 public boolean setPreprocessor(String plugin_class_name) {
3798 if (null == plugin_class_name || 0 == plugin_class_name.length()) {
3799 this.preprocessor = null;
3800 return true;
3802 // just for the sake of it:
3803 plugin_class_name = plugin_class_name.replace(' ', '_');
3804 // check that it can be instantiated
3805 try {
3806 startSetTempCurrentImage(null);
3807 IJ.redirectErrorMessages();
3808 Object ob = IJ.runPlugIn(plugin_class_name, "");
3809 if (null == ob) {
3810 Utils.showMessageT("The preprocessor plugin " + plugin_class_name + " was not found.");
3811 } else if (!(ob instanceof PlugInFilter)) {
3812 Utils.showMessageT("Plug in '" + plugin_class_name + "' is invalid: does not implement PlugInFilter");
3813 } else { // all is good:
3814 this.preprocessor = plugin_class_name;
3816 } catch (Exception e) {
3817 e.printStackTrace();
3818 Utils.showMessageT("Plug in " + plugin_class_name + " is invalid: ImageJ has thrown an exception when testing it with a null image.");
3819 return false;
3820 } finally {
3821 finishSetTempCurrentImage();
3823 return true;
3826 public String getPreprocessor() {
3827 return preprocessor;
3830 /** Preprocess an image before TrakEM2 ever has a look at it with a system-wide defined preprocessor plugin, specified in the XML file and/or from within the Display properties dialog. Does not lock, and should always run within locking/unlocking statements. */
3831 protected final void preProcess(final ImagePlus imp) {
3832 if (null == preprocessor || null == imp) return;
3833 // access to WindowManager.setTempCurrentImage(...) is locked within the Loader
3834 try {
3835 startSetTempCurrentImage(imp);
3836 IJ.redirectErrorMessages();
3837 IJ.runPlugIn(preprocessor, "");
3838 } catch (Exception e) {
3839 IJError.print(e);
3840 } finally {
3841 finishSetTempCurrentImage();
3843 // reset flag
3844 imp.changes = false;
3847 ///////////////////////
3850 /** List of jobs running on this Loader. */
3851 private ArrayList al_jobs = new ArrayList();
3852 private JPopupMenu popup_jobs = null;
3853 private final Object popup_lock = new Object();
3854 private boolean popup_locked = false;
3856 /** Adds a new job to monitor.*/
3857 public void addJob(Bureaucrat burro) {
3858 synchronized (popup_lock) {
3859 while (popup_locked) try { popup_lock.wait(); } catch (InterruptedException ie) {}
3860 popup_locked = true;
3861 al_jobs.add(burro);
3862 popup_locked = false;
3863 popup_lock.notifyAll();
3866 public void removeJob(Bureaucrat burro) {
3867 synchronized (popup_lock) {
3868 while (popup_locked) try { popup_lock.wait(); } catch (InterruptedException ie) {}
3869 popup_locked = true;
3870 if (null != popup_jobs && popup_jobs.isVisible()) {
3871 popup_jobs.setVisible(false);
3873 al_jobs.remove(burro);
3874 popup_locked = false;
3875 popup_lock.notifyAll();
3878 public JPopupMenu getJobsPopup(Display display) {
3879 synchronized (popup_lock) {
3880 while (popup_locked) try { popup_lock.wait(); } catch (InterruptedException ie) {}
3881 popup_locked = true;
3882 this.popup_jobs = new JPopupMenu("Cancel jobs:");
3883 int i = 1;
3884 for (Iterator it = al_jobs.iterator(); it.hasNext(); ) {
3885 Bureaucrat burro = (Bureaucrat)it.next();
3886 JMenuItem item = new JMenuItem("Job " + i + ": " + burro.getTaskName());
3887 item.addActionListener(display);
3888 popup_jobs.add(item);
3889 i++;
3891 popup_locked = false;
3892 popup_lock.notifyAll();
3894 return popup_jobs;
3896 /** Names as generated for popup menu items in the getJobsPopup method. If the name is null, it will cancel the last one. Runs in a separate thread so that it can immediately return. */
3897 public void quitJob(final String name) {
3898 new Thread () { public void run() { setPriority(Thread.NORM_PRIORITY);
3899 Object ob = null;
3900 synchronized (popup_lock) {
3901 while (popup_locked) try { popup_lock.wait(); } catch (InterruptedException ie) {}
3902 popup_locked = true;
3903 if (null == name && al_jobs.size() > 0) {
3904 ob = al_jobs.get(al_jobs.size()-1);
3905 } else {
3906 int i = Integer.parseInt(name.substring(4, name.indexOf(':')));
3907 if (i >= 1 && i <= al_jobs.size()) ob = al_jobs.get(i-1); // starts at 1
3909 popup_locked = false;
3910 popup_lock.notifyAll();
3912 if (null != ob) {
3913 // will wait until worker returns
3914 ((Bureaucrat)ob).quit(); // will require the lock
3916 synchronized (popup_lock) {
3917 while (popup_locked) try { popup_lock.wait(); } catch (InterruptedException ie) {}
3918 popup_locked = true;
3919 popup_jobs = null;
3920 popup_locked = false;
3921 popup_lock.notifyAll();
3923 Utils.showStatus("Job canceled.", false);
3924 }}.start();
3927 public final void printMemState() {
3928 Utils.log2(new StringBuffer("mem in use: ").append((IJ.currentMemory() * 100.0f) / max_memory).append('%')
3929 .append("\n\timps: ").append(imps.size())
3930 .append("\n\tmawts: ").append(mawts.size())
3931 .toString());
3934 /** Fixes paths before presenting them to the file system, in an OS-dependent manner. */
3935 protected final ImagePlus openImage(String path) {
3936 if (null == path) return null;
3937 // supporting samba networks
3938 if (path.startsWith("//")) {
3939 path = path.replace('/', '\\');
3941 // debug:
3942 Utils.log2("opening image " + path);
3943 //Utils.printCaller(this, 25);
3944 IJ.redirectErrorMessages();
3945 try {
3946 return opener.openImage(path);
3947 } catch (Exception e) {
3948 Utils.log("Could not open image at " + path);
3949 e.printStackTrace();
3950 return null;
3954 /** Equivalent to File.getName(), does not subtract the slice info from it.*/
3955 protected final String getInternalFileName(Patch p) {
3956 final String path = getAbsolutePath(p);
3957 if (null == path) return null;
3958 int i = path.length() -1;
3959 // Safer than lastIndexOf: never returns -1
3960 while (i > -1) {
3961 if ('/' == path.charAt(i)) {
3962 break;
3964 i--;
3966 return path.substring(i+1);
3969 /** Equivalent to File.getName(), but subtracts the slice info from it if any.*/
3970 public final String getFileName(final Patch p) {
3971 String name = getInternalFileName(p);
3972 int i = name.lastIndexOf("-----#slice=");
3973 if (-1 == i) return name;
3974 return name.substring(0, i);
3977 /** Check if an awt exists to paint as a snap. */
3978 public boolean isSnapPaintable(final long id) {
3979 synchronized (db_lock) {
3980 lock();
3981 if (mawts.contains(id)) {
3982 unlock();
3983 return true;
3985 unlock();
3987 return false;
3990 /** If mipmaps regeneration is enabled or not. */
3991 protected boolean mipmaps_regen = true;
3993 // used to prevent generating them when, for example, importing a montage
3994 public void setMipMapsRegeneration(boolean b) {
3995 mipmaps_regen = b;
3998 /** Does nothing unless overriden. */
3999 public void flushMipMaps(boolean forget_dir_mipmaps) {}
4001 /** Does nothing unless overriden. */
4002 public void flushMipMaps(final long id) {}
4004 /** Generates mipmaps for the given Patch and flushes away all presently cached ones for the Patch. */
4005 public boolean update(final Patch patch) {
4006 // 1 - generate mipmaps
4007 final boolean b = generateMipMaps(patch);
4008 // 2 - flush away all cached images
4009 // Independently of whether the mipmap generation failed, the ImagePlus has been updated anyway.
4010 synchronized (db_lock) {
4011 lock();
4012 mawts.removeAndFlush(patch.getId());
4013 unlock();
4015 return b;
4018 /** Does nothing and returns false unless overriden. */
4019 public boolean generateMipMaps(final Patch patch) { return false; }
4021 /** Does nothing unless overriden. */
4022 public void removeMipMaps(final Patch patch) {}
4024 /** Returns generateMipMaps(al, false). */
4025 public Bureaucrat generateMipMaps(final ArrayList al) {
4026 return generateMipMaps(al, false);
4029 /** Does nothing and returns null unless overriden. */
4030 public Bureaucrat generateMipMaps(final ArrayList al, boolean overwrite) { return null; }
4032 /** Does nothing and returns false unless overriden. */
4033 public boolean isMipMapsEnabled() { return false; }
4035 /** Does nothing and returns zero unless overriden. */
4036 public int getClosestMipMapLevel(final Patch patch, int level) {return 0;}
4038 /** Does nothing and returns null unless overriden. */
4039 protected Image fetchMipMapAWT(final Patch patch, final int level) { return null; }
4041 /** Does nothing and returns false unless overriden. */
4042 public boolean checkMipMapFileExists(Patch p, double magnification) { return false; }
4044 public void adjustChannels(final Patch p, final int old_channels) {
4046 if (0xffffffff == old_channels) {
4047 // reuse any loaded mipmaps
4048 Hashtable<Integer,Image> ht = null;
4049 synchronized (db_lock) {
4050 lock();
4051 ht = mawts.getAll(p.getId());
4052 unlock();
4054 for (Map.Entry<Integer,Image> entry : ht.entrySet()) {
4055 // key is level, value is awt
4056 final int level = entry.getKey();
4057 PatchLoadingLock plock = null;
4058 synchronized (db_lock) {
4059 lock();
4060 plock = getOrMakePatchLoadingLock(p, level);
4061 unlock();
4063 synchronized (plock) {
4064 plock.lock(); // block loading of this file
4065 Image awt = null;
4066 try {
4067 awt = p.adjustChannels(entry.getValue());
4068 } catch (Exception e) {
4069 IJError.print(e);
4070 if (null == awt) continue;
4072 synchronized (db_lock) {
4073 lock();
4074 mawts.replace(p.getId(), awt, level);
4075 removePatchLoadingLock(plock);
4076 unlock();
4078 plock.unlock();
4081 } else {
4083 // flush away any loaded mipmap for the id
4084 synchronized (db_lock) {
4085 lock();
4086 mawts.removeAndFlush(p.getId());
4087 unlock();
4089 // when reloaded, the channels will be adjusted
4093 static public ImageProcessor scaleImage(final ImagePlus imp, double mag, final boolean quality) {
4094 if (mag > 1) mag = 1;
4095 ImageProcessor ip = imp.getProcessor();
4096 if (Math.abs(mag - 1) < 0.000001) return ip;
4097 // else, make a properly scaled image:
4098 // - gaussian blurred for best quality when resizing with nearest neighbor
4099 // - direct nearest neighbor otherwise
4100 final int w = ip.getWidth();
4101 final int h = ip.getHeight();
4102 // TODO releseToFit !
4103 if (quality) {
4104 // apply proper gaussian filter
4105 double sigma = Math.sqrt(Math.pow(2, getMipMapLevel(mag, Math.max(imp.getWidth(), imp.getHeight()))) - 0.25); // sigma = sqrt(level^2 - 0.5^2)
4106 ip = new FloatProcessorT2(w, h, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])ip.convertToFloat().getPixels(), w, h), (float)sigma).data, ip.getDefaultColorModel(), ip.getMin(), ip.getMax());
4107 ip = ip.resize((int)(w * mag), (int)(h * mag)); // better while float
4108 return Utils.convertTo(ip, imp.getType(), false);
4109 } else {
4110 return ip.resize((int)(w * mag), (int)(h * mag));
4114 static public ImageProcessor scaleImage(final ImagePlus imp, final int level, final boolean quality) {
4115 if (level <= 0) return imp.getProcessor();
4116 // else, make a properly scaled image:
4117 // - gaussian blurred for best quality when resizing with nearest neighbor
4118 // - direct nearest neighbor otherwise
4119 ImageProcessor ip = imp.getProcessor();
4120 final int w = ip.getWidth();
4121 final int h = ip.getHeight();
4122 final double mag = 1 / Math.pow(2, level);
4123 // TODO releseToFit !
4124 if (quality) {
4125 // apply proper gaussian filter
4126 double sigma = Math.sqrt(Math.pow(2, level) - 0.25); // sigma = sqrt(level^2 - 0.5^2)
4127 ip = new FloatProcessorT2(w, h, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])ip.convertToFloat().getPixels(), w, h), (float)sigma).data, ip.getDefaultColorModel(), ip.getMin(), ip.getMax());
4128 ip = ip.resize((int)(w * mag), (int)(h * mag)); // better while float
4129 return Utils.convertTo(ip, imp.getType(), false);
4130 } else {
4131 return ip.resize((int)(w * mag), (int)(h * mag));
4135 /* =========================== */
4137 /** Serializes the given object into the path. Returns false on failure. */
4138 public boolean serialize(final Object ob, final String path) {
4139 try {
4140 // 1 - Check that the parent chain of folders exists, and attempt to create it when not:
4141 File fdir = new File(path).getParentFile();
4142 if (null == fdir) return false;
4143 fdir.mkdirs();
4144 if (!fdir.exists()) {
4145 Utils.log2("Could not create folder " + fdir.getAbsolutePath());
4146 return false;
4148 // 2 - Serialize the given object:
4149 final ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(path));
4150 out.writeObject(ob);
4151 out.close();
4152 return true;
4153 } catch (Exception e) {
4154 IJError.print(e);
4156 return false;
4158 /** Attempts to find a file containing a serialized object. Returns null if no suitable file is found, or an error occurs while deserializing. */
4159 public Object deserialize(final String path) {
4160 try {
4161 if (!new File(path).exists()) return null;
4162 final ObjectInputStream in = new ObjectInputStream(new FileInputStream(path));
4163 final Object ob = in.readObject();
4164 in.close();
4165 return ob;
4166 } catch (Exception e) {
4167 //IJError.print(e); // too much output if a whole set is wrong
4168 e.printStackTrace();
4170 return null;
4173 public void insertXMLOptions(StringBuffer sb_body, String indent) {}
4175 // OBSOLETE
4176 public Bureaucrat optimizeContrast(final ArrayList al_patches) {
4177 final Patch[] pa = new Patch[al_patches.size()];
4178 al_patches.toArray(pa);
4179 Worker worker = new Worker("Optimize contrast") {
4180 public void run() {
4181 startedWorking();
4182 final Worker wo = this;
4183 try {
4184 ///////// Multithreading ///////
4185 final AtomicInteger ai = new AtomicInteger(0);
4186 final Thread[] threads = MultiThreading.newThreads();
4188 for (int ithread = 0; ithread < threads.length; ++ithread) {
4189 threads[ithread] = new Thread() {
4190 public void run() {
4191 setPriority(Thread.NORM_PRIORITY);
4192 /////////////////////////
4193 for (int g = ai.getAndIncrement(); g < pa.length; g = ai.getAndIncrement()) {
4194 if (wo.hasQuitted()) break;
4195 ImagePlus imp = fetchImagePlus(pa[g]);
4196 ImageStatistics stats = imp.getStatistics();
4197 int type = imp.getType();
4198 imp = null;
4199 // Compute autoAdjust min and max values
4200 // extracting code from ij.plugin.frame.ContrastAdjuster, method autoAdjust
4201 int autoThreshold = 0;
4202 // once for 8-bit and color, twice for 16 and 32-bit (thus the 2501 autoThreshold value)
4203 int limit = stats.pixelCount/10;
4204 int[] histogram = stats.histogram;
4205 //if (autoThreshold<10) autoThreshold = 5000;
4206 //else autoThreshold /= 2;
4207 if (ImagePlus.GRAY16 == type || ImagePlus.GRAY32 == type) autoThreshold = 2500;
4208 else autoThreshold = 5000;
4209 int threshold = stats.pixelCount / autoThreshold;
4210 int i = -1;
4211 boolean found = false;
4212 int count;
4213 double min=0, max=0;
4214 do {
4215 i++;
4216 count = histogram[i];
4217 if (count>limit) count = 0;
4218 found = count > threshold;
4219 } while (!found && i<255);
4220 int hmin = i;
4221 i = 256;
4222 do {
4223 i--;
4224 count = histogram[i];
4225 if (count > limit) count = 0;
4226 found = count > threshold;
4227 } while (!found && i>0);
4228 int hmax = i;
4229 if (hmax >= hmin) {
4230 min = stats.histMin + hmin*stats.binSize;
4231 max = stats.histMin + hmax*stats.binSize;
4232 if (min == max) {
4233 min = stats.min;
4234 max = stats.max;
4237 pa[g].setMinAndMax(min, max);
4241 ///////////////////////// - where are my lisp macros .. and no, mapping a function with reflection is not elegant, but rather a verbosity and constriction attack
4245 MultiThreading.startAndJoin(threads);
4246 /////////////////////////
4248 if (wo.hasQuitted()) {
4249 rollback();
4250 } else {
4252 // recreate mipmap files
4253 if (isMipMapsEnabled()) {
4254 ArrayList al = new ArrayList();
4255 for (int k=0; k<pa.length; k++) al.add(pa[k]);
4256 Thread task = generateMipMaps(al, true); // yes, overwrite files!
4257 task.join();
4259 // flush away any existing awt images, so that they'll be reloaded or recreated
4260 synchronized (db_lock) {
4261 lock();
4262 for (int i=0; i<pa.length; i++) {
4263 mawts.removeAndFlush(pa[i].getId());
4264 Utils.log2(i + " removing mawt for " + pa[i].getId());
4266 unlock();
4268 for (int i=0; i<pa.length; i++) {
4269 Display.repaint(pa[i].getLayer(), pa[i], 0);
4273 } catch (Exception e) {
4274 IJError.print(e);
4276 finishedWorking();
4279 return Bureaucrat.createAndStart(worker, pa[0].getProject());
4283 public Bureaucrat homogenizeContrast(final Layer[] la) {
4284 return homogenizeContrast(la, null);
4287 /** Homogenize contrast layer-wise, for all given layers, in a multithreaded manner. */
4288 public Bureaucrat homogenizeContrast(final Layer[] la, final Worker parent) {
4289 if (null == la || 0 == la.length) return null;
4290 Worker worker = new Worker("Enhancing contrast") {
4291 public void run() {
4292 startedWorking();
4293 final Worker wo = this;
4294 try {
4296 // USING one single thread, for the locking is so bad, to access
4297 // the imps and to releaseToFit, that it's not worth it: same images
4298 // are being reloaded many times just because they all don't fit in
4299 // at the same time.
4301 // when quited, rollback() and Display.repaint(layer)
4302 for (int i = 0; i < la.length; i++) {
4303 if (wo.hasQuitted()) {
4304 break;
4306 setTaskName("Enhance contrast, layer z=" + Utils.cutNumber(la[i].getZ(), 2) + " " + (i+1) + "/" + la.length);
4307 ArrayList al = la[i].getDisplayables(Patch.class);
4308 Patch[] pa = new Patch[al.size()];
4309 al.toArray(pa);
4310 if (!homogenizeContrast(la[i], pa, null == parent ? wo : parent)) {
4311 Utils.log("Could not homogenize contrast for images in layer " + la[i]);
4315 if (wo.hasQuitted()) {
4316 rollback();
4317 for (int i=0; i<la.length; i++) Display.repaint(la[i]);
4320 } catch (Exception e) {
4321 IJError.print(e);
4323 finishedWorking();
4326 return Bureaucrat.createAndStart(worker, la[0].getProject());
4329 public Bureaucrat homogenizeContrast(final ArrayList<Patch> al) {
4330 return homogenizeContrast(al, null);
4333 public Bureaucrat homogenizeContrast(final ArrayList<Patch> al, final Worker parent) {
4334 if (null == al || al.size() < 1) return null;
4335 final Patch[] pa = new Patch[al.size()];
4336 al.toArray(pa);
4337 Worker worker = new Worker("Enhance contrast") {
4338 public void run() {
4339 startedWorking();
4340 try {
4341 homogenizeContrast(pa[0].getLayer(), pa, null == parent ? this : parent);
4342 } catch (Exception e) {
4343 IJError.print(e);
4345 finishedWorking();
4348 return Bureaucrat.createAndStart(worker, pa[0].getProject());
4351 /** Homogenize contrast for all given Patch objects, which must be all of the same size and type. Returns false on failure. Needs a layer to repaint when done. */
4352 public boolean homogenizeContrast(final Layer layer, final Patch[] pa, final Worker worker) {
4353 try {
4354 if (null == pa) return false; // error
4355 if (0 == pa.length) return true; // done
4356 // 0 - check that all images are of the same size and type
4357 final int ptype = pa[0].getType();
4358 double pw = pa[0].getOWidth();
4359 double ph = pa[0].getOHeight();
4360 for (int e=1; e<pa.length; e++) {
4361 if (pa[e].getType() != ptype) {
4362 // can't continue
4363 Utils.log("Can't homogenize histograms: images are not all of the same type.\nFirst offending image is: " + pa[e]);
4364 return false;
4366 if (pa[e].getOWidth() != pw || pa[e].getOHeight() != ph) {
4367 Utils.log("Can't homogenize histograms: images are not all of the same size.\nFirst offending image is: " + pa[e]);
4368 return false;
4372 // 1 - fetch statistics for each image
4373 final ArrayList al_st = new ArrayList();
4374 final ArrayList al_p = new ArrayList(); // list of Patch ordered by stdDev ASC
4375 int type = -1;
4376 for (int i=0; i<pa.length; i++) {
4377 if (null != worker && worker.hasQuitted()) {
4378 return false;
4380 ImagePlus imp = fetchImagePlus(pa[i]);
4381 if (-1 == type) type = imp.getType();
4382 releaseToFit(measureSize(imp));
4383 ImageStatistics i_st = imp.getStatistics();
4384 // insert ordered by stdDev, from small to big
4385 int q = 0;
4386 for (Iterator it = al_st.iterator(); it.hasNext(); ) {
4387 ImageStatistics st = (ImageStatistics)it.next();
4388 q++;
4389 if (st.stdDev > i_st.stdDev) break;
4391 if (q == pa.length) {
4392 al_st.add(i_st); // append at the end. WARNING if importing thousands of images, this is a potential source of out of memory errors. I could just recompute it when I needed it again below
4393 al_p.add(pa[i]);
4394 } else {
4395 al_st.add(q, i_st);
4396 al_p.add(q, pa[i]);
4399 final ArrayList al_p2 = (ArrayList)al_p.clone(); // shallow copy of the ordered list
4400 // 2 - discard the first and last 25% (TODO: a proper histogram clustering analysis and histogram examination should apply here)
4401 if (pa.length > 3) { // under 4 images, use them all
4402 int i=0;
4403 final int quarter = pa.length / 4;
4404 while (i < quarter) {
4405 al_p.remove(i);
4406 i++;
4408 i = 0;
4409 int last = al_p.size() -1;
4410 while (i < quarter) { // I know that it can be done better, but this is CLEAR
4411 al_p.remove(last); // why doesn't ArrayList have a removeLast() method ?? And why is removeRange() 'protected' ??
4412 last--;
4413 i++;
4417 final ImageStatistics stats;
4418 PatchStack ps = null;
4420 if (al_p.size() > 1) {
4421 // USE internal ContrastEnhancer plugin with a virtual stack made of the middle 50% of images
4422 final Patch[] p50 = new Patch[al_p.size()];
4423 al_p.toArray(p50);
4424 ps = new PatchStack(p50, 1); // is an ImagePlus
4425 stats = new StackStatistics(ps);
4426 } else {
4427 stats = fetchImagePlus((Patch)al_p.get(0)).getStatistics();
4430 final ContrastEnhancer ce = new ContrastEnhancer();
4431 Field fnormalize = ContrastEnhancer.class.getDeclaredField("normalize");
4432 fnormalize.setAccessible(true);
4433 fnormalize.set(ce, true);
4435 Utils.log2("Worker is: " + worker);
4436 if (null != worker) Utils.log2("property is: " + worker.getProperty("ContrastEnhancer-dialog"));
4438 if (null == worker || Boolean.FALSE != worker.getProperty("ContrastEnhancer-dialog")) {
4439 // Show the dialog
4440 Method m = ContrastEnhancer.class.getDeclaredMethod("showDialog", new Class[]{ImagePlus.class});
4441 m.setAccessible(true);
4442 if (Boolean.FALSE == m.invoke(ce, new Object[]{ null != ps ? ps : fetchImagePlus((Patch)al_p.get(0)) } )) {
4443 Utils.log2("Canceled ContrastEnhancer dialog.");
4444 return false;
4447 if (null != worker && null == worker.getProperty("ContrastEnhancer-dialog")) {
4448 // Avoid subsequent calls to the dialog
4449 worker.setProperty("ContrastEnhancer-dialog", Boolean.FALSE);
4452 // The above ContrastEnhancer will be applied to all, but the stats are computed for the middle 50%. This is a patched solution to avoid noise-rich tiles.
4454 // Apply ContrastEnhancer to all
4455 for (Patch p : pa) {
4456 ImageProcessor ip = p.getImageProcessor();
4457 ip.resetMinAndMax();
4458 ce.stretchHistogram(ip, 0.5, stats); // 0.5 saturation
4459 p.setMinAndMax(ip.getMin(), ip.getMax());
4462 // 7 - recreate mipmap files
4463 if (isMipMapsEnabled()) {
4464 ArrayList al = new ArrayList();
4465 for (int k=0; k<pa.length; k++) al.add(pa[k]);
4466 Thread task = generateMipMaps(al, true); // yes, overwrite files!
4467 task.join();
4468 // not threaded:
4469 //for (int k=0; k<pa.length; k++) generateMipMaps(pa[k], true);
4471 // 8 - flush away any existing awt images, so that they'll be reloaded or recreated
4472 synchronized (db_lock) {
4473 lock();
4474 for (int k=0; k<pa.length; k++) {
4475 mawts.removeAndFlush(pa[k].getId());
4476 Utils.log2(k + " removing mawt for " + pa[k].getId());
4478 unlock();
4480 // problem: if the user starts navigating the display, it will maybe end up recreating mipmaps more than once for a few tiles
4481 if (null != layer) Display.repaint(layer, new Rectangle(0, 0, (int)layer.getParent().getLayerWidth(), (int)layer.getParent().getLayerHeight()), 0);
4482 } catch (Exception e) {
4483 IJError.print(e);
4484 return false;
4486 return true;
4489 public Bureaucrat setMinAndMax(final List<Displayable> patches, final double min, final double max) {
4490 Worker worker = new Worker("Set min and max") {
4491 public void run() {
4492 try {
4493 startedWorking();
4494 if (Double.isNaN(min) || Double.isNaN(max)) {
4495 Utils.log("WARNING:\nUnacceptable min and max values: " + min + ", " + max);
4496 finishedWorking();
4497 return;
4499 final List<Displayable> pa = new ArrayList<Displayable>(patches);
4500 final AtomicInteger ai = new AtomicInteger(0);
4501 final AtomicInteger completed = new AtomicInteger(0);
4502 final Thread[] threads = MultiThreading.newThreads();
4503 for (int ithread = 0; ithread < threads.length; ithread++) {
4504 threads[ithread] = new Thread() {
4505 public void run() {
4506 for (int i=ai.getAndIncrement(); i<patches.size(); i = ai.getAndIncrement()) {
4507 Displayable d = pa.get(i);
4508 if (d.getClass() != Patch.class) continue;
4509 Patch p = (Patch)d;
4510 p.setMinAndMax(min, max);
4511 p.updateMipmaps();
4512 Display.repaint(p);
4513 Utils.showProgress(completed.incrementAndGet() / (double)pa.size());
4518 MultiThreading.startAndJoin(threads);
4519 } catch (Exception e) {
4520 IJError.print(e);
4521 } finally {
4522 finishedWorking();
4526 return Bureaucrat.createAndStart(worker, Project.findProject(this));
4529 public long estimateImageFileSize(final Patch p, final int level) {
4530 if (0 == level) {
4531 return (long)(p.getWidth() * p.getHeight() * 5 + 1024); // conservative
4533 // else, compute scale
4534 final double scale = 1 / Math.pow(2, level);
4535 return (long)(p.getWidth() * scale * p.getHeight() * scale * 5 + 1024); // conservative
4538 // Dummy class to provide access the notifyListeners from Image
4539 static private final class ImagePlusAccess extends ImagePlus {
4540 final int CLOSE = CLOSED; // from super class ImagePlus
4541 final int OPEN = OPENED;
4542 final int UPDATE = UPDATED;
4543 private Vector<ij.ImageListener> my_listeners;
4544 public ImagePlusAccess() {
4545 super();
4546 try {
4547 java.lang.reflect.Field f = ImagePlus.class.getDeclaredField("listeners");
4548 f.setAccessible(true);
4549 this.my_listeners = (Vector<ij.ImageListener>)f.get(this);
4550 } catch (Exception e) {
4551 IJError.print(e);
4554 public final void notifyListeners(final ImagePlus imp, final int action) {
4555 try {
4556 for (ij.ImageListener listener : my_listeners) {
4557 switch (action) {
4558 case CLOSED:
4559 listener.imageClosed(imp);
4560 break;
4561 case OPENED:
4562 listener.imageOpened(imp);
4563 break;
4564 case UPDATED:
4565 listener.imageUpdated(imp);
4566 break;
4569 } catch (Exception e) {}
4572 static private final ImagePlusAccess ipa = new ImagePlusAccess();
4574 /** Workaround for ImageJ's ImagePlus.flush() method which calls the System.gc() unnecessarily.<br />
4575 * A null pointer as argument is accepted. */
4576 static public final void flush(final ImagePlus imp) {
4577 if (null == imp) return;
4578 final Roi roi = imp.getRoi();
4579 if (null != roi) roi.setImage(null);
4580 //final ImageProcessor ip = imp.getProcessor(); // the nullifying makes no difference, and in low memory situations some bona fide imagepluses may end up failing on the calling method because of lack of time to grab the processor etc.
4581 //if (null != ip) ip.setPixels(null);
4582 ipa.notifyListeners(imp, ipa.CLOSE);
4585 /** Returns the user's home folder unless overriden. */
4586 public String getStorageFolder() { return System.getProperty("user.home").replace('\\', '/'); }
4588 public String getImageStorageFolder() { return getStorageFolder(); }
4590 /** Returns null unless overriden. */
4591 public String getMipMapsFolder() { return null; }
4593 public Patch addNewImage(final ImagePlus imp) {
4594 return addNewImage(imp, 0, 0);
4597 public Patch addNewImage(final ImagePlus imp, final double x, final double y) {
4598 String filename = imp.getTitle();
4599 if (!filename.toLowerCase().endsWith(".tif")) filename += ".tif";
4600 String path = getStorageFolder() + "/" + filename;
4601 new FileSaver(imp).saveAsTiff(path);
4602 Patch pa = new Patch(Project.findProject(this), imp.getTitle(), x, y, imp);
4603 addedPatchFrom(path, pa);
4604 if (isMipMapsEnabled()) generateMipMaps(pa);
4605 return pa;
4608 public String makeProjectName() {
4609 return "Untitled " + ControlWindow.getTabIndex(Project.findProject(this));
4613 /** Will preload in the background as many as possible of the given images for the given magnification, if and only if (1) there is more than one CPU core available [and only the extra ones will be used], and (2) there is more than 1 image to preload. */
4615 static private ImageLoaderThread[] imageloader = null;
4616 static private Preloader preloader = null;
4618 // TODO update all this to use an ExecutorService
4619 static public final void setupPreloader(final ControlWindow master) {
4620 if (null == imageloader) {
4621 int n = Runtime.getRuntime().availableProcessors()-1;
4622 if (0 == n) n = 1; // !@#$%^
4623 imageloader = new ImageLoaderThread[n];
4624 for (int i=0; i<imageloader.length; i++) {
4625 imageloader[i] = new ImageLoaderThread();
4628 if (null == preloader) preloader = new Preloader();
4630 static public final void destroyPreloader(final ControlWindow master) {
4631 if (null != preloader) { preloader.quit(); preloader = null; }
4632 if (null != imageloader) {
4633 for (int i=0; i<imageloader.length; i++) {
4634 if (null != imageloader[i]) { imageloader[i].quit(); }
4636 imageloader = null;
4641 // Java is pathetically low level.
4642 static private final class Tuple {
4643 final Patch patch;
4644 double mag;
4645 boolean repaint;
4646 private boolean valid = true;
4647 Tuple(final Patch patch, final double mag, final boolean repaint) {
4648 this.patch = patch;
4649 this.mag = mag;
4650 this.repaint = repaint;
4652 public final boolean equals(final Object ob) {
4653 // DISABLED: private class Tuple will never be used in lists that contain objects that are not Tuple as well.
4654 //if (ob.getClass() != Tuple.class) return false;
4655 final Tuple tu = (Tuple)ob;
4656 return patch == tu.patch && mag == tu.mag && repaint == tu.repaint;
4658 final void invalidate() {
4659 //Utils.log2("@@@@ called invalidate for mag " + mag);
4660 valid = false;
4664 /** Manages available CPU cores for loading images in the background. */
4665 static private final class Preloader extends Thread {
4666 private final LinkedList<Tuple> queue = new LinkedList<Tuple>();
4667 /** IdentityHashMap uses ==, not .equals() ! */
4668 private final IdentityHashMap<Patch,HashMap<Integer,Tuple>> map = new IdentityHashMap<Patch,HashMap<Integer,Tuple>>();
4669 private boolean go = true;
4670 /** Controls access to the queue. */
4671 private final Lock lock = new Lock();
4672 private final Lock lock2 = new Lock();
4673 Preloader() {
4674 super("T2-Preloader");
4675 setPriority(Thread.NORM_PRIORITY);
4676 try { setDaemon(true); } catch (Exception e) { e.printStackTrace(); }
4677 start();
4679 /** WARNING this method effectively limits zoom out to 0.00000001. */
4680 private final int makeKey(final double mag) {
4681 // get the nearest equal or higher power of 2
4682 return (int)(0.000005 + Math.abs(Math.log(mag) / Math.log(2)));
4684 public final void quit() {
4685 this.go = false;
4686 synchronized (lock) { lock.lock(); queue.clear(); lock.unlock(); }
4687 synchronized (lock2) { lock2.unlock(); }
4689 private final void addEntry(final Patch patch, final double mag, final boolean repaint) {
4690 synchronized (lock) {
4691 lock.lock();
4692 final Tuple tu = new Tuple(patch, mag, repaint);
4693 HashMap<Integer,Tuple> m = map.get(patch);
4694 final int key = makeKey(mag);
4695 if (null == m) {
4696 m = new HashMap<Integer,Tuple>();
4697 m.put(key, tu);
4698 map.put(patch, m);
4699 } else {
4700 // invalidate previous entry if any
4701 Tuple old = m.get(key);
4702 if (null != old) old.invalidate();
4703 // in any case:
4704 m.put(key, tu);
4706 queue.add(tu);
4707 lock.unlock();
4710 private final void addPatch(final Patch patch, final double mag, final boolean repaint) {
4711 if (patch.getProject().getLoader().isCached(patch, mag)) return;
4712 if (repaint && !Display.willPaint(patch, mag)) return;
4713 // else, queue:
4714 addEntry(patch, mag, repaint);
4716 public final void add(final Patch patch, final double mag, final boolean repaint) {
4717 addPatch(patch, mag, repaint);
4718 synchronized (lock2) { lock2.unlock(); }
4720 public final void add(final ArrayList<Patch> patches, final double mag, final boolean repaint) {
4721 //Utils.log2("@@@@ Adding " + patches.size() + " for mag " + mag);
4722 for (Patch p : patches) {
4723 addPatch(p, mag, repaint);
4725 synchronized (lock2) { lock2.unlock(); }
4727 public final void remove(final ArrayList<Patch> patches, final double mag) {
4728 // WARNING: this method only makes sense of the canceling of the offscreen thread happens before the issuing of the new offscreen thread, which is currently the case.
4729 int sum = 0;
4730 synchronized (lock) {
4731 lock.lock();
4732 for (Patch p : patches) {
4733 HashMap<Integer,Tuple> m = map.get(p);
4734 if (null == m) {
4735 continue;
4737 final Tuple tu = m.remove(makeKey(mag)); // if present.
4738 //Utils.log2("@@@@ mag is " + mag + " and tu is null == " + (null == tu));
4739 if (null != tu) {
4740 tu.invalidate(); // never removed from the queue, just invalidated. Will be removed by the preloader thread itself, when poping from the end.
4741 if (m.isEmpty()) map.remove(p);
4742 sum++;
4745 lock.unlock();
4747 //Utils.log2("@@@@ invalidated " + sum + " for mag " + mag);
4749 private final void removeMapping(final Tuple tu) {
4750 final HashMap<Integer,Tuple> m = map.get(tu.patch);
4751 if (null == m) return;
4752 m.remove(makeKey(tu.mag));
4753 if (m.isEmpty()) map.remove(tu.patch);
4755 public void run() {
4756 final int size = imageloader.length; // as many as Preloader threads
4757 final ArrayList<Tuple> list = new ArrayList<Tuple>(size);
4758 while (go) {
4759 try {
4760 synchronized (lock2) { lock2.lock(); }
4761 // read out a group of imageloader.length patches to load
4762 while (true) {
4763 // block 1: pop out 'size' valid tuples from the queue (and all invalid in between as well)
4764 synchronized (lock) {
4765 lock.lock();
4766 int len = queue.size();
4767 //Utils.log2("@@@@@ Queue size: " + len);
4768 if (0 == len) {
4769 lock.unlock();
4770 break;
4772 // When more than a hundred images, multiply by 10 the remove/read -out batch for preloading.
4773 // (if the batch_size is too large, then the removing/invalidating tuples from the queue would not work properly, i.e. they would never get invalidated and thus they'd be preloaded unnecessarily.)
4774 final int batch_size = size * (len < 100 ? 1 : 10);
4776 for (int i=0; i<batch_size && i<len; len--) {
4777 final Tuple tuple = queue.remove(len-1); // start removing from the end, since those are the latest additions, hence the ones the user wants to see immediately.
4778 removeMapping(tuple);
4779 if (!tuple.valid) {
4780 //Utils.log2("@@@@@ skipping invalid tuple");
4781 continue;
4783 list.add(tuple);
4784 i++;
4786 //Utils.log2("@@@@@ Queue size after: " + queue.size());
4787 lock.unlock();
4790 // changes may occur now to the queue, so work on the list
4792 //Utils.log2("@@@@@ list size: " + list.size());
4794 // block 2: now iterate until each tuple in the list has been assigned to a preloader thread
4795 while (!list.isEmpty()) {
4796 final Iterator<Tuple> it = list.iterator();
4797 int i = 0;
4798 while (it.hasNext()) {
4799 final Tuple tu = it.next();
4800 if (i == imageloader.length) {
4801 try { Thread.sleep(10); } catch (Exception e) {}
4802 i = 0; // circular array
4804 if (!imageloader[i].isLoading()) {
4805 it.remove();
4806 imageloader[i].load(tu.patch, tu.mag, tu.repaint);
4808 i++;
4810 if (!list.isEmpty()) try {
4811 //Utils.log2("@@@@@ list not empty, waiting 50 ms");
4812 Thread.sleep(50);
4813 } catch (InterruptedException ie) {}
4816 } catch (Exception e) {
4817 e.printStackTrace();
4818 synchronized (lock) { lock.unlock(); } // just in case ...
4824 static public final void preload(final Patch patch, final double magnification, final boolean repaint) {
4825 preloader.add(patch, magnification, repaint);
4827 static public final void preload(final ArrayList<Patch> patches, final double magnification, final boolean repaint) {
4828 preloader.add(patches, magnification, repaint);
4830 static public final void quitPreloading(final ArrayList<Patch> patches, final double magnification) {
4831 preloader.remove(patches, magnification);
4834 static private final class ImageLoaderThread extends Thread {
4835 /** Controls access to Patch etc. */
4836 private final Lock lock = new Lock();
4837 /** Limits access to the load method while a previous image is being worked on. */
4838 private final Lock lock2 = new Lock();
4839 private Patch patch = null;
4840 private double mag = 1.0;
4841 private boolean repaint = false;
4842 private boolean go = true;
4843 private boolean loading = false;
4844 public ImageLoaderThread() {
4845 super("T2-Image-Loader");
4846 setPriority(Thread.NORM_PRIORITY);
4847 try { setDaemon(true); } catch (Exception e) { e.printStackTrace(); }
4848 start();
4850 public final void quit() {
4851 this.go = false;
4852 synchronized (lock) { try { this.patch = null; lock.unlock(); } catch (Exception e) {} }
4853 synchronized (lock2) { lock2.unlock(); }
4855 /** Sets the given Patch to be loaded, and returns. A second call to this method will wait until the first call has finished, indicating the Thread is busy loading the previous image. */
4856 public final void load(final Patch p, final double mag, final boolean repaint) {
4857 synchronized (lock) {
4858 try {
4859 lock.lock();
4860 this.patch = p;
4861 this.mag = mag;
4862 this.repaint = repaint;
4863 if (null != patch) {
4864 synchronized (lock2) {
4865 try { lock2.unlock(); } catch (Exception e) { e.printStackTrace(); }
4868 } catch (Exception e) {
4869 e.printStackTrace();
4873 final boolean isLoading() {
4874 return loading;
4876 public void run() {
4877 while (go) {
4878 Patch p = null;
4879 double mag = 1.0;
4880 boolean repaint = false;
4881 synchronized (lock2) {
4882 try {
4883 // wait until there's a Patch to preload.
4884 lock2.lock();
4885 // ready: catch locally (no need to synch on lock because it can't change, considering the load method.
4886 p = this.patch;
4887 mag = this.mag;
4888 repaint = this.repaint;
4889 } catch (Exception e) {}
4891 if (null != p && !p.getProject().getLoader().hs_unloadable.contains(p)) {
4892 try {
4893 if (repaint) {
4894 // wait a bit in case the user has browsed past
4895 Thread.yield();
4896 if (mag >= 0.25) try { sleep(50); } catch (InterruptedException ie) {}
4897 if (Display.willPaint(p, mag)) {
4898 loading = true;
4899 Object ob = p.getProject().getLoader().fetchImage(p, mag);
4900 if (null != ob) Display.repaint(p.getLayer(), p, p.getBoundingBox(null), 1, false); // not the navigator
4902 } else {
4903 // just load it into the cache if possible
4904 loading = true;
4905 p.getProject().getLoader().fetchImage(p, mag);
4907 p = null;
4908 } catch (Exception e) { e.printStackTrace(); }
4910 // signal done
4911 try {
4912 synchronized (lock) { loading = false; lock.unlock(); }
4913 } catch (Exception e) {}
4919 /** Returns the highest mipmap level for which a mipmap image may have been generated given the dimensions of the Patch. The minimum that this method may return is zero. */
4920 public static final int getHighestMipMapLevel(final Patch p) {
4922 int level = 0;
4923 int w = (int)p.getWidth();
4924 int h = (int)p.getHeight();
4925 while (w >= 64 && h >= 64) {
4926 w /= 2;
4927 h /= 2;
4928 level++;
4930 return level;
4932 // Analytically:
4933 // For images of width or height of at least 32 pixels, need to test for log(64) like in the loop above
4934 // because this is NOT a do/while but a while, so we need to stop one step earlier.
4935 return (int)(0.5 + (Math.log(Math.min(p.getWidth(), p.getHeight())) - Math.log(64)) / Math.log(2));
4936 // Same as:
4937 // return getHighestMipMapLevel(Math.min(p.getWidth(), p.getHeight()));
4940 public static final int getHighestMipMapLevel(final double size) {
4941 return (int)(0.5 + (Math.log(size) - Math.log(64)) / Math.log(2));
4944 static public final int NEAREST_NEIGHBOR = 0;
4945 static public final int BILINEAR = 1;
4946 static public final int BICUBIC = 2;
4947 static public final int GAUSSIAN = 3;
4948 static public final int AREA_AVERAGING = 4;
4949 static public final String[] modes = new String[]{"Nearest neighbor", "Bilinear", "Bicubic", "Gaussian"}; //, "Area averaging"};
4951 static public final int getMode(final String mode) {
4952 for (int i=0; i<modes.length; i++) {
4953 if (mode.equals(modes[i])) return i;
4955 return 0;
4958 /** Does nothing unless overriden. */
4959 public Bureaucrat generateLayerMipMaps(final Layer[] la, final int starting_level) {
4960 return null;
4963 /** Recover from an OutOfMemoryError: release 1/3 of all memory AND execute the garbage collector. */
4964 public void recoverOOME() {
4965 releaseToFit(IJ.maxMemory() / 3);
4966 long start = System.currentTimeMillis();
4967 long end = start;
4968 for (int i=0; i<3; i++) {
4969 System.gc();
4970 Thread.yield();
4971 end = System.currentTimeMillis();
4972 if (end - start > 2000) break; // garbage collecion catched and is running.
4973 start = end;
4977 static public boolean canReadAndWriteTo(final String dir) {
4978 final File fsf = new File(dir);
4979 return fsf.canWrite() && fsf.canRead();
4983 /** Does nothing and returns null unless overridden. */
4984 public String setImageFile(Patch p, ImagePlus imp) { return null; }
4986 public boolean isUnloadable(final Patch p) { return hs_unloadable.contains(p); }
4988 public void removeFromUnloadable(final Patch p) { hs_unloadable.remove(p); }
4990 protected static final BufferedImage createARGBImage(final int width, final int height, final int[] pix) {
4991 final BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
4992 // In one step, set pixels that contain the alpha byte already:
4993 bi.setRGB( 0, 0, width, height, pix, 0, width );
4994 return bi;
4997 /** Embed the alpha-byte into an int[], changes the int[] in place and returns it */
4998 protected static final int[] embedAlpha( final int[] pix, final byte[] alpha){
4999 return embedAlpha(pix, alpha, null);
5002 protected static final int[] embedAlpha( final int[] pix, final byte[] alpha, final byte[] outside) {
5003 if (null == outside) {
5004 if (null == alpha)
5005 return pix;
5006 for (int i=0; i<pix.length; ++i)
5007 pix[i] = (pix[i]&0x00ffffff) | ((alpha[i]&0xff)<<24);
5008 } else {
5009 for (int i=0; i<pix.length; ++i) {
5010 pix[i] = (pix[i]&0x00ffffff) | ( (outside[i]&0xff) != 255 ? 0 : ((alpha[i]&0xff)<<24) );
5013 return pix;
5016 /** Does nothing unless overriden. */
5017 public void queueForMipmapRemoval(final Patch p, boolean yes) {}
5019 /** Get the Universal Near-Unique Id for the project hosted by this loader. */
5020 public String getUNUId() {
5021 // FSLoader overrides this method
5022 return Long.toString(System.currentTimeMillis());
5025 // FSLoader overrides this method
5026 public String getUNUIdFolder() {
5027 return "trakem2." + getUNUId() + "/";
5030 /** Does nothing unless overriden. */
5031 public boolean regenerateMipMaps(final Patch patch) { return false; }
5033 /** Read out the width,height of an image using LOCI BioFormats. */
5034 static public Dimension getDimensions(final String path) {
5035 IFormatReader fr = null;
5036 try {
5037 fr = new ChannelSeparator();
5038 fr.setId(path);
5039 return new Dimension(fr.getSizeX(), fr.getSizeY());
5040 } catch (FormatException fe) {
5041 Utils.log("Error in reading image file at " + path + "\n" + fe);
5042 } catch (Exception e) {
5043 IJError.print(e);
5044 } finally {
5045 if (null != fr) try {
5046 fr.close();
5047 } catch (IOException ioe) { Utils.log2("Could not close IFormatReader: " + ioe); }
5049 return null;
5052 public Dimension getDimensions(final Patch p) {
5053 String path = getAbsolutePath(p);
5054 int i = path.lastIndexOf("-----#slice=");
5055 if (-1 != i) path = path.substring(0, i);
5056 return Loader.getDimensions(path);