/* * ------------------------------------------------------------------------ * * 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.base.data.img; import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.DataInput; import java.io.DataOutput; import java.io.File; import java.io.IOException; import java.util.Arrays; import org.knime.core.data.DataCell; import org.knime.core.data.DataCellDataInput; import org.knime.core.data.DataType; import org.knime.core.data.StringValue; import org.knime.core.data.filestore.FileStore; import org.knime.core.data.filestore.FileStoreCell; import org.knime.knip.base.KNIMEKNIPPlugin; import org.knime.knip.base.data.CachedObjectAccess; import org.knime.knip.base.data.CachedObjectAccess.StreamSkipper; import org.knime.knip.base.data.FileStoreCellMetadata; import org.knime.knip.base.data.IntervalValue; import org.knime.knip.base.renderer.ThumbnailRenderer; import org.knime.knip.core.KNIPGateway; import org.knime.knip.core.awt.AWTImageTools; import org.knime.knip.core.awt.Real2GreyColorRenderer; import org.knime.knip.core.data.img.DefaultImgMetadata; import org.knime.knip.core.io.externalization.BufferedDataInputStream; import org.knime.knip.core.io.externalization.ExternalizerManager; import org.knime.knip.core.util.MinimaUtils; import org.scijava.Named; import org.scijava.cache.CacheService; import net.imagej.ImgPlus; import net.imagej.ImgPlusMetadata; import net.imagej.Sourced; import net.imagej.axis.CalibratedAxis; import net.imagej.space.CalibratedSpace; import net.imglib2.FinalInterval; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.Img; import net.imglib2.img.ImgView; import net.imglib2.img.WrappedImg; import net.imglib2.ops.operation.SubsetOperations; import net.imglib2.ops.util.MetadataUtil; import net.imglib2.type.numeric.RealType; import net.imglib2.util.Util; import net.imglib2.view.Views; /** * * File cell keeping {@link ImgPlus}. * * @param <T> Type of cell * @author <a href="mailto:dietzc85@googlemail.com">Christian Dietz</a> * @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a> * @author <a href="mailto:michael.zinsmaier@googlemail.com">Michael Zinsmaier</a> */ public class ImgPlusCell<T extends RealType<T>> extends FileStoreCell implements ImgPlusValue<T>, StringValue, IntervalValue { /** * Type * * FIXME: Replace */ public static DataType TYPE = DataType.getType(ImgPlusCell.class); /** * ObjectRepository */ private static final CacheService CACHE = KNIPGateway.cache(); /** * UID */ private static final long serialVersionUID = 1L; private FileStoreCellMetadata m_fileMetadata; private CachedObjectAccess<Img<T>> m_imgAccess; private CachedObjectAccess<ImgPlusCellMetadata> m_metadataAccess; /** * @param img * @return */ private static long[] getMinFromImg(final Img<?> img) { long[] min = new long[img.numDimensions()]; img.min(min); return min; } /** * Creates a new img plus cell using the given file store, i.e. writes the image data into the file provided by the * file store. * * @param img * @param metadata * * @param fileStore */ @SuppressWarnings("unchecked") protected ImgPlusCell(final Img<T> img, final ImgPlusMetadata metadata, final FileStore fileStore) { super(fileStore); Img<T> tmpImg = img; while (tmpImg instanceof WrappedImg) { tmpImg = ((WrappedImg<T>)tmpImg).getImg(); } m_imgAccess = new CachedObjectAccess<Img<T>>(fileStore, tmpImg); final long[] dimensions = new long[img.numDimensions()]; img.dimensions(dimensions); m_metadataAccess = new CachedObjectAccess<ImgPlusCellMetadata>(fileStore, new ImgPlusCellMetadata( MetadataUtil.copyImgPlusMetadata(metadata, new DefaultImgMetadata(dimensions.length)), img.size(), getMinFromImg(img), dimensions, img.firstElement().getClass(), null)); m_fileMetadata = new FileStoreCellMetadata(-1, false, null); CACHE.put(this, this); } /** * @throws IOException */ protected ImgPlusCell() throws IOException { super(); } /** * @param fileStore */ protected ImgPlusCell(final FileStore fileStore) { super(fileStore); } private BufferedImage createThumbnail(final double factor) { final Img<T> tmpImg = m_imgAccess.get(); // make sure that at least two dimensions exist RandomAccessibleInterval<T> img2d; if (tmpImg.numDimensions() > 1) { img2d = tmpImg; } else { img2d = Views.addDimension(tmpImg, 0, 0); } int i = 0; final long[] max = new long[img2d.numDimensions()]; final long[] min = new long[img2d.numDimensions()]; max[0] = img2d.max(0); max[1] = img2d.max(1); min[0] = img2d.min(0); min[1] = img2d.min(1); boolean thirdActive = false; for (i = 2; i < img2d.numDimensions(); i++) { if (((img2d.dimension(i) == 2) || (img2d.dimension(i) == 3)) && !thirdActive) { max[i] = img2d.max(i); min[i] = img2d.min(i); thirdActive = true; } else { min[i] = img2d.min(i); max[i] = img2d.min(i); } } RandomAccessibleInterval<T> toRender; if (img2d == tmpImg) { toRender = SubsetOperations.subsetview(tmpImg, new FinalInterval(min, max)); } else { toRender = img2d; } // Workaround if (toRender.numDimensions() == 1 && toRender.dimension(0) < 3) { toRender = tmpImg.factory().create(toRender, tmpImg.firstElement().createVariable()); } return AWTImageTools.renderScaledStandardColorImg(toRender, new Real2GreyColorRenderer<T>(2, Util.getTypeFromInterval(toRender).getMinValue()), factor, new long[max.length]); } /** * {@inheritDoc} */ @Override protected boolean equalsDataCell(final DataCell dc) { return dc.hashCode() == hashCode(); } /** * {@inheritDoc} */ @Override public synchronized CalibratedSpace<CalibratedAxis> getCalibratedSpace() { return m_metadataAccess.get().getMetadata(); } /** * {@inheritDoc} */ @Override public synchronized long[] getDimensions() { return m_metadataAccess.get().getDimensions().clone(); } private ImgPlusCellMetadata getImgMetadataToWrite() { final ImgPlusCellMetadata tmpImgMetadata = m_metadataAccess.get(); int height = KNIMEKNIPPlugin.getMaximumImageCellHeight(); if (height == 0) { height = (int)tmpImgMetadata.getDimensions()[1]; } final int width = getThumbnailWidth(height); if (((height * width) / (double)tmpImgMetadata.getSize()) < KNIMEKNIPPlugin.getThumbnailImageRatio()) { getThumbnail(null); return tmpImgMetadata; } else { return new ImgPlusCellMetadata(tmpImgMetadata.getMetadata(), tmpImgMetadata.getSize(), tmpImgMetadata.getMinimum(), tmpImgMetadata.getDimensions(), tmpImgMetadata.getPixelType(), null); } } /** * {@inheritDoc} */ @Override public synchronized ImgPlus<T> getImgPlus() { Img<T> tmpImg = m_imgAccess.get(); final long[] minimum = getMinimum(); final long[] localMin = new long[minimum.length]; tmpImg.min(localMin); if (!Arrays.equals(minimum, localMin)) { for (int d = 0; d < minimum.length; d++) { if (minimum[d] != 0) { tmpImg = ImgView.wrap(Views.translate(tmpImg, minimum), tmpImg.factory()); break; } } } final ImgPlus<T> imgPlus = new ImgPlus<T>(tmpImg, m_metadataAccess.get().getMetadata()); imgPlus.setSource(m_metadataAccess.get().getMetadata().getSource()); return imgPlus; } /** * {@inheritDoc} */ @Override public synchronized ImgPlus<T> getImgPlusCopy() { final ImgPlus<T> source = getImgPlus(); final ImgPlus<T> dest = new ImgPlus<T>(source.copy()); MetadataUtil.copyImgPlusMetadata(source, dest); return MinimaUtils.getTranslatedImgPlus(source, dest); } /** * {@inheritDoc} */ @Override public synchronized long[] getMaximum() { final ImgPlusCellMetadata imgPlusMetadta = m_metadataAccess.get(); final long[] max = new long[imgPlusMetadta.getDimensions().length]; if (imgPlusMetadta.getMinimum() == null) { for (int i = 0; i < max.length; i++) { max[i] = imgPlusMetadta.getDimensions()[i] - 1; } } else { for (int i = 0; i < max.length; i++) { max[i] = imgPlusMetadta.getMinimum()[i] + imgPlusMetadta.getDimensions()[i] - 1; } } return max; } /** * {@inheritDoc} */ @Override public synchronized ImgPlusMetadata getMetadata() { final ImgPlusMetadata res = new DefaultImgMetadata(m_metadataAccess.get().getDimensions().length); MetadataUtil.copyImgPlusMetadata(m_metadataAccess.get().getMetadata(), res); return res; } /** * {@inheritDoc} * * @deprecated */ @Deprecated @Override public synchronized long[] getMinimum() { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); long[] min; if (tmp.getMinimum() == null) { min = new long[tmp.getDimensions().length]; } else { min = tmp.getMinimum().clone(); } return min; } /** * {@inheritDoc} */ @Override public synchronized Named getName() { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); return tmp.getMetadata(); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public synchronized Class<T> getPixelType() { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); return (Class<T>)tmp.getPixelType(); } /** * {@inheritDoc} */ @Override public synchronized Sourced getSource() { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); return tmp.getMetadata(); } /** * {@inheritDoc} */ @Override public synchronized String getStringValue() { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); final StringBuffer sb = new StringBuffer(); sb.append("Image[\nname="); sb.append(tmp.getMetadata().getName()); sb.append(";\nsource="); sb.append(tmp.getMetadata().getSource()); sb.append(";\ndimensions="); final int numDims = tmp.getDimensions().length; for (int i = 0; i < (numDims - 1); i++) { sb.append(tmp.getDimensions()[i]); sb.append(","); } sb.append(tmp.getDimensions()[numDims - 1]); sb.append(" ("); for (int i = 0; i < (numDims - 1); i++) { sb.append(tmp.getMetadata().axis(i).type().getLabel()); sb.append(","); } sb.append(tmp.getMetadata().axis(numDims - 1).type().getLabel()); sb.append(")"); sb.append(";\nmin="); final long[] min = tmp.getMinimum() == null ? new long[numDims] : tmp.getMinimum(); for (int i = 0; i < (numDims - 1); i++) { sb.append(min[i]); sb.append(","); } sb.append(min[numDims - 1]); sb.append(";\npixel type="); sb.append(tmp.getPixelType().getSimpleName()); sb.append(")]"); return sb.toString(); } /** * {@inheritDoc} */ @Override public synchronized Image getThumbnail(final RenderingHints renderingHints) { boolean renderMetadata; if ((renderingHints != null) && (renderMetadata = renderingHints.get(ThumbnailRenderer.RENDERING_HINT_KEY_METADATA) != null) && renderMetadata) { final int height = 200; return AWTImageTools.makeTextBufferedImage(getStringValue(), height * 3, height, "\n"); } else { double height = 1; // default for 1d images double fullHeight = 1; ImgPlusCellMetadata tmp = m_metadataAccess.get(); if (tmp.getDimensions().length > 1) { height = Math.min(tmp.getDimensions()[1], KNIMEKNIPPlugin.getMaximumImageCellHeight()); if (height == 0) { height = tmp.getDimensions()[1]; } fullHeight = tmp.getDimensions()[1]; } if ((tmp.getThumbnail() == null) || (tmp.getThumbnail().getHeight() != height)) { m_metadataAccess = new CachedObjectAccess<>(getFileStore(), tmp = new ImgPlusCellMetadata(tmp.getMetadata(), tmp.getSize(), tmp.getMinimum(), tmp.getDimensions(), tmp.getPixelType(), createThumbnail(height / fullHeight))); // update cached object CACHE.put(this, this); } return tmp.getThumbnail(); } } private int getThumbnailWidth(final int height) { final ImgPlusCellMetadata tmp = m_metadataAccess.get(); if (tmp.getDimensions().length == 1) { return (int)tmp.getDimensions()[0]; } else { return (int)(((double)tmp.getDimensions()[0] / tmp.getDimensions()[1]) * height); } } /** * {@inheritDoc} */ @Override public int hashCode() { return (int)(getFileStore().getFile().hashCode() + (31 * m_fileMetadata.getOffset())); } /** * Loads the cell content. To be called after the {@link #FileImgPlusCell(DataCellDataInput)} constructor. * * @param input * @throws IOException */ @SuppressWarnings("javadoc") protected synchronized void load(final DataInput input) throws IOException { try { // m_fileMetadata = // ExternalizerManager.read(stream); // work-around bug#3578 // to be backwards-compatible: final int length = input.readInt(); for (int i = 0; i < length; i++) { input.readChar(); } } catch (final Exception e) { if (e instanceof IOException) { throw (IOException)e; } } m_fileMetadata = new FileStoreCellMetadata(input.readLong(), input.readBoolean(), null); } /** * {@inheritDoc} */ @Override protected void postConstruct() { @SuppressWarnings("unchecked") final ImgPlusCell<T> tmp = (ImgPlusCell<T>)CACHE.get(this); if (tmp == null) { m_metadataAccess = new CachedObjectAccess<ImgPlusCellMetadata>(getFileStore(), null, m_fileMetadata.getOffset(), null); m_imgAccess = new CachedObjectAccess<Img<T>>(getFileStore(), null, m_fileMetadata.getOffset(), new StreamSkipper() { /** * {@inheritDoc} */ @Override public void skip(final BufferedDataInputStream in) { try { m_metadataAccess.setObject(ExternalizerManager.read(in)); } catch (Exception e) { throw new RuntimeException(e); } } }); CACHE.put(this, this); } else { m_metadataAccess = tmp.m_metadataAccess; m_imgAccess = tmp.m_imgAccess; } } /** * Stores the cell content. A few meta information to the data output, the heavy data to the file. * * @param output * @throws IOException */ protected synchronized void save(final DataOutput output) throws IOException { long offset = m_fileMetadata.getOffset(); try { // if the image data wasn't made persitent yet ... flushToFileStore(); // ExternalizerManager.write(stream, // m_fileMetadata); // work-around for bug #3578: if the output is // wrapped // with the BufferedDataOutputStream, the data // cell // cannot be wrapped into a ListCell output.writeInt(0); // to be // backwards-compatible output.writeLong(offset); output.writeBoolean(true); } catch (final Exception e) { if (e instanceof IOException) { throw (IOException)e; } } } /** * {@inheritDoc} */ @Override protected synchronized void flushToFileStore() throws IOException { long offset = m_fileMetadata.getOffset(); final boolean isPersistent = m_fileMetadata.isPersistent(); // if the image data wasn't made persitent yet ... if (!isPersistent) { final File file = getFileStore().getFile(); offset = file.length(); // write image data m_metadataAccess.setObject(getImgMetadataToWrite()); m_metadataAccess.serialize(); m_imgAccess.serialize(); } m_fileMetadata = new FileStoreCellMetadata(offset, true, null); } /** * {@inheritDoc} */ @Override public String toString() { return getStringValue(); } }