/* * ------------------------------------------------------------------------ * * Copyright (C) 2003 - 2013 * University of Konstanz, Germany and * KNIME GmbH, Konstanz, Germany * Website: http://www.knime.org; Email: contact@knime.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, Version 3, as * published by the Free Software Foundation. * * 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>. * * Additional permission under GNU GPL version 3 section 7: * * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. * Hence, KNIME and ECLIPSE are both independent programs and are not * derived from each other. Should, however, the interpretation of the * GNU GPL Version 3 ("License") under any applicable laws result in * KNIME and ECLIPSE being a combined program, KNIME GMBH herewith grants * you the additional permission to use and propagate KNIME together with * ECLIPSE with only the license terms in place for ECLIPSE applying to * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the * license terms of ECLIPSE themselves allow for the respective use and * propagation of ECLIPSE together with KNIME. * * Additional permission relating to nodes for KNIME that extend the Node * Extension (and in particular that are based on subclasses of NodeModel, * NodeDialog, and NodeView) and that only interoperate with KNIME through * standard APIs ("Nodes"): * Nodes are deemed to be separate and independent programs and to not be * covered works. Notwithstanding anything to the contrary in the * License, the License does not apply to Nodes, you are not required to * license Nodes under the License, and you are granted a license to * prepare and propagate Nodes, in each case even if such Nodes are * propagated with or for interoperation with KNIME. The owner of a Node * may freely choose the license terms applicable to such Node, including * when such Node is propagated with or for interoperation with KNIME. * --------------------------------------------------------------------- * * */ package org.knime.knip.io.nodes.imgwriter; import java.io.IOException; import java.util.HashMap; import loci.common.DataTools; import loci.common.services.DependencyException; import loci.common.services.ServiceException; import loci.common.services.ServiceFactory; import loci.formats.ClassList; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.IFormatWriter; import loci.formats.ImageWriter; import loci.formats.MetadataTools; import loci.formats.MissingLibraryException; import loci.formats.meta.IMetadata; import loci.formats.services.OMEXMLService; import net.imglib2.Cursor; import net.imglib2.FinalInterval; import net.imglib2.img.Img; import net.imglib2.iterator.IntervalIterator; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.ByteType; import net.imglib2.type.numeric.integer.IntType; import net.imglib2.type.numeric.integer.ShortType; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.integer.UnsignedIntType; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.view.Views; import org.knime.core.node.NodeLogger; import ome.xml.model.enums.DimensionOrder; import ome.xml.model.enums.PixelType; /** * Provides the functionality to write {@link Img}s using the <a href = * "http://loci.wisc.edu/bio-formats/">bioformats</a>-library. * * * @author hornm, University of Konstanz */ public class ImgWriter { private static final NodeLogger LOGGER = NodeLogger.getLogger(ImgWriter.class); /* * List of the names of the available writers associated with specific * formats. */ private String[] m_writers; /* map from the writer names to the actual writers */ private HashMap<String, IFormatWriter> m_mapWriters = null; /* Frame rate to use when writing in frames per second, if applicable. */ private int m_fps = 10; /** * Creates an new writer. * */ public ImgWriter() { } /* helper to get the list of the supported writers */ private void retrieveSupportedWriters() { if (m_mapWriters == null) { loci.formats.ImageWriter writer; final ClassList<IFormatWriter> defaultClasses = ImageWriter .getDefaultWriterClasses(); writer = new loci.formats.ImageWriter(defaultClasses); final IFormatWriter[] writers = writer.getWriters(); m_writers = new String[writers.length]; m_mapWriters = new HashMap<String, IFormatWriter>(); for (int i = 0; i < writers.length; i++) { m_writers[i] = writers[i].getFormat() + " (" + writers[i].getSuffixes()[0] + ")"; m_mapWriters.put(m_writers[i], writers[i]); } } } /** * Returns the list of the possible compression types of the specific * writer. * * @param writer * the name of the writer * @return the list of possible compressions, <code>null</code> if there are * no compression types */ public String[] getCompressionTypes(final String writer) { retrieveSupportedWriters(); final IFormatWriter w = m_mapWriters.get(writer); if (w == null) { return null; } return w.getCompressionTypes(); } /** * @return the available writers as strings */ public String[] getWriters() { retrieveSupportedWriters(); return m_writers; } /** * Gets one suffix normally used to identify the format associated with the * specific writer. * * @param writer * the writer * @return the suffix, e.g. '.tif' */ public String getSuffix(final String writer) { retrieveSupportedWriters(); final IFormatWriter w = m_mapWriters.get(writer); if (w == null) { return null; } return w.getSuffixes()[0]; } /** * Writes the image plane stack to the given file. The resulting image * format is determined by the given writer. * * @param img * the image to be written * @param <T> * the image type * * @param outfile * the absolute path of the file to write in * @param writer * the writer * @param compressionType * the compression type, if available, can be <code>null</code>. * @param dimMapping * mapping of the image dimensions (without X and Y) to the * dimension order ZCT (array must be of length 3), If * <code>null</code> Z=dim2, C=dim3, T=dim4. If a mapping is -1 , * that particular dimensions is assumed to be not existent. * @throws IOException * @throws FormatException * @throws MissingLibraryException * @throws ServiceException * @throws DependencyException */ public <T extends RealType<T>> void writeImage(final Img<T> img, final String outfile, final String writer, final String compressionType, final int[] dimMapping) throws FormatException, IOException, MissingLibraryException, ServiceException, DependencyException { retrieveSupportedWriters(); writeImage(img, outfile, m_mapWriters.get(writer), compressionType, dimMapping); } /** * Writes the image plane stack to the given file. The resulting image * format is determined by the given writer. * * @param img * the image to be written * @param <T> * the image type * * @param outfile * the absolute path of the file to write in * @param writer * the writer * @param compressionType * the compression type, if available, can be <code>null</code>. * @param dimMapping * mapping of the image dimensions (without X and Y) to the * dimension order ZCT (array must be of length 3), If * <code>null</code> Z=dim2, C=dim3, T=dim4. If a mapping is -1 , * that particular dimensions is assumed to be not existent. * @throws IOException * @throws FormatException * @throws MissingLibraryException * @throws ServiceException * @throws DependencyException */ public <T extends RealType<T>> void writeImage(final Img<T> img, final String outfile, final IFormatWriter writer, final String compressionType, final int[] dimMapping) throws FormatException, IOException, MissingLibraryException, ServiceException, DependencyException { // create metadata object with minimum required metadata // fields final ServiceFactory factory = new ServiceFactory(); final OMEXMLService service = factory.getInstance(OMEXMLService.class); final IMetadata store = service.createOMEXMLMetadata(); // retrieve the pixeltype PixelType ptype = null; int ftptype; final T val = img.firstElement().createVariable(); if (val instanceof BitType) { ptype = PixelType.INT8; ftptype = FormatTools.INT8; } else if (val instanceof ByteType) { ptype = PixelType.INT8; ftptype = FormatTools.INT8; } else if (val instanceof UnsignedByteType) { ptype = PixelType.UINT8; ftptype = FormatTools.UINT8; } else if (val instanceof ShortType) { ptype = PixelType.INT16; ftptype = FormatTools.INT16; } else if (val instanceof UnsignedShortType) { ptype = PixelType.UINT16; ftptype = FormatTools.UINT16; } else if (val instanceof IntType) { ptype = PixelType.INT32; ftptype = FormatTools.INT32; } else if (val instanceof UnsignedIntType) { ptype = PixelType.UINT32; ftptype = FormatTools.UINT32; } else if (val instanceof FloatType) { ptype = PixelType.FLOAT; ftptype = FormatTools.FLOAT; } else if (val instanceof DoubleType) { ptype = PixelType.DOUBLE; ftptype = FormatTools.DOUBLE; } else { throw new FormatException("The given image format (" + val.getClass().toString() + ") can't be writen!"); } if (store == null) { throw new MissingLibraryException("OME-XML Java library not found."); } int[] map; if ((dimMapping == null) || (dimMapping.length != 3)) { map = new int[] { 2, 3, 4 }; } else { map = dimMapping.clone(); } final int numDim = img.numDimensions(); final int sizeX = (int) img.dimension(0); final int sizeY = (int) img.dimension(1); final int sizeZ = ((img.numDimensions() > map[0]) && (map[0] != -1)) ? (int) img .dimension(2 + map[0]) : 1; int sizeC = (img.numDimensions() > map[1]) && (map[1] != -1) ? (int) img .dimension(2 + map[1]) : 1; if (sizeC > 3) { LOGGER.warn("Image has more than 3 channels. These channels will be ignored."); sizeC = 3; } final int sizeT = (img.numDimensions() > map[2]) && (map[2] != -1) ? (int) img .dimension(2 + map[2]) : 1; MetadataTools.populateMetadata(store, 0, outfile, false, "XYZCT", FormatTools.getPixelTypeString(ftptype), sizeX, sizeY, sizeZ, sizeC, sizeT, sizeC); if (img.numDimensions() > 5) { LOGGER.warn("Image has more than five dimension. These dimensions will be ignored."); } if ((sizeC > 1) && !((val instanceof ByteType) || (val instanceof UnsignedByteType))) { throw new FormatException("RGB images must be of type byte!"); } // write image plane to disk writer.setMetadataRetrieve(store); writer.setFramesPerSecond(m_fps); writer.setId(outfile); if ((compressionType != null) && (writer.getCompressionTypes() != null)) { writer.setCompression(compressionType); } if (!writer.isSupportedType(ftptype)) { final int[] supportedPTypes = writer.getPixelTypes(); final StringBuffer types = new StringBuffer(); for (int i = 0; i < supportedPTypes.length; i++) { types.append(FormatTools.getPixelTypeString(supportedPTypes[i]) + " "); } throw new FormatException( ptype.toString() + " not supported by the selected image format. Supported are " + types.toString() + "."); } // convert and save slices final boolean doStack = writer.canDoStacks(); if (!doStack && ((sizeT > 1) || (sizeZ > 1))) { throw new FormatException( "Seleted format doesn't support image stacks."); } writer.setInterleaved(false); final boolean littleEndian = !writer.getMetadataRetrieve() .getPixelsBinDataBigEndian(0, 0).booleanValue(); final IntervalIterator fakeCursor = new IntervalIterator(new int[] { sizeZ, sizeT }); final byte[][] planes = new byte[sizeC][]; long[] pos; final int numSteps = sizeT * sizeZ; while (fakeCursor.hasNext()) { fakeCursor.fwd(); // iterate through the channels for (int i = 0; i < sizeC; i++) { final long[] zctPos = new long[] { fakeCursor.getLongPosition(0), i, fakeCursor.getLongPosition(1) }; // map xyzct pos to img dimensions switch (numDim) { case 2: pos = new long[] { 0, 0 }; break; case 3: pos = new long[3]; for (int j = 0; j < map.length; j++) { if ((map[j] != -1) && (map[j] < 3)) { pos[2 + map[j]] = zctPos[j]; break; } } break; case 4: pos = new long[4]; pos[2] = -1; for (int j = 0; j < map.length; j++) { if ((map[j] != -1) && (map[j] < 4)) { if (pos[2] == -1) { pos[2 + map[j]] = zctPos[j]; } else { pos[2 + map[j]] = zctPos[j]; break; } } } break; // five or more dimensions default: pos = new long[numDim]; for (int j = 0; j < map.length; j++) { if (map[j] != -1) { pos[2 + map[j]] = zctPos[j]; } } break; } long[] min = pos.clone(); long[] max = pos.clone(); min[0] = 0; min[1] = 0; max[0] = img.max(0); max[1] = img.max(1); Cursor<T> c = Views.interval(img, new FinalInterval(min, max)).cursor(); if (val instanceof ByteType) { planes[i] = new byte[(int) (img.dimension(0) * img .dimension(1))]; while (c.hasNext()) { c.fwd(); planes[i][c.getIntPosition(0) + ((int) img.dimension(0) * c.getIntPosition(1))] = ((ByteType) c .get()).get(); } planes[i] = DataTools.makeSigned(planes[i]); } else if (val instanceof UnsignedByteType) { planes[i] = new byte[(int) (img.dimension(0) * img .dimension(1))]; while (c.hasNext()) { c.fwd(); planes[i][c.getIntPosition(0) + ((int) img.dimension(0) * c.getIntPosition(1))] = (byte) (((UnsignedByteType) c .get()).get() - 128); } planes[i] = DataTools.makeSigned(planes[i]); } else if (val instanceof ShortType) { final short[] tmp = new short[(int) (img.dimension(0) * img .dimension(1))]; while (c.hasNext()) { c.fwd(); tmp[c.getIntPosition(0) + ((int) img.dimension(0) * c.getIntPosition(1))] = ((ShortType) c .get()).get(); } planes[i] = DataTools.shortsToBytes(tmp, littleEndian); } else if (val instanceof FloatType) { final float[] tmp = new float[(int) (img.dimension(0) * img .dimension(1))]; while (c.hasNext()) { c.fwd(); tmp[c.getIntPosition(0) + ((int) img.dimension(0) * c.getIntPosition(1))] = ((FloatType) c .get()).get(); } planes[i] = DataTools.floatsToBytes(tmp, littleEndian); } else { throw new FormatException( "Pixel type not supported by this format."); } } final int index = FormatTools.getIndex( DimensionOrder.XYZCT.toString(), sizeZ, 1, sizeT, numSteps, fakeCursor.getIntPosition(0), 0, fakeCursor.getIntPosition(1)); // merge channel planes if ((sizeC > 1) && ((val instanceof ByteType) || (val instanceof UnsignedByteType))) { final byte[] rgb = new byte[planes[0].length * sizeC]; for (int j = 0; j < sizeC; j++) { System.arraycopy(planes[j], 0, rgb, planes[j].length * j, planes[j].length); } writer.saveBytes(index, rgb); } else { writer.saveBytes(index, planes[0]); } } writer.close(); } public void setFramesPerSecond(final int fps) { m_fps = fps; } /** * All operations to be done to close the image writer. * * @throws IOException */ public void close() throws IOException { // } }