diff --git a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffStorage.java b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffStorage.java index b70daf5..666dca5 100644 --- a/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffStorage.java +++ b/java/src/main/java/org/micromanager/ndtiffstorage/NDTiffStorage.java @@ -14,38 +14,49 @@ // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. // + package org.micromanager.ndtiffstorage; import java.awt.Point; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import jdk.nashorn.internal.codegen.CompilerConstants; +import javax.swing.JOptionPane; import mmcorej.TaggedImage; import mmcorej.org.json.JSONException; import mmcorej.org.json.JSONObject; -import javax.swing.*; /** - * This class manages multiple multipage Tiff datasets, averaging multiple 2x2 + * This class manages pyramidal multipage Tiff datasets, averaging multiple 2x2 * squares of pixels to create successively lower resolutions until the * downsample factor is greater or equal to the number of tiles in a given * direction. This condition ensures that pixels will always be divisible by the - * downsample factor without truncation + * downsample factor without truncation. * */ public class NDTiffStorage implements NDTiffAPI, MultiresNDTiffAPI { @@ -60,25 +71,31 @@ public class NDTiffStorage implements NDTiffAPI, MultiresNDTiffAPI { private static final String FULL_RES_SUFFIX = "Full resolution"; private static final String DOWNSAMPLE_SUFFIX = "Downsampled_x"; private ResolutionLevel fullResStorage_; - private TreeMap lowResStorages_; //map of resolution index to storage instance - private String directory_; + //map of resolution index to storage instance + private final TreeMap lowResStorages_; + private final String directory_; private final JSONObject summaryMD_; private JSONObject displaySettings_; - private int xOverlap_, yOverlap_; - private int fullResTileWidthIncludingOverlap_ = -1, fullResTileHeightIncludingOverlap_ = -1; - private int tileWidth_ = -1, tileHeight_ = -1; //Indpendent of zoom level because tile sizes stay the same--which means overlap is cut off + private int xOverlap_; + private int yOverlap_; + private int fullResTileWidthIncludingOverlap_ = -1; + private int fullResTileHeightIncludingOverlap_ = -1; + // Independent of zoom level because tile sizes stay the same--which means overlap is cut off + private int tileWidth_ = -1; + private int tileHeight_ = -1; private volatile boolean finished_; private String uniqueAcqName_; private ExecutorService writingExecutor_; private volatile int maxResolutionLevel_ = 0; - private boolean loaded_, tiled_; + private final boolean loaded_; + private boolean tiled_; //this is how to create a concurrent set - private Set> imageAxes_ = new ConcurrentHashMap, Boolean>().newKeySet(); - private final Integer externalMaxResLevel_; + private final Set> imageAxes_ = + new ConcurrentHashMap, Boolean>().newKeySet(); private String prefix_; Consumer debugLogger_ = null; private LinkedBlockingQueue> writingTaskQueue_; - private HashMap axisTypes_ = new HashMap(); + private final HashMap axisTypes_ = new HashMap<>(); private boolean firstImageAdded_ = false; private static final int BUFFER_DIRECT_THRESHOLD = 8192; @@ -91,18 +108,18 @@ public class NDTiffStorage implements NDTiffAPI, MultiresNDTiffAPI { /** * Constructor to load existing storage from disk dir --top level saving - * directory + * directory. */ public NDTiffStorage(String dir) throws IOException { - externalMaxResLevel_ = null; loaded_ = true; directory_ = dir; finished_ = true; pooledBuffers_ = null; + lowResStorages_ = new TreeMap<>(); dir += (dir.endsWith(File.separator) ? "" : File.separator); // Differentiate between NDTiff v2 and v3 here. - if (dir.endsWith(FULL_RES_SUFFIX + File.separator) ) { + if (dir.endsWith(FULL_RES_SUFFIX + File.separator)) { // The wrong one was selected dir = new File(dir).getParent() + File.separator; } @@ -117,7 +134,6 @@ public NDTiffStorage(String dir) throws IOException { fullResDir = dir + FULL_RES_SUFFIX; } - //create fullResStorage fullResStorage_ = new ResolutionLevel(fullResDir, false, null, this, null); summaryMD_ = fullResStorage_.getSummaryMetadata(); @@ -128,17 +144,20 @@ public NDTiffStorage(String dir) throws IOException { } try { - String path = dir + (dir.endsWith(File.separator) ? "" : File.separator) + "display_settings.txt"; + String path = dir + (dir.endsWith(File.separator) ? "" : File.separator) + + "display_settings.txt"; byte[] data = Files.readAllBytes(Paths.get(path)); displaySettings_ = new JSONObject(new String(data)); } catch (Exception e) { System.err.println("Couldn't read displaysettings"); } - imageAxes_.addAll(fullResStorage_.imageKeys().stream().map(s -> IndexEntryData.deserializeAxes(s)) + imageAxes_.addAll(fullResStorage_.imageKeys().stream() + .map(s -> IndexEntryData.deserializeAxes(s)) .collect(Collectors.toSet())); - //read width from the first image, to allow for datasets with different widths/heights per image + // Read width from the first image, to allow for datasets with + // different widths/heights per image // Even though this won't be allowed for tiled datasets fullResTileHeightIncludingOverlap_ = fullResStorage_.getFirstImageHeight(); fullResTileWidthIncludingOverlap_ = fullResStorage_.getFirstImageWidth(); @@ -148,7 +167,6 @@ public NDTiffStorage(String dir) throws IOException { tileWidth_ = fullResTileWidthIncludingOverlap_ - xOverlap_; tileHeight_ = fullResTileHeightIncludingOverlap_ - yOverlap_; - lowResStorages_ = new TreeMap(); //create low res storages int resIndex = 1; while (true) { @@ -169,18 +187,18 @@ public NDTiffStorage(String dir) throws IOException { } /** - * Constructor for new storage that doesn't parse summary metadata + * Constructor for new storage that doesn't parse summary metadata. */ public NDTiffStorage(String dir, String name, JSONObject summaryMetadata, int overlapX, int overlapY, boolean tiled, Integer externalMaxResLevel, int savingQueueSize, Consumer debugLogger, boolean createDir) { - externalMaxResLevel_ = externalMaxResLevel; tiled_ = tiled; xOverlap_ = overlapX; yOverlap_ = overlapY; prefix_ = name; debugLogger_ = debugLogger; + lowResStorages_ = new TreeMap<>(); if (BUFFER_POOL_SIZE > 0) { pooledBuffers_ = new ConcurrentHashMap>(); @@ -248,7 +266,6 @@ public IndexEntryData call() { } catch (IOException ex) { throw new RuntimeException("couldn't create Full res storage"); } - lowResStorages_ = new TreeMap(); return null; } }); @@ -279,10 +296,6 @@ public void setDisplaySettings(JSONObject displaySettings) { } } -// public static JSONObject readSummaryMetadata(String dir) throws IOException { -// String fullResDir = dir + (dir.endsWith(File.separator) ? "" : File.separator) + FULL_RES_SUFFIX; -// return TaggedImageStorageMultipageTiff.readSummaryMD(fullResDir); -// } public String getUniqueAcqName() { return uniqueAcqName_ + ""; //make new instance } @@ -321,7 +334,7 @@ public int[] getImageBounds() { } private int getNumRows() { - if (imageAxes_ == null || imageAxes_.size() == 0) { + if (imageAxes_.isEmpty()) { return 1; } int maxRow = imageAxes_.stream().mapToInt(value -> (Integer) value.get(ROW_AXIS)).max().getAsInt(); @@ -330,7 +343,7 @@ private int getNumRows() { } private int getNumCols() { - if (imageAxes_ == null || imageAxes_.size() == 0) { + if (imageAxes_.isEmpty()) { return 1; } int maxCol = imageAxes_.stream().mapToInt(value -> (Integer) value.get(COL_AXIS)).max().getAsInt(); @@ -339,8 +352,8 @@ private int getNumCols() { } /* - * It doesnt matter what resolution level the pixel is at since tiles - * are the same size at every level + * It does not matter what resolution level the pixel is at since tiles + * are the same size at every level. */ private long tileIndexFromPixelIndex(long i, boolean xDirection) { if (i >= 0) { @@ -369,28 +382,10 @@ private boolean hasImage(int dsIndex, HashMap axes) { return storage.hasImage(IndexEntryData.serializeAxes(axes)); } -// @Override -// public boolean hasTileByRowCol(HashMap axes, int resIndex, int row, int col) { - // Integer posIndex; -// if (resIndex == 0) { -// if (posManager_ == null) { -// posIndex = axes.containsKey(POSITION_AXIS) ? axes.get(POSITION_AXIS) : -// (tiled_ ? null : 0); -// } else { -// posIndex = posManager_.getPositionIndexFromTilePosition(resIndex, row, col); -// } -// } else { -// posIndex = posManager_.getPositionIndexFromTilePosition(resIndex, row, col); -// } -// if (posIndex == null) { -// return false; -// } -// return hasImage(resIndex, axes); -// } /** - * Return a subimage of the larger stitched image at the appropriate zoom - * level, loading only the tiles neccesary to form the subimage + * Returns a subimage of the larger stitched image at the appropriate zoom + * level, loading only the tiles needed to form the subimage. * * @param axes * @param dsIndex 0 for full res, 1 for 2x downsample, 2 for 4x downsample, @@ -513,49 +508,27 @@ public TaggedImage getDisplayImage(HashMap axes, } xOffset += lineWidths.get((int) (col - colStart)); } - return new TaggedImage(pixels, topLeftMD); } -// /** -// * Called before any images have been added to initialize the resolution to -// * the specifiec zoom level -// * -// * @param resIndex -// */ -// public void initializeToLevel(int resIndex) { -// //create a null pointer in lower res storages to signal addToLoResStorage function -// //to continue downsampling to this level -// maxResolutionLevel_ = resIndex; -// //Make sure position nodes for lower resolutions are created if they weren't automatically -// posManager_.updateLowerResolutionNodes(resIndex); -// } /** - * create an additional lower resolution levels for zooming purposes - * Create the resolutions, but dont do anything + * Down-samples previousLevelPix into currentLevelPix, averaging 2x2 squares + * of pixels to create the down-sampled image. + * + * @param currentLevelPix pixels at current resolution level + * @param previousLevelPix pixels at previous resolution level + * @param previousLevelRow row of previous level imagel + * @param previousLevelCol column of previous level image + * @param resolutionIndex Only used to see if we are at the highest resolution. + * @param rgb Where this is an rgb image */ - private void addResolutionsUpTo(int index) - throws InterruptedException, ExecutionException { - if (index <= maxResolutionLevel_) { - return; - } -// // take the max -// int oldLevel = lowResStorages_.keySet().stream().max(Integer::compare).orElse(0); -// -// for (int i = oldLevel + 1; i <= maxResolutionLevel_; i++) { -// populateNewResolutionLevel(i); -// } - } - - private void downsample(Object currentLevelPix, Object previousLevelPix, int fullResRow, - int fullResCol, int resolutionIndex, + private void downsample(Object currentLevelPix, Object previousLevelPix, int previousLevelRow, + int previousLevelCol, int resolutionIndex, boolean rgb) { - int byteDepth = currentLevelPix instanceof byte[] ? 1: 2; + int byteDepth = currentLevelPix instanceof byte[] ? 1 : 2; //Determine which position in 2x2 this tile sits in - int resAboveCol = (int) Math.floor(fullResCol / Math.pow(2, resolutionIndex - 1)); - int resAboveRow = (int) Math.floor(fullResRow / Math.pow(2, resolutionIndex - 1)); - int xPos = (int) Math.abs(resAboveCol % 2); - int yPos = (int) Math.abs(resAboveRow % 2); + int xPos = Math.abs(previousLevelCol % 2); + int yPos = Math.abs(previousLevelRow % 2); //Add one if top or left so border pixels from an odd length image gets added in for (int x = 0; x < tileWidth_; x += 2) { //iterate over previous res level pixels for (int y = 0; y < tileHeight_; y += 2) { @@ -579,26 +552,31 @@ private void downsample(Object currentLevelPix, Object previousLevelPix, int ful previousLevelHeight = tileHeight_; } - int rgbMultiplier_ = rgb ? 4 : 1; + int rgbMultiplier = rgb ? 4 : 1; for (int compIndex = 0; compIndex < (rgb ? 4 : 1); compIndex++) { - int count = 1; //count is number of pixels (out of 4) used to create a pixel at this level + //count is number of pixels (out of 4) used to create a pixel at this level + int count = 1; //always take top left pixel, maybe take others depending on whether at image edge int sum = 0; if (rgb) { - sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX) * rgbMultiplier_ + compIndex] & 0xff; + sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX) + * rgbMultiplier + compIndex] & 0xff; } else if (byteDepth == 1) { sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX)] & 0xff; } else { - sum += ((short[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX)] & 0xffff; + sum += ((short[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX)] + & 0xffff; } - //pixel index can be different from index in tile at resolution level 0 if there is nonzero overlap - if (x < previousLevelWidth - 1 && y < previousLevelHeight - 1) { //if not bottom right corner, add three more pix + // pixel index can be different from index in tile at resolution level 0 + // if there is nonzero overlap. + if (x < previousLevelWidth - 1 && y < previousLevelHeight - 1) { + //if not bottom right corner, add three more pix count += 3; if (rgb) { - sum += (((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX + 1) * rgbMultiplier_ + compIndex] & 0xff) - + (((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) * rgbMultiplier_ + compIndex] & 0xff) - + (((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) * rgbMultiplier_ + compIndex] & 0xff); + sum += (((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX + 1) * rgbMultiplier + compIndex] & 0xff) + + (((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) * rgbMultiplier + compIndex] & 0xff) + + (((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) * rgbMultiplier + compIndex] & 0xff); } else if (byteDepth == 1) { sum += (((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX + 1) ] & 0xff) + (((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) ] & 0xff) @@ -611,8 +589,8 @@ private void downsample(Object currentLevelPix, Object previousLevelPix, int ful } else if (x < previousLevelWidth - 1) { //if not right edge, add one more pix count++; if (rgb) { - sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) * rgbMultiplier_ + compIndex] & 0xff; - } else if (byteDepth == 1 ) { + sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) * rgbMultiplier + compIndex] & 0xff; + } else if (byteDepth == 1) { sum += ((byte[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) ] & 0xff; } else { sum += ((short[]) previousLevelPix)[(pixelY * previousLevelWidth + pixelX + 1) ] & 0xffff; @@ -620,8 +598,8 @@ private void downsample(Object currentLevelPix, Object previousLevelPix, int ful } else if (y < previousLevelHeight - 1) { // if not bottom edge, add one more pix count++; if (rgb) { - sum += ((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) * rgbMultiplier_ + compIndex] & 0xff; - } else if (byteDepth == 1 ) { + sum += ((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) * rgbMultiplier + compIndex] & 0xff; + } else if (byteDepth == 1) { sum += ((byte[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) ] & 0xff; } else { sum += ((short[]) previousLevelPix)[((pixelY + 1) * previousLevelWidth + pixelX) ] & 0xffff; @@ -633,11 +611,14 @@ private void downsample(Object currentLevelPix, Object previousLevelPix, int ful //if full res tile has an odd number of pix, the last one gets chopped off //to make it fit into tile containers try { - int index = (((y + yPos * tileHeight_) / 2) * tileWidth_ + (x + xPos * tileWidth_) / 2) - * (rgb ? rgbMultiplier_ : 1) + compIndex; + int index = (((y + yPos * tileHeight_) / 2) * tileWidth_ + + (x + xPos * tileWidth_) / 2) * (rgb ? rgbMultiplier : 1) + + compIndex; if (byteDepth == 1 || rgb) { + // this looks suspicious.... ((byte[]) currentLevelPix)[index] = (byte) Math.round(sum / count); } else { + // this looks suspicious.... ((short[]) currentLevelPix)[index] = (short) Math.round(sum / count); } } catch (Exception e) { @@ -652,32 +633,36 @@ private void downsample(Object currentLevelPix, Object previousLevelPix, int ful private void populateNewResolutionLevel(int resolutionIndex, boolean addToLowResStorage) { // could be accessed from UI or from data arriving - synchronized (lowResStorages_) { - if (lowResStorages_.containsKey(resolutionIndex)) { - return; // already happened for another reason - } + if (!lowResStorages_.containsKey(resolutionIndex)) { createDownsampledStorage(resolutionIndex); + } - if (addToLowResStorage) { - //add all tiles from existing resolution levels to this new one - ResolutionLevel previousLevelStorage - = resolutionIndex == 1 ? fullResStorage_ : lowResStorages_.get(resolutionIndex - 1); - Set imageKeys = previousLevelStorage.imageKeys(); - for (String key : imageKeys) { - HashMap axes = IndexEntryData.deserializeAxes(key); - int fullResCol = (Integer) axes.get(COL_AXIS); - int fullResRow = (Integer) axes.get(ROW_AXIS); - - TaggedImage ti = previousLevelStorage.getImage(key); - int bitDepth = previousLevelStorage.getEssentialImageMetadata(key).bitDepth; - boolean rgb = previousLevelStorage.getEssentialImageMetadata(key).rgb; - addToLowResStorage(ti, axes, - resolutionIndex - 1, fullResRow, fullResCol, rgb, bitDepth); - } + if (addToLowResStorage) { + //add all tiles from existing resolution levels to this new one + ResolutionLevel previousLevelStorage + = resolutionIndex == 1 ? fullResStorage_ : lowResStorages_.get(resolutionIndex - 1); + Set imageKeys = previousLevelStorage.imageKeys(); + for (String key : imageKeys) { + HashMap axes = IndexEntryData.deserializeAxes(key); + int previousResCol = (Integer) axes.get(COL_AXIS); + int previousResRow = (Integer) axes.get(ROW_AXIS); + + TaggedImage ti = previousLevelStorage.getImage(key); + int bitDepth = previousLevelStorage.getEssentialImageMetadata(key).bitDepth; + boolean rgb = previousLevelStorage.getEssentialImageMetadata(key).rgb; + addToLowResStorage(ti, axes, + resolutionIndex - 1, previousResRow, previousResCol, rgb, bitDepth); } } } + + /** + * Increases the reosolutions in the pyramid. Most often called by the UI if it + * needs to zoom out further. + * + * @param newMaxResolutionLevel Next new resolution level. + */ @Override public void increaseMaxResolutionLevel(int newMaxResolutionLevel) { int oldMaxResolutionLevel = maxResolutionLevel_; @@ -689,85 +674,98 @@ public void increaseMaxResolutionLevel(int newMaxResolutionLevel) { for (int i = oldMaxResolutionLevel + 1; i <= maxResolutionLevel_; i++) { //add any new one this requires - populateNewResolutionLevel(i, true); + final int level = i; + blockingWritingTaskHandoff(() -> { + populateNewResolutionLevel(level, true); + return null; + }); } } /** - * Add to all low res storages - * return a future for when the current res level is done writing + * Adds the specified image to all lower resolution storages. + * The number of lower resolution storages is given by the variable maxResolutionLevel_. + * If the lower resolution image already exists, the given image will be added as a + * tile at the correct location. + * + * @param img The image to add + * @param axes The axes of the image + * @param originalResIndex The resolution index of the image + * @param originalLevelRow The row of the image at the input resolution + * @param originalLevelCol The column of the image at the input resolution + * @param rgb Whether the image is rgb + * @param bitDepth The bit depth of the image */ private void addToLowResStorage(TaggedImage img, HashMap axes, - int previousResIndex, int fullResRow, int fullResCol, + int originalResIndex, int originalLevelRow, int originalLevelCol, boolean rgb, int bitDepth) { - //Read indices - Object previousLevelPix = img.pix; - int resolutionIndex = previousResIndex + 1; + //Read indices + Object previousLevelPix = img.pix; + int resolutionIndex = originalResIndex + 1; + + int row = originalLevelRow; + int col = originalLevelCol; while (resolutionIndex <= maxResolutionLevel_) { - //Create this storage level if needed and add all existing tiles form the previous one - if (!lowResStorages_.containsKey(resolutionIndex)) { - //re add all tiles from previous res level - populateNewResolutionLevel(resolutionIndex, false); - } + //Create this storage level if needed and add all existing tiles form the previous one + if (!lowResStorages_.containsKey(resolutionIndex)) { + //re-add all tiles from previous res level + populateNewResolutionLevel(resolutionIndex, false); + } - //copy and change and row and col to reflect lower resolution - HashMap axesCopy = IndexEntryData.deserializeAxes(IndexEntryData.serializeAxes(axes)); - axesCopy.put(COL_AXIS, (int) Math.floor(fullResCol / Math.pow(2, resolutionIndex))); - axesCopy.put(ROW_AXIS, (int) Math.floor(fullResRow / Math.pow(2, resolutionIndex))); + //copy and change row and col to reflect lower resolution + HashMap axesCopy = IndexEntryData.deserializeAxes(IndexEntryData.serializeAxes(axes)); + axesCopy.put(COL_AXIS, Math.floorDiv(col, 2)); + axesCopy.put(ROW_AXIS, Math.floorDiv(row, 2)); - //Create pixels or get appropriate pixels to add to - TaggedImage existingImage = lowResStorages_.get(resolutionIndex).getImage(IndexEntryData.serializeAxes(axesCopy)); + if (lowResStorages_.get(resolutionIndex) == null) { + System.out.println("null storage at res index: " + resolutionIndex); + } - Object currentLevelPix; - if (existingImage == null) { - if (rgb) { - currentLevelPix = new byte[tileWidth_ * tileHeight_ * 4]; - } else if (img.pix instanceof byte[]) { - currentLevelPix = new byte[tileWidth_ * tileHeight_]; - } else { - currentLevelPix = new short[tileWidth_ * tileHeight_]; - } + //Create pixels or get appropriate pixels to add to + TaggedImage existingImage = lowResStorages_.get(resolutionIndex).getImage(IndexEntryData.serializeAxes(axesCopy)); + + Object currentLevelPix; + if (existingImage == null) { + if (rgb) { + currentLevelPix = new byte[tileWidth_ * tileHeight_ * 4]; + } else if (img.pix instanceof byte[]) { + currentLevelPix = new byte[tileWidth_ * tileHeight_]; } else { - currentLevelPix = existingImage.pix; + currentLevelPix = new short[tileWidth_ * tileHeight_]; } + } else { + currentLevelPix = existingImage.pix; + } - downsample(currentLevelPix, previousLevelPix, fullResRow, fullResCol, resolutionIndex, rgb); - - //store this tile in the storage class correspondign to this resolution - try { - - String indexKey = IndexEntryData.serializeAxes(axesCopy); - if (existingImage == null) { //Image doesn't yet exist at this level, so add it - - //create a copy of tags so tags from a different res level arent inadverntanly modified - // while waiting for being written to disk - JSONObject tags = new JSONObject(img.tags.toString()); - //modify tags to reflect image size, and correct position index -// MultiresMetadata.setWidth(tags, tileWidth_); -// MultiresMetadata.setHeight(tags, tileHeight_); - - IndexEntryData ied = lowResStorages_.get(resolutionIndex).putImage( - indexKey, currentLevelPix, tags.toString().getBytes(), - rgb, tileHeight_, tileWidth_, bitDepth); -// for (ImageWrittenListener l : imageWrittenListeners_) { -// l.imageWritten(ied); -// } - - } else { - //Image already exists, only overwrite pixels to include new tiles - lowResStorages_.get(resolutionIndex).overwritePixels(indexKey, currentLevelPix, rgb); - } + downsample(currentLevelPix, previousLevelPix, row, col, resolutionIndex, rgb); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Couldnt modify tags for lower resolution level"); + // store this tile in the storage class corresponding to this resolution + try { + String indexKey = IndexEntryData.serializeAxes(axesCopy); + if (existingImage == null) { //Image doesn't yet exist at this level, so add it + // create a copy of tags so tags from a different res level are not inadvertently modified + // while waiting for being written to disk + JSONObject tags = new JSONObject(img.tags.toString()); + // modify tags to reflect image size, and correct position index + lowResStorages_.get(resolutionIndex).putImage( + indexKey, currentLevelPix, tags.toString().getBytes(), + rgb, tileHeight_, tileWidth_, bitDepth); + } else { + lowResStorages_.get(resolutionIndex).overwritePixels(indexKey, currentLevelPix, rgb); } - //go on to next level of downsampling - previousLevelPix = currentLevelPix; - resolutionIndex++; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Could not modify tags for lower resolution level"); } + + // go on to next level of downsampling + previousLevelPix = currentLevelPix; + resolutionIndex++; + row = Math.floorDiv(row, 2); + col = Math.floorDiv(col, 2); + } } private void createDownsampledStorage(int resIndex) { @@ -788,7 +786,7 @@ private void createDownsampledStorage(int resIndex) { } /** - * Throw an exception if there was an error writing to disk + * Throws an exception if there was an error writing to disk. */ public void checkForWritingException() throws Exception { if (writingException_ != null) { @@ -797,17 +795,18 @@ public void checkForWritingException() throws Exception { } /** - * This version works for regular, non-multiresolution data + * This version works for regular, non-multiresolution data. * */ - public Future putImage(Object pixels, JSONObject metadata, HashMap axessss, - boolean rgb, int bitDepth, int imageHeight, int imageWidth) { + public Future putImage(Object pixels, JSONObject metadata, + HashMap axessss, boolean rgb, + int bitDepth, int imageHeight, int imageWidth) { try { checkForWritingException(); } catch (Exception e) { throw new RuntimeException(e); } - TaggedImage ti = new TaggedImage(pixels, metadata); + final TaggedImage ti = new TaggedImage(pixels, metadata); // Make sure each axis takes all integer or all string values for (String axisName : axessss.keySet()) { @@ -830,8 +829,8 @@ public Future putImage(Object pixels, JSONObject metadata, HashM if (debugLogger_ != null) { debugLogger_.accept( - "Adding image " + getAxesString(axessss) + - "\nwriting_queue_size= " + writingTaskQueue_.size()); + "Adding image " + getAxesString(axessss) + + "\nwriting_queue_size= " + writingTaskQueue_.size()); } // serialize metadata before adding to writing thread to speed performance byte[] metadataBytes = metadata.toString().getBytes(StandardCharsets.UTF_8); @@ -839,47 +838,47 @@ public Future putImage(Object pixels, JSONObject metadata, HashM fullResStorage_.addToWritePendingImages(indexKey, ti, new EssentialImageMetadata(imageWidth, imageHeight, bitDepth, rgb)); - //Submit writing task on a dedicated thread - return blockingWritingTaskHandoff(new Callable() { - @Override - public IndexEntryData call() { - try { - //Make a local copy - HashMap axes = new HashMap(axessss); - imageAxes_.add(axes); + //Submit writing task on a dedicated thread + return blockingWritingTaskHandoff(new Callable() { + @Override + public IndexEntryData call() { + try { + //Make a local copy + HashMap axes = new HashMap<>(axessss); + imageAxes_.add(axes); - if (debugLogger_ != null) { - debugLogger_.accept("putting image in storage"); + if (debugLogger_ != null) { + debugLogger_.accept("putting image in storage"); + } + //write to full res storage as normal (i.e. with overlap pixels present) + IndexEntryData ied = fullResStorage_.putImage(indexKey, pixels, metadataBytes, + rgb, imageHeight, imageWidth, bitDepth); + return ied; + } catch (Exception ex) { + writingException_ = ex; + return null; } - //write to full res storage as normal (i.e. with overlap pixels present) - IndexEntryData ied = fullResStorage_.putImage(indexKey, pixels, metadataBytes, - rgb, imageHeight, imageWidth, bitDepth); - return ied; - } catch (Exception ex) { - writingException_ = ex; - return null; } - } - }); + }); } static String getAxesString(HashMap axes) { - String s = ""; + StringBuilder s = new StringBuilder(); for (String key : axes.keySet()) { - s += key + " " + axes.get(key) + ", "; + s.append(key).append(" ").append(axes.get(key)).append(", "); } - return s; + return s.toString(); } /** * This version is called by programs doing dynamic stitching (i.e. - * micro-magellan). axes must contain "position" mapping to a desired position index + * micro-magellan). Axes must contain "position" mapping to a desired position index * * @return */ @Override - public Future putImageMultiRes( Object pixels, JSONObject metadata, final HashMap axes, - boolean rgb, int bitDepth, int imageHeight, int imageWidth) { + public Future putImageMultiRes(Object pixels, JSONObject metadata, final HashMap axes, boolean rgb, int bitDepth, int imageHeight, int imageWidth) { try { checkForWritingException(); } catch (Exception e) { @@ -887,8 +886,8 @@ public Future putImageMultiRes( Object pixels, JSONObject metadata, final HashMa } TaggedImage ti = new TaggedImage(pixels, metadata); if (!firstImageAdded_) { - //technically this doesnt need to be parsed here, because it should be fixed for the whole - //dataset, NOT interpretted at runtime, but whatever + // Technically this doesn't need to be parsed here, because it should be fixed + // for the whole dataset, NOT interpretted at runtime, but whatever firstImageAdded_ = true; fullResTileWidthIncludingOverlap_ = imageWidth; fullResTileHeightIncludingOverlap_ = imageHeight; @@ -900,40 +899,32 @@ public Future putImageMultiRes( Object pixels, JSONObject metadata, final HashMa String indexKey = IndexEntryData.serializeAxes(StorageMD.getAxes(ti.tags)); fullResStorage_.addToWritePendingImages(indexKey, ti, new EssentialImageMetadata(imageWidth, imageHeight, bitDepth, rgb)); - return blockingWritingTaskHandoff(new Callable() { - @Override - public IndexEntryData call() { - IndexEntryData ied; - try { - if (tiled_) { - if (!axes.containsKey(ROW_AXIS) || !axes.containsKey(COL_AXIS)) { - throw new RuntimeException("axes must contain row and column infor"); - } + return blockingWritingTaskHandoff(() -> { + IndexEntryData ied; + try { + if (tiled_) { + if (!axes.containsKey(ROW_AXIS) || !axes.containsKey(COL_AXIS)) { + throw new RuntimeException("axes must contain row and column infor"); } - imageAxes_.add(axes); + } + imageAxes_.add(axes); - //write to full res storage as normal (i.e. with overlap pixels present) - ied = fullResStorage_.putImage( - indexKey, pixels, metadataBytes, rgb, imageHeight, imageWidth, bitDepth); - - if (tiled_) { - //check if maximum resolution level needs to be updated based on full size of image - long fullResPixelWidth = getNumCols() * tileWidth_; - long fullResPixelHeight = getNumRows() * tileHeight_; - int maxResIndex = externalMaxResLevel_ != null ? externalMaxResLevel_ : - (int) Math.ceil(Math.log((Math.max(fullResPixelWidth, fullResPixelHeight) - / 4)) / Math.log(2)); - int row = (Integer) axes.get(ROW_AXIS); - int col = (Integer) axes.get(COL_AXIS); - addResolutionsUpTo(maxResIndex); - addToLowResStorage(ti, axes, 0, row, col, rgb, bitDepth); - } + //write to full res storage as normal (i.e. with overlap pixels present) + ied = fullResStorage_.putImage( + indexKey, pixels, metadataBytes, rgb, imageHeight, imageWidth, bitDepth); - } catch (IOException | ExecutionException | InterruptedException ex) { - throw new RuntimeException(ex.toString()); + if (tiled_) { + //check if maximum resolution level needs to be updated based on full size of image + int row = (Integer) axes.get(ROW_AXIS); + int col = (Integer) axes.get(COL_AXIS); + // addResolutionsUpTo(maxResIndex); + addToLowResStorage(ti, axes, 0, row, col, rgb, bitDepth); } - return ied; + + } catch (IOException ex) { + throw new RuntimeException(ex.toString()); } + return ied; }); } @@ -997,8 +988,8 @@ public boolean hasImage(HashMap axes) { @Override public TaggedImage getImage(HashMap axes, int dsIndex) { //return a single tile from the full res image - if (fullResStorage_ == null || (tiled_ && lowResStorages_ == null) || - (tiled_ && !lowResStorages_.containsKey(dsIndex) && dsIndex != 0) ){ + if (fullResStorage_ == null || (tiled_ && lowResStorages_ == null) + || (tiled_ && !lowResStorages_.containsKey(dsIndex) && dsIndex != 0)) { return null; } if (dsIndex == 0) { @@ -1029,7 +1020,7 @@ public IndexEntryData call() { } /** - * Signal to finish writing and block until everything pending is done + * Signal to finish writing and block until everything pending is done. */ public void finishedWriting() { if (loaded_) { @@ -1121,12 +1112,12 @@ public void closeAndWait() throws InterruptedException { public void close() { //run close on a new thread - new Thread(new Runnable() { - @Override - public void run() { - doClose(); - } - }).start(); + new Thread(new Runnable() { + @Override + public void run() { + doClose(); + } + }).start(); } private void doClose() { @@ -1135,7 +1126,9 @@ private void doClose() { if (!loaded_) { while (true) { try { - if (writingExecutor_.awaitTermination(10, TimeUnit.MILLISECONDS)) break; + if (writingExecutor_.awaitTermination(10, TimeUnit.MILLISECONDS)) { + break; + } } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -1172,6 +1165,9 @@ private String getUniqueAcqDirName(String root, String prefix) throws Exception private static int getCurrentMaxDirIndex(File rootDir, String prefix) throws NumberFormatException { int maxNumber = 0; + if (rootDir == null) { + return maxNumber; + } int number; String theName; for (File acqDir : rootDir.listFiles()) { @@ -1200,20 +1196,17 @@ private static int getCurrentMaxDirIndex(File rootDir, String prefix) throws Num * added at this slice index */ public Set getTileIndicesWithDataAt(String name, int sliceIndex) { - Set exploredTiles = new TreeSet(new Comparator() { - @Override - public int compare(Point o1, Point o2) { - if (o1.x != o2.x) { - return o1.x - o2.x; - } else if (o1.y != o2.y) { - return o1.y - o2.y; - } - return 0; + Set exploredTiles = new TreeSet<>((o1, o2) -> { + if (o1.x != o2.x) { + return o1.x - o2.x; + } else if (o1.y != o2.y) { + return o1.y - o2.y; } + return 0; }); for (HashMap s : imageAxes_) { if ((Integer) s.get(name) == sliceIndex) { - exploredTiles.add(new Point( (Integer) s.get(COL_AXIS), (Integer) s.get(ROW_AXIS))); + exploredTiles.add(new Point((Integer) s.get(COL_AXIS), (Integer) s.get(ROW_AXIS))); } } @@ -1226,14 +1219,14 @@ public Set getTileIndicesWithDataAt(int zIndex) { } private long getMinRow() { - if (imageAxes_ == null || imageAxes_.size() == 0) { + if (imageAxes_.isEmpty()) { return 0; } return imageAxes_.stream().mapToInt(value -> (Integer) value.get(ROW_AXIS)).min().getAsInt(); } private long getMinCol() { - if (imageAxes_ == null || imageAxes_.size() == 0) { + if (imageAxes_.isEmpty()) { return 0; } return imageAxes_.stream().mapToInt(value -> (Integer) value.get(COL_AXIS)).min().getAsInt(); @@ -1259,12 +1252,7 @@ public Set> getAxesSet() { private static Buffer allocateByteBuffer(int capacity) { -// Buffer b = capacity >= BUFFER_DIRECT_THRESHOLD ? -// ByteBuffer.allocateDirect(capacity) : -// ByteBuffer.allocate(capacity); - Buffer b = ByteBuffer.allocateDirect(capacity); - b = ((ByteBuffer) b).order(BYTE_ORDER); - return b; + return ByteBuffer.allocateDirect(capacity).order(BYTE_ORDER); } Buffer getSmallBuffer(int capacity) { @@ -1276,8 +1264,7 @@ Buffer getLargeBuffer(int capacity) { return allocateByteBuffer(capacity); } if (BUFFER_POOL_SIZE == 0) { - Buffer b = allocateByteBuffer(capacity); - return b; + return allocateByteBuffer(capacity); } if (!pooledBuffers_.containsKey(capacity)) { @@ -1285,11 +1272,11 @@ Buffer getLargeBuffer(int capacity) { } // Recycle in LIFO order (smaller images may still be in L3 cache) - Buffer b = pooledBuffers_.get(capacity).pollFirst(); + ByteBuffer b = pooledBuffers_.get(capacity).pollFirst(); if (b != null) { // Ensure correct byte order in case recycled from other source - ((ByteBuffer)b).order(BYTE_ORDER); + b.order(BYTE_ORDER); //You can't chain the previous and following calls together or you get a weird java error b.clear(); return b; diff --git a/java/src/main/java/org/micromanager/ndtiffstorage/ResolutionLevel.java b/java/src/main/java/org/micromanager/ndtiffstorage/ResolutionLevel.java index e61a757..9fcdaf9 100644 --- a/java/src/main/java/org/micromanager/ndtiffstorage/ResolutionLevel.java +++ b/java/src/main/java/org/micromanager/ndtiffstorage/ResolutionLevel.java @@ -21,8 +21,6 @@ // package org.micromanager.ndtiffstorage; -import java.awt.*; -import java.awt.geom.Rectangle2D; import java.io.*; import java.util.*; import java.util.concurrent.*;