/* * #%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 java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import loci.common.Location; 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; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.meta.MetadataConverter; import loci.formats.meta.MetadataStore; import loci.formats.ome.OMEXMLMetadata; import loci.formats.services.OMEXMLService; import ome.xml.model.primitives.Color; import ome.xml.model.primitives.NonNegativeInteger; import ome.xml.model.primitives.PositiveFloat; import ome.xml.model.primitives.PositiveInteger; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Length; import ome.units.quantity.Time; import ome.units.UNITS; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * A bioformat reader for the Yokagawa Cellvoyager CV1000 automated microscope ( * {@linkplain <a href="http://www.yokogawa.com/scanner/products/cv1000e.htm">http://www.yokogawa.com/scanner/products/cv1000e.htm</a>}). * <p> * This reader opens the dataset generated by the Yokagawa Cellvoyager automated * microscope. The user should point the reader to the file named * <code>MeasurementResult.xml</code> in the dataset folder. * <p> * The file format itself consists of several nested folder containing a lot of * - sometimes redundant - information. This specific reader exploits the * following files: * <ul> * <li><code>MeasurementResult.xml</code> that contains high level information * about image geometry, well (if any) organization, fields arrangement, etc.. * <li><code>MeasurementResult.ome.xml</code> is a malformed OME xml file that * contains information on one of the numerous TIF files that compose the pixel * data. It is used to easily extract common metadata. * <li>All the TIF files in the <code>Image</code> folder: They contain the * pixel data itself. Each well (if any) can have several Areas (fields in the * HCS vocable), in turn composed of several tiles that are to be stitched * border to border, without the possibility of a more subtle stitching process. * </ul> * This reader stitches each area on the fly upon loading. It yields one series * per well x area. If there is 20 wells each made of 2 areas, 40 series will be * available. * <p> * This reader version was derived from reverse-engineering data files generated * on a system hosted by the imaging facility of the Institut Pasteur, Paris * (Imagopole / PFID). It spits file versioned <code>1.0</code>. The file format * contains several obvious typos, notably in some XML tags, and as mentionned * above, the <code>ome.xml</code> is malformed. It is likely that that * subsequent versions of the CellVoyager format will fix these problems, alas * incapacitating this reader. Should it happen, mail the author to help them * update this reader. * * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> Oct-Nov 2013 * @author Melissa Linkert */ public class CellVoyagerReader extends FormatReader { private static final String SINGLE_TIFF_PATH_BUILDER = "Image/W%dF%03dT%04dZ%02dC%d.tif"; private Location measurementFolder; private List< ChannelInfo > channelInfos; private List< WellInfo > wells; private List< Integer > timePoints; private Location measurementResultFile; private Location omeMeasurementFile; public CellVoyagerReader() { super( "CellVoyager", new String[] { "tif", "xml" } ); this.suffixNecessary = false; this.suffixSufficient = false; this.hasCompanionFiles = true; this.datasetDescription = "Directory with 2 master files 'MeasurementResult.xml' and 'MeasurementResult.ome.xml', used to stich together several TIF files."; this.domains = new String[] { FormatTools.HISTOLOGY_DOMAIN, FormatTools.LM_DOMAIN, FormatTools.HCS_DOMAIN }; } @Override public byte[] openBytes( final int no, final byte[] buf, final int x, final int y, final int w, final int h ) throws FormatException, IOException { FormatTools.checkPlaneParameters( this, no, buf.length, x, y, w, h ); final CoreMetadata cm = core.get( getSeries() ); final int nImagesPerTimepoint = cm.sizeC * cm.sizeZ; final int targetTindex = no / nImagesPerTimepoint; final int rem = no % nImagesPerTimepoint; final int targetZindex = rem / cm.sizeC; final int targetCindex = rem % cm.sizeC; final int[] indices = seriesToWellArea( getSeries() ); final int wellIndex = indices[ 0 ]; final int areaIndex = indices[ 1 ]; final WellInfo well = wells.get( wellIndex ); final AreaInfo area = well.areas.get( areaIndex ); final MinimalTiffReader tiffReader = new MinimalTiffReader(); for ( final FieldInfo field : area.fields ) { String filename = String.format( SINGLE_TIFF_PATH_BUILDER, wellIndex + 1, field.index, targetTindex + 1, targetZindex + 1, targetCindex + 1 ); filename = filename.replace( '\\', File.separatorChar ); final Location image = new Location( measurementFolder, filename ); if ( !image.exists() ) { throw new IOException( "Could not find required file: " + image ); } tiffReader.setId( image.getAbsolutePath() ); // Tile size final int tw = channelInfos.get( 0 ).tileWidth; final int th = channelInfos.get( 0 ).tileHeight; // Field bounds in full final image, full width, full height // (referential named '0', as if x=0 and y=0). final int xbs0 = ( int ) field.xpixels; final int ybs0 = ( int ) field.ypixels; // Subimage bounds in full final image is simply x, y, x+w, y+h // Do they intersect? if ( x + w < xbs0 || xbs0 + tw < x || y + h < ybs0 || ybs0 + th < y ) { continue; } // Common rectangle in reconstructed image referential. final int xs0 = Math.max( xbs0 - x, 0 ); final int ys0 = Math.max( ybs0 - y, 0 ); // Common rectangle in tile referential (named with '1'). final int xs1 = Math.max( x - xbs0, 0 ); final int ys1 = Math.max( y - ybs0, 0 ); final int xe1 = Math.min( tw, x + w - xbs0 ); final int ye1 = Math.min( th, y + h - ybs0 ); final int w1 = xe1 - xs1; final int h1 = ye1 - ys1; if ( w1 <= 0 || h1 <= 0 ) { continue; } // Get corresponding data. final byte[] bytes = tiffReader.openBytes( 0, xs1, ys1, w1, h1 ); final int nbpp = cm.bitsPerPixel / 8; for ( int row1 = 0; row1 < h1; row1++ ) { // Line index in tile coords final int ls1 = nbpp * ( row1 * w1 ); final int length = nbpp * w1; // Line index in reconstructed image coords final int ls0 = nbpp * ( ( ys0 + row1 ) * w + xs0 ); // Transfer System.arraycopy( bytes, ls1, buf, ls0, length ); } tiffReader.close(); } return buf; } @Override public int fileGroupOption( final String id ) throws FormatException, IOException { return FormatTools.MUST_GROUP; } @Override public int getRequiredDirectories( final String[] files ) throws FormatException, IOException { /* * We only need the directory where there is the two xml files. The * parent durectory seems to contain only hardware macros to load and * eject the plate or slide. */ return 0; } @Override public boolean isSingleFile( final String id ) throws FormatException, IOException { return false; } @Override public boolean isThisType( final String name, final boolean open ) { /* * We want to be pointed to any file in the directory that contains * 'MeasurementResult.xml'. */ final String localName = new Location( name ).getName(); if ( localName.equals( "MeasurementResult.xml" ) ) { return true; } final Location parent = new Location( name ).getAbsoluteFile().getParentFile(); final Location xml = new Location( parent, "MeasurementResult.xml" ); if ( !xml.exists() ) { return false; } return super.isThisType( name, open ); } @Override protected void initFile( final String id ) throws FormatException, IOException { super.initFile( id ); measurementFolder = new Location( id ).getAbsoluteFile(); if ( !measurementFolder.exists() ) { throw new IOException( "File " + id + " does not exist." ); } if ( !measurementFolder.isDirectory() ) { measurementFolder = measurementFolder.getParentFile(); } measurementResultFile = new Location( measurementFolder, "MeasurementResult.xml" ); if ( !measurementResultFile.exists() ) { throw new IOException( "Could not find " + measurementResultFile + " in folder." ); } omeMeasurementFile = new Location( measurementFolder, "MeasurementResult.ome.xml" ); if ( !omeMeasurementFile.exists() ) { throw new IOException( "Could not find " + omeMeasurementFile + " in folder." ); } /* * Open MeasurementSettings file */ final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder dBuilder = null; try { dBuilder = dbFactory.newDocumentBuilder(); } catch ( final ParserConfigurationException e ) { LOGGER.debug( "", e ); } Document msDocument = null; try { msDocument = dBuilder.parse( measurementResultFile.getAbsolutePath() ); } catch ( final SAXException e ) { LOGGER.debug( "", e ); } msDocument.getDocumentElement().normalize(); /* * Check file version */ final String fileVersionMajor = getChildText( msDocument.getDocumentElement(), new String[] { "FileVersion", "Major" } ); // Note the typo here. final String fileVersionMinor = getChildText( msDocument.getDocumentElement(), new String[] { "FileVersion", "Miner" } ); if ( !fileVersionMajor.equals( "1" ) || !fileVersionMinor.equals( "0" ) ) { LOGGER.warn( "Detected a file version " + fileVersionMajor + "." + fileVersionMinor + ". This reader was built by reverse-engineering v1.0 files only. Errors might occur." ); } /* * Open OME metadata file */ Document omeDocument = null; try { omeDocument = dBuilder.parse( omeMeasurementFile.getAbsolutePath() ); } catch ( final SAXException e ) { LOGGER.debug( "", e ); } omeDocument.getDocumentElement().normalize(); /* * Extract metadata from MeasurementSetting.xml & OME xml file. This is * where the core of parsing and fetching useful info happens */ readInfo( msDocument, omeDocument ); } @Override public String[] getSeriesUsedFiles( final boolean noPixels ) { FormatTools.assertId( currentId, true, 1 ); if ( noPixels ) { return new String[] { measurementResultFile.getAbsolutePath(), omeMeasurementFile.getAbsolutePath() }; } else { final int[] indices = seriesToWellArea( getSeries() ); final int wellIndex = indices[ 0 ]; final int areaIndex = indices[ 1 ]; final AreaInfo area = wells.get( wellIndex ).areas.get( areaIndex ); final int nFields = area.fields.size(); final String[] images = new String[ getImageCount() * nFields + 2 ]; int index = 0; images[ index++ ] = measurementResultFile.getAbsolutePath(); images[ index++ ] = omeMeasurementFile.getAbsolutePath(); for ( final Integer timepoint : timePoints ) { for ( int zslice = 1; zslice <= getSizeZ(); zslice++ ) { for ( int channel = 1; channel <= getSizeC(); channel++ ) { for ( final FieldInfo field : area.fields ) { /* * Here we compose file names on the fly assuming * they follow the pattern below. Fragile I guess. */ images[ index++ ] = measurementFolder.getAbsolutePath() + String.format( SINGLE_TIFF_PATH_BUILDER, wellIndex + 1, field.index, timepoint, zslice, channel ); } } } } return images; } } /* * PRIVATE METHODS */ /** * Returns the well index (in the field {@link #wells}) and the area index * (in the field {@link WellInfo#areas} corresponding to the specified * series. * * @param series * the desired series. * @return the corresponding well index and area index, as a 2-element array * of <code>int[] { well, area }</code>. */ private int[] seriesToWellArea( final int series ) { int nWell = -1; int seriesInc = -1; for ( final WellInfo well : wells ) { nWell++; int nAreas = -1; for ( @SuppressWarnings( "unused" ) final AreaInfo area : well.areas ) { seriesInc++; nAreas++; if ( series == seriesInc ) { return new int[] { nWell, nAreas }; } } } throw new IllegalStateException( "Cannot find a well for series " + series ); } private void readInfo( final Document msDocument, final Document omeDocument ) throws FormatException { /* * Magnification. * * We need it early, because the file format reports only un-magnified * sizes. So if we are to put proper metadata, we need to make the * conversion to size measured at the sample level ourselves. I feel * like this is fragile and most likely to change in a future version of * the file format. */ final Element msRoot = msDocument.getDocumentElement(); final double objectiveMagnification = Double.parseDouble( getChildText( msRoot, new String[] { "ObjectiveLens", "Magnification" } ) ); // final double zoomLensMagnification = Double.parseDouble( // getChildText( msRoot, new String[] { "ZoomLens", "Magnification", // "Value" } ) ); final double magnification = objectiveMagnification; // * // zoomLensMagnification; /* * Read the ome.xml file. Since it is malformed, we need to parse all * nodes, and add an "ID" attribute to those who do not have it. */ final NodeList nodeList = omeDocument.getElementsByTagName( "*" ); for ( int i = 0; i < nodeList.getLength(); i++ ) { final Node node = nodeList.item( i ); if ( node.getNodeType() == Node.ELEMENT_NODE ) { final NamedNodeMap atts = node.getAttributes(); final Node namedItem = atts.getNamedItem( "ID" ); if ( namedItem == null ) { final String name = node.getNodeName(); final String id = name + ":" + i; if ( !node.getParentNode().getNodeName().equals( "LightSource" ) ) { ( ( Element ) node ).setAttribute( "ID", id ); } } } } /* * For single-slice image, the PhysicalSizeZ can be 0, which will make * the metadata read fail. Correct that. */ final Element pszEl = getChild( omeDocument.getDocumentElement(), new String[] { "Image", "Pixels" } ); final double physicalSizeZ = Double.parseDouble( pszEl.getAttribute( "PhysicalSizeZ" ) ); if ( physicalSizeZ <= 0 ) { // default to 1 whatever pszEl.setAttribute( "PhysicalSizeZ", "" + 1 ); } /* * Now that the XML document is properly formed, we can build a metadata * object from it. */ OMEXMLService service = null; String xml = null; try { xml = XMLTools.getXML( omeDocument ); } catch ( final TransformerConfigurationException e2 ) { LOGGER.debug( "", e2 ); } catch ( final TransformerException e2 ) { e2.printStackTrace(); } try { service = new ServiceFactory().getInstance( OMEXMLService.class ); } catch ( final DependencyException e1 ) { LOGGER.debug( "", e1 ); } OMEXMLMetadata omeMD = null; try { omeMD = service.createOMEXMLMetadata( xml ); } catch ( final ServiceException e ) { LOGGER.debug( "", e ); } catch ( final NullPointerException npe ) { LOGGER.debug( "", npe ); throw npe; } // Correct pixel size for magnification omeMD.setPixelsPhysicalSizeX( FormatTools.createLength( omeMD.getPixelsPhysicalSizeX( 0 ).value().doubleValue() / magnification , omeMD.getPixelsPhysicalSizeX( 0 ).unit()), 0 ); omeMD.setPixelsPhysicalSizeY( FormatTools.createLength( omeMD.getPixelsPhysicalSizeY( 0 ).value().doubleValue() / magnification , omeMD.getPixelsPhysicalSizeY( 0 ).unit()), 0 ); // Time interval if (Double.valueOf( readFrameInterval( msDocument ) ) != null) { omeMD.setPixelsTimeIncrement(new Time( Double.valueOf( readFrameInterval( msDocument ) ), UNITS.S), 0 ); } /* * Channels */ final Element channelsEl = getChild( msRoot, "Channels" ); final List< Element > channelEls = getChildren( channelsEl, "Channel" ); channelInfos = new ArrayList< ChannelInfo >(); int channelIndex = 0; for ( final Element channelEl : channelEls ) { final boolean isEnabled = Boolean.parseBoolean( getChildText( channelEl, "IsEnabled" ) ); if ( !isEnabled ) { continue; } final ChannelInfo ci = readChannel( channelEl ); channelInfos.add( ci ); omeMD.setChannelColor( ci.color, 0, channelIndex++ ); } /* * Fix missing IDs. * * Some IDs are missing in the malformed OME.XML file. We must put them * back manually. Some are fixed here */ omeMD.setProjectID( MetadataTools.createLSID( "Project", 0 ), 0 ); omeMD.setScreenID( MetadataTools.createLSID( "Screen", 0 ), 0 ); omeMD.setPlateID( MetadataTools.createLSID( "Plate", 0 ), 0 ); omeMD.setInstrumentID( MetadataTools.createLSID( "Instrument", 0 ), 0 ); // Read pixel sizes from OME metadata. final double pixelWidth = omeMD.getPixelsPhysicalSizeX( 0 ).value().doubleValue(); final double pixelHeight = omeMD.getPixelsPhysicalSizeY( 0 ).value().doubleValue(); /* * Read tile size from channel info. This is weird, but it's like that. * Since we build a multi-C image, we have to assume that all channels * have the same dimension, even if the file format allows for changing * the size, binning, etc. from channel to channel. Failure to load * datasets that have this exoticity is to be sought here. */ final int tileWidth = channelInfos.get( 0 ).tileWidth; final int tileHeight = channelInfos.get( 0 ).tileHeight; /* * Handle multiple wells. * * The same kind of remark apply: We assume that a channel setting can * be applied to ALL wells. So this file reader will fail for dataset * that have one well that has a different dimension that of others. */ /* * First remark: there can be two modes to store Areas in the xml file: * Either we define different areas for each well, and in that case, the * areas are found as a child element of the well element. Either the * definition of areas is common to all wells, and in that case they * area defined in a separate element. */ final boolean sameAreaPerWell = Boolean.parseBoolean( getChildText( msRoot, "UsesSameAreaParWell" ) ); List< AreaInfo > areas = null; if ( sameAreaPerWell ) { final Element areasEl = getChild( msRoot, new String[] { "SameAreaUsingWell", "Areas" } ); final List< Element > areaEls = getChildren( areasEl, "Area" ); int areaIndex = 0; areas = new ArrayList< AreaInfo >( areaEls.size() ); int fieldIndex = 1; for ( final Element areaEl : areaEls ) { final AreaInfo area = readArea( areaEl, fieldIndex, pixelWidth, pixelHeight, tileWidth, tileHeight ); area.index = areaIndex++; areas.add( area ); // Continue incrementing field index across areas. fieldIndex = area.fields.get( area.fields.size() - 1 ).index + 1; } } final Element wellsEl = getChild( msRoot, "Wells" ); final List< Element > wellEls = getChildren( wellsEl, "Well" ); wells = new ArrayList< WellInfo >(); for ( final Element wellEl : wellEls ) { final boolean isWellEnabled = Boolean.parseBoolean( getChild( wellEl, "IsEnabled" ).getTextContent() ); if ( isWellEnabled ) { final WellInfo wi = readWellInfo( wellEl, pixelWidth, pixelHeight, tileWidth, tileHeight ); if ( sameAreaPerWell ) { wi.areas = areas; } wells.add( wi ); } } /* * Z range. * * In this file format, the Z range appears to be general: it applies to * all fields of all wells. */ final int nZSlices = Integer.parseInt( getChildText( msRoot, new String[] { "ZStackConditions", "NumberOfSlices" } ) ); /* * Time points. They are general as well. Which just makes sense. */ timePoints = readTimePoints( msDocument ); /* * Populate CORE metadata for each area. * * This reader takes to convention that state that 1 area = 1 series. So * if you have 10 wells with 2 areas in each well, and each area is made * of 20 fields, you will get 20 series, and each series will be * stitched from 20 fields. */ core.clear(); for ( final WellInfo well : wells ) { for ( final AreaInfo area : well.areas ) { final CoreMetadata ms = new CoreMetadata(); core.add( ms ); ms.sizeX = area.width; ms.sizeY = area.height; ms.sizeZ = nZSlices; ms.sizeC = channelInfos.size(); ms.sizeT = timePoints.size(); ms.dimensionOrder = "XYCZT"; ms.rgb = false; ms.imageCount = nZSlices * channelInfos.size() * timePoints.size(); // Bit depth. switch ( omeMD.getPixelsType( 0 ) ) { case UINT8: ms.pixelType = FormatTools.UINT8; ms.bitsPerPixel = 8; break; case UINT16: ms.pixelType = FormatTools.UINT16; ms.bitsPerPixel = 16; break; case UINT32: ms.pixelType = FormatTools.UINT32; ms.bitsPerPixel = 32; break; default: throw new FormatException( "Cannot read image with pixel type = " + omeMD.getPixelsType( 0 ) ); } // Determined manually on sample data. Check here is the image // you get is weird. ms.littleEndian = true; } } /* * Populate the MetadataStore. */ final MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels( store, this, true ); MetadataConverter.convertMetadata( omeMD, store ); /* * Pinhole disk */ final double pinholeSize = Double.parseDouble( getChildText( msRoot, new String[] { "PinholeDisk", "PinholeSize_um" } ) ); /* * MicroPlate specific stuff */ final Element containerEl = getChild( msRoot, new String[] { "Attachment", "HolderInfoList", "HolderInfo", "MountedSampleContainer" } ); final String type = containerEl.getAttribute( "xsi:type" ); if ( type.equals( "WellPlate" ) ) { // I don't know an other case. I can just hope that if there is // another name for microplate I will find out quickly. final int nrows = Integer.parseInt( getChildText( containerEl, "RowCount" ) ); final int ncols = Integer.parseInt( getChildText( containerEl, "ColumnCount" ) ); store.setPlateRows( new PositiveInteger( nrows ), 0 ); store.setPlateColumns( new PositiveInteger( ncols ), 0 ); final String plateAcqID = MetadataTools.createLSID( "PlateAcquisition", 0, 0 ); store.setPlateAcquisitionID( plateAcqID, 0, 0 ); final Element dimInfoEl = getChild( msRoot, "DimensionsInfo" ); final int maxNFields = Integer.parseInt( getChild( dimInfoEl, "F" ).getAttribute( "Max" ) ); final PositiveInteger fieldCount = FormatTools.getMaxFieldCount( maxNFields ); if ( fieldCount != null ) { store.setPlateAcquisitionMaximumFieldCount( fieldCount, 0, 0 ); } // Plate acquisition time final String beginTime = getChildText( msRoot, "BeginTime" ); final String endTime = getChildText( msRoot, "EndTime" ); store.setPlateAcquisitionStartTime( new Timestamp( beginTime ), 0, 0 ); store.setPlateAcquisitionEndTime( new Timestamp( endTime ), 0, 0 ); } // Wells position on the plate int seriesIndex = -1; int wellIndex = -1; for ( final WellInfo well : wells ) { wellIndex++; final int wellNumber = well.number; store.setWellRow( new NonNegativeInteger( well.row ), 0, wellIndex ); store.setWellColumn( new NonNegativeInteger( well.col ), 0, wellIndex ); store.setWellID( MetadataTools.createLSID( "Well", well.UID ), 0, wellIndex ); int areaIndex = -1; for ( final AreaInfo area : well.areas ) { seriesIndex++; areaIndex++; final String imageName = "Well " + wellNumber + " (r=" + well.row + ", c=" + well.col + ") - Area " + areaIndex; store.setImageName( imageName, seriesIndex ); Length posX = new Length(Double.valueOf(well.centerX), UNITS.REFERENCEFRAME); Length posY = new Length(Double.valueOf(well.centerY), UNITS.REFERENCEFRAME); store.setWellSampleIndex( new NonNegativeInteger( area.index ), 0, wellIndex, areaIndex ); store.setWellSampleID( MetadataTools.createLSID( "WellSample", area.UID ), 0, wellIndex, areaIndex ); store.setWellSamplePositionX(posX, 0, wellIndex, areaIndex); store.setWellSamplePositionY(posY, 0, wellIndex, areaIndex); channelIndex = 0; for ( int i = 0; i < channelInfos.size(); i++ ) { store.setChannelPinholeSize( new Length(pinholeSize, UNITS.MICROM), seriesIndex, channelIndex++ ); store.setChannelName( channelInfos.get( i ).name, seriesIndex, i ); } } } } private ChannelInfo readChannel( final Element channelEl ) { final ChannelInfo ci = new ChannelInfo(); ci.isEnabled = Boolean.parseBoolean( getChildText( channelEl, "IsEnabled" ) ); ci.channelNumber = Integer.parseInt( getChildText( channelEl, "Number" ) ); final Element acquisitionSettings = getChild( channelEl, "AcquisitionSetting" ); final Element cameraEl = getChild( acquisitionSettings, "Camera" ); ci.tileWidth = Integer.parseInt( getChildText( cameraEl, "EffectiveHorizontalPixels_pixel" ) ); ci.tileHeight = Integer.parseInt( getChildText( cameraEl, "EffectiveVerticalPixels_pixel" ) ); ci.unmagnifiedPixelWidth = Double.parseDouble( getChildText( cameraEl, "HorizonalCellSize_um" ) ); ci.unmagnifiedPixelHeight = Double.parseDouble( getChildText( cameraEl, "VerticalCellSize_um" ) ); final Element colorElement = getChild( channelEl, new String[] { "ContrastEnhanceParam", "Color" } ); final int r = Integer.parseInt( getChildText( colorElement, "R" ) ); final int g = Integer.parseInt( getChildText( colorElement, "G" ) ); final int b = Integer.parseInt( getChildText( colorElement, "B" ) ); final int a = Integer.parseInt( getChildText( colorElement, "A" ) ); final Color channelColor = new Color( r, g, b, a ); ci.color = channelColor; // Build a channel name from excitation, emission and fluorophore name final String excitationType = getChild( channelEl, "Excitation" ).getAttribute( "xsi:type" ); final String excitationName = getChildText( channelEl, new String[] { "Excitation", "Name", "Value" } ); final String emissionName = getChildText( channelEl, new String[] { "Emission", "Name", "Value" } ); String fluorophoreName = getChildText( channelEl, new String[] { "Emission", "FluorescentProbe", "Value" } ); if ( null == fluorophoreName ) { fluorophoreName = "ø"; } final String channelName = "Ex: " + excitationType + "(" + excitationName + ") / Em: " + emissionName + " / Fl: " + fluorophoreName; ci.name = channelName; return ci; } private WellInfo readWellInfo( final Element wellEl, final double pixelWidth, final double pixelHeight, final int tileWidth, final int tileHeight ) { final WellInfo info = new WellInfo(); info.UID = Integer.parseInt( getChildText( wellEl, "UniqueID" ) ); info.number = Integer.parseInt( getChildText( wellEl, "Number" ) ); info.row = Integer.parseInt( getChildText( wellEl, "Row" ) ); info.col = Integer.parseInt( getChildText( wellEl, "Column" ) ); info.centerX = Double.parseDouble( getChildText( wellEl, new String[] { "CenterCoord_mm", "X" } ) ); info.centerY = Double.parseDouble( getChildText( wellEl, new String[] { "CenterCoord_mm", "Y" } ) ); final Element areasEl = getChild( wellEl, "Areas" ); final List< Element > areaEls = getChildren( areasEl, "Area" ); int areaIndex = 0; int fieldIndex = 1; for ( final Element areaEl : areaEls ) { final AreaInfo area = readArea( areaEl, fieldIndex, pixelWidth, pixelHeight, tileWidth, tileHeight ); area.index = areaIndex++; info.areas.add( area ); // Continue incrementing field index across areas. fieldIndex = area.fields.get( area.fields.size() - 1 ).index + 1; } return info; } private AreaInfo readArea( final Element areaEl, int startingFieldIndex, final double pixelWidth, final double pixelHeight, final int tileWidth, final int tileHeight ) { final AreaInfo info = new AreaInfo(); info.UID = Integer.parseInt( getChildText( areaEl, "UniqueID" ) ); // Read field position in um double xmin = Double.POSITIVE_INFINITY; double ymin = Double.POSITIVE_INFINITY; double xmax = Double.NEGATIVE_INFINITY; double ymax = Double.NEGATIVE_INFINITY; final Element fieldsEl = getChild( areaEl, "Fields" ); final List< Element > fieldEls = getChildren( fieldsEl, "Field" ); // Read basic info and get min & max. for ( final Element fieldEl : fieldEls ) { final FieldInfo finfo = readField( fieldEl ); info.fields.add( finfo ); final double xum = finfo.x; if ( xum < xmin ) { xmin = xum; } if ( xum > xmax ) { xmax = xum; } final double yum = -finfo.y; if ( yum < ymin ) { ymin = yum; } if ( yum > ymax ) { ymax = yum; } } for ( final FieldInfo finfo : info.fields ) { final long xpixels = Math.round( ( finfo.x - xmin ) / pixelWidth ); /* * Careful! For the fields to be padded correctly, we need to invert * their Y position, so that it matches the pixel orientation. */ final long ypixels = Math.round( ( -ymin - finfo.y ) / pixelHeight ); finfo.xpixels = xpixels; finfo.ypixels = ypixels; /* * Field index. * * Now there is a complexity regarding the way fields (that is: * tiles in common meaning) are indexed in the 'ImageIndex.xml' * file. Even if for a well you have two areas made of 5 tiles each, * there is no indexing of the areas. The field index simply keeps * increasing when you go from one area to the next one, and this * index follows the appearance order of the 'Field' xml element in * the 'MeasurementResult.xml' file. */ finfo.index = startingFieldIndex++; } final int width = 1 + ( int ) ( ( xmax - xmin ) / pixelWidth ); final int height = 1 + ( int ) ( ( ymax - ymin ) / pixelHeight ); info.width = width + tileWidth; info.height = height + tileHeight; return info; } private FieldInfo readField( final Element fieldEl ) { final FieldInfo info = new FieldInfo(); info.x = Double.parseDouble( getChildText( fieldEl, "StageX_um" ) ); info.y = Double.parseDouble( getChildText( fieldEl, "StageY_um" ) ); // I discarded the other info (BottomOffset & co) for I don't what to do // with them. return info; } private List< Integer > readTimePoints( final Document document ) { final Element root = document.getDocumentElement(); final int nTimePoints = Integer.parseInt( getChildText( root, new String[] { "TimelapsCondition", "Iteration" } ) ); // final List< Integer > timepoints = new ArrayList< Integer >( nTimePoints ); for ( int i = 0; i < nTimePoints; i++ ) { timepoints.add( Integer.valueOf( i ) ); } return timepoints; } private double readFrameInterval( final Document document ) { final Element root = document.getDocumentElement(); final double dt = Double.parseDouble( getChildText( root, new String[] { "TimelapsCondition", "Interval" } ) ); return dt; } /* * INNER CLASSES */ private static final class FieldInfo { public int index; public long ypixels; public long xpixels; public double y; public double x; @Override public String toString() { return "\t\tField index = " + index + "\n\t\t\tX = " + x + " µm\n\t\t\tY = " + y + " µm\n" + "\t\t\txi = " + xpixels + " pixels\n" + "\t\t\tyi = " + ypixels + " pixels\n"; } } private static final class AreaInfo { public int index; public int height; public int width; public List< FieldInfo > fields = new ArrayList< FieldInfo >(); public int UID; @Override public String toString() { final StringBuilder str = new StringBuilder(); str.append( "\tArea ID = " + UID + '\n' ); str.append( "\t\ttotal width = " + width + " pixels\n" ); str.append( "\t\ttotal height = " + height + " pixels\n" ); for ( final FieldInfo fieldInfo : fields ) { str.append( fieldInfo.toString() ); } return str.toString(); } } private static final class WellInfo { public List< AreaInfo > areas = new ArrayList< AreaInfo >(); public double centerY; public double centerX; public int col; public int row; public int number; public int UID; @Override public String toString() { final StringBuilder str = new StringBuilder(); str.append( "Well ID = " + UID + '\n' ); str.append( "\tnumber = " + number + '\n' ); str.append( "\trow = " + row + '\n' ); str.append( "\tcol = " + col + '\n' ); str.append( "\tcenter X = " + centerX + " mm\n" ); str.append( "\tcenter Y = " + centerY + " mm\n" ); for ( final AreaInfo areaInfo : areas ) { str.append( areaInfo.toString() ); } return str.toString(); } } private static final class ChannelInfo { public String name; public Color color; public int height; public int width; public boolean isEnabled; public double unmagnifiedPixelHeight; public double unmagnifiedPixelWidth; public int tileHeight; public int tileWidth; public int channelNumber; @Override public String toString() { final StringBuffer str = new StringBuffer(); str.append( "Channel " + channelNumber + ": \n" ); str.append( " - name: " + name + "\n" ); str.append( " - isEnabled: " + isEnabled + "\n" ); str.append( " - width: " + width + "\n" ); str.append( " - height: " + height + "\n" ); str.append( " - tile width: " + tileWidth + "\n" ); str.append( " - tile height: " + tileHeight + "\n" ); str.append( " - unmagnifiedPixelWidth: " + unmagnifiedPixelWidth + "\n" ); str.append( " - unmagnifiedPixelHeight: " + unmagnifiedPixelHeight + "\n" ); return str.toString(); } } private static final Element getChild( final Element parent, final String childName ) { final NodeList childNodes = parent.getChildNodes(); for ( int i = 0; i < childNodes.getLength(); i++ ) { final Node item = childNodes.item( i ); if ( item.getNodeName().equals( childName ) ) { return ( Element ) item; } } return null; } private static final Element getChild( final Element parent, final String[] path ) { if ( path.length == 1 ) { return getChild( parent, path[ 0 ] ); } final NodeList childNodes = parent.getChildNodes(); for ( int i = 0; i < childNodes.getLength(); i++ ) { final Node item = childNodes.item( i ); if ( item.getNodeName().equals( path[ 0 ] ) ) { return getChild( ( Element ) item, Arrays.copyOfRange( path, 1, path.length ) ); } } return null; } private static final List< Element > getChildren( final Element parent, final String name ) { final NodeList nodeList = parent.getElementsByTagName( name ); final int nEls = nodeList.getLength(); final List< Element > children = new ArrayList< Element >( nEls ); for ( int i = 0; i < nEls; i++ ) { children.add( ( Element ) nodeList.item( i ) ); } return children; } private static final String getChildText( final Element parent, final String[] path ) { if ( path.length == 1 ) { return getChildText( parent, path[ 0 ] ); } final NodeList childNodes = parent.getChildNodes(); for ( int i = 0; i < childNodes.getLength(); i++ ) { final Node item = childNodes.item( i ); if ( item.getNodeName().equals( path[ 0 ] ) ) { return getChildText( ( Element ) item, Arrays.copyOfRange( path, 1, path.length ) ); } } return null; } private static final String getChildText( final Element parent, final String childName ) { final NodeList childNodes = parent.getChildNodes(); for ( int i = 0; i < childNodes.getLength(); i++ ) { final Node item = childNodes.item( i ); if ( item.getNodeName().equals( childName ) ) { return item.getTextContent(); } } return null; } public static void main( final String[] args ) throws IOException, FormatException, ServiceException { // final String id = // "/Users/tinevez/Projects/EArena/Data/TestDataset/20131025T092701/MeasurementSetting.xml"; // final String id = // "/Users/tinevez/Projects/EArena/Data/30um sections at 40x - last round/1_3_1_2_2/20130731T133622/MeasurementResult.xml"; final String id = "/Users/tinevez/Projects/EArena/Data/TestDataset/20131030T142837"; final CellVoyagerReader importer = new CellVoyagerReader(); importer.setId( id ); final List< CoreMetadata > cms = importer.getCoreMetadataList(); for ( final CoreMetadata coreMetadata : cms ) { System.out.println( coreMetadata ); } final Hashtable< String, Object > meta = importer.getGlobalMetadata(); final String[] keys = MetadataTools.keys( meta ); for ( final String key : keys ) { System.out.println( key + " = " + meta.get( key ) ); } importer.openBytes( 0 ); importer.setSeries( 1 ); final String[] usedFiles = importer.getSeriesUsedFiles(); for ( final String file : usedFiles ) { System.out.println( " " + file ); } importer.close(); } }