/* * ------------------------------------------------------------------------ * * 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. * --------------------------------------------------------------------- * * Created on 13.11.2013 by Christian Dietz */ package org.knime.knip.base.node; import org.knime.core.data.DataRow; import org.knime.core.data.DataTableSpec; import org.knime.core.node.ExecutionContext; import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelBoolean; import org.knime.core.node.defaultnodesettings.SettingsModelString; import org.knime.core.node.port.PortObject; import org.knime.core.node.port.PortObjectSpec; import org.knime.knip.base.data.img.ImgPlusCell; import org.knime.knip.base.data.img.ImgPlusCellFactory; import org.knime.knip.base.data.img.ImgPlusValue; import org.knime.knip.base.data.labeling.LabelingValue; import org.knime.knip.base.exceptions.KNIPException; import org.knime.knip.base.node.nodesettings.SettingsModelDimSelection; import org.knime.knip.core.KNIPGateway; import org.knime.knip.core.util.EnumUtils; import org.knime.knip.core.util.MinimaUtils; import net.imagej.ImgPlus; import net.imglib2.Cursor; import net.imglib2.IterableInterval; import net.imglib2.RandomAccessibleInterval; import net.imglib2.ops.operation.ImgOperations; import net.imglib2.ops.operation.SubsetOperations; import net.imglib2.ops.operation.UnaryOperation; import net.imglib2.roi.Regions; import net.imglib2.roi.labeling.LabelRegion; import net.imglib2.roi.labeling.LabelRegions; import net.imglib2.roi.labeling.LabelingType; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; import net.imglib2.view.Views; /** * Remark: Note this class has some redundant implementations to {@link ImgPlusToImgPlusNodeModel}. Anyway, the * functionality provided by this class differs a lot. * * @author <a href="mailto:dietzc85@googlemail.com">Christian Dietz</a> * @author <a href="mailto:horn_martin@gmx.de">Martin Horn</a> * * @param <T> Type of Input * @param <V> Type of Output * @param <L> Type of Labeling * */ public abstract class IterableIntervalsNodeModel<T extends RealType<T>, V extends RealType<V> & NativeType<V>, L extends Comparable<L>> extends ValueToCellNodeModel<ImgPlusValue<T>, ImgPlusCell<V>> { private static final NodeLogger LOGGER = NodeLogger.getLogger(IterableIntervalsNodeModel.class); /** * Filling Mode. How areas outside the rois are treated * * @author Christian Dietz */ protected enum FillingMode { /** * fill with the value of the source img */ SOURCE("Value of Source"), /** * fill with the minimum of the resulting type */ RESMIN("Minimum of Result Type"), /** * fill with the maximum of the resulting type */ RESMAX("Maximum of Result Type"), /** * Keep as is (in most cases it's default zero) */ NOFILLING("No Filling"); private String name; private FillingMode(@SuppressWarnings("hiding") final String name) { this.name = name; } /** * {@inheritDoc} */ @Override public String toString() { return name; } } /** * Create the {@link SettingsModelDimSelection} * * @param axes * @return {@link SettingsModelDimSelection} */ protected static SettingsModelDimSelection createDimSelectionModel(final String... axes) { return new SettingsModelDimSelection("dim_selection", axes); } /** * Create the optional column model (here: labeling) * * @return {@link SettingsModelString} */ protected static SettingsModelString createOptionalColumnModel() { return new SettingsModelString("optinal_column_selection", ""); } /** * Create model to store whether pixels outside the ROIs should be filled with the values of the source img * * @return {@link SettingsModelBoolean} */ protected static SettingsModelString createFillingModeModel() { return new SettingsModelString("fill_non_roi_pixels", FillingMode.NOFILLING.toString()); } /* * Stores the first selected column. */ private final SettingsModelString m_optionalColumnModel = createOptionalColumnModel(); /* * Store the settings model for outside roi pixel filling */ private final SettingsModelString m_fillNonROIPixels = createFillingModeModel(); /* * The optional column (here: labeling) */ private int m_optionalColIdx; /* * Labeling of the current {@link DataRow}. Can be null if no column with labeling is selected by the user. */ private RandomAccessibleInterval<LabelingType<L>> m_currentLabeling; /** * Stores the dimension selection. Can be null. */ protected SettingsModelDimSelection m_dimSelectionModel = null; /** * Factory to create cells */ protected ImgPlusCellFactory m_cellFactory; // indicator whether dimension selection should be added private boolean m_hasDimSelection; /** * Convienience constructor. If you want to avoid dimension selection, you can do so using the other constructor. */ public IterableIntervalsNodeModel() { this(true); } /** * @param hasDimSelection set false if you don't want to use dimension selection */ public IterableIntervalsNodeModel(final boolean hasDimSelection) { this.m_hasDimSelection = hasDimSelection; if (hasDimSelection) { m_dimSelectionModel = createDimSelectionModel("X", "Y"); } } /** * {@inheritDoc} */ @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws Exception { return super.execute(inObjects, exec); } /** * {@inheritDoc} */ @Override protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws InvalidSettingsException { // for consistency with the GUI we need to disable our dim selection if a labeling is set (which absolutely makes sense) // if there exists a labeling, we need to decide what we do with the pixels outside the ROIs int idx = getOptionalColumnIdx((DataTableSpec)inSpecs[0]); if (idx == -1) { if (m_hasDimSelection) { m_dimSelectionModel.setEnabled(true); } m_fillNonROIPixels.setEnabled(false); } else { if (m_hasDimSelection) { m_dimSelectionModel.setEnabled(false); } m_fillNonROIPixels.setEnabled(true); } m_optionalColIdx = getOptionalColumnIdx((DataTableSpec)inSpecs[0]); return super.configure(inSpecs); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override protected void computeDataRow(final DataRow row) { if (m_optionalColIdx != -1 && !row.getCell(m_optionalColIdx).isMissing()) { m_currentLabeling = ((LabelingValue<L>)row.getCell(m_optionalColIdx)).getLabeling(); } } /** * {@inheritDoc} */ @Override protected void prepareExecute(final ExecutionContext exec) { m_cellFactory = new ImgPlusCellFactory(exec); } /** * {@inheritDoc} */ @Override protected void collectSettingsModels() { super.collectSettingsModels(); m_settingsModels.add(m_optionalColumnModel); if (m_hasDimSelection) { m_settingsModels.add(m_dimSelectionModel); } m_settingsModels.add(m_fillNonROIPixels); } /** * Get the optional column index. * * @param inSpec spec of the table * @return returns -1 if no column is selected. * * @throws InvalidSettingsException */ protected int getOptionalColumnIdx(final DataTableSpec inSpec) throws InvalidSettingsException { int optionalColIdx = -1; if (m_optionalColumnModel.getStringValue() != null && !m_optionalColumnModel.getStringValue().equalsIgnoreCase("")) { optionalColIdx = NodeUtils.autoColumnSelection(inSpec, m_optionalColumnModel, LabelingValue.class, this.getClass()); } return optionalColIdx; } /** * {@inheritDoc} */ @Override protected ImgPlusCell<V> compute(final ImgPlusValue<T> cellValue) throws Exception { ImgPlus<T> imgPlus = cellValue.getImgPlus(); ImgPlus<T> in = MinimaUtils.getZeroMinImgPlus(imgPlus); ImgPlus<V> res = createResultImage(in); if (m_hasDimSelection && !m_dimSelectionModel.isContainedIn(cellValue.getMetadata())) { LOGGER.warn("image " + cellValue.getMetadata().getName() + " does not provide all selected dimensions."); } V outType = getOutType(in.firstElement()); int[] selectedDimIndices = null; if (m_hasDimSelection) { selectedDimIndices = m_dimSelectionModel.getSelectedDimIndices(in); } else { // set all as selected selectedDimIndices = new int[in.numDimensions()]; for (int i = 0; i < selectedDimIndices.length; i++) { selectedDimIndices[i] = i; } } if (m_dimSelectionModel != null && m_dimSelectionModel.getNumSelectedDimLabels(cellValue.getMetadata()) == 0 && m_dimSelectionModel.getNumSelectedDimLabels() > 0) { throw new KNIPException("Not enough selected dimensions provided by image."); } prepareOperation(in.firstElement()); if (!isLabelingPresent()) { UnaryOperation<IterableInterval<T>, IterableInterval<V>> operation = operation(); if (selectedDimIndices.length != 0) { SubsetOperations.iterate(ImgOperations.wrapII(operation, outType), selectedDimIndices, in, res, getExecutorService()); } else { ImgOperations.wrapII(operation, outType).compute(in, res); } } else { final LabelRegions<L> regions = KNIPGateway.regions().regions(m_currentLabeling); for (LabelRegion<L> region : regions) { IterableInterval<T> inII = Regions.sample(region, in); IterableInterval<V> outII = Regions.sample(region, res); UnaryOperation<IterableInterval<T>, IterableInterval<V>> operation = operation(); // TODO parallelize over ROIs (tbd) operation.compute(inII, outII); } // TODO can we speed this up (or is it even slower if we would have empty pixel information?) FillingMode mode = EnumUtils.valueForName(m_fillNonROIPixels.getStringValue(), FillingMode.values()); switch (mode) { case NOFILLING: break; case RESMIN: fill(Views.flatIterable(res), outType.getMinValue()); break; case RESMAX: fill(Views.flatIterable(res), outType.getMaxValue()); break; case SOURCE: // here we need to do something special final Cursor<LabelingType<L>> cursor = Views.flatIterable(m_currentLabeling).cursor(); final Cursor<V> outCursor = Views.flatIterable(res).cursor(); final Cursor<T> inCursor = Views.flatIterable(in).cursor(); while (cursor.hasNext()) { if (cursor.next().isEmpty()) { outCursor.next().setReal(inCursor.next().getRealDouble()); } else { outCursor.fwd(); inCursor.fwd(); } } break; } } postExecute(); m_currentLabeling = null; return m_cellFactory.createCell(MinimaUtils.getTranslatedImgPlus(imgPlus, res)); } /** * Can be overriden by implementors */ protected void postExecute() { // can be overriden by implemnetors } // fills the res with val if labeling contains no labels at a certain position private void fill(final IterableInterval<V> res, final double val) { Cursor<LabelingType<L>> cursor = Views.flatIterable(m_currentLabeling).cursor(); Cursor<V> outCursor = res.cursor(); while (cursor.hasNext()) { if (cursor.next().isEmpty()) { outCursor.next().setReal(val); } else { outCursor.fwd(); } } } /** * This method can be overriden. If you do so, make sure, that you also copy the metadata of the incoming * {@link ImgPlus} into the resulting {@link ImgPlus}. * * This methods assumes that the incoming {@link ImgPlus} has the same dimensionality as the outgoing * {@link ImgPlus} * * @return */ private ImgPlus<V> createResultImage(final ImgPlus<T> in) { return new ImgPlus<V>(KNIPGateway.ops().create().img(in, getOutType(in.firstElement())), in); } /** * @return true if labeling is selected by user */ protected boolean isLabelingPresent() { return m_optionalColIdx > -1; } /** * Type of the output. Must be of type {@link RealType} * * @param inType the type of the incoming {@link IterableInterval} * * @return the type of the output */ protected abstract V getOutType(final T inType); /** * The {@link UnaryOperation} which will be used to compute the output {@link IterableInterval} of type V given the * input {@link IterableInterval} of type T * * @return the {@link UnaryOperation} which will be utilized for processing * */ public abstract UnaryOperation<IterableInterval<T>, IterableInterval<V>> operation(); /** * @param inputType of input {@link IterableInterval} */ public abstract void prepareOperation(final T inputType); }