/*
* ------------------------------------------------------------------------
*
* 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 28.11.2013 by Tim-Oliver Buchholz
*/
package org.knime.knip.base.nodes.proc.ucm;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import net.imglib2.Cursor;
import net.imglib2.RandomAccess;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.ops.operation.BinaryOperation;
import net.imglib2.ops.operation.randomaccessibleinterval.unary.regiongrowing.AbstractRegionGrowing;
import net.imglib2.roi.labeling.LabelRegions;
import net.imglib2.roi.labeling.LabelingType;
import net.imglib2.type.numeric.RealType;
import net.imglib2.type.numeric.real.FloatType;
import net.imglib2.util.Util;
import net.imglib2.view.Views;
import org.knime.knip.core.KNIPGateway;
/**
* Operation encapsulating functionality of UltraMetricContourMaps UCMs are a extraction system that combines several
* types of low-level image information into a generic notion of segmentation scale. This system constructs a
* hierarchical representation of the image boundaries called Ultrametric Contour Map (UCM). Thresholding an UCM at
* level k provides by definition a set of closed curves, the boundaries of the segmentation at scale k (from
* http://www.cs.berkeley.edu/~arbelaez/UCM.html).
*
* @author Jan Dirk Verbeek (University of Konstanz)
* @author Martin Horn (University of Konstanz)
* @author Tim-Oliver Buchholz (University of Konstanz)
* @author Christian Dietz (University of Konstanz)
*
* @param <L>
* @param <T>
*/
public class UCMOp<L extends Comparable<L>, T extends RealType<T>>
implements
BinaryOperation<RandomAccessibleInterval<LabelingType<L>>, RandomAccessibleInterval<T>, RandomAccessibleInterval<FloatType>> {
private final int maxNumFaces;
private final double maxFacePercent;
private final int minBoundaryWeight;
private final String boundaryLabel;
/**
* Default Constructor
*
* TODO
*
* @param paramMaxNumFaces
* @param paramMaxFacePercent
* @param paramMinBoundaryWeight
* @param paramBoundaryLabel name of label which is boundary between some other Labels
*/
public UCMOp(final int paramMaxNumFaces, final double paramMaxFacePercent, final int paramMinBoundaryWeight,
final String paramBoundaryLabel) {
this.maxNumFaces = paramMaxNumFaces;
this.maxFacePercent = paramMaxFacePercent;
this.minBoundaryWeight = paramMinBoundaryWeight;
this.boundaryLabel = paramBoundaryLabel;
}
/**
* {@inheritDoc}
*/
@Override
public RandomAccessibleInterval<FloatType> compute(final RandomAccessibleInterval<LabelingType<L>> labeling,
final RandomAccessibleInterval<T> inImg,
final RandomAccessibleInterval<FloatType> result) {
// check for dimensions
if (labeling.numDimensions() != inImg.numDimensions() && inImg.numDimensions() != result.numDimensions()) {
throw new IllegalArgumentException("Dimensions do not match.");
}
// check for dimensions
if (labeling.numDimensions() != 2) {
throw new IllegalArgumentException("Only two dimension are supported.");
}
// Create containers for faces and boundaries
final HashMap<String, UCMFace> faces = new HashMap<String, UCMFace>();
final ArrayList<UCMBoundary> boundaries = new ArrayList<UCMBoundary>();
// access on img
final RandomAccess<FloatType> resultAccess = result.randomAccess();
final RandomAccess<T> imgAccess = inImg.randomAccess();
// | Create Faces
final LabelRegions<L> regions = KNIPGateway.regions().regions(labeling);
for (L label : regions.getExistingLabels()) {
faces.put(label.toString(), new UCMFace(label.toString()));
}
// random access cursor with extended borders
final Cursor<LabelingType<L>> labCur = Views.iterable(labeling).localizingCursor();
final LabelingType<L> empty = Util.getTypeFromInterval(labeling).createVariable();
empty.clear();
final RandomAccess<LabelingType<L>> labAccess = Views.extendValue(labeling, empty).randomAccess();
// the 8 neighbors
final long[][] strucElement = AbstractRegionGrowing.get8ConStructuringElement(labeling.numDimensions());
// the 16 neighbors
final long[][] ring16 = ring16dim2();
if (labeling.numDimensions() != 2) {
posRing(ring16, labeling.numDimensions(), 2);
}
// temporary list of labels of neighboring faces
HashSet<String> tempLabels = null;
// for all pixels
while (labCur.hasNext()) {
labCur.fwd();
// if pixel is part of a boundary
if (labCur.get().iterator().next().toString().equals(boundaryLabel)) {
tempLabels = new HashSet<String>();
// iterate neighborhood to collect the faces
for (int s = 0; s < strucElement.length; s++) {
for (int d = 0; d < labeling.numDimensions(); d++) {
// the neighboring pixel
labAccess.setPosition(labCur.getLongPosition(d) + strucElement[s][d], d);
// if not outside the image
if (labAccess.get().size() != 0) {
L label = labAccess.get().iterator().next();
if (!label.toString().equals(boundaryLabel)) {
// add face as neighbor
tempLabels.add(label.toString());
}
}
}
}
// special case: not two faces in direct neighborhood
if (tempLabels.size() < 2) {
for (int s = 0; s < ring16.length; s++) {
for (int d = 0; d < labeling.numDimensions(); d++) {
// the neighboring pixel
try {
labAccess.setPosition(labCur.getLongPosition(d) + ring16[s][d], d);
} catch (Exception e) {
// extremly rare, but ugly!
continue;
}
// if not outside the image
if (labAccess.get().size() != 0) {
String label = labAccess.get().iterator().next().toString();
if (!label.toString().equals(boundaryLabel)) {
// add face as neighbor
tempLabels.add(label);
}
}
}
}
}
// add pixel to boundary(ies)
HashMap<String, UCMBoundary> tempBoundaryMap = null;
UCMBoundary tempBoundary = null;
for (String firstLabel : tempLabels) {
tempBoundaryMap = faces.get(firstLabel).getBoundaries();
for (String secondLabel : tempLabels) {
if (firstLabel.compareTo(secondLabel) >= 0) {
continue;
}
int[] pos = new int[labCur.numDimensions()];
labCur.localize(pos);
// check if boundary exists
tempBoundary = tempBoundaryMap.get(secondLabel);
if (tempBoundary == null) {
UCMFace faceA = faces.get(firstLabel);
UCMFace faceB = faces.get(secondLabel);
// create the boundary
tempBoundary = new UCMBoundary();
tempBoundary.getFaces().add(faceA);
tempBoundary.getFaces().add(faceB);
boundaries.add(tempBoundary);
// add to faces
faceA.addBoundary(secondLabel, tempBoundary);
faceB.addBoundary(firstLabel, tempBoundary);
}
try {
imgAccess.setPosition(pos);
tempBoundary.addPixel(pos, imgAccess.get().getRealDouble());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
// | sort according to boundary weights
Collections.sort(boundaries);
final double maxFacesSize = faces.size();
double facesWished = maxNumFaces;
if (facesWished < 1) {
facesWished = 1;
}
double calcedMaxFacePercent = maxFacePercent / 100;
if (calcedMaxFacePercent < 0.01) {
calcedMaxFacePercent = 0.01;
}
boolean drawAllowed = false;
double tempDouble = 0;
// | Merge regions & draw boundaries
while (faces.size() > 1 && boundaries.size() > 0) {
// get the next boundary
UCMBoundary boundary = boundaries.get(0);
tempDouble = boundary.getWeight();
// merge neighboring regions
mergeFaces(boundary, faces, boundaries);
// check draw conditions
if (!drawAllowed) {
if (faces.size() <= facesWished && tempDouble >= minBoundaryWeight
&& faces.size() / maxFacesSize <= calcedMaxFacePercent) {
drawAllowed = true;
}
}
// draw boundary
if (drawAllowed) {
for (int[] pos : boundary.getPixels()) {
resultAccess.setPosition(pos);
resultAccess.get().setReal(tempDouble);
}
}
}
return result;
}
/**
* Ring around a Pixel with distance 2 in a two-dimensional space
*
* @return list of positions
*/
private long[][] ring16dim2() {
final long[][] faul = new long[16][2];
// 00000
// 0xxx0
// 0xPx0
// 0xxx0
// 00000
faul[0][0] = -2;
faul[0][1] = -2;
faul[1][0] = -2;
faul[1][1] = -1;
faul[2][0] = -2;
faul[3][0] = -2;
faul[3][1] = 1;
faul[4][0] = -2;
faul[4][1] = 2;
// -
faul[5][0] = -1;
faul[5][1] = -2;
faul[6][1] = -2;
faul[7][0] = 1;
faul[7][1] = -2;
// -
faul[8][0] = -1;
faul[8][1] = 2;
faul[9][1] = 2;
faul[10][0] = 1;
faul[10][1] = 2;
// -
faul[11][0] = 2;
faul[11][1] = -2;
faul[12][0] = 2;
faul[12][1] = -1;
faul[13][0] = 2;
faul[14][0] = 2;
faul[14][1] = 1;
faul[15][0] = 2;
faul[15][1] = 2;
return faul;
}
/**
* returns a long[][] containing all points which differ by a certain distance from a central point in one
* dimension, but not more than it in any other dimension
*
* @param wantsPositions
* @param dimensions
* @param distance
*/
private void posRing(long[][] wantsPositions, final int dimensions, final int distance) {
int ringLength = (int)(Math.pow(distance * 2 + 1, dimensions) - Math.pow(distance * 2 - 1, dimensions));
int posMaxi = (int)Math.pow(distance * 4 + 1, dimensions);
long tempLong;
wantsPositions = new long[ringLength][dimensions];
final long[][] possiblePosition = new long[posMaxi][dimensions];
final long[] numlock = new long[dimensions];
// create core
for (int i = 0; i < numlock.length; i++) {
numlock[i] = -distance;
}
while (numlock[0] <= distance) {
for (int dim = 0; dim < dimensions; dim++) {
possiblePosition[posMaxi][dim] = numlock[dim];
}
posMaxi--;
numlock[dimensions - 1] += 1;
for (int dim = dimensions - 1; dim > 0; dim--) {
if (numlock[dim] > distance) {
numlock[dim] = -distance;
numlock[dim - 1] += 1;
}
}
}
// collect diamond
for (int i = 0; i < possiblePosition.length; i++) {
tempLong = 0;
for (int dim = 0; dim < dimensions; dim++) {
tempLong += Math.abs(possiblePosition[i][dim]);
}
if (tempLong == distance * dimensions) {
for (int dim = 0; dim < dimensions; dim++) {
wantsPositions[ringLength][dim] = possiblePosition[i][dim];
}
ringLength--;
}
}
// crush to cube
for (int i = 0; i < wantsPositions.length; i++) {
for (int dim = 0; dim < dimensions; dim++) {
tempLong = wantsPositions[i][dim];
if (tempLong < -distance) {
wantsPositions[i][dim] = -distance;
}
if (tempLong > distance) {
wantsPositions[i][dim] = distance;
}
}
}
}
/**
* merges the two faces of an given boundary
*
* @param mergingBoundary
* @param faces
* @param boundaries
* @return
*/
private UCMFace mergeFaces(final UCMBoundary mergingBoundary, final HashMap<String, UCMFace> faces,
final ArrayList<UCMBoundary> boundaries) {
// Faces
UCMFace faceA = null;
UCMFace faceB = null;
UCMFace thirdFace;
// Boundaries
UCMBoundary manipulatedBoundary;
UCMBoundary existingBoundary;
// temporary Variables
Iterator<UCMFace> tempFaceIterator;
// get faces
tempFaceIterator = mergingBoundary.getFaces().iterator();
faceA = tempFaceIterator.next();
faceB = tempFaceIterator.next();
// decide which face stays
if (faceA.getBoundaries().size() < faceB.getBoundaries().size()) {
thirdFace = faceA;
faceA = faceB;
faceB = thirdFace;
}
// remove connecting boundary
faceA.getBoundaries().remove(faceB.getLabel());
faceB.getBoundaries().remove(faceA.getLabel());
boundaries.remove(mergingBoundary);
// visit every connected face and shift corresponding boundary
Iterator<String> faceLabelIterator = faceB.getBoundaries().keySet().iterator();
String conFaceLabel;
while (faceLabelIterator.hasNext()) {
// the label of the neighbor
conFaceLabel = faceLabelIterator.next();
// get connecting boundary
manipulatedBoundary = faceB.getBoundaries().get(conFaceLabel);
// get connected Face
thirdFace = faces.get(conFaceLabel);
// remove connection to vanishing face
thirdFace.getBoundaries().remove(faceB.getLabel());
// boundary to staying face?
existingBoundary = thirdFace.getBoundaries().get(faceA.getLabel());
if (existingBoundary == null) {
// change faces of shifted boundary
manipulatedBoundary.getFaces().remove(faceB);
manipulatedBoundary.getFaces().add(faceA);
// add boundary to faces
faceA.getBoundaries().put(conFaceLabel, manipulatedBoundary);
thirdFace.getBoundaries().put(faceA.getLabel(), manipulatedBoundary);
} else {
// remove from general list
boundaries.remove(manipulatedBoundary);
// merge into existing boundary
existingBoundary.mergeBoundary(manipulatedBoundary);
Collections.sort(boundaries);
}
}
// remove swallowed face
faces.remove(faceB.getLabel());
return faceA;
}
/**
* {@inheritDoc}
*/
@Override
public
BinaryOperation<RandomAccessibleInterval<LabelingType<L>>, RandomAccessibleInterval<T>, RandomAccessibleInterval<FloatType>>
copy() {
return new UCMOp<L, T>(maxNumFaces, maxFacePercent, minBoundaryWeight, boundaryLabel);
}
}