Skip to content

Commit

Permalink
Adapts to PALM dataset:
Browse files Browse the repository at this point in the history
- make sure the data opens (downscaling can go below 1: superresolution)
- Some subblock may be absent for metadata reading
- Correctly counts the number of channels, slices, and timepoints (they should always start at 0)
Adapt to Young Mouse image:
- Some decompressed subblock do not have the expected size. The decompressed size is used to estimate what's missing and read the data that is present
Adapts to multi-file image
- be robust to absence of sub-directory segment
Other:
- There may be some information in slide previews that we do not want to keep. Hence a DummyMetadata is used when reading slide preview and macro images
  • Loading branch information
NicoKiaru committed Jan 30, 2024
1 parent 1487216 commit 557745c
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 40 deletions.
114 changes: 76 additions & 38 deletions src/main/java/ch/epfl/biop/formats/in/ZeissQuickStartCZIReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
import loci.common.Region;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.common.xml.XMLTools;
import loci.formats.CoreMetadata;
import loci.formats.FormatException;
Expand All @@ -74,9 +73,8 @@
import loci.formats.in.JPEGReader;
import loci.formats.in.MetadataOptions;
import ch.epfl.biop.formats.in.libczi.LibCZI;
import loci.formats.meta.DummyMetadata;
import loci.formats.meta.MetadataStore;
import loci.formats.ome.OMEXMLMetadata;
import loci.formats.services.OMEXMLService;
import ome.units.UNITS;
import ome.units.quantity.Length;
import ome.units.quantity.Power;
Expand Down Expand Up @@ -667,7 +665,7 @@ private byte[] readRawPixelData(MinDimEntry block,
options.interleaved = isInterleaved();
options.littleEndian = isLittleEndian();
options.bitsPerSample = bytesPerPixel * 8;
options.maxBytes = getSizeX() * getSizeY() * getRGBChannelCount() * bytesPerPixel;
options.maxBytes = block.storedSizeX * block.storedSizeY * getRGBChannelCount() * bytesPerPixel; // The maximal size is the one of the subblock

This comment has been minimized.

Copy link
@swg08

swg08 Feb 3, 2024

@NicoKiaru I adapted the decoder a bit and now the "options" hand back the corrected width and height.
We could consider introducing new class that hands back the byte array and the sizes though.
Similar to this:

public class ImageData {
public byte[] data;
public int width;
public int height;

public ImageData(byte[] data, int width, int height) {  
    this.data = data;  
    this.width = width;  
    this.height = height;  
}  

}

(Just naming the decompress method decompress2 for this test) -> in JPEGXRServiceImpl
public ImageData decompress2(byte[] compressed) throws FormatException {
LOGGER.trace("begin tile decode; compressed size = {}", compressed.length);
try {
Decode decoder = new Decode(compressed);
byte[] raw = decoder.decodeFirstFrame(compressed, 0, compressed.length);
short[] format = getPixelFormat(compressed);

    if (isBGR(format)) {
      int bpp = getBGRComponents(format);
      // only happens with 8 bits per channel,
      // 3 (BGR) or 4 (BGRA) channel data
      for (int p=0; p<raw.length; p+=bpp) {
        byte tmp = raw[p];
        raw[p] = raw[p + 2];
        raw[p + 2] = tmp;
      }
    }
    ImageData decompressedData = new ImageData(raw, (int)decoder.getWidth(), (int)decoder.getHeight());
    return decompressedData;
  }
  // really only want to catch ome.jxrlib.FormatError, but that doesn't compile
  catch (Exception e) {
    throw new FormatException(e);
  }

}

And changing decompress in JPEGXRCodec

public byte[] decompress(byte[] buf, CodecOptions options)
throws FormatException
{
initialize();

int bpp = options.bitsPerSample / 8;
int pixels = options.width * options.height;

//byte[] uncompressed = service.decompress(buf);
ImageData uncompressed = service.decompress2(buf);
int channels = uncompressed.data.length / (pixels * bpp);

if (options.height != uncompressed.height || options.width != uncompressed.width)
{
	System.out.print("Yes!");
}

options.height = uncompressed.height;
options.width = uncompressed.width;

if (channels == 1 || options.interleaved) {
  return uncompressed.data;
}

byte[] deinterleaved = new byte[uncompressed.data.length];

for (int p=0; p<pixels; p++) {
  for (int c=0; c<channels; c++) {
    for (int b=0; b<bpp; b++) {
      int bb = options.littleEndian ? b : bpp - b - 1;
      int src = bpp * (p * channels + c) + b;
      int dest = bpp * (c * pixels + p) + bb;
      deinterleaved[dest] = uncompressed.data[src];
    }
  }
}

return deinterleaved;

}


Again we already could use this "ImageData" (this needs a better name...) here and not just alter the CompressionOptions.

I'd see this as a valid way to make it absolutely clear what the byte array represents. -> bytes + width + height (+datatype).
Then adjusting the information in SubBlock accordingly.

This comment has been minimized.

Copy link
@NicoKiaru

NicoKiaru Feb 4, 2024

Author Member

Nice! This makes a lot of sense.

It requires changes on the JPEGXRCodec though and given that there's uncertainty whether this reader can makes its way as an official replacement, I'm not certain the OME team will want to adopt these changes. As a workaround, I think I understood the IFD story and I'm quite confident I can get the width and height without any change to the codec.

Just to be clear: your change makes more sense, but as a workaround, finding a solution that does not touch other classes has a higher chance to makes its way to bio-formats (+ it could be used right now with the Quick Start Reader Fiji update site).

switch (compression) {
case JPEG:
Expand Down Expand Up @@ -983,7 +981,41 @@ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws F
compression==UNCOMPRESSED? DataTools.allocate(tileInBlock.width, tileInBlock.height, nCh, bpp): null,
bpp, bytesPerPixel);

// We need to basically crop a rectangle with a rectangle, of potentially different sizes
int nLinesMissing = 0;
int nColumnsMissing = 0;

if (compression!=UNCOMPRESSED) {
// Sometimes (see post https://forum.image.sc/t/would-anyone-have-a-palm-czi-example-file/85900/12),
// decompressed subblock does not return the proper number of pixels
// try this code on the Young-Mouse czi image from - resolution level 6 https://zenodo.org/records/10577621:
/* ZeissQuickStartCZIReader r = new ZeissQuickStartCZIReader();
r.setId("image path to \\Young_mouse.czi");
r.setSeries(5);
r.openPlane(0,0,0,5947,2168); */
int expectedRawDataSize = block.storedSizeX*block.storedSizeY*bpp*nCh;
if (rawData.length!=expectedRawDataSize) {
// Bad scenario: columns or rows are missing
// let's try to decipher what is missing - a line or a column
boolean probableColumnMissing = rawData.length % (block.storedSizeY*bpp*nCh) == 0;
boolean probableLineMissing = rawData.length % (block.storedSizeX*bpp*nCh) == 0;
if (probableLineMissing&&probableColumnMissing) {
logger.error("SubBlock at position "+block.filePosition+" returned an unexpected number of pixels.");
continue;
}
if (probableLineMissing) {
logger.warn("SubBlock at position "+block.filePosition+" returned an unexpected number of pixels.");
nLinesMissing = block.storedSizeY-(rawData.length / (block.storedSizeX*bpp*nCh));
} else if (probableColumnMissing) {
logger.warn("SubBlock at position "+block.filePosition+" returned an unexpected number of pixels.");
nColumnsMissing = block.storedSizeX-(rawData.length / (block.storedSizeY*bpp*nCh));
} else {
logger.error("SubBlock at position "+block.filePosition+" returned an unexpected number of pixels.");
continue;
}
}
}

// We need to crop a rectangle with another rectangle, of potentially different sizes
// Let's find out the position of the block in the image referential
int blockOriX = regionRead.x-image.x;
int skipBytesStartX = 0;
Expand All @@ -998,7 +1030,7 @@ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws F
if (blockEndX>0) {
skipBytesEndX = blockEndX*bytesPerPixel;
}
int nBytesToCopyPerLine = (regionRead.width*bytesPerPixel-skipBytesStartX-skipBytesEndX);
int nBytesToCopyPerLine = ((regionRead.width-nColumnsMissing) * bytesPerPixel - skipBytesStartX - skipBytesEndX);
int blockOriY = regionRead.y-image.y;
int skipLinesRawDataStart = 0;
int skipLinesBufStart = 0;
Expand All @@ -1009,13 +1041,13 @@ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws F
}
int blockEndY = (regionRead.y+regionRead.height)-(image.y+image.height);
int skipLinesEnd = Math.max(blockEndY, 0);
int totalLines = regionRead.height-skipLinesRawDataStart-skipLinesEnd;
int nBytesPerLineRawData = regionRead.width*bytesPerPixel;
int totalLines = regionRead.height-nLinesMissing-skipLinesRawDataStart-skipLinesEnd;
int nBytesPerLineRawData = (regionRead.width-nColumnsMissing)*bytesPerPixel;
int nBytesPerLineBuf = image.width*bytesPerPixel;
int offsetRawData = skipLinesRawDataStart*nBytesPerLineRawData+skipBytesStartX;
int offsetBuf = skipLinesBufStart*nBytesPerLineBuf+skipBytesBufStartX;

for (int i=0; i<totalLines;i++) { // TODO: totalines or totalines + 1 ?
for (int i=0; i<totalLines;i++) {
System.arraycopy(rawData,offsetRawData,buf,offsetBuf,nBytesToCopyPerLine);
offsetRawData=offsetRawData+nBytesPerLineRawData;
offsetBuf=offsetBuf+nBytesPerLineBuf;
Expand Down Expand Up @@ -1140,9 +1172,10 @@ protected void initFile(String id) throws FormatException, IOException {
// Then we look at the max value in each dimension, to know how many digits are needed to write the signature
// and proper alphabetical ordering
cziPartToSegments.forEach((part, cziSegments) -> { // For each part
Arrays.asList(cziSegments.subBlockDirectory.data.entries).forEach( // and each entry
if (cziSegments.subBlockDirectory!=null) {
Arrays.asList(cziSegments.subBlockDirectory.data.entries).forEach( // and each entry
entry -> {
for (LibCZI.SubBlockSegment.SubBlockSegmentData.SubBlockDirectoryEntryDV.DimensionEntry dimEntry: entry.getDimensionEntries()) {
for (LibCZI.SubBlockSegment.SubBlockSegmentData.SubBlockDirectoryEntryDV.DimensionEntry dimEntry : entry.getDimensionEntries()) {
//int nDigits = String.valueOf(dimEntry.start).length(); // TODO: Can this be negative ?
int val = dimEntry.start;
if (!maxValuePerDimension.containsKey(dimEntry.dimension)) {
Expand All @@ -1155,7 +1188,8 @@ protected void initFile(String id) throws FormatException, IOException {
}
}
}
);
);
}
});

nIlluminations = maxValuePerDimension.containsKey("I")? maxValuePerDimension.get("I")+1:1;
Expand Down Expand Up @@ -1197,9 +1231,15 @@ protected void initFile(String id) throws FormatException, IOException {

// Write all signatures
cziPartToSegments.forEach((part, cziSegments) -> { // For each part
Arrays.asList(cziSegments.subBlockDirectory.data.entries).forEach( // and each entry
if (cziSegments.subBlockDirectory!=null) {
Arrays.asList(cziSegments.subBlockDirectory.data.entries).forEach( // and each entry
entry -> {
int downscalingFactor = (int) Math.round((double)(entry.getDimension("X").size)/(double)(entry.getDimension("X").storedSize));
double doubleDownscalingFactor = (double)(entry.getDimension("X").size) / (double)(entry.getDimension("X").storedSize);
if (doubleDownscalingFactor<1) {
// PALM dataset -> forcing pyramid level to 0 will lead to create a new series
doubleDownscalingFactor = 0;
}
int downscalingFactor = (int) Math.round(doubleDownscalingFactor);

hasPyramid = hasPyramid || (downscalingFactor!=1);

Expand All @@ -1221,6 +1261,7 @@ protected void initFile(String id) throws FormatException, IOException {
coreSignatureToBlocks.get(coreSignature).add(moduloEntry);
}
});
}
});

// Sort them
Expand Down Expand Up @@ -1409,15 +1450,12 @@ private void addLabelIfExists(List<Integer> sortedFileParts, Map<Integer, CZISeg
byte[] bytes = LibCZI.getLabelBytes(cziPartToSegments.get(filePart).attachmentDirectory, id, BUFFER_SIZE, isLittleEndian());
if (bytes!=null) {
int nSeries = getSeriesCount();
ServiceFactory factory = new ServiceFactory();
OMEXMLService service = factory.getInstance(OMEXMLService.class);
OMEXMLMetadata omeXML = service.createOMEXMLMetadata();
ZeissQuickStartCZIReader labelReader = new ZeissQuickStartCZIReader();
String placeHolderName = "label.czi";
// thumbReader.setMetadataOptions(getMetadataOptions());
ByteArrayHandle stream = new ByteArrayHandle(bytes);
Location.mapFile(placeHolderName, stream);
labelReader.setMetadataStore(omeXML);
labelReader.setMetadataStore(new DummyMetadata());
labelReader.setId(placeHolderName);

CoreMetadata c = labelReader.getCoreMetadataList().get(0);
Expand Down Expand Up @@ -1451,15 +1489,12 @@ private void addSlidePreviewIfExists(List<Integer> sortedFileParts, Map<Integer,
byte[] bytes = LibCZI.getPreviewBytes(cziPartToSegments.get(filePart).attachmentDirectory, id, BUFFER_SIZE, isLittleEndian());
if (bytes!=null) {
int nSeries = getSeriesCount();
ServiceFactory factory = new ServiceFactory();
OMEXMLService service = factory.getInstance(OMEXMLService.class);
OMEXMLMetadata omeXML = service.createOMEXMLMetadata();
ZeissQuickStartCZIReader labelReader = new ZeissQuickStartCZIReader();
String placeHolderName = "slide_preview.czi";
labelReader.setMetadataOptions(getMetadataOptions());
ByteArrayHandle stream = new ByteArrayHandle(bytes);
Location.mapFile(placeHolderName, stream);
labelReader.setMetadataStore(omeXML);
labelReader.setMetadataStore(new DummyMetadata());
labelReader.setId(placeHolderName);

CoreMetadata c = labelReader.getCoreMetadataList().get(0);
Expand Down Expand Up @@ -1584,9 +1619,13 @@ private int[] setOriginAndSize(CoreMetadata ms0,
if (minT>t_min) minT = t_min;
}

ms0.sizeZ = maxZ - minZ;
ms0.sizeC = maxC - minC;
ms0.sizeT = maxT - minT;
if (minZ!=0) logger.warn("No block found with Z = 0, first Z block found at Z = "+minZ);
if (minC!=0) logger.warn("No block found with C = 0, first C block found at C = "+minC);
if (minT!=0) logger.warn("No block found with T = 0, first T block found at T = "+minT);

ms0.sizeZ = maxZ - 0;//minZ;
ms0.sizeC = maxC - 0;//minC;
ms0.sizeT = maxT - 0;//minT;

if ((downScale!=1)&&(allowAutostitching())) {
ms0.sizeX = nPixX_maxRes/downScale;
Expand Down Expand Up @@ -1886,9 +1925,9 @@ public ModuloDimensionEntries(LibCZI.SubBlockDirectorySegment.SubBlockDirectoryS
this.nPhases = nPhases;
this.pixelType = entry.getPixelType();
this.compression = entry.getCompression();
this.downSampling = (int) Math.round((double)(entry.getDimension("X").size)/(double)(entry.getDimension("X").storedSize));
int ds = (int) Math.round((double)(entry.getDimension("X").size)/(double)(entry.getDimension("X").storedSize));
this.downSampling = Math.max(ds,1); // The downsampling factor could go below 1 for PALM dataset, but within the block, the real data is such that there's no downsampling - just the pixel size changes
this.filePosition = entry.getFilePosition();
//System.out.println(entry);

int iRotation = 0;
int iIllumination = 0;
Expand Down Expand Up @@ -2079,7 +2118,7 @@ public ZeissQuickStartCZIReader copy() {
*/
private static class CZISegments {
final LibCZI.FileHeaderSegment fileHeader;
final LibCZI.SubBlockDirectorySegment subBlockDirectory;
final LibCZI.SubBlockDirectorySegment subBlockDirectory; // Some multipart files could have a part without this segment
final LibCZI.AttachmentDirectorySegment attachmentDirectory;
final LibCZI.MetaDataSegment metadata;
final double[] timeStamps;
Expand All @@ -2091,21 +2130,21 @@ public CZISegments(String id, boolean littleEndian) throws IOException {
this.attachmentDirectory = LibCZI.getAttachmentDirectorySegment(this.fileHeader.data.attachmentDirectoryPosition, id, BUFFER_SIZE, littleEndian);

// For searching of blocks at the end of the file, just in case the metadata subblock has been deleted
long lastBlockPosition = LibCZI.getPositionLastBlock(subBlockDirectory);
long lastBlockPosition = subBlockDirectory!=null?LibCZI.getPositionLastBlock(subBlockDirectory):0;
this.metadata = LibCZI.getMetaDataSegment(this.fileHeader.data.metadataPosition, id, BUFFER_SIZE, littleEndian, lastBlockPosition);

if (attachmentDirectory!=null) {
this.timeStamps = LibCZI.getTimeStamps(this.attachmentDirectory, id, BUFFER_SIZE, littleEndian);
//System.out.println("#ts="+timeStamps.length);
/*for (double timeStamp: timeStamps) {
System.out.println(timeStamp);
}*/
} else {
this.timeStamps = new double[0];
}
}
}

/**
* A least recently used cache for CZI subblocks. Its goal is to prevent multiple decompression of the same subblock
* when only subregions are requested.
*/
static class SubBlockLRUCache extends
LinkedHashMap<MinDimEntry, SoftReference<byte[]>>
{
Expand Down Expand Up @@ -2143,7 +2182,6 @@ protected boolean removeEldestEntry(
totalWeight.addAndGet(-cost.get(eldest.getKey()));
cost.remove(eldest.getKey());
eldest.getValue().clear();
//System.out.println("Remove");
return true;
}
else return false;
Expand All @@ -2154,10 +2192,9 @@ synchronized public void touch(final MinDimEntry key,
{
final SoftReference<byte[]> ref = get(key);
if (ref == null) {
long costValue = value.length;//getWeight(value);
long costValue = value.length;
totalWeight.addAndGet(costValue);
cost.put(key, costValue);
//System.out.println(totalWeight.get()/(1024*1024)+" Mb");
put(key, new SoftReference<>(value));
}
else if (ref.get() == null) {
Expand Down Expand Up @@ -3401,11 +3438,12 @@ private void setSpaceAndTimeInformation( // of series and of planes

Length planePosX, planePosY, planePosZ = null; // plane position of the current coreindex - do not vary over z and t, but that could happen

blocks = mapCoreCZTToBlocks.get(iCoreIndex).get(cztKeyForXYOffset);
CZTKey cztKeySeriesForXYOffset = mapCoreCZTToBlocks.get(iCoreIndex).keySet().stream().min(keyComparator::compare).get();
blocks = mapCoreCZTToBlocks.get(iCoreIndex).get(cztKeySeriesForXYOffset);

if (!resolutionLevel0) {
// Use the same position as the higher resolution level
// Keeping the last highest resolutio works because bio-formats forces the resolution
// Keeping the last highest resolution works because bio-formats forces the resolution
// level series to be sorted according to the core series index:
// res 0 series i / res 1 series i / res 2 series i / res 0 series i+1 / res 1 series i+1 etc.
planePosX = planePosXResolutionLevel0;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/ch/epfl/biop/formats/in/libczi/LibCZI.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ public static SubBlockDirectorySegment getSubBlockDirectorySegment(long blockPos
}
return directorySegment;
} else {
throw new IOException(ZISRAWDIRECTORY+" segment expected, found "+segmentID+" instead.");
logger.warn(ZISRAWDIRECTORY+" segment expected, found "+segmentID+" instead.");
return null; // Some multipart file could be deprived of this segment. The reader needs to deal with null in this case
}
}
}
Expand Down Expand Up @@ -159,7 +160,6 @@ public static AttachmentDirectorySegment getAttachmentDirectorySegment(long bloc
} else {
logger.warn("No "+ZISRAWATTDIR+" segment found.");
return null;
//throw new IOException(ZISRAWATTDIR+" segment expected, found "+segmentID+" instead.");
}
}
}
Expand Down

0 comments on commit 557745c

Please sign in to comment.