/* * ------------------------------------------------------------------------ * * 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.misc.merger; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.knime.core.data.DataCell; 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.node.BufferedDataContainer; import org.knime.core.node.BufferedDataTable; import org.knime.core.node.BufferedDataTableHolder; import org.knime.core.node.CanceledExecutionException; import org.knime.core.node.ExecutionContext; import org.knime.core.node.ExecutionMonitor; import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeModel; import org.knime.core.node.NodeSettingsRO; import org.knime.core.node.NodeSettingsWO; import org.knime.core.node.defaultnodesettings.SettingsModelFilterString; import org.knime.core.node.defaultnodesettings.SettingsModelString; 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.core.util.TypeUtils; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; import net.imglib2.Cursor; import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgFactory; import net.imglib2.img.planar.PlanarImgFactory; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; /** * Merges an images to one. * * @param <T> source image type * * @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 MergerNodeModel<T extends RealType<T>, V extends RealType<V>> extends NodeModel implements BufferedDataTableHolder { /** * Key to store the column to work on in the settings. */ static final String CFG_COLUMNS = "columns"; static final String CFG_MERGE_SETTINGS_DIM_NAMES = "merge_settings_dim_names"; static final String CFG_MERGE_SETTINGS_DIMS = "merge_settings_dims"; static final String CFG_IMG_FACTORY = "img_factory"; private static final NodeLogger LOGGER = NodeLogger.getLogger(MergerNodeModel.class); static final String[] IMG_FACTORIES = {"Array Image Factory", "Planar Image Factory"}; /* * The selected columns */ private final SettingsModelFilterString m_columns = new SettingsModelFilterString(MergerNodeModel.CFG_COLUMNS); /* * data table for the table cell view */ private BufferedDataTable m_data; private final SettingsModelString m_mergeSettingsDimNames = new SettingsModelString(CFG_MERGE_SETTINGS_DIM_NAMES, "X,Y"); /* * */ private final SettingsModelString m_mergeSettingsDims = new SettingsModelString(CFG_MERGE_SETTINGS_DIMS, ""); private final SettingsModelString m_imgFactory = new SettingsModelString(CFG_IMG_FACTORY, IMG_FACTORIES[0]); /* * The specification of the resulting table. */ private DataTableSpec m_outSpec; /** * One input one output. * */ public MergerNodeModel() { super(1, 1); } /* Helper to collect all columns of the given type. */ private void collectAllColumns(final List<String> colNames, final DataTableSpec spec) { colNames.clear(); for (final DataColumnSpec c : spec) { if (c.getType().isCompatible(ImgPlusValue.class)) { colNames.add(c.getName()); } } if (colNames.size() == 0) { LOGGER.warn("No columns of type " + ImgPlusValue.class.getSimpleName() + " available!"); return; } LOGGER.info("All available columns of type " + ImgPlusValue.class.getSimpleName() + " are selected!"); } /** * {@inheritDoc} */ @Override protected DataTableSpec[] configure(final DataTableSpec[] inSpecs) throws InvalidSettingsException { getSelectedColumnIndices(inSpecs[0]); if (m_mergeSettingsDims.getStringValue().trim().length() == 0) { throw new InvalidSettingsException("Merge dimensions must not be empty!"); } // try { // parseMergeSettings(); // } catch (NumberFormatException e) { // throw new InvalidSettingsException( // "Invalid Merge Settings!"); // } final DataColumnSpecCreator c = new DataColumnSpecCreator("Merged Img", ImgPlusCell.TYPE); m_outSpec = new DataTableSpec(c.createSpec()); return new DataTableSpec[]{m_outSpec}; } /** * {@inheritDoc} */ @Override protected BufferedDataTable[] execute(final BufferedDataTable[] inData, final ExecutionContext exec) throws Exception { final int[] selectedColIndices = getSelectedColumnIndices(inData[0].getDataTableSpec()); // parse the merge settings final String[] mergeSet = m_mergeSettingsDims.getStringValue().trim().split(","); final RowIterator it = inData[0].iterator(); final BufferedDataContainer con = exec.createDataContainer(m_outSpec); DataRow row; final int rowNum = inData[0].getRowCount(); int rowCount = 0; final ImgPlusCellFactory imgCellFactory = new ImgPlusCellFactory(exec); while (it.hasNext()) { row = it.next(); RealType[] types = new RealType[selectedColIndices.length]; boolean resultTypeChanged = false; // determine total pixel number long numTotalPixels = 0; ImgPlus<T> img = null; DataCell cell; for (int i = 0; i < selectedColIndices.length; i++) { cell = row.getCell(selectedColIndices[i]); if (cell.isMissing()) { setWarningMessage("Some rows have been skipped due to missing cells!"); LOGGER.warn("Missing cell in row " + row.getKey() + ". Image skipped."); continue; } img = ((ImgPlusValue<T>)cell).getImgPlus(); numTotalPixels += img.size(); types[i] = img.firstElement().createVariable(); if (i > 0 && types[i - 1] != null && types[i - 1].getClass() != types[i].getClass()) { resultTypeChanged = true; } } if (resultTypeChanged) { LOGGER.warn("Input images are not of the same pixel type. Result image has the least common pixel type."); } if (img == null) { break; } // calculate the dimensions of the new image long tmp = 1; long[] dims = null; try { for (int numDim = 0; numDim < mergeSet.length; numDim++) { tmp *= getDim(mergeSet, img, numDim); if (numTotalPixels <= tmp) { dims = new long[numDim + 1]; for (int d = 0; d < (numDim + 1); d++) { dims[d] = getDim(mergeSet, img, d); } } } if (dims == null) { dims = new long[mergeSet.length + 1]; for (int d = 0; d < mergeSet.length; d++) { // System.arraycopy(mergeSet, 0, // dims, // 0, // mergeSet.length); dims[d] = getDim(mergeSet, img, d); } dims[dims.length - 1] = (int)Math.ceil(numTotalPixels / (double)tmp); } } catch (final InvalidSettingsException e) { LOGGER.warn("Error in row " + row.getKey() + ". Row skipped!", e); continue; } // create result image Img<V> res = null; V leastCommonType = (V)TypeUtils.leastFittingRealType(types); if (m_imgFactory.getStringValue().equals(IMG_FACTORIES[0])) { res = new ArrayImgFactory().create(dims, (NativeType)leastCommonType); } else { res = new PlanarImgFactory().create(dims, (NativeType)leastCommonType); } final Cursor<V> resCur = res.cursor(); for (int i = 0; i < selectedColIndices.length; i++) { cell = row.getCell(selectedColIndices[i]); if (cell.isMissing()) { continue; } img = ((ImgPlusValue<T>)cell).getImgPlus(); final Cursor<T> imgCur = img.cursor(); while (imgCur.hasNext()) { imgCur.fwd(); resCur.fwd(); resCur.get().setReal(imgCur.get().getRealDouble()); } } final AxisType[] axes = new AxisType[res.numDimensions()]; final String[] axisNames = m_mergeSettingsDimNames.getStringValue().trim().split(","); int a; for (a = 0; a < Math.min(axisNames.length, axes.length); a++) { axes[a] = Axes.get(axisNames[a]); } for (int j = a; j < axes.length; j++) { axes[j] = Axes.get("Unknown" + (j - a)); } ImgPlus newImgPlus = new ImgPlus(res, img.getName(), axes); newImgPlus.setSource(img.getSource()); con.addRowToTable(new DefaultRow(row.getKey(), imgCellFactory.createCell(newImgPlus))); exec.checkCanceled(); exec.setProgress((double)rowCount++ / rowNum); } con.close(); m_data = con.getTable(); return new BufferedDataTable[]{m_data}; } private long getDim(final String[] dimStrings, final ImgPlus<T> img, final int dimIndex) throws InvalidSettingsException { try { return Long.parseLong(dimStrings[dimIndex]); } catch (final NumberFormatException e) { for (int d = 0; d < img.numDimensions(); d++) { if (dimStrings[dimIndex].equals(img.axis(d).type().getLabel())) { return img.dimension(d); } } } throw new InvalidSettingsException( "Wrong dimension settings. Can not be parsed or dimension label doesn't exist for image " + img.getName() + "!"); } /** * {@inheritDoc} */ @Override public BufferedDataTable[] getInternalTables() { return new BufferedDataTable[]{m_data}; } /* * Retrieves the selected column indices from the given DataTableSpec * and the column selection. If the selection turned out to be invalid, * all columns are selected. */ private int[] getSelectedColumnIndices(final DataTableSpec inSpec) { final List<String> colNames; if ((m_columns.getIncludeList().size() == 0) || m_columns.isKeepAllSelected()) { colNames = new ArrayList<String>(); collectAllColumns(colNames, inSpec); m_columns.setIncludeList(colNames); } else { colNames = new ArrayList<String>(); colNames.addAll(m_columns.getIncludeList()); if (!validateColumnSelection(colNames, inSpec)) { setWarningMessage("Invalid column selection. All columns are selected!"); collectAllColumns(colNames, inSpec); } } // get column indices final List<Integer> colIndices = new ArrayList<Integer>(colNames.size()); for (int i = 0; i < colNames.size(); i++) { final int colIdx = inSpec.findColumnIndex(colNames.get(i)); if (colIdx == -1) { // can not occur, actually LOGGER.warn("Column " + colNames.get(i) + " doesn't exist!"); } else { colIndices.add(colIdx); } } final int[] colIdx = new int[colIndices.size()]; for (int i = 0; i < colIdx.length; i++) { colIdx[i] = colIndices.get(i); } return colIdx; } /** * {@inheritDoc} */ @Override protected void loadInternals(final File nodeInternDir, final ExecutionMonitor exec) throws IOException, CanceledExecutionException { // } /** * {@inheritDoc} */ @Override protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws InvalidSettingsException { m_columns.loadSettingsFrom(settings); m_mergeSettingsDims.loadSettingsFrom(settings); m_mergeSettingsDimNames.loadSettingsFrom(settings); try { m_imgFactory.loadSettingsFrom(settings); } catch (InvalidSettingsException e) { //added with 1.1 } } /** * {@inheritDoc} */ @Override protected void reset() { m_data = null; } /** * {@inheritDoc} */ @Override protected void saveInternals(final File nodeInternDir, final ExecutionMonitor exec) throws IOException, CanceledExecutionException { // } /** * {@inheritDoc} */ @Override protected void saveSettingsTo(final NodeSettingsWO settings) { m_columns.saveSettingsTo(settings); m_mergeSettingsDims.saveSettingsTo(settings); m_mergeSettingsDimNames.saveSettingsTo(settings); m_imgFactory.saveSettingsTo(settings); } /** * {@inheritDoc} */ @Override public void setInternalTables(final BufferedDataTable[] tables) { m_data = tables[0]; } /* Checks if a column is not present in the DataTableSpec */ private boolean validateColumnSelection(final List<String> colNames, final DataTableSpec spec) { for (int i = 0; i < colNames.size(); i++) { final int colIdx = spec.findColumnIndex(colNames.get(i)); if (colIdx == -1) { return false; } } return true; } /** * {@inheritDoc} */ @Override protected void validateSettings(final NodeSettingsRO settings) throws InvalidSettingsException { m_columns.validateSettings(settings); m_mergeSettingsDims.validateSettings(settings); m_mergeSettingsDimNames.validateSettings(settings); try { m_imgFactory.validateSettings(settings); } catch (InvalidSettingsException e) { //added with 1.1 } } }