3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
23 package ini
.trakem2
.persistence
;
25 import ini
.trakem2
.utils
.IJError
;
30 import ij
.WindowManager
;
31 import ij
.gui
.GenericDialog
;
33 import ij
.gui
.YesNoCancelDialog
;
34 import ij
.io
.DirectoryChooser
;
35 import ij
.io
.FileInfo
;
36 import ij
.io
.FileSaver
;
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
;
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;
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
++) {
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
);
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
);
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
223 if (null == v_loaders
) v_loaders
= new Vector
<Loader
>();
225 if (!ControlWindow
.isGUIEnabled()) {
226 opener
.setSilentMode(true);
229 // debug: report cache status every ten seconds
231 final Loader lo = this;
234 setPriority(Thread.NORM_PRIORITY);
236 try { Thread.sleep(1000); } catch (InterruptedException ie) {}
237 synchronized(db_lock) {
239 //if (!v_loaders.contains(lo)) {
242 //} // TODO BROKEN: not registered!
243 Utils.log2("CACHE: \n\timps: " + imps.size() + "\n\tmawts: " + mawts.size());
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
) {} }
265 /** To be called within a synchronized(db_lock) */
266 protected final void unlock() {
267 //Utils.printCaller(this, 7);
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);
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(),
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.");
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() {
349 // for cache flushing
350 public final void setMassiveMode(boolean 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
) {
359 // Reading a zipped tiff file in the database
362 /* // works but not faster
364 // new style: RAM only
365 ByteArrayOutputStream out = new ByteArrayOutputStream();
366 byte[] buf = new byte[4096];
370 len = i_stream.read(buf);
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
380 ArrayList al = new ArrayList();
382 len = infl.inflate(buf, offset, buf.length);
384 if (0 == infl.getRemaining()) break;
385 buf = new byte[length*2];
389 byte[][] b = new byte[al.size()][];
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
409 out
.write(buf
, 0, len
);
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);
419 //ij.IJ.redirectErrorMessages();
420 //imp = new Opener().openTiff(i_stream, title);
421 } catch (Exception e
) {
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
) {
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()) {
440 cache((Patch
)d
, imp
);
443 Utils
.log("Loader.cache: don't know how to cache: " + d
);
449 public void cache(final Patch p
, final ImagePlus imp
) {
450 if (null == imp
|| null == imp
.getProcessor()) return;
451 synchronized (db_lock
) {
453 final long id
= p
.getId();
454 final ImagePlus cached
= imps
.get(id
);
457 || imp
.getProcessor().getPixels() != cached
.getProcessor().getPixels()
461 imps
.get(id
); // send to the end
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
) {
471 imps
.put(id
, imp
); // TODO this looks totally unnecessary
476 public void decacheImagePlus(long id
) {
477 synchronized (db_lock
) {
479 ImagePlus imp
= imps
.remove(id
);
485 public void decacheImagePlus(long[] id
) {
486 synchronized (db_lock
) {
488 for (int i
=0; i
<id
.length
; i
++) {
489 ImagePlus imp
= imps
.remove(id
[i
]);
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());
509 // remove the layer where the Displayable paints to
510 if (null == d.getLayer()) {
511 // Top Level LayerSet has no layer
514 decacheImagePlus(d.getLayer().getId());
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
) {
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
;
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
551 case ImagePlus
.COLOR_RGB
:
554 case ImagePlus
.GRAY16
:
555 bytes
*= 3; // 2 for the ShortProcessor, and 1 for the pixels8
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.");
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
) {
577 result
= releaseToFit2(bytes
);
580 setMassiveMode(previous
);
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
591 while (iterations
> 0) {
592 if (0 == imps
.size() && 0 == mawts
.size()) {
595 try { Thread
.sleep(300); } catch (InterruptedException ie
) {}
597 if (enoughFreeMemory(n_bytes
)) 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();
609 long sleep
= 50; // initial value
612 //Runtime.getRuntime().runFinalization(); // enforce it
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());
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
++) {
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
:
664 case ImagePlus
.COLOR_256
:
665 return size
+ 868; // 100 + 3 * 256 (the LUT)
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
);
679 for (Loader loader
: v_loaders
) mem
+= loader
.releaseMemory(percent
);
684 public long releaseMemory(double percent
) {
685 if (percent
<= 0) return 0;
686 if (percent
> 1) percent
= 1;
687 synchronized (db_lock
) {
690 return releaseMemory(percent
, false, MIN_FREE_BYTES
);
691 } catch (Throwable e
) {
694 // gets called by the 'return' above and by any other sort of try{}catch interruption
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 < a <= 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
) {
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)
714 // release others regardless of the 'release_others' boolean
715 released
+= releaseOthers(0.5D
);
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
);
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
;
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
;
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
);
764 ImagePlus imp
= imps
.removeFirst();
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:
779 } catch (Exception e
) {
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();
791 while (it
.hasNext()) {
792 Loader loader
= (Loader
)it
.next();
793 if (loader
== this) continue;
795 loader
.setMassiveMode(false); // otherwise would loop back!
796 released
+= loader
.releaseMemory(a
, false, MIN_FREE_BYTES
);
802 static public void releaseAllCaches() {
803 for (final Loader lo
: new Vector
<Loader
>(v_loaders
)) {
808 /** Empties the caches. */
809 public void releaseAll() {
810 synchronized (db_lock
) {
813 for (ImagePlus imp
: imps
.removeAll()) {
816 mawts
.removeAndFlushAll();
817 } catch (Exception e
) {
824 private void destroyCache() {
825 synchronized (db_lock
) {
828 if (null != IJ
.getInstance() && IJ
.getInstance().quitting()) {
832 for (ImagePlus imp
: imps
.removeAll()) {
838 mawts
.removeAndFlushAll();
840 } catch (Exception e
) {
849 /** Removes from the cache all awt images bond to the given id. */
850 public void decacheAWT(final long id
) {
851 synchronized (db_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
858 public void cacheOffscreen(final Layer layer
, final Image awt
) {
859 synchronized (db_lock
) {
861 mawts
.put(layer
.getId(), awt
, 0);
866 /** Transform mag to nearest scale level that delivers an equally sized or larger image.<br />
867 * Requires 0 < mag <= 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
) {
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
);
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
);
885 Utils.log2("ERROR max_level > 6: " + max_level + ", size: " + size);
888 return Math
.min(level
, max_level
);
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
898 } else if (scale < mag) {
899 // provide the previous one
903 // else, continue search
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
) {
918 boolean b
= mawts
.contains(p
.getId(), Loader
.getMipMapLevel(mag
, maxDim(p
)));
924 public Image
getCached(final long id
, final int level
) {
926 synchronized (db_lock
) {
928 awt
= mawts
.getClosestAbove(id
, level
);
934 /** Above or equal in size. */
935 public Image
getCachedClosestAboveImage(final Patch p
, final double mag
) {
937 synchronized (db_lock
) {
939 awt
= mawts
.getClosestAbove(p
.getId(), Loader
.getMipMapLevel(mag
, maxDim(p
)));
945 /** Below, not equal. */
946 public Image
getCachedClosestBelowImage(final Patch p
, final double mag
) {
948 synchronized (db_lock
) {
950 awt
= mawts
.getClosestBelow(p
.getId(), Loader
.getMipMapLevel(mag
, maxDim(p
)));
956 protected final class PatchLoadingLock
extends Lock
{
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
);
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
;
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
) {
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
);
1011 //Utils.log2("returning cached exact mawt for level " + level);
1016 plock
= getOrMakePatchLoadingLock(p
, level
);
1018 } catch (Exception e
) {
1028 // 2 - check if the exact file is present for the desired level
1029 if (level
>= 0 && isMipMapsEnabled()) {
1030 synchronized (plock
) {
1033 synchronized (db_lock
) {
1035 mawt
= mawts
.get(id
, level
);
1040 return mawt
; // was loaded by a different thread
1045 synchronized (db_lock
) {
1047 n_bytes
= estimateImageFileSize(p
, level
);
1048 max_memory
-= n_bytes
;
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
) {
1057 max_memory
+= n_bytes
;
1059 if (REGENERATING
!= mawt
) mawts
.put(id
, mawt
, level
);
1060 //Utils.log2("returning exact mawt from file for level " + level);
1061 Display
.repaintSnapshot(p
);
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);
1068 mawt
= mawts
.getClosestAbove(id
, lev
);
1069 boolean newly_cached
= false;
1071 // reload existing scaled file
1072 releaseToFit(n_bytes
); // overshooting
1073 mawt
= fetchMipMapAWT2(p
, lev
);
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);
1082 if (newly_cached
) Display
.repaintSnapshot(p
);
1083 //Utils.log2("returning from getClosestMipMapAWT with level " + lev);
1086 } else if (ERROR_PATH_NOT_FOUND
== lev
) {
1089 } catch (Exception e
) {
1092 removePatchLoadingLock(plock
);
1100 // level is zero or nonsensically lower than zero, or was not found
1101 //Utils.log2("not found!");
1103 synchronized (db_lock
) {
1107 // 4 - check if any suitable level is cached (whithout mipmaps, it may be the large image)
1108 mawt
= mawts
.getClosestAbove(id
, level
);
1110 //Utils.log2("returning from getClosest with level " + level);
1113 } catch (Exception e
) {
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
) {
1128 plock
= getOrMakePatchLoadingLock(p
, level
);
1129 } catch (Exception e
) {
1136 synchronized (plock
) {
1140 // Check if a previous call made it while waiting:
1141 mawt
= mawts
.getClosestAbove(id
, level
);
1143 synchronized (db_lock
) {
1145 removePatchLoadingLock(plock
);
1151 // Else, create the mawt:
1153 Patch
.PatchImage pai
= p
.createTransformedImage();
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
;
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()));
1168 mawt
= ip
.createImage();
1170 } catch (Exception e
) {
1171 Utils
.log2("Could not create an image for Patch " + p
);
1178 synchronized (db_lock
) {
1182 mawts
.put(id
, mawt
, level
);
1183 Display
.repaintSnapshot(p
);
1184 //Utils.log2("Created mawt from scratch.");
1187 } catch (Exception e
) {
1190 removePatchLoadingLock(plock
);
1199 public ByteProcessor
fetchImageMask(final Patch p
) {
1203 public String
getAlphaPath(final Patch p
) {
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
);
1218 Image mawt
= fetchMipMapAWT(p
, level
);
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;
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()) {
1259 IJ.showMessage("Virtual stacks not supported.");
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;
1271 /* // works, but not significantly faster and breaks older databases (can't read zipped images properly)
1272 byte[] bytes = null;
1274 o_stream = new DataOutputStream(new BufferedOutputStream(o_bytes));
1278 Deflater defl = new Deflater();
1279 byte[] unzipped_bytes = o_bytes.toByteArray();
1280 defl.setInput(unzipped_bytes);
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);
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()));
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:
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.");
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
);
1329 Utils
.showMessage("File " + dir
+ file_name
+ " does not exist.");
1332 // avoid opening trakem2 projects
1333 if (file_name
.toLowerCase().endsWith(".xml")) {
1334 Utils
.showMessage("Cannot import " + file_name
+ " as a stack.");
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.
1351 ImagePlus imp_stack
= null;
1353 IJ
.redirectErrorMessages();
1354 imp_stack
= opener
.openImage(f
.getCanonicalPath());
1355 } catch (Exception e
) {
1359 if (null == imp_stack
) {
1360 Utils
.showMessage("Can't open the stack.");
1362 } else if (1 == imp_stack
.getStackSize()) {
1363 Utils
.showMessage("Not a stack!");
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
) {
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]);
1387 int w
= imp
.getWidth();
1388 int h
= imp
.getHeight();
1391 int cc_scale
= (int)((512.0 / (w
> h ? w
: h
)) * 100);
1392 if (cc_scale
> 100) return 100;
1396 } catch (Exception e
) {
1397 Utils
.log2("Could not get an estimate for the optimal scale.");
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
) {
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;
1418 images_dir
= new File(dir
);
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
);
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
= "";
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);
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;
1490 Utils
.showMessage("Last is smaller that first!");
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
) {
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
;
1531 file_names
= all_images
;
1534 if (0 == file_names
.length
) {
1535 Utils
.showMessage("No images found.");
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
])) {
1547 file
= file_names
[0];
1548 Utils
.log("Using " + file
+ " as the reference image for size.");
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.");
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
];
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
) {
1583 private ImagePlus
preprocess(String preprocessor
, ImagePlus imp
, String path
) {
1584 if (null == imp
) return null;
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();
1594 // discard this image
1595 Utils
.log("Ignoring " + imp
.getTitle() + " from " + path
+ " since the preprocessor " + preprocessor
+ " returned null on it.");
1597 finishSetTempCurrentImage();
1600 } catch (Exception e
) {
1602 finishSetTempCurrentImage();
1603 Utils
.log("Ignoring " + imp
.getTitle() + " from " + path
+ " since the preprocessor " + preprocessor
+ " throwed an Exception on it.");
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
) {
1618 String
[] dn
= Utils
.selectFile("Select first image");
1619 if (null == dn
) return null;
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
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);
1669 if (gd
.wasCanceled()) {
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!");
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;
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
);
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
+ "'");
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
) {
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") {
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;
1762 // check the given plugin
1763 IJ
.redirectErrorMessages();
1764 startSetTempCurrentImage(null);
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();
1772 finishSetTempCurrentImage();
1773 } catch (Exception e
) {
1775 finishSetTempCurrentImage();
1776 Utils
.showMessageT("Plug in " + preprocessor
+ " is invalid: ImageJ has trhown an exception when testing it with a null image.");
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. */
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) {
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.");
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();
1818 final Patch
[][] pall
= new Patch
[cols
.size()][((String
[])cols
.get(0)).length
];
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;
1831 for (int i
=0; i
<cols
.size(); i
++) {
1832 String
[] rows
= (String
[])cols
.get(i
);
1836 for (int j
=0; j
<rows
.length
; j
++) {
1838 Display
.repaint(layer
);
1846 String file_name
= (String
)rows
[j
];
1847 path
= dir
+ file_name
;
1848 if (null != first_img
&& file_name
.equals(first_image_name
)) {
1850 first_img
= null; // release pointer
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
);
1856 IJ
.redirectErrorMessages();
1857 img
= opener
.openImage(path
);
1858 } catch (OutOfMemoryError oome
) {
1862 // Preprocess ImagePlus
1864 img
= preprocess(preprocessor
, img
, path
);
1865 if (null == img
) continue;
1867 // use standard project wide , if any
1872 Utils
.log("null image! skipping.");
1877 width
= img
.getWidth();
1878 height
= img
.getHeight();
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]");
1893 String
[] au
= new String
[]{"fix all", "ignore all"};
1894 gdr
.addChoice("Automate:", au
, au
[1]);
1895 gdr
.addMessage("Cancel == NO OK = YES");
1897 if (gdr
.wasCanceled()) {
1899 // do nothing: don't fix/resize
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;
1909 auto_fix_all
= (0 == iau
);
1911 ignore_all
= (1 == iau
);
1912 if (ignore_all
) resize
= false;
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.
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
);
1941 x
+= img
.getWidth();
1942 if (largest_y
< y
) {
1949 final Patch
[] pa
= new Patch
[al
.size()];
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
]);
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
) {
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
));
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
1989 releaseMemory(); // need some to operate
1990 for (int i
=0; i
<pa
.length
; i
++) {
1992 Display
.repaint(layer
);
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
2007 for (Iterator it
= al_st
.iterator(); it
.hasNext(); ) {
2008 ImageStatistics st
= (ImageStatistics
)it
.next();
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
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
2024 while (i
<= pa
.length
* 0.25) {
2029 i
= pa
.length
-1 -count
;
2030 while (i
> (pa
.length
* 0.75) - count
) {
2035 // 3 - compute common histogram for the middle 50% images
2036 final Patch
[] p50
= new Patch
[al_p
.size()];
2038 StackStatistics stats
= new StackStatistics(new PatchStack(p50
, 1));
2041 case ImagePlus
.GRAY16
:
2042 case ImagePlus
.GRAY32
:
2046 // 4 - compute autoAdjust min and max values
2047 // extracting code from ij.plugin.frame.ContrastAdjuster, method autoAdjust
2048 int autoThreshold
= 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
;
2060 boolean found
= false;
2064 count
= histogram
[i
];
2065 if (count
>limit
) count
= 0;
2066 found
= count
> threshold
;
2067 } while (!found
&& i
<255);
2072 count
= histogram
[i
];
2073 if (count
> limit
) count
= 0;
2074 found
= count
> threshold
;
2075 } while (!found
&& i
>0);
2078 min
= stats
.histMin
+ hmin
*stats
.binSize
;
2079 max
= stats
.histMin
+ hmax
*stats
.binSize
;
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
);
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.");
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
) {
2106 for (i
=0; i
<pa
.length
; i
++) {
2107 mawts
.removeAndFlush(pa
[i
].getId());
2108 Utils
.log2(i
+ "removing mawt for " + pa
[i
].getId());
2112 setMipMapsRegeneration(true);
2113 Display
.repaint(layer
, new Rectangle(0, 0, (int)layer
.getParent().getLayerWidth(), (int)layer
.getParent().getLayerHeight()), 0);
2116 //getFlatImage(layer, layer.getMinimalBoundingBox(Patch.class), 0.25, 1, ImagePlus.GRAY8, Patch.class, null, false).show();
2121 setTaskName("stitching tiles");
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.
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();
2148 int new_height
= largest_y
;
2149 layer
.getParent().setMinimumDimensions(); //Math.abs(bx) + new_width, Math.abs(by) + new_height);
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!
2160 Display
.update(layer
);
2163 setMassiveMode(false);//massive_mode = false;
2165 layer
.recreateBuckets();
2168 } catch (Throwable t
) {
2171 setMassiveMode(false); //massive_mode = false;
2172 setMipMapsRegeneration(true);
2176 }// end of run method
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);
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.");
2220 calibration_
= gdd
.getNextNumber();
2221 if (0 == calibration_
|| Double
.isNaN(calibration_
)) {
2222 Utils
.log("Improper calibration value.");
2225 ref_layer
= ref_layer
.getParent().getLayer(gdd
.getNextChoiceIndex());
2226 column_separator_
= "\t";
2227 switch (gdd
.getNextChoiceIndex()) {
2229 column_separator_
= " ";
2232 column_separator_
= ",";
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);
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());
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. */
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) {
2281 final Worker worker
= new Worker("Importing images") {
2284 final Worker wo
= this;
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
);
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() {
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;
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
);
2329 // obtain coordinates
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
);
2343 z
= z
* calibration
+ z_zero
;
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
) {
2351 if ("QUIT".equals(base_dir
[1])) {
2352 // dialog to ask for directory was quitted
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();
2364 base_dir
[1] = "QUIT";
2369 // else, set the base dir
2370 base_dir
[0] = dir
.replace('\\', '/');
2371 if (!base_dir
[0].endsWith("/")) base_dir
[0] += "/";
2376 if (null != base_dir
[0]) path
= base_dir
[0] + path
;
2377 File f
= new File(path
);
2379 Utils
.log("No file found for path " + path
);
2382 synchronized (db_lock
) {
2384 releaseMemory(); //ensures a usable minimum is free
2388 IJ
.redirectErrorMessages();
2389 ImagePlus imp
= opener
.openImage(path
);
2391 Utils
.log("Ignoring unopenable image from " + path
);
2394 // add Patch and generate its mipmaps
2397 synchronized (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
) {
2411 if (null != patch
) {
2412 if (!generateMipMaps(patch
)) {
2413 Utils
.log("Failed to generate mipmaps for " + patch
);
2415 synchronized (lock
) {
2418 layer
.add(patch
, true);
2419 } catch (Exception e
) {
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.");
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
) {
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];
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
);
2474 if (gd
.wasCanceled()) {
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
) {
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") {
2507 String path
= path_
;
2509 OpenDialog od
= new OpenDialog("Select stack", "");
2510 String name
= od
.getFileName();
2511 if (null == name
|| 0 == name
.length()) {
2514 String dir
= od
.getDirectory().replace('\\', '/');
2515 if (!dir
.endsWith("/")) dir
+= "/";
2518 if (path
.toLowerCase().endsWith(".xml")) {
2519 Utils
.log("Avoided opening a TrakEM2 project.");
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);
2535 if (gd
.wasCanceled()) {
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!");
2545 alpha
= (float)(gd
.getNextNumber() / 100);
2546 add_background
= gd
.getNextBoolean();
2549 final ImagePlus imp
= opener
.openImage(path
);
2551 Utils
.log("Could not open image at " + path
);
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
) {
2565 return Bureaucrat
.createAndStart(worker
, first_layer
.getProject());
2568 public void recreateBuckets(final Collection
<Layer
> col
) {
2569 final Layer
[] lall
= new Layer
[col
.size()];
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() {
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
;
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
];
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()];
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();
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.");
2650 final Worker worker
= new Worker("making flat images") { public void run() {
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()) {
2668 } else if (yn
.cancelPressed()) {
2672 choose_dir
= false; // your own risk
2677 final DirectoryChooser dc
= new DirectoryChooser("Target directory");
2678 target_dir
= dc
.getDirectory();
2679 if (null == target_dir
|| target_dir
.toLowerCase().startsWith("null")) {
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();
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]);
2703 if (gdd
.wasCanceled()) {
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());
2719 if (null != target_dir
) {
2720 saveToPath(slice
, target_dir
, layer
[i
].getPrintableTitle(), ".tif");
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());
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
) {
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
) {
2749 Utils
.log2("Loader.saveToPath: can't save a null image.");
2752 // create a unique file name
2753 String path
= dir
+ "/" + file_name
;
2754 File file
= new File(path
+ extension
);
2756 while (file
.exists()) {
2757 file
= new File(path
+ "_" + k
+ ".tif");
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
);
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());
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
) {
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
;
2805 if (isMipMapsEnabled()) {
2806 // just double the size
2807 scaleP
= scale
+ scale
;
2808 if (scaleP
> 1.0) scaleP
= 1.0;
2820 Rectangle srcRect
= (null == srcRect_
) ?
null : (Rectangle
)srcRect_
.clone();
2821 if (null != srcRect
) {
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.");
2841 synchronized (db_lock
) {
2843 releaseMemory(); // savage ...
2846 BufferedImage bi
= null;
2848 case ImagePlus
.GRAY8
:
2849 bi
= new BufferedImage((int)Math
.ceil(w
* scaleP
), (int)Math
.ceil(h
* scaleP
), BufferedImage
.TYPE_BYTE_INDEXED
, GRAY_LUT
);
2851 case ImagePlus
.COLOR_RGB
:
2852 bi
= new BufferedImage((int)Math
.ceil(w
* scaleP
), (int)Math
.ceil(h
* scaleP
), BufferedImage
.TYPE_INT_ARGB
);
2855 Utils
.log2("Left bi,icm as null");
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
) {
2870 releaseMemory(); // savage ...
2874 ArrayList al_zdispl
= null;
2875 if (null == al_displ
) {
2876 al_displ
= layer
.getDisplayables(clazz
);
2877 al_zdispl
= layer
.getParent().getZDisplayables(clazz
);
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
) {
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();
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
) {
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
);
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);
2930 //Utils.log2("out: " + d);
2933 //Utils.log2("Painted " + count + " of " + total);
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
);
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
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);
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
);
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);
2977 // else the image was made scaled down already, and of the proper type
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
) {
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() {
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;
3065 // width is larger or equal than height
3066 thumb_scale
= 192.0 / srcRect
.width
;
3068 thumb_scale
= 192.0 / srcRect
.height
;
3071 for (int iz
=0; iz
<layer
.length
; iz
++) {
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
);
3081 // Ensure there is a usable directory:
3082 while (fdir
.exists() && !fdir
.isDirectory()) {
3083 fdir
= new File(tile_dir
+ "_" + tag
);
3085 if (!fdir
.exists()) {
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.");
3093 Utils
.log2("tile_dir: " + tile_dir
+ "\ntmp: " + 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
);
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");
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.
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);
3128 Rectangle tile_src
= new Rectangle(srcRect
.x
+ tile_side
*row
,
3129 srcRect
.y
+ tile_side
*col
,
3131 tile_side
); // in absolute coords, magnification later.
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.
3141 scale
= 1 / Math
.pow(2, scale_pow
); // works as magnification
3146 } catch (Exception e
) {
3149 Utils
.showProgress(1);
3154 }// end of run method
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
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);
3176 imp2
.getProcessor().setValue(0); // black
3177 imp2
.getProcessor().fill();
3179 imp2
.getProcessor().insert(imp
.getProcessor(), 0, 0);
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
) {
3196 int min_larger
= larger
[0];
3198 for (int i
=1; i
<larger
.length
; i
++) {
3199 if (larger
[i
] < min_larger
) {
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") {
3220 if (null == layer
) {
3221 Utils
.log("Can't import. No layer found.");
3225 Patch p
= importImage(layer
.getProject(), x
, y
, path
);
3228 layer
.getParent().enlargeToFit(p
, LayerSet
.NORTHWEST
);
3231 } catch (Exception e
) {
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
) {
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
+= "/";
3253 path
= path
.replace('\\', '/');
3255 // avoid opening trakem2 projects
3256 if (path
.toLowerCase().endsWith(".xml")) {
3257 Utils
.showMessage("Cannot import " + path
+ " as a stack.");
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;
3265 if (imp
.getNSlices() > 1) {
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
3272 if (0 == imp
.getWidth() || 0 == imp
.getHeight()) {
3273 Utils
.showMessage("Can't import image of zero width or height.");
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
);
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
];
3307 if (null == next_file
) {
3308 Utils
.showMessage("No more files after " + last_file
);
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
);
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.");
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
);
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") {
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();
3351 stks
= new ImagePlus
[]{imp_stack_
};
3353 ImagePlus imp_stack
= null;
3354 // ask to open a stack if it's null
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]);
3368 if (gd
.wasCanceled()) {
3372 int i_choice
= gd
.getNextChoiceIndex();
3373 if (list
.length
-1 == i_choice
) { // the open... command
3374 imp_stack
= first_layer
.getProject().getLoader().openStack();
3376 imp_stack
= stks
[i_choice
];
3379 imp_stack
= imp_stack_
;
3382 if (null == imp_stack
) {
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()) {
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
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);
3417 if (gd
.wasCanceled()) {
3418 if (null == stks
) { // flush only if it was not open before
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
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.");
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.");
3461 YesNoDialog yn
= new YesNoDialog("Calibration", "The layer set is already calibrated. Override with the stack calibration values?");
3462 if (!yn
.yesPressed()) {
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
);
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()) {
3507 Collection
<AreaList
> alis
= AmiraImporter
.importAmiraLabels(first_layer
, last_patch
.getX(), last_patch
.getY(), imp_stack
.getOriginalFileInfo().directory
);
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
) {
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
) {
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
);
3544 if (null == project
|| null == fxml
) return null;
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");
3567 writer
.write(sb_header
.toString());
3569 project
.exportXML(writer
, "", patches_dir
);
3570 writer
.flush(); // make sure all buffered chars are written
3572 path
= fxml
.getAbsolutePath().replace('\\', '/');
3573 } catch (Exception e
) {
3574 Utils
.log("FAILED to save the file at " + fxml
);
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();
3588 for (int k
=0; k
<ff
.length
; k
++) {
3589 if (!ff
[k
].isHidden()) {
3590 // one non-hidden file found.
3598 } catch (Exception e
) {
3599 Utils
.log2("Could not delete empty directory " + patches_dir
);
3606 } catch (Exception e
) {
3609 ControlWindow
.updateTitle(project
);
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
);
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);
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);
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
) {
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
) {
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;
3678 while (dir
.exists()) {
3679 patches_dir2
= patches_dir
+ "_" + Integer
.toString(i
);
3680 dir
= new File(patches_dir2
);
3683 if (null != patches_dir2
) patches_dir
= patches_dir2
;
3684 if (null == patches_dir
) return null;
3687 } catch (Exception e
) {
3689 Utils
.showMessage("Could not create a directory for the images.");
3692 if (File
.separatorChar
!= patches_dir
.charAt(patches_dir
.length() -1)) {
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;
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
);
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
) {
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
) {
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
) {} }
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
) {} }
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;
3802 // just for the sake of it:
3803 plugin_class_name
= plugin_class_name
.replace(' ', '_');
3804 // check that it can be instantiated
3806 startSetTempCurrentImage(null);
3807 IJ
.redirectErrorMessages();
3808 Object ob
= IJ
.runPlugIn(plugin_class_name
, "");
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.");
3821 finishSetTempCurrentImage();
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
3835 startSetTempCurrentImage(imp
);
3836 IJ
.redirectErrorMessages();
3837 IJ
.runPlugIn(preprocessor
, "");
3838 } catch (Exception e
) {
3841 finishSetTempCurrentImage();
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;
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:");
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
);
3891 popup_locked
= false;
3892 popup_lock
.notifyAll();
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
);
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);
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();
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;
3920 popup_locked
= false;
3921 popup_lock
.notifyAll();
3923 Utils
.showStatus("Job canceled.", false);
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())
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('/', '\\');
3942 Utils
.log2("opening image " + path
);
3943 //Utils.printCaller(this, 25);
3944 IJ
.redirectErrorMessages();
3946 return opener
.openImage(path
);
3947 } catch (Exception e
) {
3948 Utils
.log("Could not open image at " + path
);
3949 e
.printStackTrace();
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
3961 if ('/' == path
.charAt(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
) {
3981 if (mawts
.contains(id
)) {
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
) {
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
) {
4012 mawts
.removeAndFlush(patch
.getId());
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) {
4051 ht = mawts.getAll(p.getId());
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) {
4060 plock = getOrMakePatchLoadingLock(p, level);
4063 synchronized (plock) {
4064 plock.lock(); // block loading of this file
4067 awt = p.adjustChannels(entry.getValue());
4068 } catch (Exception e) {
4070 if (null == awt) continue;
4072 synchronized (db_lock) {
4074 mawts.replace(p.getId(), awt, level);
4075 removePatchLoadingLock(plock);
4083 // flush away any loaded mipmap for the id
4084 synchronized (db_lock
) {
4086 mawts
.removeAndFlush(p
.getId());
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 !
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);
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 !
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);
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
) {
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;
4144 if (!fdir
.exists()) {
4145 Utils
.log2("Could not create folder " + fdir
.getAbsolutePath());
4148 // 2 - Serialize the given object:
4149 final ObjectOutputStream out
= new ObjectOutputStream(new FileOutputStream(path
));
4150 out
.writeObject(ob
);
4153 } catch (Exception e
) {
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
) {
4161 if (!new File(path
).exists()) return null;
4162 final ObjectInputStream in
= new ObjectInputStream(new FileInputStream(path
));
4163 final Object ob
= in
.readObject();
4166 } catch (Exception e
) {
4167 //IJError.print(e); // too much output if a whole set is wrong
4168 e
.printStackTrace();
4173 public void insertXMLOptions(StringBuffer sb_body
, String indent
) {}
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") {
4182 final Worker wo
= this;
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() {
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();
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
;
4211 boolean found
= false;
4213 double min
=0, max
=0;
4216 count
= histogram
[i
];
4217 if (count
>limit
) count
= 0;
4218 found
= count
> threshold
;
4219 } while (!found
&& i
<255);
4224 count
= histogram
[i
];
4225 if (count
> limit
) count
= 0;
4226 found
= count
> threshold
;
4227 } while (!found
&& i
>0);
4230 min
= stats
.histMin
+ hmin
*stats
.binSize
;
4231 max
= stats
.histMin
+ hmax
*stats
.binSize
;
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()) {
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!
4259 // flush away any existing awt images, so that they'll be reloaded or recreated
4260 synchronized (db_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());
4268 for (int i
=0; i
<pa
.length
; i
++) {
4269 Display
.repaint(pa
[i
].getLayer(), pa
[i
], 0);
4273 } catch (Exception e
) {
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") {
4293 final Worker wo
= this;
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()) {
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()];
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()) {
4317 for (int i
=0; i
<la
.length
; i
++) Display
.repaint(la
[i
]);
4320 } catch (Exception e
) {
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()];
4337 Worker worker
= new Worker("Enhance contrast") {
4341 homogenizeContrast(pa
[0].getLayer(), pa
, null == parent ?
this : parent
);
4342 } catch (Exception e
) {
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
) {
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
) {
4363 Utils
.log("Can't homogenize histograms: images are not all of the same type.\nFirst offending image is: " + pa
[e
]);
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
]);
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
4376 for (int i
=0; i
<pa
.length
; i
++) {
4377 if (null != worker
&& worker
.hasQuitted()) {
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
4386 for (Iterator it
= al_st
.iterator(); it
.hasNext(); ) {
4387 ImageStatistics st
= (ImageStatistics
)it
.next();
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
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
4403 final int quarter
= pa
.length
/ 4;
4404 while (i
< quarter
) {
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' ??
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()];
4424 ps
= new PatchStack(p50
, 1); // is an ImagePlus
4425 stats
= new StackStatistics(ps
);
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")) {
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.");
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!
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
) {
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());
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
) {
4489 public Bureaucrat
setMinAndMax(final List
<Displayable
> patches
, final double min
, final double max
) {
4490 Worker worker
= new Worker("Set min and max") {
4494 if (Double
.isNaN(min
) || Double
.isNaN(max
)) {
4495 Utils
.log("WARNING:\nUnacceptable min and max values: " + min
+ ", " + max
);
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() {
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;
4510 p
.setMinAndMax(min
, max
);
4513 Utils
.showProgress(completed
.incrementAndGet() / (double)pa
.size());
4518 MultiThreading
.startAndJoin(threads
);
4519 } catch (Exception e
) {
4526 return Bureaucrat
.createAndStart(worker
, Project
.findProject(this));
4529 public long estimateImageFileSize(final Patch p
, final int 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() {
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
) {
4554 public final void notifyListeners(final ImagePlus imp
, final int action
) {
4556 for (ij
.ImageListener listener
: my_listeners
) {
4559 listener
.imageClosed(imp
);
4562 listener
.imageOpened(imp
);
4565 listener
.imageUpdated(imp
);
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
);
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(); }
4641 // Java is pathetically low level.
4642 static private final class Tuple
{
4646 private boolean valid
= true;
4647 Tuple(final Patch patch
, final double mag
, final boolean repaint
) {
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);
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();
4674 super("T2-Preloader");
4675 setPriority(Thread
.NORM_PRIORITY
);
4676 try { setDaemon(true); } catch (Exception e
) { e
.printStackTrace(); }
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() {
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
) {
4692 final Tuple tu
= new Tuple(patch
, mag
, repaint
);
4693 HashMap
<Integer
,Tuple
> m
= map
.get(patch
);
4694 final int key
= makeKey(mag
);
4696 m
= new HashMap
<Integer
,Tuple
>();
4700 // invalidate previous entry if any
4701 Tuple old
= m
.get(key
);
4702 if (null != old
) old
.invalidate();
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;
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.
4730 synchronized (lock
) {
4732 for (Patch p
: patches
) {
4733 HashMap
<Integer
,Tuple
> m
= map
.get(p
);
4737 final Tuple tu
= m
.remove(makeKey(mag
)); // if present.
4738 //Utils.log2("@@@@ mag is " + mag + " and tu is null == " + (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
);
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
);
4756 final int size
= imageloader
.length
; // as many as Preloader threads
4757 final ArrayList
<Tuple
> list
= new ArrayList
<Tuple
>(size
);
4760 synchronized (lock2
) { lock2
.lock(); }
4761 // read out a group of imageloader.length patches to load
4763 // block 1: pop out 'size' valid tuples from the queue (and all invalid in between as well)
4764 synchronized (lock
) {
4766 int len
= queue
.size();
4767 //Utils.log2("@@@@@ Queue size: " + len);
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
);
4780 //Utils.log2("@@@@@ skipping invalid tuple");
4786 //Utils.log2("@@@@@ Queue size after: " + queue.size());
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();
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()) {
4806 imageloader
[i
].load(tu
.patch
, tu
.mag
, tu
.repaint
);
4810 if (!list
.isEmpty()) try {
4811 //Utils.log2("@@@@@ list not empty, waiting 50 ms");
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(); }
4850 public final void quit() {
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
) {
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() {
4880 boolean repaint
= false;
4881 synchronized (lock2
) {
4883 // wait until there's a Patch to preload.
4885 // ready: catch locally (no need to synch on lock because it can't change, considering the load method.
4888 repaint
= this.repaint
;
4889 } catch (Exception e
) {}
4891 if (null != p
&& !p
.getProject().getLoader().hs_unloadable
.contains(p
)) {
4894 // wait a bit in case the user has browsed past
4896 if (mag
>= 0.25) try { sleep(50); } catch (InterruptedException ie
) {}
4897 if (Display
.willPaint(p
, mag
)) {
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
4903 // just load it into the cache if possible
4905 p
.getProject().getLoader().fetchImage(p
, mag
);
4908 } catch (Exception e
) { e
.printStackTrace(); }
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
) {
4923 int w = (int)p.getWidth();
4924 int h = (int)p.getHeight();
4925 while (w >= 64 && h >= 64) {
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));
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
;
4958 /** Does nothing unless overriden. */
4959 public Bureaucrat
generateLayerMipMaps(final Layer
[] la
, final int starting_level
) {
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();
4968 for (int i
=0; i
<3; i
++) {
4971 end
= System
.currentTimeMillis();
4972 if (end
- start
> 2000) break; // garbage collecion catched and is running.
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
);
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
) {
5006 for (int i
=0; i
<pix
.length
; ++i
)
5007 pix
[i
] = (pix
[i
]&0x00ffffff) | ((alpha
[i
]&0xff)<<24);
5009 for (int i
=0; i
<pix
.length
; ++i
) {
5010 pix
[i
] = (pix
[i
]&0x00ffffff) | ( (outside
[i
]&0xff) != 255 ?
0 : ((alpha
[i
]&0xff)<<24) );
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;
5037 fr
= new ChannelSeparator();
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
) {
5045 if (null != fr
) try {
5047 } catch (IOException ioe
) { Utils
.log2("Could not close IFormatReader: " + ioe
); }
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
);