
boofcv.alg.segmentation.ms.ClusterLabeledImage Maven / Gradle / Ivy
Show all versions of boofcv-feature Show documentation
/*
* Copyright (c) 2021, Peter Abeles. All Rights Reserved.
*
* This file is part of BoofCV (http://boofcv.org).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package boofcv.alg.segmentation.ms;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.struct.ConnectRule;
import boofcv.struct.image.GrayS32;
import georegression.struct.point.Point2D_I32;
import org.ddogleg.struct.DogArray_I32;
/**
*
* Given a labeled image in which pixels that contains the same label may or may not be connected to each other,
* create a new labeled image in which only connected pixels have the same label.
* A two pass algorithm is used. In the first pass pixels are examined from top to bottom, left to right. For
* each pixel (the target), the input image and output image labels of its adjacent pixels are examined. If an adjacent
* pixel has the same input label as the target then it is either assigned the same output label or marked for being
* merged. Depending if it is not labeled or has an output label already, respectively. After all the pixels are process
* the merge requests are examined and a new set of output labels is created. A pass across the output image is
* done to relabel the inputs.
*
*
*
* Clustering can be done using 4 or 8 connect, which defines what an adjacent pixel is. 4-connect just considers
* pixels which are (+1,0) (0,1) (-1,0) (0,-1). 8-connect considers (+1,0) (0,1) (-1,0) (0,-1) and
* (1,1) (-1,1) (-1,-1) (1,-1).
*
*
* @author Peter Abeles
*/
@SuppressWarnings({"NullAway.Init"})
public class ClusterLabeledImage extends RegionMergeTree {
// which connectivity rule is used. 4 or 8.
protected ConnectRule connectRule;
// offset in pixel indices for adjacent pixels
protected int[] edgesIn;
protected int[] edgesOut;
// relative coordinates of adjacent pixels
protected Point2D_I32[] edges;
// contains the number of pixels in each output label
protected DogArray_I32 regionMemberCount;
/**
* Configures labeling
*
* @param connectRule Which connectivity rule to use. 4 or 8
*/
public ClusterLabeledImage( ConnectRule connectRule ) {
this.connectRule = connectRule;
if (connectRule == ConnectRule.EIGHT) {
edgesIn = new int[4];
edgesOut = new int[4];
edges = new Point2D_I32[4];
} else if (connectRule == ConnectRule.FOUR) {
edgesIn = new int[2];
edgesOut = new int[2];
edges = new Point2D_I32[2];
} else {
throw new IllegalArgumentException("connectRule must be 4 or 8");
}
for (int i = 0; i < edges.length; i++)
edges[i] = new Point2D_I32();
}
/**
* Declares lookup tables for neighbors
*/
protected void setUpEdges( GrayS32 input, GrayS32 output ) {
if (connectRule == ConnectRule.EIGHT) {
setUpEdges8(input, edgesIn);
setUpEdges8(output, edgesOut);
edges[0].setTo(1, 0);
edges[1].setTo(1, 1);
edges[2].setTo(0, 1);
edges[3].setTo(-1, 0);
} else {
setUpEdges4(input, edgesIn);
setUpEdges4(output, edgesOut);
edges[0].setTo(1, 0);
edges[1].setTo(0, 1);
}
}
protected void setUpEdges8( GrayS32 image, int[] edges ) {
edges[0] = 1;
edges[1] = 1 + image.stride;
edges[2] = +image.stride;
edges[3] = -1 + image.stride;
}
protected void setUpEdges4( GrayS32 image, int[] edges ) {
edges[0] = 1;
edges[1] = +image.stride;
}
/**
* Relabels the image such that all pixels with the same label are a member of the same graph.
*
* @param input Labeled input image.
* @param output Labeled output image.
* @param regionMemberCount (Input/Output) Number of pixels which belong to each group.
*/
public void process( GrayS32 input, GrayS32 output, DogArray_I32 regionMemberCount ) {
// initialize data structures
this.regionMemberCount = regionMemberCount;
regionMemberCount.reset();
setUpEdges(input, output);
ImageMiscOps.fill(output, -1);
// this is a bit of a hack here. Normally you call the parent's init function.
// since the number of regions is not initially known this will grow
mergeList.reset();
connectInner(input, output);
connectLeftRight(input, output);
connectBottom(input, output);
// Merge together all the regions that are connected in the output image
performMerge(output, regionMemberCount);
}
/**
* Examines pixels inside the image without the need for bounds checking
*/
protected void connectInner( GrayS32 input, GrayS32 output ) {
int startX = connectRule == ConnectRule.EIGHT ? 1 : 0;
for (int y = 0; y < input.height - 1; y++) {
int indexIn = input.startIndex + y*input.stride + startX;
int indexOut = output.startIndex + y*output.stride + startX;
for (int x = startX; x < input.width - 1; x++, indexIn++, indexOut++) {
int inputLabel = input.data[indexIn];
int outputLabel = output.data[indexOut];
if (outputLabel == -1) { // see if it needs to create a new output segment
output.data[indexOut] = outputLabel = regionMemberCount.size;
regionMemberCount.add(1);
mergeList.add(outputLabel);
}
for (int i = 0; i < edgesIn.length; i++) {
if (inputLabel == input.data[indexIn + edgesIn[i]]) {
int outputAdj = output.data[indexOut + edgesOut[i]];
if (outputAdj == -1) { // see if not assigned
regionMemberCount.data[outputLabel]++;
output.data[indexOut + edgesOut[i]] = outputLabel;
} else if (outputLabel != outputAdj) { // see if assigned to different regions
markMerge(outputLabel, outputAdj);
} // do nothing, same input and output labels
}
}
}
}
}
/**
* Examines pixels along the left and right border
*/
protected void connectLeftRight( GrayS32 input, GrayS32 output ) {
for (int y = 0; y < input.height; y++) {
int x = input.width - 1;
int inputLabel = input.unsafe_get(x, y);
int outputLabel = output.unsafe_get(x, y);
if (outputLabel == -1) { // see if it needs to create a new output segment
outputLabel = regionMemberCount.size;
output.unsafe_set(x, y, outputLabel);
regionMemberCount.add(1);
mergeList.add(outputLabel);
}
// check right first
for (int i = 0; i < edges.length; i++) {
Point2D_I32 offset = edges[i];
// make sure it is inside the image
if (!input.isInBounds(x + offset.x, y + offset.y))
continue;
if (inputLabel == input.unsafe_get(x + offset.x, y + offset.y)) {
int outputAdj = output.unsafe_get(x + offset.x, y + offset.y);
if (outputAdj == -1) { // see if not assigned
regionMemberCount.data[outputLabel]++;
output.unsafe_set(x + offset.x, y + offset.y, outputLabel);
} else if (outputLabel != outputAdj) { // see if assigned to different regions
markMerge(outputLabel, outputAdj);
} // do nothing, same input and output labels
}
}
// skip check of left of 4-connect
if (connectRule != ConnectRule.EIGHT)
continue;
x = 0;
inputLabel = input.unsafe_get(x, y);
outputLabel = output.unsafe_get(x, y);
if (outputLabel == -1) { // see if it needs to create a new output segment
outputLabel = regionMemberCount.size;
output.unsafe_set(x, y, outputLabel);
regionMemberCount.add(1);
mergeList.add(outputLabel);
}
for (int i = 0; i < edges.length; i++) {
Point2D_I32 offset = edges[i];
// make sure it is inside the image
if (!input.isInBounds(x + offset.x, y + offset.y))
continue;
if (inputLabel == input.unsafe_get(x + offset.x, y + offset.y)) {
int outputAdj = output.unsafe_get(x + offset.x, y + offset.y);
if (outputAdj == -1) { // see if not assigned
regionMemberCount.data[outputLabel]++;
output.unsafe_set(x + offset.x, y + offset.y, outputLabel);
} else if (outputLabel != outputAdj) { // see if assigned to different regions
markMerge(outputLabel, outputAdj);
} // do nothing, same input and output labels
}
}
}
}
/**
* Examines pixels along the bottom border
*/
protected void connectBottom( GrayS32 input, GrayS32 output ) {
for (int x = 0; x < input.width - 1; x++) {
int y = input.height - 1;
int inputLabel = input.unsafe_get(x, y);
int outputLabel = output.unsafe_get(x, y);
if (outputLabel == -1) { // see if it needs to create a new output segment
outputLabel = regionMemberCount.size;
output.unsafe_set(x, y, outputLabel);
regionMemberCount.add(1);
mergeList.add(outputLabel);
}
// for 4 and 8 connect the check is only +1 x and 0 y
if (inputLabel == input.unsafe_get(x + 1, y)) {
int outputAdj = output.unsafe_get(x + 1, y);
if (outputAdj == -1) { // see if not assigned
regionMemberCount.data[outputLabel]++;
output.unsafe_set(x + 1, y, outputLabel);
} else if (outputLabel != outputAdj) { // see if assigned to different regions
markMerge(outputLabel, outputAdj);
} // do nothing, same input and output labels
}
}
}
}