/*
* Copyright (c) 2011-2016, 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.filter.binary;
import boofcv.alg.InputSanityCheck;
import boofcv.alg.filter.binary.impl.BinaryThinning;
import boofcv.alg.filter.binary.impl.ImplBinaryBorderOps;
import boofcv.alg.filter.binary.impl.ImplBinaryInnerOps;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.struct.ConnectRule;
import boofcv.struct.image.GrayS32;
import boofcv.struct.image.GrayU8;
import georegression.struct.point.Point2D_I32;
import org.ddogleg.struct.FastQueue;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
*
* Contains a standard set of operations performed on binary images. A pixel has a value of false if it is equal
* to zero or true equal to one.
*
*
*
* NOTE: If an element's value is not zero or one then each function's behavior is undefined.
*
*
* @author Peter Abeles
*/
/*
* DESIGN NOTE: 8-bit integer images ({@link GrayU8}) are used instead of images composed of boolean values because
* there is no performance advantage. According to the virtual machines specification binary arrays are stored as
* byte arrays with 1 representing true and 0 representing false.
* DESIGN NOTE: Restricting input values to zero and one was tested was compared against defining true as not zero.
* The former allowed a 2x to 3x performance boost by allowing numbers to be summed instead of compared.
*/
public class BinaryImageOps {
/**
* For each pixel it applies the logical 'and' operator between two images.
*
* @param inputA First input image. Not modified.
* @param inputB Second input image. Not modified.
* @param output Output image. Can be same as either input. If null a new instance will be declared, Modified.
* @return Output of logical operation.
*/
public static GrayU8 logicAnd(GrayU8 inputA , GrayU8 inputB , GrayU8 output )
{
InputSanityCheck.checkSameShape(inputA,inputB);
output = InputSanityCheck.checkDeclare(inputA, output);
for( int y = 0; y < inputA.height; y++ ) {
int indexA = inputA.startIndex + y*inputA.stride;
int indexB = inputB.startIndex + y*inputB.stride;
int indexOut = output.startIndex + y*output.stride;
int end = indexA + inputA.width;
for( ; indexA < end; indexA++,indexB++,indexOut++) {
int valA = inputA.data[indexA];
output.data[indexOut] = valA == 1 && valA == inputB.data[indexB] ? (byte)1 : (byte)0;
}
}
return output;
}
/**
* For each pixel it applies the logical 'or' operator between two images.
*
* @param inputA First input image. Not modified.
* @param inputB Second input image. Not modified.
* @param output Output image. Can be same as either input. If null a new instance will be declared, Modified.
* @return Output of logical operation.
*/
public static GrayU8 logicOr(GrayU8 inputA , GrayU8 inputB , GrayU8 output )
{
InputSanityCheck.checkSameShape(inputA,inputB);
output = InputSanityCheck.checkDeclare(inputA, output);
for( int y = 0; y < inputA.height; y++ ) {
int indexA = inputA.startIndex + y*inputA.stride;
int indexB = inputB.startIndex + y*inputB.stride;
int indexOut = output.startIndex + y*output.stride;
int end = indexA + inputA.width;
for( ; indexA < end; indexA++,indexB++,indexOut++) {
output.data[indexOut] =
inputA.data[indexA] == 1 || 1 == inputB.data[indexB] ? (byte)1 : (byte)0;
}
}
return output;
}
/**
* For each pixel it applies the logical 'xor' operator between two images.
*
* @param inputA First input image. Not modified.
* @param inputB Second input image. Not modified.
* @param output Output image. Can be same as either input. If null a new instance will be declared, Modified.
* @return Output of logical operation.
*/
public static GrayU8 logicXor(GrayU8 inputA , GrayU8 inputB , GrayU8 output )
{
InputSanityCheck.checkSameShape(inputA,inputB);
output = InputSanityCheck.checkDeclare(inputA, output);
for( int y = 0; y < inputA.height; y++ ) {
int indexA = inputA.startIndex + y*inputA.stride;
int indexB = inputB.startIndex + y*inputB.stride;
int indexOut = output.startIndex + y*output.stride;
int end = indexA + inputA.width;
for( ; indexA < end; indexA++,indexB++,indexOut++) {
output.data[indexOut] = inputA.data[indexA] != inputB.data[indexB] ? (byte)1 : (byte)0;
}
}
return output;
}
/**
* Inverts each pixel from true to false and vis-versa.
*
* @param input Input image. Not modified.
* @param output Output image. Can be same as input. If null a new instance will be declared, Modified.
* @return Output of logical operation.
*/
public static GrayU8 invert(GrayU8 input , GrayU8 output)
{
output = InputSanityCheck.checkDeclare(input, output);
for( int y = 0; y < input.height; y++ ) {
int index = input.startIndex + y*input.stride;
int indexOut = output.startIndex + y*output.stride;
int end = index + input.width;
for( ; index < end; index++,indexOut++) {
output.data[indexOut] = input.data[index] == 0 ? (byte)1 : (byte)0;
}
}
return output;
}
/**
*
* Erodes an image according to a 4-neighborhood. Unless a pixel is connected to all its neighbors its value
* is set to zero.
*
*
* @param input Input image. Not modified.
* @param numTimes How many times the operation will be applied to the image.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 erode4(GrayU8 input, int numTimes, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
if( numTimes <= 0 )
throw new IllegalArgumentException("numTimes must be >= 1");
ImplBinaryInnerOps.erode4(input, output);
ImplBinaryBorderOps.erode4(input, output);
if( numTimes > 1 ) {
GrayU8 tmp1 = new GrayU8(input.width,input.height);
GrayU8 tmp2 = output;
for( int i = 1; i < numTimes; i++ ) {
ImplBinaryInnerOps.erode4(tmp2, tmp1);
ImplBinaryBorderOps.erode4(tmp2, tmp1);
GrayU8 a = tmp1;
tmp1 = tmp2;
tmp2 = a;
}
if( tmp2 != output ) {
output.setTo(tmp2);
}
}
return output;
}
/**
*
* Dilates an image according to a 4-neighborhood. If a pixel is connected to any other pixel then its output
* value will be one.
*
*
* @param input Input image. Not modified.
* @param numTimes How many times the operation will be applied to the image.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 dilate4(GrayU8 input, int numTimes, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.dilate4(input, output);
ImplBinaryBorderOps.dilate4(input, output);
if( numTimes > 1 ) {
GrayU8 tmp1 = new GrayU8(input.width,input.height);
GrayU8 tmp2 = output;
for( int i = 1; i < numTimes; i++ ) {
ImplBinaryInnerOps.dilate4(tmp2, tmp1);
ImplBinaryBorderOps.dilate4(tmp2, tmp1);
GrayU8 a = tmp1;
tmp1 = tmp2;
tmp2 = a;
}
if( tmp2 != output ) {
output.setTo(tmp2);
}
}
return output;
}
/**
*
* Binary operation which is designed to remove all pixels but ones which are on the edge of an object.
* The edge is defined as lying on the object and not being surrounded by a pixel along a 4-neighborhood.
*
*
*
* NOTE: There are many ways to define an edge, this is just one of them.
*
*
* @param input Input image. Not modified.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 edge4(GrayU8 input, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.edge4(input, output);
ImplBinaryBorderOps.edge4(input, output);
return output;
}
/**
*
* Erodes an image according to a 8-neighborhood. Unless a pixel is connected to all its neighbors its value
* is set to zero.
*
*
* @param input Input image. Not modified.
* @param numTimes How many times the operation will be applied to the image.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 erode8(GrayU8 input, int numTimes, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.erode8(input, output);
ImplBinaryBorderOps.erode8(input, output);
if( numTimes > 1 ) {
GrayU8 tmp1 = new GrayU8(input.width,input.height);
GrayU8 tmp2 = output;
for( int i = 1; i < numTimes; i++ ) {
ImplBinaryInnerOps.erode8(tmp2, tmp1);
ImplBinaryBorderOps.erode8(tmp2, tmp1);
GrayU8 a = tmp1;
tmp1 = tmp2;
tmp2 = a;
}
if( tmp2 != output ) {
output.setTo(tmp2);
}
}
return output;
}
/**
*
* Dilates an image according to a 8-neighborhood. If a pixel is connected to any other pixel then its output
* value will be one.
*
*
* @param input Input image. Not modified.
* @param numTimes How many times the operation will be applied to the image.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 dilate8(GrayU8 input, int numTimes, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.dilate8(input, output);
ImplBinaryBorderOps.dilate8(input, output);
if( numTimes > 1 ) {
GrayU8 tmp1 = new GrayU8(input.width,input.height);
GrayU8 tmp2 = output;
for( int i = 1; i < numTimes; i++ ) {
ImplBinaryInnerOps.dilate8(tmp2, tmp1);
ImplBinaryBorderOps.dilate8(tmp2, tmp1);
GrayU8 a = tmp1;
tmp1 = tmp2;
tmp2 = a;
}
if( tmp2 != output ) {
output.setTo(tmp2);
}
}
return output;
}
/**
*
* Binary operation which is designed to remove all pixels but ones which are on the edge of an object.
* The edge is defined as lying on the object and not being surrounded by 8 pixels.
*
*
*
* NOTE: There are many ways to define an edge, this is just one of them.
*
*
* @param input Input image. Not modified.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 edge8(GrayU8 input, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.edge8(input, output);
ImplBinaryBorderOps.edge8(input, output);
return output;
}
/**
* Binary operation which is designed to remove small bits of spurious noise. An 8-neighborhood is used.
* If a pixel is connected to less than 2 neighbors then its value zero. If connected to more than 6 then
* its value is one. Otherwise it retains its original value.
*
* @param input Input image. Not modified.
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 removePointNoise(GrayU8 input, GrayU8 output) {
output = InputSanityCheck.checkDeclare(input, output);
ImplBinaryInnerOps.removePointNoise(input, output);
ImplBinaryBorderOps.removePointNoise(input, output);
return output;
}
/**
* Applies a morphological thinning operation to the image. Also known as skeletonization.
*
* @see BinaryThinning
*
* @param input Input image. Not modified.
* @param maxIterations Maximum number of cycles it will thin for. -1 for the maximum required
* @param output If not null, the output image. If null a new image is declared and returned. Modified.
* @return Output image.
*/
public static GrayU8 thin(GrayU8 input , int maxIterations, GrayU8 output ) {
output = InputSanityCheck.checkDeclare(input, output);
output.setTo(input);
BinaryThinning thinning = new BinaryThinning();
thinning.apply(output,maxIterations);
return output;
}
/**
*
* Given a binary image, connect together pixels to form blobs/clusters using the specified connectivity rule.
* The found blobs will be labeled in an output image and also described as a set of contours. Pixels
* in the contours are consecutive order in a clockwise or counter-clockwise direction, depending on the
* implementation. The labeled image will assign background pixels a label of 0 and each blob will be
* assigned a unique ID starting from 1.
*
*
*
* The returned contours are traces of the object. The trace of an object can be found by marking a point
* with a pen and then marking every point on the contour without removing the pen. It is possible to have
* the same point multiple times in the contour.
*
*
* @see LinearContourLabelChang2004
*
* @param input Input binary image. Not modified.
* @param rule Connectivity rule. Can be 4 or 8. 8 is more commonly used.
* @param output (Optional) Output labeled image. If null, an image will be declared internally. Modified.
* @return List of found contours for each blob.
*/
public static List contour(GrayU8 input, ConnectRule rule, GrayS32 output) {
if( output == null ) {
output = new GrayS32(input.width,input.height);
} else {
InputSanityCheck.checkSameShape(input,output);
}
LinearContourLabelChang2004 alg = new LinearContourLabelChang2004(rule);
alg.process(input,output);
return alg.getContours().toList();
}
/**
* Used to change the labels in a labeled binary image.
*
* @param input Labeled binary image.
* @param labels Look up table where the indexes are the current label and the value are its new value.
*/
public static void relabel(GrayS32 input , int labels[] ) {
for( int y = 0; y < input.height; y++ ) {
int index = input.startIndex + y*input.stride;
int end = index+input.width;
for( ; index < end; index++ ) {
int val = input.data[index];
input.data[index] = labels[val];
}
}
}
/**
* Converts a labeled image into a binary image by setting any non-zero value to one.
*
* @param labelImage Input image. Not modified.
* @param binaryImage Output image. Modified.
* @return The binary image.
*/
public static GrayU8 labelToBinary(GrayS32 labelImage , GrayU8 binaryImage ) {
binaryImage = InputSanityCheck.checkDeclare(labelImage, binaryImage, GrayU8.class);
for( int y = 0; y < labelImage.height; y++ ) {
int indexIn = labelImage.startIndex + y*labelImage.stride;
int indexOut = binaryImage.startIndex + y*binaryImage.stride;
int end = indexIn + labelImage.width;
for( ; indexIn < end; indexIn++, indexOut++ ) {
if( 0 == labelImage.data[indexIn] ) {
binaryImage.data[indexOut] = 0;
} else {
binaryImage.data[indexOut] = 1;
}
}
}
return binaryImage;
}
/**
* Only converts the specified blobs over into the binary image
*
* @param labelImage Input image. Not modified.
* @param binaryImage Output image. If null a new one will be declared. Modified.
* @param selectedBlobs Each index corresponds to a blob and specifies if it is included or not. Expected
* size is the number of found clusters + 1.
* @return The binary image.
*/
public static GrayU8 labelToBinary(GrayS32 labelImage , GrayU8 binaryImage ,
boolean selectedBlobs[] )
{
binaryImage = InputSanityCheck.checkDeclare(labelImage, binaryImage, GrayU8.class);
for( int y = 0; y < labelImage.height; y++ ) {
int indexIn = labelImage.startIndex + y*labelImage.stride;
int indexOut = binaryImage.startIndex + y*binaryImage.stride;
int end = indexIn + labelImage.width;
for( ; indexIn < end; indexIn++, indexOut++ ) {
int val = labelImage.data[indexIn];
if( selectedBlobs[val] ) {
binaryImage.data[indexOut] = 1;
} else {
binaryImage.data[indexOut] = 0;
}
}
}
return binaryImage;
}
/**
* Only converts the specified blobs over into the binary image. Easier to use version of
* {@link #labelToBinary(GrayS32, GrayU8, boolean[])}.
*
* @param labelImage Input image. Not modified.
* @param binaryImage Output image. If null a new one will be declared. Modified.
* @param numLabels Number of labels in the image. This is the number of found clusters + 1.
* @param selected The index of labels which will be marked as 1 in the output binary image.
* @return The binary image.
*/
public static GrayU8 labelToBinary(GrayS32 labelImage , GrayU8 binaryImage ,
int numLabels, int ...selected )
{
boolean selectedBlobs[] = new boolean[numLabels];
for (int i = 0; i < selected.length; i++) {
selectedBlobs[selected[i]] = true;
}
return labelToBinary(labelImage,binaryImage,selectedBlobs);
}
/**
* Scans through the labeled image and adds the coordinate of each pixel that has been
* labeled to a list specific to its label.
*
* @param labelImage The labeled image.
* @param numLabels Number of labeled objects inside the image.
* @param queue (Optional) Storage for pixel coordinates. Improves runtime performance. Can be null.
* @return List of pixels in each cluster.
*/
public static List> labelToClusters( GrayS32 labelImage ,
int numLabels ,
FastQueue queue )
{
List> ret = new ArrayList>();
for( int i = 0; i < numLabels+1; i++ ) {
ret.add( new ArrayList() );
}
if( queue == null ) {
queue = new FastQueue(numLabels,Point2D_I32.class,true);
} else
queue.reset();
for( int y = 0; y < labelImage.height; y++ ) {
int start = labelImage.startIndex + y*labelImage.stride;
int end = start + labelImage.width;
for( int index = start; index < end; index++ ) {
int v = labelImage.data[index];
if( v > 0 ) {
Point2D_I32 p = queue.grow();
p.set(index-start,y);
ret.get(v).add(p);
}
}
}
// first list is a place holder and should be empty
if( ret.get(0).size() != 0 )
throw new RuntimeException("BUG!");
ret.remove(0);
return ret;
}
/**
* Sets each pixel in the list of clusters to one in the binary image.
*
* @param clusters List of all the clusters.
* @param binary Output
*/
public static void clusterToBinary( List> clusters ,
GrayU8 binary )
{
ImageMiscOps.fill(binary, 0);
for( List l : clusters ) {
for( Point2D_I32 p : l ) {
binary.set(p.x,p.y,1);
}
}
}
/**
* Several blob rending functions take in an array of colors so that the random blobs can be drawn
* with the same color each time. This function selects a random color for each blob and returns it
* in an array.
* @param numBlobs Number of blobs found.
* @param rand Random number generator
* @return array of RGB colors for each blob + the background blob
*/
public static int[] selectRandomColors( int numBlobs , Random rand ) {
int colors[] = new int[ numBlobs+1 ];
colors[0] = 0; // black
int B = 100;
for( int i = 1; i < colors.length; i++ ) {
int c;
while( true ) {
c = rand.nextInt(0xFFFFFF);
// make sure its not too dark and can't be distriquished from the background
if( (c & 0xFF) > B || ((c >> 8) & 0xFF) > B || ((c >> 8 ) & 0xFF) > B ) {
break;
}
}
colors[i] = c;
}
return colors;
}
}
Ads help maintain this website.