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 /s 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
;
27 import ij
.VirtualStack
;
29 import ij
.process
.ByteProcessor
;
30 import ij
.process
.ImageProcessor
;
31 import ij
.process
.FloatProcessor
;
32 import ij
.process
.ColorProcessor
;
33 import ini
.trakem2
.Project
;
34 import ini
.trakem2
.ControlWindow
;
35 import ini
.trakem2
.display
.DLabel
;
36 import ini
.trakem2
.display
.Display
;
37 import ini
.trakem2
.display
.Layer
;
38 import ini
.trakem2
.display
.Patch
;
39 import ini
.trakem2
.display
.YesNoDialog
;
40 import ini
.trakem2
.utils
.*;
41 import ini
.trakem2
.io
.*;
42 import ini
.trakem2
.imaging
.FloatProcessorT2
;
44 import java
.awt
.Graphics2D
;
45 import java
.awt
.Image
;
46 import java
.awt
.image
.BufferedImage
;
47 import java
.awt
.image
.IndexColorModel
;
48 import java
.awt
.image
.ColorModel
;
49 import java
.awt
.image
.PixelGrabber
;
50 import java
.awt
.RenderingHints
;
51 import java
.awt
.geom
.Area
;
52 import java
.awt
.geom
.AffineTransform
;
53 import java
.io
.BufferedInputStream
;
55 import java
.io
.FileInputStream
;
56 import java
.io
.FilenameFilter
;
57 import java
.io
.InputStream
;
60 import javax
.swing
.JMenuItem
;
61 import javax
.swing
.JMenu
;
62 import java
.awt
.event
.ActionListener
;
63 import java
.awt
.event
.ActionEvent
;
64 import java
.awt
.event
.KeyEvent
;
65 import javax
.swing
.KeyStroke
;
67 import org
.xml
.sax
.InputSource
;
69 import javax
.xml
.parsers
.SAXParserFactory
;
70 import javax
.xml
.parsers
.SAXParser
;
72 import mpi
.fruitfly
.math
.datastructures
.FloatArray2D
;
73 import mpi
.fruitfly
.registration
.ImageFilter
;
74 import mpi
.fruitfly
.general
.MultiThreading
;
76 import java
.util
.concurrent
.atomic
.AtomicInteger
;
79 /** A class to rely on memory only; except images which are rolled from a folder or their original location and flushed when memory is needed for more. Ideally there would be a given folder for storing items temporarily of permanently as the "project folder", but I haven't implemented it. */
80 public final class FSLoader
extends Loader
{
82 /** Largest id seen so far. */
83 private long max_id
= -1;
84 private final HashMap
<Long
,String
> ht_paths
= new HashMap
<Long
,String
>();
85 /** For saving and overwriting. */
86 private String project_file_path
= null;
87 /** Path to the directory hosting the file image pyramids. */
88 private String dir_mipmaps
= null;
89 /** Path to the directory the user provided when creating the project. */
90 private String dir_storage
= null;
91 /** Path to the directory hosting the alpha masks. */
92 private String dir_masks
= null;
94 /** Path to dir_storage + "trakem2.images/" */
95 private String dir_image_storage
= null;
97 /** Queue and execute Runnable tasks. */
98 static private Dispatcher dispatcher
= new Dispatcher();
100 private Set
<Patch
> touched_mipmaps
= Collections
.synchronizedSet(new HashSet
<Patch
>());
102 /** Used to open a project from an existing XML file. */
105 super.v_loaders
.remove(this); //will be readded on successful open
108 /** Used to create a new project, NOT from an XML file. */
109 public FSLoader(final String storage_folder
) {
111 if (null == storage_folder
) this.dir_storage
= super.getStorageFolder(); // home dir
112 else this.dir_storage
= storage_folder
;
113 if (!this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
114 if (!Loader
.canReadAndWriteTo(dir_storage
)) {
115 Utils
.log("WARNING can't read/write to the storage_folder at " + dir_storage
);
117 createMipMapsDir(this.dir_storage
);
122 /** Create a new FSLoader copying some key parameters such as preprocessor plugin, and storage and mipmap folders. Used for creating subprojects. */
123 public FSLoader(final Loader source
) {
125 this.dir_storage
= source
.getStorageFolder(); // can never be null
126 this.dir_mipmaps
= source
.getMipMapsFolder();
127 if (null == this.dir_mipmaps
) createMipMapsDir(this.dir_storage
);
128 setPreprocessor(source
.getPreprocessor());
131 /** Store a hidden file in trakem2.mipmaps directory that means: "the project is open", which is deleted when the project is closed. If the file is present on opening a project, it means the project has not been closed properly, and some mipmaps may be wrong. */
132 private void crashDetector() {
133 if (null == dir_mipmaps
) {
134 Utils
.log2("Could NOT create crash detection system: null dir_mipmaps.");
137 File f
= new File(dir_mipmaps
+ ".open.t2");
138 Utils
.log2("Crash detector file is " + dir_mipmaps
+ ".open.t2");
142 askAndExecMipmapRegeneration("TrakEM detected a crash!");
144 if (!f
.createNewFile() && !dir_mipmaps
.startsWith("http:")) {
145 Utils
.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
147 Utils
.log2("Created crash detection system.");
150 } catch (Exception e
) {
151 Utils
.log2("Crash detector error:" + e
);
156 public String
getProjectXMLPath() {
157 if (null == project_file_path
) return null;
158 return project_file_path
.toString(); // a copy of it
161 public String
getStorageFolder() {
162 if (null == dir_storage
) return super.getStorageFolder(); // the user's home
163 return dir_storage
.toString(); // a copy
166 /** Returns a folder proven to be writable for images can be stored into. */
167 public String
getImageStorageFolder() {
168 if (null == dir_image_storage
) {
169 String s
= getStorageFolder() + "trakem2.images/";
170 File f
= new File(s
);
171 if (f
.exists() && f
.isDirectory() && f
.canWrite()) {
172 dir_image_storage
= s
;
173 return dir_image_storage
;
178 dir_image_storage
= s
;
179 } catch (Exception e
) {
181 return getStorageFolder(); // fall back
185 return dir_image_storage
;
188 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
189 public Object
[] openFSProject(String path
, final boolean open_displays
) {
190 // clean path of double-slashes, safely (and painfully)
192 path
= path
.replace('\\','/');
194 int itwo
= path
.indexOf("//");
196 if (0 == itwo
/* samba disk */
197 || (5 == itwo
&& "http:".equals(path
.substring(0, 5)))) {
200 path
= path
.substring(0, itwo
) + path
.substring(itwo
+1);
202 itwo
= path
.indexOf("//", itwo
+1);
207 String user
= System
.getProperty("user.name");
208 OpenDialog od
= new OpenDialog("Select Project", OpenDialog
.getDefaultDirectory(), null);
209 String file
= od
.getFileName();
210 if (null == file
|| file
.toLowerCase().startsWith("null")) return null;
211 String dir
= od
.getDirectory().replace('\\', '/');
212 if (!dir
.endsWith("/")) dir
+= "/";
213 this.project_file_path
= dir
+ file
;
214 Utils
.log2("project file path 1: " + this.project_file_path
);
216 this.project_file_path
= path
;
217 Utils
.log2("project file path 2: " + this.project_file_path
);
219 Utils
.log2("Loader.openFSProject: path is " + path
);
220 // check if any of the open projects uses the same file path, and refuse to open if so:
221 if (null != FSLoader
.getOpenProject(project_file_path
, this)) {
222 Utils
.showMessage("The project is already open.");
226 Object
[] data
= null;
228 // parse file, according to expected format as indicated by the extension:
229 if (this.project_file_path
.toLowerCase().endsWith(".xml")) {
230 InputStream i_stream
= null;
231 TMLHandler handler
= new TMLHandler(this.project_file_path
, this);
232 if (handler
.isUnreadable()) {
236 SAXParserFactory factory
= SAXParserFactory
.newInstance();
237 factory
.setValidating(true);
238 SAXParser parser
= factory
.newSAXParser();
239 if (isURL(this.project_file_path
)) {
240 i_stream
= new java
.net
.URL(this.project_file_path
).openStream();
242 i_stream
= new BufferedInputStream(new FileInputStream(this.project_file_path
));
244 InputSource input_source
= new InputSource(i_stream
);
245 setMassiveMode(true);
246 parser
.parse(input_source
, handler
);
247 } catch (java
.io
.FileNotFoundException fnfe
) {
248 Utils
.log("ERROR: File not found: " + path
);
250 } catch (Exception e
) {
254 setMassiveMode(false);
255 if (null != i_stream
) {
258 } catch (Exception e
) {
264 if (null == handler
) {
265 Utils
.showMessage("Error when reading the project .xml file.");
269 data
= handler
.getProjectData(open_displays
);
273 Utils
.showMessage("Error when parsing the project .xml file.");
277 super.v_loaders
.add(this);
282 // Only one thread at a time may access this method.
283 synchronized static private final Project
getOpenProject(final String project_file_path
, final Loader caller
) {
284 if (null == v_loaders
) return null;
285 final Loader
[] lo
= (Loader
[])v_loaders
.toArray(new Loader
[0]); // atomic way to get the list of loaders
286 for (int i
=0; i
<lo
.length
; i
++) {
287 if (lo
[i
].equals(caller
)) continue;
288 if (lo
[i
] instanceof FSLoader
&& ((FSLoader
)lo
[i
]).project_file_path
.equals(project_file_path
)) {
289 return Project
.findProject(lo
[i
]);
295 static public final Project
getOpenProject(final String project_file_path
) {
296 return getOpenProject(project_file_path
, null);
299 public boolean isReady() {
300 return null != ht_paths
;
303 public void destroy() {
305 Utils
.showStatus("", false);
306 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
307 for (final Patch p
: touched_mipmaps
) {
308 File f
= new File(getAbsolutePath(p
));
309 Utils
.log2("File f is " + f
);
311 Utils
.log2("Removing mipmaps for " + p
);
312 // Cannot run in the dispatcher: is a daemon, and would be interrupted.
313 removeMipMaps(f
.getName() + "." + p
.getId() + ".jpg", (int)p
.getWidth(), (int)p
.getHeight()); // needs the dispatcher!
318 // remove empty trakem2.mipmaps folder if any
319 if (null != dir_mipmaps
&& !dir_mipmaps
.equals(dir_storage
)) {
320 File f
= new File(dir_mipmaps
);
321 if (f
.isDirectory() && 0 == f
.list(new FilenameFilter() {
322 public boolean accept(File fdir
, String name
) {
323 File file
= new File(dir_mipmaps
+ name
);
324 if (file
.isHidden() || '.' == name
.charAt(0)) return false;
328 try { f
.delete(); } catch (Exception e
) { Utils
.log("Could not remove empty trakem2.mipmaps directory."); }
331 // remove crash detector
332 File f
= new File(dir_mipmaps
+ ".open.t2");
335 Utils
.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps
);
337 } catch (Exception e
) {
338 Utils
.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
343 /** Get the next unique id, not shared by any other object within the same project. */
344 public long getNextId() {
346 synchronized (db_lock
) {
354 /** Loaded in full from XML file */
355 public double[][][] fetchBezierArrays(long id
) {
359 /** Loaded in full from XML file */
360 public ArrayList
fetchPipePoints(long id
) {
364 /** Loaded in full from XML file */
365 public ArrayList
fetchBallPoints(long id
) {
369 /** Loaded in full from XML file */
370 public Area
fetchArea(long area_list_id
, long layer_id
) {
374 /* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlust.getProcessor().
375 * or just use the Patch.getImageProcessor() method which does it for you. */
376 public ImagePlus
fetchImagePlus(final Patch p
) {
377 return (ImagePlus
)fetchImage(p
, Layer
.IMAGEPLUS
);
380 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
381 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor,
382 * or just use the Patch.getImageProcessor() method which does it for you. */
383 public ImageProcessor
fetchImageProcessor(final Patch p
) {
384 return (ImageProcessor
)fetchImage(p
, Layer
.IMAGEPROCESSOR
);
387 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
388 public Object
fetchImage(final Patch p
, final int format
) {
389 ImagePlus imp
= null;
390 ImageProcessor ip
= null;
394 PatchLoadingLock plock
= null;
395 synchronized (db_lock
) {
397 imp
= imps
.get(p
.getId());
399 path
= getAbsolutePath(p
);
401 if (null != path
) i_sl
= path
.lastIndexOf("-----#slice=");
403 // activate proper slice
405 // check that the stack is large enough (user may have changed it)
406 final int ia
= Integer
.parseInt(path
.substring(i_sl
+ 12));
407 if (ia
<= imp
.getNSlices()) {
408 if (null == imp
.getStack() || null == imp
.getStack().getPixels(ia
)) {
409 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
410 imps
.remove(p
.getId());
415 case Layer
.IMAGEPROCESSOR
:
416 ip
= imp
.getStack().getProcessor(ia
);
419 case Layer
.IMAGEPLUS
:
423 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
429 return null; // beyond bonds!
433 // for non-stack images
437 case Layer
.IMAGEPROCESSOR
:
438 return imp
.getProcessor();
439 case Layer
.IMAGEPLUS
:
442 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
447 slice
= path
.substring(i_sl
);
449 path
= path
.substring(0, i_sl
);
452 releaseMemory(); // ensure there is a minimum % of free memory
453 plock
= getOrMakePatchLoadingLock(p
, 0);
454 } catch (Exception e
) {
463 synchronized (plock
) {
466 imp
= imps
.get(p
.getId());
468 // was loaded by a different thread
471 case Layer
.IMAGEPROCESSOR
:
472 return imp
.getProcessor();
473 case Layer
.IMAGEPLUS
:
476 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
485 synchronized (db_lock
) {
487 n_bytes
= estimateImageFileSize(p
, 0);
488 max_memory
-= n_bytes
;
492 releaseToFit(n_bytes
);
493 imp
= openImage(path
);
497 synchronized (db_lock
) {
500 max_memory
+= n_bytes
;
503 if (!hs_unloadable
.contains(p
)) {
504 Utils
.log("FSLoader.fetchImagePlus: no image exists for patch " + p
+ " at path " + path
);
505 hs_unloadable
.add(p
);
507 removePatchLoadingLock(plock
);
512 // update all clients of the stack, if any
514 String rel_path
= getPath(p
); // possibly relative
515 final int r_isl
= rel_path
.lastIndexOf("-----#slice");
516 if (-1 != r_isl
) rel_path
= rel_path
.substring(0, r_isl
); // should always happen
517 for (Iterator
<Map
.Entry
<Long
,String
>> it
= ht_paths
.entrySet().iterator(); it
.hasNext(); ) {
518 final Map
.Entry
<Long
,String
> entry
= it
.next();
519 final String str
= entry
.getValue(); // this is like calling getPath(p)
520 //Utils.log2("processing " + str);
521 if (0 != str
.indexOf(rel_path
)) {
522 //Utils.log2("SKIP str is: " + str + "\t but path is: " + rel_path);
523 continue; // get only those whose path is identical, of course!
525 final int isl
= str
.lastIndexOf("-----#slice=");
527 //int i_slice = Integer.parseInt(str.substring(isl + 12));
528 final long lid
= entry
.getKey();
532 // set proper active slice
533 final int ia
= Integer
.parseInt(slice
.substring(12));
535 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getStack().getProcessor(ia
); // otherwise creates one new for nothing
537 // for non-stack images
538 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
539 // puts the Patch min and max values into the ImagePlus processor.
540 imps
.put(p
.getId(), imp
);
541 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getProcessor();
543 // imp is cached, so:
544 removePatchLoadingLock(plock
);
546 } catch (Exception e
) {
552 case Layer
.IMAGEPROCESSOR
:
553 return ip
; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
554 case Layer
.IMAGEPLUS
:
557 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
565 /** Returns the alpha mask image from a file, or null if none stored. */
567 public ByteProcessor
fetchImageMask(final Patch p
) {
568 // Else, see if there is a file for the Patch:
569 final String path
= getAlphaPath(p
);
570 if (null == path
) return null;
571 // Open the mask image, which should be a compressed float tif.
572 final ImagePlus imp
= opener
.openImage(path
);
574 Utils
.log2("No mask found or could not open mask image for patch " + p
+ " from " + path
);
577 return (ByteProcessor
)imp
.getProcessor().convertToByte(false);
581 public String
getAlphaPath(final Patch p
) {
582 final String filename
= getInternalFileName(p
);
583 if (null == filename
) {
584 Utils
.log2("null filepath!");
587 final String dir
= getMasksFolder();
588 return new StringBuffer(dir
).append(filename
).append('.').append(p
.getId()).append(".zip").toString();
592 public void storeAlphaMask(final Patch p
, final ByteProcessor fp
) {
593 // would fail if user deletes the trakem2.masks/ folder from the storage folder after having set dir_masks. But that is his problem.
594 new FileSaver(new ImagePlus("mask", fp
)).saveAsZip(getAlphaPath(p
));
597 public final String
getMasksFolder() {
598 if (null == dir_masks
) createMasksFolder();
602 synchronized private final void createMasksFolder() {
603 if (null == dir_masks
) dir_masks
= getStorageFolder() + "trakem2.masks/";
604 final File f
= new File(dir_masks
);
605 if (f
.exists() && f
.isDirectory()) return;
608 } catch (Exception e
) {
613 /** Remove the file containing the given Patch's alpha mask. */
614 public final boolean removeAlphaMask(final Patch p
) {
616 File f
= new File(getAlphaPath(p
));
621 } catch (Exception e
) {
627 /** Loaded in full from XML file */
628 public Object
[] fetchLabel(DLabel label
) {
632 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
633 synchronized public ImagePlus
fetchOriginal(final Patch patch
) {
634 String original_path
= patch
.getOriginalPath();
635 if (null == original_path
) return null;
636 // else, reserve memory and open it:
637 long n_bytes
= estimateImageFileSize(patch
, 0);
639 synchronized (db_lock
) {
641 max_memory
-= n_bytes
;
645 return openImage(original_path
);
646 } catch (Throwable t
) {
649 synchronized (db_lock
) {
651 max_memory
+= n_bytes
;
658 public void prepare(Layer layer
) {
659 //Utils.log2("FSLoader.prepare(Layer): not implemented.");
660 super.prepare(layer
);
663 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
664 * Always returns true. Does not check if another object has the same id.
666 public boolean addToDatabase(final DBObject ob
) {
667 synchronized (db_lock
) {
670 final long id
= ob
.getId();
679 public boolean updateInDatabase(final DBObject ob
, final String key
) {
681 if (ob
.getClass() == Patch
.class) {
683 if (key
.equals("tiff_working")) return null != setImageFile(p
, fetchImagePlus(p
));
688 public boolean removeFromDatabase(final DBObject ob
) {
689 synchronized (db_lock
) {
692 // remove from the hashtable
693 final long loid
= ob
.getId();
694 Utils
.log2("removing " + Project
.getName(ob
.getClass()) + " " + ob
);
695 if (ob
.getClass() == Patch
.class) {
696 // STRATEGY change: images are not owned by the FSLoader.
698 if (!ob
.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p
);
699 ht_paths
.remove(p
.getId()); // after removeMipMaps !
700 mawts
.removeAndFlush(loid
);
701 final ImagePlus imp
= imps
.remove(loid
);
703 if (imp
.getStackSize() > 1) {
704 if (null == imp
.getProcessor()) {}
705 else if (null == imp
.getProcessor().getPixels()) {}
706 else Loader
.flush(imp
); // only once
711 cannot_regenerate
.remove(p
);
713 flushMipMaps(p
.getId()); // locks on its own
714 touched_mipmaps
.remove(p
);
722 /** Returns the absolute path to a file that contains the given ImagePlus image - which may be the path as described in the ImagePlus FileInfo object itself, or a totally new file.
723 * If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
725 public String
setImageFile(final Patch p
, final ImagePlus imp
) {
726 if (null == imp
) return null;
728 String path
= getAbsolutePath(p
);
731 // path can be null if the image is pasted, or from a copy, or totally new
733 int i_sl
= path
.lastIndexOf("-----#slice=");
735 slice
= path
.substring(i_sl
);
736 path
= path
.substring(0, i_sl
);
739 // no path, inspect image FileInfo's path if the image has no changes
741 final FileInfo fi
= imp
.getOriginalFileInfo();
742 if (null != fi
&& null != fi
.directory
&& null != fi
.fileName
) {
743 final String fipath
= fi
.directory
.replace('\\', '/') + "/" + fi
.fileName
;
744 if (new File(fipath
).exists()) {
745 // no need to save a new image, it exists and has no changes
746 updatePaths(p
, fipath
, null != slice
);
748 Utils
.log2("Reusing image file: path exists for fileinfo at " + fipath
);
755 final String starting_path
= path
;
756 // Save as a separate image in a new path within the storage folder
758 String filename
= path
.substring(path
.lastIndexOf('/') +1);
760 //Utils.log2("filename 1: " + filename);
762 // remove .tif extension if there
763 if (filename
.endsWith(".tif")) filename
= filename
.substring(0, filename
.length() -3); // keep the dot
765 //Utils.log2("filename 2: " + filename);
767 // check if file ends with a tag of form ".id1234." where 1234 is p.getId()
768 final String tag
= ".id" + p
.getId() + ".";
769 if (!filename
.endsWith(tag
)) filename
+= tag
.substring(1); // without the starting dot, since it has one already
770 // reappend extension
773 //Utils.log2("filename 3: " + filename);
775 path
= getImageStorageFolder() + filename
;
777 if (path
.equals(p
.getOriginalPath())) {
778 // Houston, we have a problem: a user reused a non-original image
781 final int itag
= path
.lastIndexOf(tag
);
783 path
= path
.substring(0, itag
) + "." + i
+ tag
+ "tif";
785 file
= new File(path
);
786 } while (file
.exists());
789 //Utils.log2("path to use: " + path);
791 final String path2
= super.exportImage(p
, imp
, path
, true);
793 //Utils.log2("path exported to: " + path2);
795 // update paths' hashtable
797 updatePaths(p
, path2
, null != slice
);
799 hs_unloadable
.remove(p
);
802 Utils
.log("WARNING could not save image at " + path
);
804 updatePaths(p
, starting_path
, null != slice
);
808 } catch (Exception e
) {
816 * Never used. Was this planned to be what we do no with DBObject.getUniqueId()?
818 private final String
makeFileTitle(final Patch p
) {
819 String title
= p
.getTitle();
820 if (null == title
) return "image-" + p
.getId();
821 title
= asSafePath(title
);
822 if (0 == title
.length()) return "image-" + p
.getId();
826 /** Associate patch with imp, and all slices as well if any. */
827 private void cacheAll(final Patch p
, final ImagePlus imp
) {
829 for (Patch pa
: p
.getStackPatches()) {
837 /** For the Patch and for any associated slices if the patch is part of a stack. */
838 private void updatePaths(final Patch patch
, final String path
, final boolean is_stack
) {
839 synchronized (db_lock
) {
842 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
844 for (Patch p
: patch
.getStackPatches()) {
845 long pid
= p
.getId();
846 String str
= ht_paths
.get(pid
);
847 int isl
= str
.lastIndexOf("-----#slice=");
848 updatePatchPath(p
, path
+ str
.substring(isl
));
851 Utils
.log2("path to set: " + path
);
852 Utils
.log2("path before: " + ht_paths
.get(patch
.getId()));
853 updatePatchPath(patch
, path
);
854 Utils
.log2("path after: " + ht_paths
.get(patch
.getId()));
856 } catch (Throwable e
) {
864 /** With slice info appended at the end; only if it exists, otherwise null. */
865 public String
getAbsolutePath(final Patch patch
) {
866 String abs_path
= patch
.getCurrentPath();
867 if (null != abs_path
) return abs_path
;
868 // else, compute, set and return it:
869 String path
= ht_paths
.get(patch
.getId());
870 if (null == path
) return null;
871 // substract slice info if there
872 int i_sl
= path
.lastIndexOf("-----#slice=");
875 slice
= path
.substring(i_sl
);
876 path
= path
.substring(0, i_sl
);
878 if (isRelativePath(path
)) {
879 // path is relative: preprend the parent folder of the xml file
880 path
= getParentFolder() + path
;
881 if (!isURL(path
) && !new File(path
).exists()) {
882 Utils
.log("Path for patch " + patch
+ " does not exist: " + path
);
885 // else assume that it exists
887 // reappend slice info if existent
888 if (null != slice
) path
+= slice
;
890 patch
.cacheCurrentPath(path
);
894 public static final boolean isURL(final String path
) {
895 return null != path
&& 0 == path
.indexOf("http://");
898 public static final boolean isRelativePath(final String path
) {
899 if (((!IJ
.isWindows() && 0 != path
.indexOf('/')) || (IJ
.isWindows() && 1 != path
.indexOf(":/"))) && 0 != path
.indexOf("http://") && 0 != path
.indexOf("//")) { // "//" is for Samba networks (since the \\ has been converted to // before)
905 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
906 public void addedPatchFrom(String path
, final Patch patch
) {
908 Utils
.log("Null path for patch: " + patch
);
911 updatePatchPath(patch
, path
);
914 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
915 private final void updatePatchPath(final Patch patch
, String path
) { // reversed order in purpose, relative to addedPatchFrom
916 // avoid W1nd0ws nightmares
917 path
= path
.replace('\\', '/'); // replacing with chars, in place
918 // remove double slashes that a user may have slipped in
919 final int start
= isURL(path
) ?
6 : (IJ
.isWindows() ?
3 : 1);
920 while (-1 != path
.indexOf("//", start
)) {
921 // avoid the potential C:// of windows and the starting // of a samba network
922 path
= path
.substring(0, start
) + path
.substring(start
).replace("//", "/");
924 // cache path as absolute
925 patch
.cacheCurrentPath(isRelativePath(path
) ?
getParentFolder() + path
: path
);
926 // if path is absolute, try to make it relative
927 path
= makeRelativePath(path
);
929 ht_paths
.put(patch
.getId(), path
);
930 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
933 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
934 public String
asSafePath(final String name
) {
935 return name
.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
938 /** Overwrites the XML file. If some images do not exist in the file system, a directory with the same name of the XML file plus an "_images" tag appended will be created and images saved there. */
939 public String
save(final Project project
) {
940 String result
= null;
941 if (null == project_file_path
) {
942 String xml_path
= super.saveAs(project
, null, false);
943 if (null == xml_path
) return null;
945 this.project_file_path
= xml_path
;
946 ControlWindow
.updateTitle(project
);
947 result
= this.project_file_path
;
950 File fxml
= new File(project_file_path
);
951 result
= super.export(project
, fxml
, false);
953 if (null != result
) {
954 Utils
.logAll(Utils
.now() + " Saved " + project
);
955 touched_mipmaps
.clear();
960 public String
saveAs(Project project
) {
961 String path
= super.saveAs(project
, null, false);
963 // update the xml path to point to the new one
964 this.project_file_path
= path
;
965 Utils
.log2("After saveAs, new xml path is: " + path
);
967 ControlWindow
.updateTitle(project
);
971 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
972 public String
saveAs(final String path
, final boolean overwrite
) {
974 Utils
.log("Cannot save on null path.");
978 if (!path2
.endsWith(".xml")) path2
+= ".xml";
979 File fxml
= new File(path2
);
980 if (!fxml
.canWrite()) {
981 // write to storage folder instead
982 String path3
= path2
;
983 path2
= getStorageFolder() + fxml
.getName();
984 Utils
.logAll("WARNING can't write to " + path3
+ "\n --> will write instead to " + path2
);
985 fxml
= new File(path2
);
989 while (fxml
.exists()) {
990 String parent
= fxml
.getParent().replace('\\','/');
991 if (!parent
.endsWith("/")) parent
+= "/";
992 String name
= fxml
.getName();
993 name
= name
.substring(0, name
.length() - 4);
994 path2
= parent
+ name
+ "-" + i
+ ".xml";
995 fxml
= new File(path2
);
999 Project project
= Project
.findProject(this);
1000 path2
= super.saveAs(project
, path2
, false);
1001 if (null != path2
) {
1002 project_file_path
= path2
;
1003 Utils
.logAll("After saveAs, new xml path is: " + path2
);
1004 ControlWindow
.updateTitle(project
);
1005 touched_mipmaps
.clear();
1010 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1011 public String
getPath(final Patch patch
) {
1012 return ht_paths
.get(patch
.getId());
1015 /** Takes the given path and tries to makes it relative to this instance's project_file_path, if possible. Otherwise returns the argument as is. */
1016 private String
makeRelativePath(String path
) {
1017 if (null == project_file_path
) {
1024 // fix W1nd0ws paths
1025 path
= path
.replace('\\', '/'); // char-based, no parsing problems
1027 String slice
= null;
1028 int isl
= path
.lastIndexOf("-----#slice");
1030 slice
= path
.substring(isl
);
1031 path
= path
.substring(0, isl
);
1034 if (isRelativePath(path
)) {
1036 if (-1 != isl
) path
+= slice
;
1039 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1040 File xf
= new File(project_file_path
);
1041 File fpath
= new File(path
);
1042 if (fpath
.getParentFile().equals(xf
.getParentFile())) {
1043 path
= path
.substring(xf
.getParent().length());
1044 // remove prepended file separator, if any, to label the path as relative
1045 if (0 == path
.indexOf('/')) path
= path
.substring(1);
1046 } else if (fpath
.equals(xf
.getParentFile())) {
1049 if (-1 != isl
) path
+= slice
;
1050 //Utils.log("made relative path: " + path);
1054 /** Adds a "Save" and "Save as" menu items. */
1055 public void setupMenuItems(final JMenu menu
, final Project project
) {
1056 ActionListener listener
= new ActionListener() {
1057 public void actionPerformed(ActionEvent ae
) {
1058 String command
= ae
.getActionCommand();
1059 if (command
.equals("Save")) {
1061 } else if (command
.equals("Save as...")) {
1067 item
= new JMenuItem("Save"); item
.addActionListener(listener
); menu
.add(item
);
1068 item
.setAccelerator(KeyStroke
.getKeyStroke(KeyEvent
.VK_S
, 0, true));
1069 item
= new JMenuItem("Save as..."); item
.addActionListener(listener
); menu
.add(item
);
1072 /** Returns the last Patch. */
1073 protected Patch
importStackAsPatches(final Project project
, final Layer first_layer
, final int x
, final int y
, final ImagePlus imp_stack
, final boolean as_copy
, final String filepath
) {
1074 Utils
.log2("FSLoader.importStackAsPatches filepath=" + filepath
);
1075 String target_dir
= null;
1077 DirectoryChooser dc
= new DirectoryChooser("Folder to save images");
1078 target_dir
= dc
.getDirectory();
1079 if (null == target_dir
) return null; // user canceled dialog
1080 if (target_dir
.length() -1 != target_dir
.lastIndexOf('/')) {
1085 final boolean virtual
= imp_stack
.getStack().isVirtual();
1087 int pos_x
= Integer
.MAX_VALUE
!= x ? x
: (int)first_layer
.getLayerWidth()/2 - imp_stack
.getWidth()/2;
1088 int pos_y
= Integer
.MAX_VALUE
!= y ? y
: (int)first_layer
.getLayerHeight()/2 - imp_stack
.getHeight()/2;
1089 final double thickness
= first_layer
.getThickness();
1090 final String title
= Utils
.removeExtension(imp_stack
.getTitle()).replace(' ', '_');
1091 Utils
.showProgress(0);
1092 Patch previous_patch
= null;
1093 final int n
= imp_stack
.getStackSize();
1094 for (int i
=1; i
<=n
; i
++) {
1095 Layer layer
= first_layer
;
1096 double z
= first_layer
.getZ() + (i
-1) * thickness
;
1097 if (i
> 1) layer
= first_layer
.getParent().getLayer(z
, thickness
, true); // will create new layer if not found
1098 if (null == layer
) {
1099 Utils
.log("Display.importStack: could not create new layers.");
1102 String patch_path
= null;
1104 ImagePlus imp_patch_i
= null;
1105 if (virtual
) { // because we love inefficiency, every time all this is done again
1106 VirtualStack vs
= (VirtualStack
)imp_stack
.getStack();
1107 String vs_dir
= vs
.getDirectory().replace('\\', '/');
1108 if (!vs_dir
.endsWith("/")) vs_dir
+= "/";
1109 String iname
= vs
.getFileName(i
);
1110 patch_path
= vs_dir
+ iname
;
1112 Utils
.log2(i
+ " : " + patch_path
);
1113 imp_patch_i
= openImage(patch_path
);
1115 ImageProcessor ip
= imp_stack
.getStack().getProcessor(i
);
1116 if (as_copy
) ip
= ip
.duplicate();
1117 imp_patch_i
= new ImagePlus(title
+ "__slice=" + i
, ip
);
1119 preProcess(imp_patch_i
);
1121 String label
= imp_stack
.getStack().getSliceLabel(i
);
1122 if (null == label
) label
= "";
1125 patch_path
= target_dir
+ imp_patch_i
.getTitle() + ".zip";
1126 ini
.trakem2
.io
.ImageSaver
.saveAsZip(imp_patch_i
, patch_path
);
1127 patch
= new Patch(project
, label
+ " " + title
+ " " + i
, pos_x
, pos_y
, imp_patch_i
);
1128 } else if (virtual
) {
1129 patch
= new Patch(project
, label
, pos_x
, pos_y
, imp_patch_i
);
1131 patch_path
= filepath
+ "-----#slice=" + i
;
1132 //Utils.log2("path is "+ patch_path);
1133 final AffineTransform atp
= new AffineTransform();
1134 atp
.translate(pos_x
, pos_y
);
1135 patch
= new Patch(project
, getNextId(), label
+ " " + title
+ " " + i
, imp_stack
.getWidth(), imp_stack
.getHeight(), imp_stack
.getType(), false, imp_stack
.getProcessor().getMin(), imp_stack
.getProcessor().getMax(), atp
);
1136 patch
.addToDatabase();
1137 //Utils.log2("type is " + imp_stack.getType());
1139 Utils
.log2("B: " + i
+ " : " + patch_path
);
1140 addedPatchFrom(patch_path
, patch
);
1142 if (virtual
) cache(patch
, imp_patch_i
); // each slice separately
1143 else cache(patch
, imp_stack
); // uses the entire stack, shared among all Patch instances
1145 if (isMipMapsEnabled()) generateMipMaps(patch
);
1146 if (null != previous_patch
) patch
.link(previous_patch
);
1148 previous_patch
= patch
;
1149 Utils
.showProgress(i
* (1.0 / n
));
1151 Utils
.showProgress(1.0);
1153 // update calibration
1156 // return the last patch
1157 return previous_patch
;
1160 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1161 public void parseXMLOptions(final HashMap ht_attributes
) {
1162 Object ob
= ht_attributes
.remove("preprocessor");
1164 setPreprocessor((String
)ob
);
1166 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1167 // and also to prevent errors such as those created when manualy tinkering with the XML file
1168 // or renaming directories, etc.
1169 ob
= ht_attributes
.remove("storage_folder");
1171 String sf
= ((String
)ob
).replace('\\', '/');
1172 if (isRelativePath(sf
)) {
1173 sf
= getParentFolder() + sf
;
1177 Utils
.log2("Can't have an URL as the path of a storage folder.");
1179 File f
= new File(sf
);
1180 if (f
.exists() && f
.isDirectory()) {
1181 this.dir_storage
= sf
;
1183 Utils
.log2("storage_folder was not found or is invalid: " + ob
);
1187 if (null == this.dir_storage
) {
1188 // select the directory where the xml file lives.
1189 this.dir_storage
= getParentFolder();
1190 if (null == this.dir_storage
|| isURL(this.dir_storage
)) this.dir_storage
= null;
1191 if (null == this.dir_storage
&& ControlWindow
.isGUIEnabled()) {
1192 Utils
.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1193 DirectoryChooser dc
= new DirectoryChooser("REQUIRED: select a storage folder");
1194 this.dir_storage
= dc
.getDirectory();
1196 if (null == this.dir_storage
) {
1197 IJ
.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1198 this.dir_storage
= System
.getProperty("user.home").replace('\\', '/');
1202 if (null != this.dir_storage
&& !this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
1203 Utils
.log2("storage folder is " + this.dir_storage
);
1205 ob
= ht_attributes
.remove("mipmaps_folder");
1207 String mf
= ((String
)ob
).replace('\\', '/');
1208 if (isRelativePath(mf
)) {
1209 mf
= getParentFolder() + mf
;
1212 this.dir_mipmaps
= mf
;
1213 // TODO must disable input somehow, so that images are not edited.
1215 File f
= new File(mf
);
1216 if (f
.exists() && f
.isDirectory()) {
1217 this.dir_mipmaps
= mf
;
1219 Utils
.log2("mipmaps_folder was not found or is invalid: " + ob
);
1223 if (null == this.dir_mipmaps
) {
1224 // create a new one inside the dir_storage, which can't be null
1225 createMipMapsDir(dir_storage
);
1226 if (null != this.dir_mipmaps
&& ControlWindow
.isGUIEnabled() && null != IJ
.getInstance()) {
1227 askAndExecMipmapRegeneration(null);
1231 if (null != this.dir_mipmaps
&& !this.dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1232 Utils
.log2("mipmaps folder is " + this.dir_mipmaps
);
1235 private void askAndExecMipmapRegeneration(final String msg
) {
1236 Utils
.log2("Asking user Yes/No to generate mipmaps on the background."); // tip for headless runners whose program gets "stuck"
1237 YesNoDialog yn
= new YesNoDialog(IJ
.getInstance(), "Generate mipmaps", (null != msg ? msg
+ "\n" : "") + "Generate mipmaps in the background for all images?");
1238 if (yn
.yesPressed()) {
1239 final Loader lo
= this;
1243 // wait while parsing the rest of the XML file
1244 while (!v_loaders
.contains(lo
)) {
1247 Project pj
= Project
.findProject(lo
);
1248 lo
.generateMipMaps(pj
.getRootLayerSet().getDisplayables(Patch
.class));
1249 } catch (Exception e
) {}
1255 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1256 public void insertXMLOptions(StringBuffer sb_body
, String indent
) {
1257 if (null != preprocessor
) sb_body
.append(indent
).append("preprocessor=\"").append(preprocessor
).append("\"\n");
1258 if (null != dir_mipmaps
) sb_body
.append(indent
).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps
)).append("\"\n");
1259 if (null != dir_storage
) sb_body
.append(indent
).append("storage_folder=\"").append(makeRelativePath(dir_storage
)).append("\"\n");
1262 /** Return the path to the folder containing the project XML file. */
1263 private final String
getParentFolder() {
1264 return this.project_file_path
.substring(0, this.project_file_path
.lastIndexOf('/')+1);
1267 /* ************** MIPMAPS **********************/
1269 /** Returns the path to the directory hosting the file image pyramids. */
1270 public String
getMipMapsFolder() {
1276 static private IndexColorModel thresh_cm = null;
1278 static private final IndexColorModel getThresholdLUT() {
1279 if (null == thresh_cm) {
1280 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1281 final byte[] c = new byte[256];
1283 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1289 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1290 static public final Object
grabPixels(final BufferedImage bi
) {
1291 final PixelGrabber pg
= new PixelGrabber(bi
, 0, 0, bi
.getWidth(), bi
.getHeight(), false);
1294 return pg
.getPixels();
1295 } catch (InterruptedException e
) {
1301 private final BufferedImage
createCroppedAlpha(final BufferedImage alpha
, final BufferedImage outside
) {
1302 if (null == outside
) return alpha
;
1304 final int width
= outside
.getWidth();
1305 final int height
= outside
.getHeight();
1307 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1308 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1309 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1310 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1313 // So, instead: grab the pixels, fix them manually
1314 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1315 final byte[] o
= (byte[])grabPixels(outside
);
1316 if (null == o
) return null;
1317 final byte[] a
= null == alpha ? o
: (byte[])grabPixels(alpha
);
1319 // Set each non-255 pixel in outside to 0 in alpha:
1320 for (int i
=0; i
<o
.length
; i
++) {
1321 if ( (o
[i
]&0xff) < 255) a
[i
] = 0;
1324 // Put the pixels back into an image:
1325 final BufferedImage thresholded
= new BufferedImage(width
, height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1326 thresholded
.getRaster().setDataElements(0, 0, width
, height
, a
);
1331 static public final BufferedImage
convertToBufferedImage(final ByteProcessor bp
) {
1332 bp
.setMinAndMax(0, 255);
1333 final Image img
= bp
.createImage();
1334 if (img
instanceof BufferedImage
) return (BufferedImage
)img
;
1336 final BufferedImage bi
= new BufferedImage(bp
.getWidth(), bp
.getHeight(), BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1337 bi
.createGraphics().drawImage(bi
, 0, 0, null);
1341 /** Scale a BufferedImage.TYPE_BYTE_INDEXED into another of the same type but dimensions target_width,target_height. */
1342 static private final BufferedImage
scaleAndFlush(final Image img
, final int target_width
, final int target_height
, final boolean area_averaging
, final Object interpolation_hint
) {
1343 final BufferedImage bi
= new BufferedImage(target_width
, target_height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1344 if (area_averaging
) {
1345 bi
.createGraphics().drawImage(img
.getScaledInstance(target_width
, target_height
, Image
.SCALE_AREA_AVERAGING
), 0, 0, null);
1347 final Graphics2D g
= bi
.createGraphics();
1348 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1349 g
.drawImage(img
, 0, 0, target_width
, target_height
, null); // draws it scaled to target area w*h
1351 // Release native resources
1357 /** Image to BufferedImage. Can be used for hardware-accelerated resizing, since the whole awt is painted to a target w,h area in the returned new BufferedImage. Does not accept LUT images: only ARGB or GRAY. */
1358 private final BufferedImage
[] IToBI(final Image awt
, final int w
, final int h
, final Object interpolation_hint
, final BufferedImage alpha
, final BufferedImage outside
) {
1360 final boolean area_averaging
= interpolation_hint
.getClass() == Integer
.class && Loader
.AREA_AVERAGING
== ((Integer
)interpolation_hint
).intValue();
1361 final boolean must_scale
= (w
!= awt
.getWidth(null) || h
!= awt
.getHeight(null));
1363 if (null != alpha
|| null != outside
) bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_INT_ARGB
);
1364 else bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1365 final Graphics2D g
= bi
.createGraphics();
1366 if (area_averaging
) {
1367 final Image img
= awt
.getScaledInstance(w
, h
, Image
.SCALE_AREA_AVERAGING
); // Creates ALWAYS an RGB image, so must repaint back to a single-channel image, avoiding unnecessary blow up of memory.
1368 g
.drawImage(img
, 0, 0, null);
1370 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1371 g
.drawImage(awt
, 0, 0, w
, h
, null); // draws it scaled
1373 BufferedImage ba
= alpha
;
1374 BufferedImage bo
= outside
;
1375 if (null != alpha
&& must_scale
) {
1376 ba
= scaleAndFlush(alpha
, w
, h
, area_averaging
, interpolation_hint
);
1378 if (null != outside
&& must_scale
) {
1379 bo
= scaleAndFlush(outside
, w
, h
, area_averaging
, interpolation_hint
);
1382 BufferedImage the_alpha
= ba
;
1383 if (null != alpha
) {
1384 if (null != outside
) {
1385 the_alpha
= createCroppedAlpha(ba
, bo
);
1387 } else if (null != outside
) {
1388 the_alpha
= createCroppedAlpha(null, bo
);
1390 if (null != the_alpha
) {
1391 bi
.getAlphaRaster().setRect(the_alpha
.getRaster());
1392 //bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])new ImagePlus("", the_alpha).getProcessor().convertToFloat().getPixels());
1396 //Utils.log2("bi is: " + bi.getType() + " BufferedImage.TYPE_INT_ARGB=" + BufferedImage.TYPE_INT_ARGB);
1399 FloatProcessor fp_alpha = null;
1400 fp_alpha = (FloatProcessor) new ByteProcessor(ba).convertToFloat();
1401 // Set all non-white pixels to zero (eliminate shadowy border caused by interpolation)
1402 final float[] pix = (float[])fp_alpha.getPixels();
1403 for (int i=0; i<pix.length; i++)
1404 if (Math.abs(pix[i] - 255) > 0.001f) pix[i] = 0;
1405 bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])fp_alpha.getPixels());
1408 return new BufferedImage
[]{bi
, ba
, bo
};
1411 private final Object
getHint(final int mode
) {
1413 case Loader
.BICUBIC
:
1414 return RenderingHints
.VALUE_INTERPOLATION_BICUBIC
;
1415 case Loader
.BILINEAR
:
1416 return RenderingHints
.VALUE_INTERPOLATION_BILINEAR
;
1417 case Loader
.AREA_AVERAGING
:
1418 return new Integer(mode
);
1419 case Loader
.NEAREST_NEIGHBOR
:
1421 return RenderingHints
.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
;
1425 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1426 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source
, final int source_width
, final int source_height
, final int target_width
, final int target_height
) {
1427 source
.setPixels(source_width
, source_height
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])source
.getPixels(), source_width
, source_height
), 0.75f
).data
);
1428 source
.resizeInPlace(target_width
, target_height
);
1429 return (byte[])source
.convertToByte(false).getPixels(); // no scaling
1432 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1433 static final private byte[] meanResizeInHalf(final FloatProcessorT2 source
, final int sourceWidth
, final int sourceHeight
, final int targetWidth
, final int targetHeight
) {
1434 final float[] sourceData
= source
.getFloatPixels();
1435 final float[] targetData
= new float[targetWidth
* targetHeight
];
1437 for (int r
= 0; r
< targetData
.length
; r
+= targetWidth
) {
1439 for (int x
= 0; x
< targetWidth
; ++x
)
1440 targetData
[r
+ x
] = sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1443 for (int x
= 0; x
< targetWidth
; ++x
) {
1444 targetData
[r
+ x
] += sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1445 targetData
[r
+ x
] /= 4;
1449 source
.setPixels(targetWidth
, targetHeight
, targetData
);
1450 return (byte[])source
.convertToByte(false).getPixels();
1453 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1454 public void queueForMipmapRemoval(final Patch p
, boolean yes
) {
1455 if (yes
) touched_mipmaps
.add(p
);
1456 else touched_mipmaps
.remove(p
);
1459 /** Given an image and its source file name (without directory prepended), generate
1460 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1461 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1462 * The Patch id and a ".jpg" extension will be appended to the filename in all cases.<br />
1463 * Any equally named files will be overwritten.
1465 public boolean generateMipMaps(final Patch patch
) {
1466 //Utils.log2("mipmaps for " + patch);
1467 if (null == dir_mipmaps
) createMipMapsDir(null);
1468 if (null == dir_mipmaps
|| isURL(dir_mipmaps
)) return false;
1469 final String path
= getAbsolutePath(patch
);
1471 Utils
.log2("generateMipMaps: cannot find path for Patch " + patch
);
1472 cannot_regenerate
.add(patch
);
1475 synchronized (gm_lock
) {
1477 if (hs_regenerating_mipmaps
.contains(patch
)) {
1478 // already being done
1480 Utils
.log2("Already being done: " + patch
);
1483 hs_regenerating_mipmaps
.add(patch
);
1487 /** Record Patch as modified */
1488 touched_mipmaps
.add(patch
);
1490 /** Remove serialized features, if any */
1491 removeSerializedFeatures(patch
);
1493 /** Remove serialized pointmatches, if any */
1494 removeSerializedPointMatches(patch
);
1496 String srmode
= patch
.getProject().getProperty("image_resizing_mode");
1497 int resizing_mode
= GAUSSIAN
;
1498 if (null != srmode
) resizing_mode
= Loader
.getMode(srmode
);
1502 // 1 - Ask the Patch to apply a coordinate transform, or rather, create a function that gets the coordinate transform from the Patch and applies it to the 'ip'.
1503 // 2 - Then (1) should return both the transformed image and the alpha mask
1506 ByteProcessor alpha_mask
= null;
1507 ByteProcessor outside_mask
= null;
1508 final boolean coordinate_transformed
;
1509 int type
= patch
.getType();
1511 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1512 Patch
.PatchImage pai
= patch
.createTransformedImage();
1514 alpha_mask
= pai
.mask
; // can be null
1515 outside_mask
= pai
.outside
; // can be null
1516 coordinate_transformed
= pai
.coordinate_transformed
;
1519 final String filename
= new StringBuffer(new File(path
).getName()).append('.').append(patch
.getId()).append(".jpg").toString();
1520 int w
= ip
.getWidth();
1521 int h
= ip
.getHeight();
1523 // sigma = sqrt(2^level - 0.5^2)
1524 // where 0.5 is the estimated sigma for a full-scale image
1525 // which means sigma = 0.75 for the full-scale image (has level 0)
1526 // prepare a 0.75 sigma image from the original
1527 ColorModel cm
= ip
.getColorModel();
1528 int k
= 0; // the scale level. Proper scale is: 1 / pow(2, k)
1529 // but since we scale 50% relative the previous, it's always 0.75
1531 // Set for the level 0 image, which is a duplicate of the one on the cache in any case
1532 ip
.setMinAndMax(patch
.getMin(), patch
.getMax());
1535 // Proper support for LUT images: treat them as RGB
1536 if (ip
.isColorLut()) {
1537 ip
= ip
.convertToRGB();
1539 type
= ImagePlus
.COLOR_RGB
;
1542 if (ImagePlus
.COLOR_RGB
== type
) {
1543 // TODO releaseToFit proper
1544 releaseToFit(w
* h
* 4 * 5);
1545 final ColorProcessor cp
= (ColorProcessor
)ip
;
1546 final FloatProcessorT2 red
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(0, red
);
1547 final FloatProcessorT2 green
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(1, green
);
1548 final FloatProcessorT2 blue
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(2, blue
);
1549 FloatProcessorT2 alpha
;
1550 final FloatProcessorT2 outside
;
1551 if (null != alpha_mask
) {
1552 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1556 if (null != outside_mask
) {
1557 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1558 if ( null == alpha
) {
1560 alpha_mask
= outside_mask
;
1566 // sw,sh are the dimensions of the image to blur
1567 // w,h are the dimensions to scale the blurred image to
1571 final String target_dir0
= getLevelDir(dir_mipmaps
, 0);
1572 // No alpha channel:
1573 // - use gaussian resizing
1574 // - use standard ImageJ java.awt.Image creation
1576 // Generate level 0 first:
1577 // TODO Add alpha information into the int[] pixel array or make the image visible some ohter way
1578 if (!(null == alpha ? ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp
, target_dir0
+ filename
, 0.85f
, false)
1579 : ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(createARGBImage(w
, h
, embedAlpha((int[])cp
.getPixels(), (byte[])alpha_mask
.getPixels(), null == outside ?
null : (byte[])outside_mask
.getPixels())), target_dir0
+ filename
, 0.85f
))) {
1580 cannot_regenerate
.add(patch
);
1583 // 1 - Prepare values for the next scaled image
1589 // 2 - Check that the target folder for the desired scale exists
1590 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1591 if (null == target_dir
) continue;
1592 // 3 - Blur the previous image to 0.75 sigma, and scale it
1593 final byte[] r
= gaussianBlurResizeInHalf(red
, sw
, sh
, w
, h
); // will resize 'red' FloatProcessor in place.
1594 final byte[] g
= gaussianBlurResizeInHalf(green
, sw
, sh
, w
, h
); // idem
1595 final byte[] b
= gaussianBlurResizeInHalf(blue
, sw
, sh
, w
, h
); // idem
1596 final byte[] a
= null == alpha ?
null : gaussianBlurResizeInHalf(alpha
, sw
, sh
, w
, h
); // idem
1597 if ( null != outside
) {
1599 if (alpha
!= outside
)
1600 o
= gaussianBlurResizeInHalf(outside
, sw
, sh
, w
, h
); // idem
1603 // Remove all not completely inside pixels from the alphamask
1604 // If there was no alpha mask, alpha is the outside itself
1605 for (int i
=0; i
<o
.length
; i
++) {
1606 if ( (o
[i
]&0xff) != 255 ) a
[i
] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1610 // 4 - Compose ColorProcessor
1611 final int[] pix
= new int[w
* h
];
1612 if (null == alpha
) {
1613 for (int i
=0; i
<pix
.length
; i
++) {
1614 pix
[i
] = 0xff000000 | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1616 final ColorProcessor cp2
= new ColorProcessor(w
, h
, pix
);
1618 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp2
, target_dir
+ filename
, 0.85f
, false)) {
1619 cannot_regenerate
.add(patch
);
1623 // LIKELY no need to set alpha raster later in createARGBImage ... TODO
1624 for (int i
=0; i
<pix
.length
; i
++) {
1625 pix
[i
] = ((a
[i
]&0xff)<<24) | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1627 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1628 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1629 cannot_regenerate
.add(patch
);
1635 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1639 releaseToFit(w
* h
* 4 * 5);
1640 final boolean as_grey
= !ip
.isColorLut();
1641 if (as_grey
&& null == cm
) {
1645 if (Loader
.GAUSSIAN
== resizing_mode
) {
1646 FloatProcessor fp
= (FloatProcessor
) ip
.convertToFloat();
1649 FloatProcessor alpha
;
1650 FloatProcessor outside
;
1651 if (null != alpha_mask
) {
1652 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1656 if (null != outside_mask
) {
1657 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1658 if (null == alpha
) {
1660 alpha_mask
= outside_mask
;
1667 // 0 - blur the previous image to 0.75 sigma
1668 if (0 != k
) { // not doing so at the end because it would add one unnecessary blurring
1669 fp
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])fp
.getPixels(), sw
, sh
), 0.75f
).data
, cm
);
1670 if (null != alpha
) {
1671 alpha
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])alpha
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1672 if (alpha
!= outside
&& outside
!= null) {
1673 outside
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])outside
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1677 // 1 - check that the target folder for the desired scale exists
1678 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1679 if (null == target_dir
) continue;
1680 // 2 - generate scaled image
1682 fp
= (FloatProcessor
)fp
.resize(w
, h
);
1683 if (ImagePlus
.GRAY8
== type
) {
1684 fp
.setMinAndMax(0, 255); // the min and max was expanded into 0,255 range at convertToFloat for 8-bit images, so the only limit to be added now to the FloatProcessor is that of the 8-bit range. The latter is done automatically for FloatProcessor class, but FloatProcessorT2 doesn't, to avoid the expensive (and here superfluous) operation of looping through all pixels in the findMinAndMax method.
1686 fp
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done: the resize doesn't preserve the min and max!
1688 if (null != alpha
) {
1689 alpha
= (FloatProcessor
)alpha
.resize(w
, h
);
1690 if (alpha
!= outside
&& null != outside
) {
1691 outside
= (FloatProcessor
)outside
.resize(w
, h
);
1695 if (null != alpha
) {
1696 // 3 - save as jpeg with alpha
1697 final byte[] a
= (byte[])alpha
.convertToByte(false).getPixels();
1698 if (null != outside
) {
1700 if (alpha
!= outside
) {
1701 o
= (byte[])outside
.convertToByte(false).getPixels();
1705 // Remove all not completely inside pixels from the alpha mask
1706 // If there was no alpha mask, alpha is the outside itself
1707 for (int i
=0; i
<o
.length
; i
++) {
1708 if ( (o
[i
]&0xff) != 255 ) a
[i
] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1711 if (ImagePlus
.GRAY8
!= type
) { // for 8-bit, the min,max has been applied when going to FloatProcessor
1712 fp
.setMinAndMax(patch
.getMin(), patch
.getMax());
1714 final int[] pix
= embedAlpha((int[])fp
.convertToRGB().getPixels(), a
);
1716 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1717 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1718 cannot_regenerate
.add(patch
);
1724 // 3 - save as 8-bit jpeg
1725 final ImageProcessor ip2
= Utils
.convertTo(fp
, type
, false); // no scaling, since the conversion to float above didn't change the range. This is needed because of the min and max
1726 if (!coordinate_transformed
) ip2
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done, it's a new ImageProcessor
1727 if (null != cm
) ip2
.setColorModel(cm
); // the LUT
1729 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(ip2
, target_dir
+ filename
, 0.85f
, as_grey
)) {
1730 cannot_regenerate
.add(patch
);
1735 // 4 - prepare values for the next scaled image
1741 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1744 //final StopWatch timer = new StopWatch();
1746 // use java hardware-accelerated resizing
1747 Image awt
= ip
.createImage();
1749 BufferedImage balpha
= null == alpha_mask ?
null : convertToBufferedImage(alpha_mask
);
1750 BufferedImage boutside
= null == outside_mask ?
null : convertToBufferedImage(outside_mask
);
1752 BufferedImage bi
= null;
1753 final Object hint
= getHint(resizing_mode
);
1758 // check that the target folder for the desired scale exists
1759 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1760 if (null == target_dir
) continue;
1761 // obtain half image
1762 // for level 0 and others, when awt is not a BufferedImage or needs to be reduced in size (to new w,h)
1763 final BufferedImage
[] res
= IToBI(awt
, w
, h
, hint
, balpha
, boutside
);
1767 // prepare next iteration
1768 if (awt
!= bi
) awt
.flush();
1773 // save this iteration
1774 if ( ( (null != balpha
|| null != boutside
) &&
1775 !ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi
, target_dir
+ filename
, 0.85f
))
1776 || ( null == balpha
&& null == boutside
&& !ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(bi
, target_dir
+ filename
, 0.85f
, as_grey
))) {
1777 cannot_regenerate
.add(patch
);
1780 } while (w
>= 32 && h
>= 32);
1783 //timer.cumulative();
1787 // flush any cached tiles
1788 flushMipMaps(patch
.getId());
1791 } catch (Throwable e
) {
1793 cannot_regenerate
.add(patch
);
1796 // gets executed even when returning from the catch statement or within the try/catch block
1797 synchronized (gm_lock
) {
1799 hs_regenerating_mipmaps
.remove(patch
);
1805 /** Remove the file, if it exists, with serialized features for patch.
1806 * Returns true when no such file or on success; false otherwise. */
1807 public boolean removeSerializedFeatures(final Patch patch
) {
1808 final File f
= new File(new StringBuffer(dir_storage
).append("features.ser/features_").append(patch
.getUniqueIdentifier()).append(".ser").toString());
1813 } catch (Exception e
) {
1820 /** Remove the file, if it exists, with serialized point matches for patch.
1821 * Returns true when no such file or on success; false otherwise. */
1822 public boolean removeSerializedPointMatches(final Patch patch
) {
1823 boolean success
= true;
1824 final File d
= new File(new StringBuffer(dir_storage
).append("pointmatches.ser").toString());
1825 if (d
.exists()&&d
.isDirectory()) {
1826 final String
[] files
= d
.list();
1827 if ( files
!= null )
1829 for ( final String f
: files
) {
1830 if ( f
.matches( ".*_" + patch
.getUniqueIdentifier() + "(_|\\.).*" ) ) {
1832 new File( d
.getAbsolutePath() + "/" + f
).delete();
1833 } catch (Exception e
) {
1844 /** Generate image pyramids and store them into files under the dir_mipmaps for each Patch object in the Project. The method is multithreaded, using as many processors as available to the JVM.
1846 * @param al : the list of Patch instances to generate mipmaps for.
1847 * @param overwrite : whether to overwrite any existing mipmaps, or save only those that don't exist yet for whatever reason. This flag provides the means for minimal effort mipmap regeneration.)
1849 public Bureaucrat
generateMipMaps(final ArrayList al
, final boolean overwrite
) {
1850 if (null == al
|| 0 == al
.size()) return null;
1851 if (null == dir_mipmaps
) createMipMapsDir(null);
1852 if (isURL(dir_mipmaps
)) {
1853 Utils
.log("Mipmaps folder is an URL, can't save files into it.");
1856 final Worker worker
= new Worker("Generating MipMaps") {
1858 this.setAsBackground(true);
1859 this.startedWorking();
1862 final Worker wo
= this;
1864 Utils
.log2("starting mipmap generation ..");
1866 final int size
= al
.size();
1867 final Patch
[] pa
= new Patch
[size
];
1868 final Thread
[] threads
= MultiThreading
.newThreads();
1870 final AtomicInteger ai
= new AtomicInteger(0);
1872 for (int ithread
= 0; ithread
< threads
.length
; ++ithread
) {
1873 threads
[ithread
] = new Thread(new Runnable() {
1876 for (int k
= ai
.getAndIncrement(); k
< size
; k
= ai
.getAndIncrement()) {
1877 if (wo
.hasQuitted()) {
1880 wo
.setTaskName("Generating MipMaps " + (k
+1) + "/" + size
);
1882 boolean ow
= overwrite
;
1884 // check if all the files exist. If one doesn't, then overwrite all anyway
1885 int w
= (int)pa
[k
].getWidth();
1886 int h
= (int)pa
[k
].getHeight();
1888 final String filename
= new File(getAbsolutePath(pa
[k
])).getName() + "." + pa
[k
].getId() + ".jpg";
1893 if (!new File(dir_mipmaps
+ level
+ "/" + filename
).exists()) {
1897 } while (w
>= 32 && h
>= 32);
1900 if ( ! generateMipMaps(pa
[k
]) ) {
1901 // some error ocurred
1902 Utils
.log2("Could not generate mipmaps for patch " + pa
[k
]);
1904 } catch (Exception e
) {
1912 MultiThreading
.startAndJoin(threads
);
1914 } catch (Exception e
) {
1918 this.finishedWorking();
1921 return Bureaucrat
.createAndStart(worker
, ((Patch
)al
.get(0)).getProject());
1924 private final String
getLevelDir(final String dir_mipmaps
, final int level
) {
1925 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
1926 synchronized (db_lock
) {
1928 final String path
= new StringBuffer(dir_mipmaps
).append(level
).append('/').toString();
1929 if (isURL(dir_mipmaps
)) {
1933 final File file
= new File(path
);
1934 if (file
.exists() && file
.isDirectory()) {
1943 } catch (Exception e
) {
1951 /** If parent path is null, it's asked for.*/
1952 public boolean createMipMapsDir(String parent_path
) {
1953 if (null == parent_path
) {
1954 // try to create it in the same directory where the XML file is
1955 if (null != dir_storage
) {
1956 File f
= new File(dir_storage
+ "trakem2.mipmaps");
1960 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
1961 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1964 } catch (Exception e
) {}
1965 } else if (f
.isDirectory()) {
1966 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
1967 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1970 // else can't use it
1971 } else if (null != project_file_path
) {
1972 final File fxml
= new File(project_file_path
);
1973 final File fparent
= fxml
.getParentFile();
1974 if (null != fparent
&& fparent
.isDirectory()) {
1975 File f
= new File(fparent
.getAbsolutePath().replace('\\', '/') + "/" + fxml
.getName() + ".mipmaps");
1978 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
1979 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1982 } catch (Exception e
) {}
1985 // else, ask for a new folder
1986 final DirectoryChooser dc
= new DirectoryChooser("Select MipMaps parent directory");
1987 parent_path
= dc
.getDirectory();
1988 if (null == parent_path
) return false;
1989 if (!parent_path
.endsWith("/")) parent_path
+= "/";
1991 // examine parent path
1992 final File file
= new File(parent_path
);
1993 if (file
.exists()) {
1994 if (file
.isDirectory()) {
1996 this.dir_mipmaps
= parent_path
+ "trakem2.mipmaps/";
1998 File f
= new File(this.dir_mipmaps
);
2002 } catch (Exception e
) {
2007 Utils
.showMessage("Selected parent path is not a directory. Please choose another one.");
2008 return createMipMapsDir(null);
2011 Utils
.showMessage("Parent path does not exist. Please select a new one.");
2012 return createMipMapsDir(null);
2017 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2018 public void flushMipMaps(boolean forget_dir_mipmaps
) {
2019 if (null == dir_mipmaps
) return;
2020 synchronized (db_lock
) {
2022 if (forget_dir_mipmaps
) this.dir_mipmaps
= null;
2023 mawts
.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2028 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2029 public void flushMipMaps(final long id
) {
2030 if (null == dir_mipmaps
) return;
2031 synchronized (db_lock
) {
2034 //mawts.removePyramid(id); // does not remove level 0 awts (i.e. the 100% images)
2035 // Need to remove ALL now, since level 0 is also included as a mipmap:
2036 for (final Image img
: mawts
.remove(id
)) {
2037 if (null != img
) img
.flush();
2039 } catch (Exception e
) { e
.printStackTrace(); }
2044 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2045 public void removeMipMaps(final Patch p
) {
2046 if (null == dir_mipmaps
) return;
2048 final int width
= (int)p
.getWidth();
2049 final int height
= (int)p
.getHeight();
2050 final String path
= getAbsolutePath(p
);
2051 if (null == path
) return; // missing file
2052 final String filename
= new File(path
).getName() + "." + p
.getId() + ".jpg";
2053 // cue the task in a dispatcher:
2054 dispatcher
.exec(new Runnable() { public void run() { // copy-paste as a replacement for (defmacro ... we luv java
2055 removeMipMaps(filename
, width
, height
);
2057 } catch (Exception e
) {
2062 private void removeMipMaps(final String filename
, final int width
, final int height
) {
2065 int k
= 0; // the level
2067 final File f
= new File(dir_mipmaps
+ k
+ "/" + filename
);
2071 Utils
.log2("Could not remove file " + f
.getAbsolutePath());
2073 } catch (Exception e
) {
2080 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
2083 /** Checks whether this Loader is using a directory of image pyramids for each Patch or not. */
2084 public boolean isMipMapsEnabled() {
2085 return null != dir_mipmaps
;
2088 /** Return the closest level to @param level that exists as a file.
2089 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2091 public int getClosestMipMapLevel(final Patch patch
, int level
) {
2092 if (null == dir_mipmaps
) return 0;
2094 final String path
= getAbsolutePath(patch
);
2095 if (null == path
) return ERROR_PATH_NOT_FOUND
;
2096 final String filename
= new File(path
).getName() + ".jpg";
2097 if (isURL(dir_mipmaps
)) {
2098 if (level
<= 0) return 0;
2099 // choose the smallest dimension
2100 // find max level that keeps dim over 32 pixels
2101 final int lev
= getHighestMipMapLevel(Math
.min(patch
.getWidth(), patch
.getHeight()));
2102 if (level
> lev
) return lev
;
2106 final File f
= new File(new StringBuffer(dir_mipmaps
).append(level
).append('/').append(filename
).toString());
2110 // try the next level
2112 } while (level
>= 0);
2114 } catch (Exception e
) {
2120 /** A temporary list of Patch instances for which a pyramid is being generated. */
2121 final private HashSet hs_regenerating_mipmaps
= new HashSet();
2123 /** A lock for the generation of mipmaps. */
2124 final private Object gm_lock
= new Object();
2125 private boolean gm_locked
= false;
2127 protected final void gm_lock() {
2128 //Utils.printCaller(this, 7);
2129 while (gm_locked
) { try { gm_lock
.wait(); } catch (InterruptedException ie
) {} }
2132 protected final void gm_unlock() {
2133 //Utils.printCaller(this, 7);
2136 gm_lock
.notifyAll();
2140 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2141 public boolean checkMipMapFileExists(final Patch p
, final double magnification
) {
2142 if (null == dir_mipmaps
) return false;
2143 final int level
= getMipMapLevel(magnification
, maxDim(p
));
2144 if (isURL(dir_mipmaps
)) return true; // just assume that it does
2145 if (new File(dir_mipmaps
+ level
+ "/" + new File(getAbsolutePath(p
)).getName() + "." + p
.getId() + ".jpg").exists()) return true;
2149 final HashSet
<Patch
> cannot_regenerate
= new HashSet
<Patch
>();
2151 /** Loads the file containing the scaled image corresponding to the given level (or the maximum possible level, if too large) and returns it as an awt.Image, or null if not found. Will also regenerate the mipmaps, i.e. recreate the pre-scaled jpeg images if they are missing. Does not frees memory on its own. */
2152 protected Image
fetchMipMapAWT(final Patch patch
, final int level
) {
2153 if (null == dir_mipmaps
) {
2154 Utils
.log2("null dir_mipmaps");
2158 // TODO should wait if the file is currently being generated
2159 // (it's somewhat handled by a double-try to open the jpeg image)
2161 final int max_level
= getHighestMipMapLevel(patch
);
2163 //Utils.log2("level is: " + max_level);
2165 final String filepath
= getInternalFileName(patch
);
2166 if (null == filepath
) {
2167 Utils
.log2("null filepath!");
2170 final String path
= new StringBuffer(dir_mipmaps
).append( level
> max_level ? max_level
: level
).append('/').append(filepath
).append('.').append(patch
.getId()).append(".jpg").toString();
2173 if (patch
.hasAlphaChannel()) {
2174 img
= ImageSaver
.openJpegAlpha(path
);
2176 switch (patch
.getType()) {
2177 case ImagePlus
.GRAY16
:
2178 case ImagePlus
.GRAY8
:
2179 case ImagePlus
.GRAY32
:
2180 img
= ImageSaver
.openGreyJpeg(path
);
2183 IJ
.redirectErrorMessages();
2184 ImagePlus imp
= opener
.openImage(path
); // considers URL as well
2185 if (null != imp
) return patch
.createImage(imp
); // considers c_alphas
2186 //img = patch.adjustChannels(Toolkit.getDefaultToolkit().createImage(path)); // doesn't work
2187 //img = patch.adjustChannels(ImageSaver.openColorJpeg(path)); // doesn't work
2188 //Utils.log2("color jpeg path: "+ path);
2189 //Utils.log2("exists ? " + new File(path).exists());
2193 if (null != img
) return img
;
2196 // if we got so far ... try to regenerate the mipmaps
2197 if (!mipmaps_regen
) {
2201 // check that REALLY the file doesn't exist.
2202 if (cannot_regenerate
.contains(patch
)) {
2203 Utils
.log("Cannot regenerate mipmaps for patch " + patch
);
2207 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + ".jpg");
2209 // Regenerate in the case of not asking for an image under 32x32
2210 double scale
= 1 / Math
.pow(2, level
);
2211 if (level
>= 0 && patch
.getWidth() * scale
>= 32 && patch
.getHeight() * scale
>= 32 && isMipMapsEnabled()) {
2213 synchronized (gm_lock
) {
2216 if (hs_regenerating_mipmaps
.contains(patch
)) {
2217 // already being done
2222 Worker worker
= new Worker("Regenerating mipmaps") {
2224 this.setAsBackground(true);
2225 this.startedWorking();
2227 generateMipMaps(patch
);
2228 } catch (Exception e
) {
2232 Display
.repaint(patch
.getLayer(), patch
, 0);
2234 this.finishedWorking();
2237 Bureaucrat burro
= Bureaucrat
.create(worker
, patch
.getProject());
2238 burro
.goHaveBreakfast();
2244 } catch (Exception e
) {
2250 /** Compute the number of bytes that the ImagePlus of a Patch will take. Assumes a large header of 1024 bytes. If the image is saved as a grayscale jpeg the returned bytes will be 5 times as expected, because jpeg images are opened as int[] and then copied to a byte[] if all channels have the same values for all pixels. */ // The header is unnecessary because it's read, but not stored except for some of its variables; it works here as a safety buffer space.
2251 public long estimateImageFileSize(final Patch p
, final int level
) {
2253 // jpeg image to be loaded:
2254 final double scale
= 1 / Math
.pow(2, level
);
2255 return (long)(p
.getWidth() * scale
* p
.getHeight() * scale
* 5 + 1024);
2257 long size
= (long)(p
.getWidth() * p
.getHeight());
2258 int bytes_per_pixel
= 1;
2259 final int type
= p
.getType();
2261 case ImagePlus
.GRAY32
:
2262 bytes_per_pixel
= 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2264 case ImagePlus
.GRAY16
:
2265 bytes_per_pixel
= 3; // 2 for the ShortProcessor, and 1 for the pixels8
2266 case ImagePlus
.COLOR_RGB
:
2267 bytes_per_pixel
= 4;
2269 case ImagePlus
.GRAY8
:
2270 case ImagePlus
.COLOR_256
:
2271 bytes_per_pixel
= 1;
2272 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2273 String path
= ht_paths
.get(p
.getId());
2274 if (null != path
&& path
.endsWith(".jpg")) bytes_per_pixel
= 5; //4 for the int[] and 1 for the byte[]
2277 bytes_per_pixel
= 5; // conservative
2281 return size
* bytes_per_pixel
+ 1024;
2284 public String
makeProjectName() {
2285 if (null == project_file_path
|| 0 == project_file_path
.length()) return super.makeProjectName();
2286 final String name
= new File(project_file_path
).getName();
2287 final int i_dot
= name
.lastIndexOf('.');
2288 if (-1 == i_dot
) return name
;
2289 if (0 == i_dot
) return super.makeProjectName();
2290 return name
.substring(0, i_dot
);
2294 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2295 public String
handlePathlessImage(final ImagePlus imp
) {
2296 final FileInfo fi
= imp
.getOriginalFileInfo();
2297 if (null == fi
.fileName
|| fi
.fileName
.equals("")) {
2298 fi
.fileName
= "img_" + System
.currentTimeMillis() + ".tif";
2300 if (!fi
.fileName
.endsWith(".tif")) fi
.fileName
+= ".tif";
2301 fi
.directory
= dir_storage
;
2302 if (imp
.getNSlices() > 1) {
2303 new FileSaver(imp
).saveAsTiffStack(dir_storage
+ fi
.fileName
);
2305 new FileSaver(imp
).saveAsTiff(dir_storage
+ fi
.fileName
);
2307 Utils
.log2("Saved a copy into the storage folder:\n" + dir_storage
+ fi
.fileName
);
2308 return dir_storage
+ fi
.fileName
;
2311 /** Generates layer-wise mipmaps with constant tile width and height. The mipmaps include only images.
2312 * Mipmaps area generated all the way down until the entire canvas fits within one single tile.
2314 public Bureaucrat
generateLayerMipMaps(final Layer
[] la
, final int starting_level
) {
2315 // hard-coded dimensions for layer mipmaps.
2316 final int WIDTH
= 512;
2317 final int HEIGHT
= 512;
2319 // Each tile needs some coding system on where it belongs. For example in its file name, such as <layer_id>_Xi_Yi
2321 // Generate the starting level mipmaps, and then the others from it by gaussian or whatever is indicated in the project image_resizing_mode property.