/* * #%L * OME Bio-Formats package for reading and converting biological file formats. * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package loci.formats.in; import ch.systemsx.cisd.base.mdarray.MDIntArray; import ch.systemsx.cisd.hdf5.HDF5CompoundDataMap; import java.io.IOException; import java.util.ArrayList; import java.util.List; import loci.common.DataTools; import loci.common.RandomAccessInputStream; import loci.common.services.DependencyException; import loci.common.services.ServiceFactory; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.MissingLibraryException; import loci.formats.meta.MetadataStore; import loci.formats.services.JHDFService; import loci.formats.services.JHDFServiceImpl; import ome.xml.model.primitives.Color; import ome.xml.model.primitives.NonNegativeInteger; /** * Reader for CellH5 (HDF) files. */ public class CellH5Reader extends FormatReader { // -- Constants -- public static final String HDF_MAGIC_STRING = "HDF"; public class CellH5Constants { public final static String PREFIX_PATH = "/sample/0/"; public final static String IMAGE_PATH = "image/channel/"; public final static String SEGMENTATION_PATH = "image/region/"; public final static String PLATE = "plate/"; public final static String WELL = "/experiment/"; public final static String SITE = "/position/"; public final static String DEFINITION = "/definition/"; public final static String OBJECT = "object/"; public final static String FEATURE = "feature/"; public final static String IMAGE = "image/"; public final static String BBOX = "bounding_box/"; public final static String CLASS_LABELS = "object_classification/class_labels/"; public final static String PREDICTED_CLASS_LABELS = "object_classification/prediction"; } public class CellH5Coordinate { public String plate; public String well; public String site; protected String pathToImageData; protected String pathToSegmentationData; protected String pathToPosition; CellH5Coordinate(String plate, String well, String site) { this.plate = plate; this.well = well; this.site = site; pathToPosition = CellH5Constants.PREFIX_PATH + CellH5Constants.PLATE + this.plate + CellH5Constants.WELL + this.well + CellH5Constants.SITE + this.site + "/"; this.pathToImageData = pathToPosition + CellH5Constants.IMAGE_PATH; this.pathToSegmentationData = pathToPosition + CellH5Constants.SEGMENTATION_PATH; LOGGER.trace(pathToImageData); } public String toString() { return String.format("%s %s_%s", plate, well, site); } } // -- Fields -- private double pixelSizeX, pixelSizeY, pixelSizeZ; private double minX, minY, minZ, maxX, maxY, maxZ; private int seriesCount; private transient JHDFService jhdf; private MetadataStore store; private int lastChannel = 0; private List<CellH5Coordinate> CellH5PositionList = new ArrayList<CellH5Coordinate>(); private List<String> CellH5PathsToImageData = new ArrayList<String>(); private List<String> cellObjectNames = new ArrayList<String>(); // Default colors for bounding box colors if no classification present private final int[][] COLORS = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {0, 255, 255}, {255, 0, 255}, {255, 255, 255}, {255, 0, 128}, {0, 255, 128}, {0, 128, 256}, {128, 0, 128}, {255, 128, 0}, {64, 128, 0}, {0, 64, 128}, {128, 0, 64}}; private HDF5CompoundDataMap[] times = null; private HDF5CompoundDataMap[] classes = null; private HDF5CompoundDataMap[] bbox = null; // -- Constructor -- /** * Constructs a new CellH5 HDF reader. */ public CellH5Reader() { super("CellH5 (HDF)", "ch5"); suffixSufficient = true; domains = new String[] {FormatTools.UNKNOWN_DOMAIN}; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#getOptimalTileHeight() */ @Override public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); return getSizeY(); } /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @Override public boolean isThisType(String name, boolean open) { String[] tokens = name.split("\\.(?=[^\\.]+$)"); return tokens.length > 1 && tokens[1].equals("ch5"); } /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = 8; if (!FormatTools.validStream(stream, blockLen, false)) { return false; } return stream.readString(blockLen).contains(HDF_MAGIC_STRING); } /* @see loci.formats.IFormatReader#get8BitLookupTable() */ @Override public byte[][] get8BitLookupTable() { FormatTools.assertId(currentId, true, 1); if (getPixelType() != FormatTools.UINT8 || !isIndexed()) { return null; } if (lastChannel < 0) { return null; } byte[][] lut = new byte[3][256]; for (int i = 0; i < 256; i++) { switch (lastChannel) { case 0: // red lut[0][i] = (byte) (i & 0xff); break; case 1: // green lut[1][i] = (byte) (i & 0xff); break; case 2: // blue lut[2][i] = (byte) (i & 0xff); break; case 3: // cyan lut[1][i] = (byte) (i & 0xff); lut[2][i] = (byte) (i & 0xff); break; case 4: // magenta lut[0][i] = (byte) (i & 0xff); lut[2][i] = (byte) (i & 0xff); break; case 5: // yellow lut[0][i] = (byte) (i & 0xff); lut[1][i] = (byte) (i & 0xff); break; default: // gray lut[0][i] = (byte) (i & 0xff); lut[1][i] = (byte) (i & 0xff); lut[2][i] = (byte) (i & 0xff); } } return lut; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); lastChannel = getZCTCoords(no)[1]; // pixel data is stored in XYZ blocks Object image = getImageData(no, y, h); boolean little = isLittleEndian(); // images is of type byte[][]. Left these checks and unpacking // in the code for feature data types int bpp = FormatTools.getBytesPerPixel(getPixelType()); for (int row = 0; row < h; row++) { int base = row * w * bpp; if (image instanceof byte[][]) { byte[][] data = (byte[][]) image; byte[] rowData = data[row]; System.arraycopy(rowData, x, buf, row * w, w); } else if (image instanceof short[][]) { short[][] data = (short[][]) image; short[] rowData = data[row]; for (int i = 0; i < w; i++) { DataTools.unpackBytes(rowData[i + x], buf, base + 2 * i, 2, little); } } else if (image instanceof int[][]) { int[][] data = (int[][]) image; int[] rowData = data[row]; for (int i = 0; i < w; i++) { DataTools.unpackBytes(rowData[i + x], buf, base + i * 4, 4, little); } } else if (image instanceof float[][]) { float[][] data = (float[][]) image; float[] rowData = data[row]; for (int i = 0; i < w; i++) { int v = Float.floatToIntBits(rowData[i + x]); DataTools.unpackBytes(v, buf, base + i * 4, 4, little); } } else if (image instanceof double[][]) { double[][] data = (double[][]) image; double[] rowData = data[row]; for (int i = 0; i < w; i++) { long v = Double.doubleToLongBits(rowData[i + x]); DataTools.unpackBytes(v, buf, base + i * 8, 8, little); } } } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { seriesCount = 0; pixelSizeX = pixelSizeY = pixelSizeZ = 0; if (jhdf != null) { jhdf.close(); } jhdf = null; lastChannel = 0; } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); initializeJHDFService(id); parseStructure(); // The ImageJ RoiManager can not distinguish ROIs from different // Series. This is why they only will be loaded if the CellH5 contains two // image / series assuming that the first is the image and 2nd the labels if (seriesCount <= 2 && getMetadataOptions().getMetadataLevel() == MetadataLevel.ALL) { parseROIs(0); } } /* @see loci.formats.FormatReader#reopenFile() */ @Override public void reopenFile() throws IOException { try { initializeJHDFService(currentId); } catch (MissingLibraryException e) { throw new IOException(e); } } // -- Helper methods -- private void initializeJHDFService(String id) throws IOException, MissingLibraryException { try { ServiceFactory factory = new ServiceFactory(); jhdf = factory.getInstance(JHDFService.class); jhdf.setFile(id); } catch (DependencyException e) { throw new MissingLibraryException(JHDFServiceImpl.NO_JHDF_MSG, e); } } private Object getImageData(int no, int y, int height) throws FormatException { int[] zct = getZCTCoords(no); int zslice = zct[0]; int channel = zct[1]; int time = zct[2]; int width = getSizeX(); int elementSize = jhdf.getElementSize(CellH5PathsToImageData.get(series)); int[] arrayOrigin = new int[] {channel, time, zslice, 0, 0}; int[] arrayDimension = new int[] {1, 1, 1, height, width}; MDIntArray test = jhdf.readIntBlockArray(CellH5PathsToImageData.get(series), arrayOrigin, arrayDimension); if (elementSize == 1) { byte[][] image = new byte[height][width]; // Slice x, y dimension for (int yy = 0; yy < height; yy++) { for (int xx = 0; xx < width; xx++) { image[yy][xx] = (byte) test.get(0, 0, 0, yy, xx); } } return image; } else if (elementSize == 2) { short[][] image = new short[height][width]; // Slice x, y dimension for (int yy = 0; yy < height; yy++) { for (int xx = 0; xx < width; xx++) { image[yy][xx] = (short) test.get(0, 0, 0, yy, xx); } } return image; } else { int[][] image = new int[height][width]; // Slice x, y dimension for (int yy = 0; yy < height; yy++) { for (int xx = 0; xx < width; xx++) { image[yy][xx] = (int) test.get(0, 0, 0, yy, xx); } } return image; } } private void parseStructure() throws FormatException { seriesCount = 0; pixelSizeX = pixelSizeY = pixelSizeZ = 1; core.clear(); // read experiment structure and collect coordinates String path_to_plate = CellH5Constants.PREFIX_PATH + CellH5Constants.PLATE; LOGGER.info("Plate :" + path_to_plate ); for (String plate : jhdf.getMember(path_to_plate)) { String path_to_well = path_to_plate + plate + CellH5Constants.WELL; LOGGER.info("Well :" + path_to_well ); for (String well : jhdf.getMember(path_to_well)) { String path_to_site = path_to_well + well + CellH5Constants.SITE; LOGGER.info("Site :" + path_to_site ); for (String site : jhdf.getMember(path_to_site)) { CellH5PositionList.add(new CellH5Coordinate(plate, well, site)); } } } if (CellH5PositionList.size() == 0) { throw new FormatException("No series found in file..."); } List<String> seriesNames = new ArrayList<String>(); List<String> seriesPlate = new ArrayList<String>(); List<String> seriesWell = new ArrayList<String>(); List<String> seriesSite = new ArrayList<String>(); for (CellH5Coordinate coord : CellH5PositionList) { if (jhdf.exists(coord.pathToImageData)) { CoreMetadata m = new CoreMetadata(); core.add(m); setSeries(seriesCount); LOGGER.debug(coord.pathToImageData); int[] ctzyx = jhdf.getShape(coord.pathToImageData); m.sizeC = ctzyx[0]; m.sizeT = ctzyx[1]; m.sizeZ = ctzyx[2]; m.sizeY = ctzyx[3]; m.sizeX = ctzyx[4]; m.resolutionCount = 1; m.thumbnail = false; m.imageCount = getSizeC() * getSizeT() * getSizeZ(); m.dimensionOrder = "XYZTC"; m.rgb = false; m.thumbSizeX = 128; m.thumbSizeY = 128; m.orderCertain = false; m.littleEndian = true; m.interleaved = false; m.indexed = true; int bpp = jhdf.getElementSize(coord.pathToImageData); if (bpp==1) { m.pixelType = FormatTools.UINT8; } else if (bpp==2) { m.pixelType = FormatTools.UINT16; } else if (bpp==4) { m.pixelType = FormatTools.INT32; } else { throw new FormatException("Pixel type not understood. Only 8, " + "16 and 32 bit images supported"); } seriesNames.add(String.format("P_%s, W_%s_%s", coord.plate, coord.well, coord.site)); seriesPlate.add(coord.plate); seriesWell.add(coord.well); seriesSite.add(coord.site); CellH5PathsToImageData.add(coord.pathToImageData); seriesCount++; } } for (CellH5Coordinate coord : CellH5PositionList) { if (jhdf.exists(coord.pathToSegmentationData)) { CoreMetadata m = new CoreMetadata(); core.add(m); setSeries(seriesCount); LOGGER.debug(coord.pathToSegmentationData); int[] ctzyx = jhdf.getShape(coord.pathToSegmentationData); m.sizeC = ctzyx[0]; m.sizeT = ctzyx[1]; m.sizeZ = ctzyx[2]; m.sizeY = ctzyx[3]; m.sizeX = ctzyx[4]; m.resolutionCount = 1; m.thumbnail = false; m.imageCount = getSizeC() * getSizeT() * getSizeZ(); m.dimensionOrder = "XYZTC"; m.rgb = false; m.thumbSizeX = 128; m.thumbSizeY = 128; m.orderCertain = false; m.littleEndian = true; m.interleaved = false; m.indexed = true; int bpp = jhdf.getElementSize(coord.pathToSegmentationData); if (bpp==1) { m.pixelType = FormatTools.UINT8; } else if (bpp==2) { m.pixelType = FormatTools.UINT16; } else if (bpp==4) { m.pixelType = FormatTools.INT32; } else { throw new FormatException("Pixel type not understood. Only 8, " + "16 and 32 bit images supported"); } seriesNames.add(String.format("P_%s, W_%s_%s label image", coord.plate, coord.well, coord.site)); seriesPlate.add(coord.plate); seriesWell.add(coord.well); seriesSite.add(coord.site); CellH5PathsToImageData.add(coord.pathToSegmentationData); seriesCount++; } } if (seriesCount == 0) { throw new FormatException("No image data found..."); } store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); for (int s=0; s<seriesNames.size(); s++) { String image_id = MetadataTools.createLSID("Image", s); store.setImageName(seriesNames.get(s), s); String plate_id = MetadataTools.createLSID("Plate", 0); store.setPlateID(plate_id, 0); store.setPlateName(seriesPlate.get(s), 0); String well_id = MetadataTools.createLSID("Well", 0); store.setWellID(well_id, 0, 0); String cellh5WellCoord = seriesWell.get(s); String wellRowLetter = cellh5WellCoord.substring(0, 1); String wellColNumber = cellh5WellCoord.substring(1); int wellRowLetterIndex = "ABCDEFGHIJKLMNOP".indexOf(wellRowLetter); int wellColNumberIndex = -1; try { wellColNumberIndex = Integer.parseInt(wellColNumber); } catch (NumberFormatException e){ // } if (wellRowLetterIndex > -1 && wellColNumberIndex > 0) { store.setWellRow(new NonNegativeInteger(wellRowLetterIndex), 0, 0); store.setWellColumn(new NonNegativeInteger(wellColNumberIndex - 1), 0, 0); } else { store.setWellRow(new NonNegativeInteger(0), 0, 0); store.setWellColumn(new NonNegativeInteger(0), 0, 0); } store.setWellExternalIdentifier(cellh5WellCoord, 0, 0); String site_id = MetadataTools.createLSID("WellSample", 0); store.setWellSampleID(site_id, 0, 0, 0); store.setWellSampleIndex(NonNegativeInteger.valueOf(seriesSite.get(s)), 0, 0, 0); store.setWellSampleImageRef(image_id, 0, 0, 0); } setSeries(0); parseCellObjects(); } private void parseCellObjects() { String rootObject = CellH5Constants.DEFINITION + CellH5Constants.OBJECT; List<String> allObjects = jhdf.getMember(rootObject); for (String objectName : allObjects) { String objectType = (String) jhdf.readCompoundArrayDataMap( rootObject + objectName)[0].get("type"); if (objectType.equals("region")) { cellObjectNames.add(objectName); LOGGER.debug("CellH5Reader: Found cell object {}", objectName); } } } private int getChannelIndexOfCellObjectName(String cellObjectName) { HDF5CompoundDataMap[] allImageRegions = jhdf.readCompoundArrayDataMap( CellH5Constants.DEFINITION + CellH5Constants.SEGMENTATION_PATH); for (int regionIdx = 0; regionIdx < allImageRegions.length; regionIdx++) { String regionName = (String) allImageRegions[regionIdx].get("region_name"); Integer channelIdx = (Integer) allImageRegions[regionIdx].get("channel_idx"); if (regionName.endsWith(cellObjectName)) { return channelIdx.intValue(); } } return -1; } private static Color hex2Rgb(String colorStr) { int red = Integer.parseInt(colorStr.substring(1, 3), 16); int green = Integer.parseInt(colorStr.substring(3, 5), 16); int blue = Integer.parseInt(colorStr.substring(5, 7), 16); return new Color(red, green, blue, 0xff); } private void parseROIs(int s) { int objectIdx = 0; List<Color> classColors = new ArrayList<Color>(); int roiIndexOffset = 0; CellH5Coordinate coord = CellH5PositionList.get(0); for (String cellObjectName : cellObjectNames) { LOGGER.info("Parse segmentation ROIs for cell object {} : {}", cellObjectName, objectIdx); String featureName = CellH5Constants.FEATURE + cellObjectName + "/"; String pathToBoundingBox = coord.pathToPosition + featureName + CellH5Constants.BBOX; String pathToClassDefinition = CellH5Constants.DEFINITION + featureName + CellH5Constants.CLASS_LABELS; boolean hasClassification = false; if (jhdf.exists(pathToClassDefinition)) { String classColorHexString; HDF5CompoundDataMap[] classDef = jhdf.readCompoundArrayDataMap(pathToClassDefinition); for (int cls = 0; cls < classDef.length; cls++) { classColorHexString = (String) classDef[cls].get("color"); classColors.add(hex2Rgb(classColorHexString)); } if (classDef.length > 0) { hasClassification = true; classes = jhdf.readCompoundArrayDataMap(coord.pathToPosition + featureName + CellH5Constants.PREDICTED_CLASS_LABELS); } } if (jhdf.exists(pathToBoundingBox)) { bbox = jhdf.readCompoundArrayDataMap(pathToBoundingBox); times = jhdf.readCompoundArrayDataMap( coord.pathToPosition + CellH5Constants.OBJECT + cellObjectName); int roiChannel = getChannelIndexOfCellObjectName(cellObjectName); int roiZSlice = 0; for (int roiIndex = 0; roiIndex < bbox.length; roiIndex++) { int roiManagerRoiIndex = roiIndex + roiIndexOffset; int roiTime = (Integer) times[roiIndex].get("time_idx"); int objectLabelId = (Integer) times[roiIndex].get("obj_label_id"); int left = (Integer) bbox[roiIndex].get("left"); int right = (Integer) bbox[roiIndex].get("right"); int top = (Integer) bbox[roiIndex].get("top"); int bottom = (Integer) bbox[roiIndex].get("bottom"); int width = right - left; int height = bottom - top; String roiID = MetadataTools.createLSID("ROI", roiManagerRoiIndex); store.setROIID(roiID, roiManagerRoiIndex); store.setImageROIRef(roiID, s, roiManagerRoiIndex); store.setROIName( cellObjectName + " " + objectLabelId, roiManagerRoiIndex); String shapeID = MetadataTools.createLSID("Shape", roiManagerRoiIndex, 0); store.setRectangleID(shapeID, roiManagerRoiIndex, 0); store.setRectangleX((double) left, roiManagerRoiIndex, 0); store.setRectangleY((double) top, roiManagerRoiIndex, 0); store.setRectangleWidth((double) width, roiManagerRoiIndex, 0); store.setRectangleHeight((double) height, roiManagerRoiIndex, 0); store.setRectangleText(cellObjectName, roiManagerRoiIndex, 0); store.setRectangleTheT( new NonNegativeInteger(roiTime), roiManagerRoiIndex, 0); store.setRectangleTheC( new NonNegativeInteger(roiChannel), roiManagerRoiIndex, 0); store.setRectangleTheZ( new NonNegativeInteger(roiZSlice), roiManagerRoiIndex, 0); Color strokeColor; if (hasClassification) { int classLabelIDx = (Integer) classes[roiIndex].get("label_idx"); strokeColor = classColors.get(classLabelIDx); } else { strokeColor = new Color(COLORS[objectIdx][0], COLORS[objectIdx][1], COLORS[objectIdx][2], 0xff); } store.setRectangleStrokeColor(strokeColor, roiManagerRoiIndex, 0); } objectIdx++; roiIndexOffset += bbox.length; } else { LOGGER.info("No Segmentation data found..."); break; } } } }