diff --git a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java index 135fa6f2..526b4f51 100644 --- a/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java +++ b/src/main/java/com/glencoesoftware/bioformats2raw/Converter.java @@ -203,6 +203,9 @@ public class Converter implements Callable { private IProgressListener progressListener; private Map tileCounts = new HashMap(); + /** Whether or not to preserve channel storage for RGB data. */ + private boolean keepRGB = false; + // Option setters /** @@ -838,6 +841,21 @@ public void setDimensionOrder(DimensionOrder order) { dimensionOrder = order; } + /** + * Set whether or not to preserve channel ordering for input RGB data. + * False by default, so channels will be separated. + * + * @param keep true if RGB storage should be preserved + */ + @Option( + names = "--keep-rgb", + description = "Keep RGB data (if present) as RGB", + defaultValue = "false" + ) + public void setKeepRGB(boolean keep) { + keepRGB = keep; + } + // Option getters /** @@ -1075,12 +1093,19 @@ public int getMinImageSize() { } /** - * @return current dimension roder + * @return current dimension order */ public DimensionOrder getDimensionOrder() { return dimensionOrder; } + /** + * @return whether or not to preserve RGB data + */ + public boolean getKeepRGB() { + return keepRGB; + } + // Conversion methods /** @@ -1237,17 +1262,20 @@ public void convert() memoizer.setFlattenedResolutions(false); memoizer.setMetadataFiltered(true); memoizer.setMetadataStore(createMetadata()); - ChannelSeparator separator = new ChannelSeparator(memoizer); - separator.setId(inputPath.toString()); - separator.setResolution(0); + IFormatReader wrappedReader = memoizer; + if (!getKeepRGB()) { + wrappedReader = new ChannelSeparator(wrappedReader); + } + wrappedReader.setId(inputPath.toString()); + wrappedReader.setResolution(0); if (reader instanceof MiraxReader) { ((MiraxReader) reader).setTileCache(tileCache); } if (omeroMetadata) { - readers.add(new MinMaxCalculator(separator)); + readers.add(new MinMaxCalculator(wrappedReader)); } else { - readers.add(separator); + readers.add(wrappedReader); } savedMemoFile = savedMemoFile || memoizer.isSavedToMemo(); } @@ -1777,10 +1805,30 @@ private byte[] getTileDownsampled( String.format(scaleFormatString, getScaleFormatStringArgs(series, resolution - 1)); final ZarrArray zarr = ZarrArray.open(getRootPath().resolve(pathName)); + int[] dimensions = zarr.getShape(); int[] blockSizes = zarr.getChunks(); - int activeTileWidth = blockSizes[blockSizes.length - 1]; - int activeTileHeight = blockSizes[blockSizes.length - 2]; + + boolean interleaved = false; + IFormatReader r = readers.take(); + try { + interleaved = r.isInterleaved(); + } + finally { + readers.put(r); + } + + int xIndex = blockSizes.length - 1; + int yIndex = blockSizes.length - 2; + int channels = 1; + if (getKeepRGB() && interleaved) { + xIndex--; + yIndex--; + channels = blockSizes[blockSizes.length - 1]; + } + + int activeTileWidth = blockSizes[xIndex]; + int activeTileHeight = blockSizes[yIndex]; // Upscale our base X and Y offsets, and sizes to the previous resolution // based on the pyramid scaling factor @@ -1788,10 +1836,10 @@ private byte[] getTileDownsampled( yy *= PYRAMID_SCALE; width = (int) Math.min( activeTileWidth * PYRAMID_SCALE, - dimensions[dimensions.length - 1] - xx); + dimensions[xIndex] - xx); height = (int) Math.min( activeTileHeight * PYRAMID_SCALE, - dimensions[dimensions.length - 2] - yy); + dimensions[yIndex] - yy); IFormatReader reader = readers.take(); int[] offset; @@ -1804,13 +1852,18 @@ private byte[] getTileDownsampled( int bytesPerPixel = FormatTools.getBytesPerPixel(pixelType); int[] shape = new int[] {1, 1, 1, height, width}; + if (getKeepRGB() && interleaved) { + shape[2] = height; + shape[3] = width; + shape[4] = channels; + } byte[] tileAsBytes = readAsBytes(zarr, shape, offset); if (downsampling == Downsampling.SIMPLE) { return scaler.downsample(tileAsBytes, width, height, PYRAMID_SCALE, bytesPerPixel, false, FormatTools.isFloatingPoint(pixelType), - 1, false); + channels, interleaved); } return OpenCVTools.downsample( @@ -1878,6 +1931,19 @@ private int[] getDimensions( dimensions[o.indexOf("Z")] = sizeZ; dimensions[o.indexOf("C")] = sizeC; dimensions[o.indexOf("T")] = sizeT; + + if (getKeepRGB() && reader.getRGBChannelCount() > 1) { + if (reader.isInterleaved()) { + dimensions[1] = scaledDepth; + dimensions[2] = scaledHeight; + dimensions[3] = scaledWidth; + dimensions[4] = reader.getRGBChannelCount(); + } + else { + dimensions[1] = scaledDepth; + dimensions[2] = reader.getRGBChannelCount(); + } + } return dimensions; } @@ -1902,6 +1968,14 @@ private int[] getOffset( offset[o.indexOf("Z")] = zct[0]; offset[o.indexOf("C")] = zct[1]; offset[o.indexOf("T")] = zct[2]; + + if (getKeepRGB() && reader.getRGBChannelCount() > 1) { + if (reader.isInterleaved()) { + offset[2] = y; + offset[3] = x; + offset[4] = 0; + } + } return offset; } @@ -2000,21 +2074,45 @@ public void saveResolutions(int series) int sizeC; int imageCount; String readerDimensionOrder; + int rgbChannels; + boolean interleaved; try { // calculate a reasonable pyramid depth if not specified as an argument sizeX = workingReader.getSizeX(); sizeY = workingReader.getSizeY(); sizeZ = workingReader.getSizeZ(); sizeT = workingReader.getSizeT(); - sizeC = workingReader.getSizeC(); + sizeC = workingReader.getEffectiveSizeC(); readerDimensionOrder = workingReader.getDimensionOrder(); imageCount = workingReader.getImageCount(); pixelType = workingReader.getPixelType(); + + rgbChannels = workingReader.getRGBChannelCount(); + interleaved = workingReader.isInterleaved(); + + if (rgbChannels != workingReader.getSizeC() && getKeepRGB()) { + throw new UnsupportedOperationException( + "Mixed channel types not supported with '--keep-rgb'"); + } } finally { readers.put(workingReader); } + boolean opencv = getDownsampling() != Downsampling.SIMPLE; + if ((pixelType == FormatTools.INT8 || pixelType == FormatTools.INT32) && + opencv && resolutions > 0) + { + String type = FormatTools.getPixelTypeString(pixelType); + throw new UnsupportedOperationException( + "OpenCV does not support downsampling " + type + " data. " + + "See https://github.com/opencv/opencv/issues/7862"); + } + else if (interleaved && rgbChannels > 1 && opencv) { + throw new UnsupportedOperationException( + "Downsampling RGB data with OpenCV not supported"); + } + LOGGER.info( "Preparing to write pyramid sizeX {} (tileWidth: {}) " + "sizeY {} (tileWidth: {}) sizeZ {} (tileDepth: {}) imageCount {}", @@ -2066,14 +2164,27 @@ public void saveResolutions(int series) activeChunkDepth = scaledDepth; } + int[] chunkSize = new int[] { + 1, 1, activeChunkDepth, activeTileHeight, activeTileWidth}; + if (getKeepRGB() && rgbChannels > 1) { + chunkSize[1] = activeChunkDepth; + if (interleaved) { + chunkSize[2] = activeTileHeight; + chunkSize[3] = activeTileWidth; + chunkSize[4] = rgbChannels; + } + else { + chunkSize[2] = rgbChannels; + } + } + DataType dataType = getZarrType(pixelType); String resolutionString = String.format( scaleFormatString, getScaleFormatStringArgs(series, resolution)); ArrayParams arrayParams = new ArrayParams() .shape(getDimensions( workingReader, scaledWidth, scaledHeight, scaledDepth)) - .chunks(new int[] {1, 1, activeChunkDepth, activeTileHeight, - activeTileWidth}) + .chunks(chunkSize) .dataType(dataType) .dimensionSeparator(getDimensionSeparator()) .compressor(CompressorFactory.create( @@ -2109,6 +2220,14 @@ public void saveResolutions(int series) executor.execute(() -> { try { int[] shape = {1, 1, 1, height, width}; + if (getKeepRGB() && rgbChannels > 1) { + if (interleaved) { + shape = new int[] {1, 1, height, width, rgbChannels}; + } + else { + shape = new int[] {1, 1, rgbChannels, height, width}; + } + } int[] offset; IFormatReader reader = readers.take(); try { @@ -2376,6 +2495,9 @@ private void setSeriesLevelMetadata(int series, int resolutions) else { axisOrder = v.getDimensionOrder(); } + if (getKeepRGB() && v.isInterleaved()) { + axisOrder = "C" + axisOrder.replaceAll("[Cc]", ""); + } } finally { readers.put(v);