/*
* ------------------------------------------------------------------------
*
* 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.nodes.seg.cropper;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.knime.core.data.DataCell;
import org.knime.core.data.DataColumnProperties;
import org.knime.core.data.DataColumnSpec;
import org.knime.core.data.DataColumnSpecCreator;
import org.knime.core.data.DataRow;
import org.knime.core.data.DataTableSpec;
import org.knime.core.data.RowIterator;
import org.knime.core.data.def.DefaultRow;
import org.knime.core.data.def.StringCell;
import org.knime.core.data.renderer.DataValueRenderer;
import org.knime.core.node.BufferedDataContainer;
import org.knime.core.node.BufferedDataTable;
import org.knime.core.node.BufferedDataTableHolder;
import org.knime.core.node.ExecutionContext;
import org.knime.core.node.ExecutionMonitor;
import org.knime.core.node.InvalidSettingsException;
import org.knime.core.node.NodeModel;
import org.knime.core.node.NodeSettingsRO;
import org.knime.core.node.NodeSettingsWO;
import org.knime.core.node.defaultnodesettings.SettingsModelBoolean;
import org.knime.core.node.defaultnodesettings.SettingsModelString;
import org.knime.knip.base.KNIPConstants;
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.LabelingCell;
import org.knime.knip.base.data.labeling.LabelingValue;
import org.knime.knip.base.node.NodeUtils;
import org.knime.knip.base.node.nodesettings.SettingsModelFilterSelection;
import org.knime.knip.base.nodes.seg.cropper.SegmentCropperNodeSettings.BACKGROUND_OPTION;
import org.knime.knip.core.KNIPGateway;
import org.knime.knip.core.data.DefaultNamed;
import org.knime.knip.core.data.DefaultSourced;
import org.knime.knip.core.data.img.DefaultImageMetadata;
import org.knime.knip.core.data.img.DefaultImgMetadata;
import org.knime.knip.core.data.img.LabelingMetadata;
import org.knime.knip.core.ops.misc.LabelingDependency;
import org.knime.knip.core.ui.imgviewer.events.RulebasedLabelFilter;
import org.knime.knip.core.util.EnumUtils;
import org.knime.knip.core.util.MiscViews;
import net.imagej.ImgPlus;
import net.imagej.ImgPlusMetadata;
import net.imglib2.Cursor;
import net.imglib2.FinalInterval;
import net.imglib2.IterableInterval;
import net.imglib2.RandomAccess;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.img.Img;
import net.imglib2.img.ImgFactory;
import net.imglib2.img.ImgView;
import net.imglib2.img.array.ArrayImgFactory;
import net.imglib2.ops.operation.Operations;
import net.imglib2.ops.operation.img.unary.ImgCopyOperation;
import net.imglib2.ops.operation.iterable.unary.Fill;
import net.imglib2.roi.IterableRegion;
import net.imglib2.roi.Regions;
import net.imglib2.roi.labeling.ImgLabeling;
import net.imglib2.roi.labeling.LabelRegions;
import net.imglib2.roi.labeling.LabelingType;
import net.imglib2.type.logic.BitType;
import net.imglib2.type.logic.BoolType;
import net.imglib2.type.numeric.RealType;
import net.imglib2.view.Views;
/**
* Crop BitMasks or parts of images according to a Labeling
*
* @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>
* @param <L>
* @param <T>
*/
public class SegmentCropperNodeModel<L extends Comparable<L>, T extends RealType<T>> extends NodeModel
implements BufferedDataTableHolder {
// SM addDependencies
private final SettingsModelBoolean m_addOverlappingLabels = SegmentCropperNodeSettings.createAddOverlappingLabels();
//if segments have to completely overlap or not
private final SettingsModelBoolean m_noCompleteOverlap =
SegmentCropperNodeSettings.createNotEnforceCompleteOverlapModel(false);
/* Resulting BufferedDataTable */
private BufferedDataTable m_data;
// SettingsModel to store Img column
private final SettingsModelString m_imgColumn = SegmentCropperNodeSettings.createImgColumnSelectionModel();
// SettingsModel to store Labeling column
private final SettingsModelString m_labelingColumn = SegmentCropperNodeSettings.createSMLabelingColumnSelection();
// SM left filter
private final SettingsModelFilterSelection<L> m_labelFilter = SegmentCropperNodeSettings.createLabelFilterModel();
/* Specification of the resulting table */
private DataTableSpec m_outSpec;
// SM right filter
private final SettingsModelFilterSelection<L> m_overlappingLabelFilter =
SegmentCropperNodeSettings.createOverlappingLabelFilterModel(false);
//value for the label background
private final SettingsModelString m_backgroundSelection =
SegmentCropperNodeSettings.createBackgroundSelectionModel();
/**
* Constructor SegementCropperNodeModel
*/
public SegmentCropperNodeModel() {
super(1, 1);
m_backgroundSelection.setEnabled(!"".equals(m_imgColumn.getStringValue()));
}
/**
* {@inheritDoc}
*/
@Override
protected DataTableSpec[] configure(final DataTableSpec[] inSpecs) throws InvalidSettingsException {
int labColIndex = inSpecs[0].findColumnIndex(m_labelingColumn.getStringValue());
if (labColIndex == -1) {
if ((labColIndex =
NodeUtils.autoOptionalColumnSelection(inSpecs[0], m_labelingColumn, LabelingValue.class)) >= 0) {
setWarningMessage("Auto-configure Label Column: " + m_labelingColumn.getStringValue());
} else {
throw new InvalidSettingsException("No column selected!");
}
}
final ArrayList<DataColumnSpec> specs = new ArrayList<>();
specs.add(new DataColumnSpecCreator("CroppedImg", ImgPlusCell.TYPE).createSpec());
DataColumnSpecCreator colspecCreator;
int imgColIndex = inSpecs[0].findColumnIndex(m_imgColumn.getStringValue());
if (imgColIndex != -1) {
colspecCreator = new DataColumnSpecCreator("Source Image", ImgPlusCell.TYPE);
colspecCreator.setProperties(new DataColumnProperties(
Collections.singletonMap(DataValueRenderer.PROPERTY_PREFERRED_RENDERER, "String")));
specs.add(colspecCreator.createSpec());
}
colspecCreator = new DataColumnSpecCreator("Source Labeling", LabelingCell.TYPE);
colspecCreator.setProperties(new DataColumnProperties(
Collections.singletonMap(DataValueRenderer.PROPERTY_PREFERRED_RENDERER, "String")));
specs.add(colspecCreator.createSpec());
specs.add(new DataColumnSpecCreator("Label", StringCell.TYPE).createSpec());
if (m_addOverlappingLabels.getBooleanValue()) {
specs.add(new DataColumnSpecCreator("DependedLabels", StringCell.TYPE).createSpec());
}
m_outSpec = new DataTableSpec(specs.toArray(new DataColumnSpec[specs.size()]));
return new DataTableSpec[]{m_outSpec};
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
protected BufferedDataTable[] execute(final BufferedDataTable[] inData, final ExecutionContext exec)
throws Exception {
//retrieve column indices
int labColIndex = inData[0].getDataTableSpec().findColumnIndex(m_labelingColumn.getStringValue());
if (labColIndex == -1) {
if ((labColIndex = NodeUtils.autoOptionalColumnSelection(inData[0].getDataTableSpec(), m_labelingColumn,
LabelingValue.class)) >= 0) {
setWarningMessage("Auto-configure Label Column: " + m_labelingColumn.getStringValue());
} else {
throw new InvalidSettingsException("No column selected!");
}
}
int imgColIndex = inData[0].getDataTableSpec().findColumnIndex(m_imgColumn.getStringValue());
final BufferedDataContainer con = exec.createDataContainer(m_outSpec);
final RulebasedLabelFilter<L> leftFilter = m_labelFilter.getRulebasedFilter();
final RulebasedLabelFilter<L> rightFilter = m_overlappingLabelFilter.getRulebasedFilter();
final RowIterator it = inData[0].iterator();
final long rowCount = inData[0].size();
DataRow row;
int rowIndex = 0;
final ImgPlusCellFactory imgCellFactory = new ImgPlusCellFactory(exec);
while (it.hasNext()) {
row = it.next();
if (row.getCell(labColIndex).isMissing()) {
setWarningMessage("Rows with missing cells (labeling) have been skipped!");
continue;
}
final LabelingValue<L> labelingValue = (LabelingValue<L>)row.getCell(labColIndex);
final LabelingDependency<L> labelingDependency =
new LabelingDependency<>(leftFilter, rightFilter, m_noCompleteOverlap.getBooleanValue());
// If no img selected, create bitmasks
ImgPlus<T> img = null;
if (imgColIndex != -1) {
if (row.getCell(imgColIndex).isMissing()) {
setWarningMessage("Rows with missing cells (image) have been skipped!");
continue;
}
img = ((ImgPlusValue<T>)row.getCell(imgColIndex)).getImgPlus();
}
RandomAccessibleInterval<LabelingType<L>> labeling = labelingValue.getLabeling();
if (img != null) {
labeling = MiscViews.synchronizeDimensionality(labeling, labelingValue.getLabelingMetadata(), img, img);
}
ImgFactory<T> fac;
if (labeling instanceof ImgLabeling && ((ImgLabeling)labeling).getIndexImg() instanceof Img) {
fac = ((Img)((ImgLabeling)labeling).getIndexImg()).factory();
} else if (img != null) {
fac = img.factory();
} else {
// if we don't have an image and no labeling where we can derive the factory from we simply create ArrayImg BitMasks
fac = (ImgFactory<T>)new ArrayImgFactory<>();
}
final Map<L, List<L>> dependedLabels = Operations.compute(labelingDependency, labeling);
final StringBuilder stringBuffer = new StringBuilder();
final LabelRegions<L> regions = KNIPGateway.regions().regions(labeling);
for (final L l : regions.getExistingLabels()) {
if (!leftFilter.isValid(l)) {
continue;
}
final IterableRegion<BoolType> roi = Regions.iterable(regions.getLabelRegion(l));
final long[] min = new long[roi.numDimensions()];
final long[] max = new long[roi.numDimensions()];
final long[] negativeMin = new long[roi.numDimensions()];
for (int k = 0; k < max.length; k++) {
min[k] = (long)Math.floor(roi.realMin(k));
max[k] = (long)Math.ceil(roi.realMax(k));
negativeMin[k] = -min[k];
}
Img<T> res;
if (img != null) {
T type = img.firstElement().createVariable();
T minType = type.createVariable();
minType.setReal(type.getMinValue());
Fill<T> fill = new Fill<>();
BACKGROUND_OPTION backGroundOption =
EnumUtils.valueForName(m_backgroundSelection.getStringValue(), BACKGROUND_OPTION.values());
if (backGroundOption == BACKGROUND_OPTION.MIN) {
res = fac.create(new FinalInterval(min, max), type.createVariable());
if (minType.getRealDouble() != 0) {
fill.compute(minType, res.iterator());
}
writeInRes(res, img, roi);
} else if (backGroundOption == BACKGROUND_OPTION.MAX) {
T maxType = type.createVariable();
maxType.setReal(type.getMaxValue());
res = fac.create(new FinalInterval(min, max), type.createVariable());
fill.compute(maxType, res.iterator());
writeInRes(res, img, roi);
} else if (backGroundOption == BACKGROUND_OPTION.ZERO) {
res = fac.create(new FinalInterval(min, max), type.createVariable());
writeInRes(res, img, roi);
} else if (backGroundOption == BACKGROUND_OPTION.SOURCE) { // BACKGROUND_OPTION.SOURCE
res = fac.create(new FinalInterval(min, max), type.createVariable());
new ImgCopyOperation()
.compute(new ImgView<T>(Views.zeroMin(Views.interval(img, min, max)), fac),
Views.flatIterable(res));
} else {
throw new UnsupportedOperationException(
"This Background option is not supported: " + backGroundOption.toString());
}
} else {
res = (Img<T>)fac.imgFactory(new BitType()).create(new FinalInterval(min, max), new BitType());
copy((ImgView<BoolType>)new ImgView<>(Views
.zeroMin((RandomAccessibleInterval<T>)Views.interval(roi, new FinalInterval(min, max))),
fac),
(IterableInterval<BitType>)Views.flatIterable(res));
}
final List<DataCell> cells = new ArrayList<>();
// TODO: What about color tables?
final LabelingMetadata lmdata = labelingValue.getLabelingMetadata();
final ImgPlusMetadata metadata =
new DefaultImgMetadata(img == null ? lmdata : img, new DefaultNamed(l.toString()),
new DefaultSourced(lmdata.getSource()), new DefaultImageMetadata());
ImgPlus<T> resImgPlus = new ImgPlus<>(ImgView.wrap(Views.translate(res, min), res.factory()), metadata);
resImgPlus.setSource(metadata.getSource());
cells.add(imgCellFactory.createCell(resImgPlus));
if (imgColIndex != -1) {
cells.add(row.getCell(imgColIndex));
}
cells.add(row.getCell(labColIndex));
cells.add(new StringCell(l.toString()));
if (m_addOverlappingLabels.getBooleanValue()) {
List<L> labels;
if ((labels = dependedLabels.get(l)) != null) {
stringBuffer.setLength(0);
for (final L s : labels) {
stringBuffer.append(s.toString());
stringBuffer.append(";");
}
if (stringBuffer.length() > 0) {
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
}
cells.add(new StringCell(stringBuffer.toString()));
} else {
cells.add(new StringCell(""));
}
}
con.addRowToTable(new DefaultRow(
row.getKey().toString() + KNIPConstants.IMGID_LABEL_DELIMITER + l.toString(),
cells.toArray(new DataCell[cells.size()])));
}
exec.checkCanceled();
exec.setProgress((double)++rowIndex / rowCount);
}
con.close();
m_data = con.getTable();
return new BufferedDataTable[]{m_data};
}
/**
* @param imgView
* @param flatIterable
*/
private void copy(final ImgView<BoolType> imgView, final IterableInterval<BitType> iterable) {
Cursor<BoolType> inCursor = Views.flatIterable(imgView).cursor();
Cursor<BitType> outCursor = iterable.cursor();
while (inCursor.hasNext()) {
outCursor.next().set(inCursor.next().get());
}
}
/**
* @param res
* @param img
* @param roi
*/
private void writeInRes(final Img<T> res, final ImgPlus<T> img, final IterableRegion<BoolType> roi) {
final IterableInterval<T> interval = Regions.sample(roi, img);
final Cursor<T> roiCursor = interval.cursor();
final RandomAccess<T> ra = res.randomAccess();
final long[] pos = new long[img.numDimensions()];
while (roiCursor.hasNext()) {
roiCursor.next();
for (int d = 0; d < pos.length; d++) {
ra.setPosition(roiCursor.getLongPosition(d) - interval.min(d), d);
}
ra.get().setReal(roiCursor.get().getRealDouble());
}
}
/**
* {@inheritDoc}
*/
@Override
public BufferedDataTable[] getInternalTables() {
return new BufferedDataTable[]{m_data};
}
/**
* {@inheritDoc}
*/
@Override
protected void loadInternals(final File nodeInternDir, final ExecutionMonitor exec) {
//
}
/**
* {@inheritDoc}
*/
@Override
protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws InvalidSettingsException {
m_imgColumn.loadSettingsFrom(settings);
m_labelingColumn.loadSettingsFrom(settings);
m_labelFilter.loadSettingsFrom(settings);
m_overlappingLabelFilter.loadSettingsFrom(settings);
m_addOverlappingLabels.loadSettingsFrom(settings);
m_backgroundSelection.loadSettingsFrom(settings);
try {
m_noCompleteOverlap.loadSettingsFrom(settings);
} catch (InvalidSettingsException e) {
//new setting with 1.1.2, use default if not existent
}
}
/**
* {@inheritDoc}
*/
@Override
protected void reset() {
m_data = null;
}
/**
* {@inheritDoc}
*/
@Override
protected void saveInternals(final File nodeInternDir, final ExecutionMonitor exec) {
//
}
/**
* {@inheritDoc}
*/
@Override
protected void saveSettingsTo(final NodeSettingsWO settings) {
m_imgColumn.saveSettingsTo(settings);
m_labelingColumn.saveSettingsTo(settings);
m_labelFilter.saveSettingsTo(settings);
m_overlappingLabelFilter.saveSettingsTo(settings);
m_addOverlappingLabels.saveSettingsTo(settings);
m_backgroundSelection.saveSettingsTo(settings);
m_noCompleteOverlap.saveSettingsTo(settings);
}
/**
* {@inheritDoc}
*/
@Override
public void setInternalTables(final BufferedDataTable[] tables) {
m_data = tables[0];
}
/**
* {@inheritDoc}
*/
@Override
protected void validateSettings(final NodeSettingsRO settings) throws InvalidSettingsException {
m_imgColumn.validateSettings(settings);
m_labelingColumn.validateSettings(settings);
m_labelFilter.validateSettings(settings);
m_overlappingLabelFilter.validateSettings(settings);
m_addOverlappingLabels.validateSettings(settings);
m_backgroundSelection.validateSettings(settings);
try {
m_noCompleteOverlap.validateSettings(settings);
} catch (InvalidSettingsException e) {
//new setting with 1.1.2, use default if not existent
}
}
}