Skip to content

Commit

Permalink
- Fix for pathological case in canny edge
Browse files Browse the repository at this point in the history
  * Thanks Lucaro for finding the bug
  * improved canny edge unit tests
- Cleared up documentation in edge non-maximum suppression
- Basic mean-shift segmentation is working, but needs additional work to produce more meaningful results
  * visualization code
  • Loading branch information
lessthanoptimal committed Jan 13, 2014
1 parent df36b83 commit 2e30cc8
Show file tree
Hide file tree
Showing 14 changed files with 517 additions and 42 deletions.
7 changes: 7 additions & 0 deletions change.txt
Expand Up @@ -14,6 +14,13 @@ Version : Alpha 0.17
- Added ImageMiscOps.flipHorizontal()
- Added WeightSample2D_F32 for computing the weight of a sample in an abstract manor
* Reduced MeanShiftPeak from 3 classes into one
- Edge Non-Maximum suppression's documentation now clearly states that it suppresses only if adjacent pixel is
less than. This is not true non-maximum suppression since it allows equal values but seems to produce
better results
* Improved unit tests to explicitly test for this behavior
- Canny edge detector would fail if threshold was zero and the image had no texture
* Improved unit tests for canny edge
* Thanks Lucaro for finding this bug

- TODO make those two failed unit tests ago away
- TODO BoofMiscOps move XML serialization into IO package
Expand Down
Expand Up @@ -58,11 +58,12 @@ private void printPreamble() {
"/**\n" +
" * <p>\n" +
" * Implementations of the crude version of non-maximum edge suppression. If the gradient is positive or negative\n" +
" * is used to determine the direction of suppression.\n" +
" * is used to determine the direction of suppression. This is faster since an expensive orientation calculation\n" +
" * is avoided.\n" +
" * </p>\n" +
" *\n" +
" * <p>\n" +
" * DO NOT MODIFY. Generated by {@link GenerateImplEdgeNonMaxSuppressionCrude}.\n" +
" * DO NOT MODIFY. Generated by {@link "+getClass().getSimpleName()+"}.\n" +
" * </p>\n" +
" *\n" +
" * @author Peter Abeles\n" +
Expand Down
14 changes: 11 additions & 3 deletions main/feature/src/boofcv/alg/feature/detect/edge/CannyEdge.java
Expand Up @@ -68,8 +68,8 @@ public class CannyEdge<T extends ImageSingleBand, D extends ImageSingleBand> {
private ImageUInt8 work = new ImageUInt8(1,1);

// different algorithms for performing hysteresis thresholding
private HysteresisEdgeTracePoints hysteresisPts; // saves a list of points
private HysteresisEdgeTraceMark hysteresisMark; // just marks a binary image
protected HysteresisEdgeTracePoints hysteresisPts; // saves a list of points
protected HysteresisEdgeTraceMark hysteresisMark; // just marks a binary image

/**
* Specify internal algorithms and behavior.
Expand Down Expand Up @@ -97,16 +97,24 @@ public CannyEdge(BlurFilter<T> blur, ImageGradient<T, D> gradient, boolean saveT
}

/**
* <p>
* Runs a canny edge detector on the input image given the provided thresholds. If configured to save
* a list of trace points then the output image is optional.
*
* </p>
* <p>
* NOTE: Input and output can be the same instance if the image type allows it.
* </p>
* @param input Input image. Not modified.
* @param threshLow Lower threshold.
* @param threshHigh Upper threshold.
* @param output (Might be option) Output binary image. Edge pixels are marked with 1 and everything else 0.
*/
public void process(T input , float threshLow, float threshHigh , ImageUInt8 output ) {

// can't handle this case because it assumes edges are at most one pixel thick
if( threshLow <= 0 && threshHigh <= 0 )
throw new IllegalArgumentException("Both thresholds are <= 0. This will make every pixel an edge!");

if( hysteresisMark != null ) {
if( output == null )
throw new IllegalArgumentException("An output image must be specified when configured to mark edge points");
Expand Down
Expand Up @@ -20,6 +20,7 @@

import boofcv.abst.filter.blur.BlurFilter;
import boofcv.abst.filter.derivative.ImageGradient;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.alg.misc.ImageStatistics;
import boofcv.struct.image.ImageSingleBand;
import boofcv.struct.image.ImageUInt8;
Expand Down Expand Up @@ -55,7 +56,16 @@ protected void performThresholding(float threshLow, float threshHigh, ImageUInt8
threshLow = max*threshLow;
threshHigh = max*threshHigh;

super.performThresholding(threshLow, threshHigh, output);
// test for the pathological case
if( threshLow <= 0 && threshHigh <= 0 ) {
// return nothing
if( hysteresisPts != null )
hysteresisPts.getContours().clear();
if( output != null )
ImageMiscOps.fill(output,0);
} else {
super.performThresholding(threshLow, threshHigh, output);
}
}

}
Expand Up @@ -306,7 +306,7 @@ static public ImageSInt8 discretizeDirection8( ImageFloat32 angle , ImageSInt8 d

/**
* <p>
* Sets edge intensities to zero if the pixel has an intensity which is not greater than any of
* Sets edge intensities to zero if the pixel has an intensity which is less than either of
* the two adjacent pixels. Pixel adjacency is determined by the gradients discretized direction.
* </p>
*
Expand All @@ -328,7 +328,7 @@ static public ImageFloat32 nonMaxSuppression4( ImageFloat32 intensity , ImageSIn

/**
* <p>
* Sets edge intensities to zero if the pixel has an intensity which is not greater than any of
* Sets edge intensities to zero if the pixel has an intensity which is less than either of
* the two adjacent pixels. Pixel adjacency is determined by the gradients discretized direction.
* </p>
*
Expand All @@ -350,7 +350,7 @@ static public ImageFloat32 nonMaxSuppression8( ImageFloat32 intensity , ImageSIn

/**
* <p>
* Sets edge intensities to zero if the pixel has an intensity which is not greater than any of
* Sets edge intensities to zero if the pixel has an intensity which is less than any of
* the two adjacent pixels. Pixel adjacency is determined based upon the sign of the image gradient. Less precise
* than other methods, but faster.
* </p>
Expand All @@ -374,7 +374,7 @@ static public ImageFloat32 nonMaxSuppressionCrude4( ImageFloat32 intensity , Ima

/**
* <p>
* Sets edge intensities to zero if the pixel has an intensity which is not greater than any of
* Sets edge intensities to zero if the pixel has an intensity which is less than any of
* the two adjacent pixels. Pixel adjacency is determined based upon the sign of the image gradient. Less precise
* than other methods, but faster.
* </p>
Expand All @@ -398,7 +398,7 @@ static public ImageFloat32 nonMaxSuppressionCrude4( ImageFloat32 intensity , Ima

/**
* <p>
* Sets edge intensities to zero if the pixel has an intensity which is not greater than any of
* Sets edge intensities to zero if the pixel has an intensity which is less than any of
* the two adjacent pixels. Pixel adjacency is determined based upon the sign of the image gradient. Less precise
* than other methods, but faster.
* </p>
Expand Down
Expand Up @@ -26,9 +26,11 @@

/**
* Algorithms for performing non-max suppression. Edge intensities are set to zero if adjacent pixels
* have a value equal to or greater than the current value. Adjacency is determined by the gradients
* have a value greater than the current value. Adjacency is determined by the gradients
* discretized direction.
*
* NOTE: This is technically not true non-maximum suppression because equal values are allowed.
*
* @author Peter Abeles
*/
public class ImplEdgeNonMaxSuppression {
Expand Down
Expand Up @@ -29,7 +29,8 @@
/**
* <p>
* Implementations of the crude version of non-maximum edge suppression. If the gradient is positive or negative
* is used to determine the direction of suppression.
* is used to determine the direction of suppression. This is faster since an expensive orientation calculation
* is avoided.
* </p>
*
* <p>
Expand Down
194 changes: 194 additions & 0 deletions main/feature/src/boofcv/alg/segmentation/MergeRegionMeanShiftGray.java
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2011-2013, 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;

import boofcv.alg.filter.binary.BinaryImageOps;
import boofcv.struct.image.ImageSInt32;
import org.ddogleg.struct.GrowQueue_F32;
import org.ddogleg.struct.GrowQueue_I32;

/**
* In a region with uniform color, mean-shift segmentation will produce lots of regions with identical colors since they
* are all local maximums. This will find all such neighbors and merge them into one group. For each
* pixel it checks its 4-connect neighbors to see if they are in the same region or not. If not in the same
* region it checks to see if their peaks have the same color to within tolerance. If so a mark will be
* made in an integer list of regions that one should be merged into another. A check is made to see
* if the region it is merged into doesn't merge into another one. If it does a link will be made directly to
* the last one it gets merged into.
*
* @author Peter Abeles
*/
public class MergeRegionMeanShiftGray {

// list used to convert the original region ID's into their new compacted ones
protected GrowQueue_I32 mergeList = new GrowQueue_I32();

// How similar two region's pixel intensity values need to be for them to be merged
protected float tolerance;

public MergeRegionMeanShiftGray(float tolerance) {
this.tolerance = tolerance;
}

public void merge( ImageSInt32 pixelToRegion , GrowQueue_I32 regionMemberCount , GrowQueue_F32 regionColor )
{
// see which ones need to be merged into which ones
createMergeList(pixelToRegion, regionColor);

// update member counts
updateMemberCount(regionMemberCount);

// compact the region id's
compactRegionId(regionMemberCount, regionColor);

// update the pixelToregion
for( int i = 0; i < mergeList.size; i++ ) {
if( mergeList.data[i] == -1 )
mergeList.data[i] = i;
}
BinaryImageOps.relabel(pixelToRegion, mergeList.data);

}

/**
* After merging regions there is likely to be many unused regions. This removes those unused regions
* and updates all references.
*
* mergeList is changed to use the new compacted IDs. the two input lists are compacted.
*/
protected void compactRegionId(GrowQueue_I32 regionMemberCount, GrowQueue_F32 regionColor) {
int offset = 0;
for( int i = 0; i < mergeList.size; i++ ) {
int w = mergeList.data[i];
if( w == -1 ){
// see if there is a need to change anything
if( offset == 0 )
continue;

// find all references to the original region and change it to the new index
int dst = i-offset;
for( int j = 0; j < mergeList.size; j++ ) {
if( mergeList.data[j] == i ) {
mergeList.data[j] = dst;
}
}

// shift over data which describes the region
regionMemberCount.data[dst] = regionMemberCount.data[i];
regionColor.data[dst] = regionColor.data[i];
} else {
// skip over these since it will be over written later
offset++;
}
}

regionMemberCount.size -= offset;
regionColor.size -= offset;
}

/**
* Adds the counts of all the regions that refer to others.
*/
protected void updateMemberCount(GrowQueue_I32 regionMemberCount) {
for( int i = 0; i < mergeList.size; i++ ) {
int w = mergeList.data[i];
if( w != -1 ) {
regionMemberCount.data[w] += regionMemberCount.data[i];
}
}
}

/**
* Checks the 4-connect of each pixel to see if it references a different region. If it does it checks to
* see if their pixel intensity values are within tolerance of each other. If so they are then marked for
* merging.
*/
private void createMergeList(ImageSInt32 pixelToregion, GrowQueue_F32 regionColor) {
// merge the merge list as initial all no merge
mergeList.resize(regionColor.getSize());
for( int i = 0; i < mergeList.size; i++ ) {
mergeList.data[i] = -1;
}

// TODO handle border case
for( int y = 0; y < pixelToregion.height-1; y++ ) {
int pixelIndex = y*pixelToregion.width;
for( int x = 0; x < pixelToregion.width-1; x++ , pixelIndex++) {
int a = pixelToregion.data[pixelIndex];
int b = pixelToregion.data[pixelIndex+1]; // pixel +1 x
int c = pixelToregion.data[pixelIndex+pixelToregion.width]; // pixel +1 y

float colorA = regionColor.get(a);

if( a != b ) {
float colorB = regionColor.get(b);
boolean merge = Math.abs(colorA-colorB) <= tolerance;
if( merge ) {
checkMerge(a,b);
}
}

if( a != c ) {
float colorC = regionColor.get(c);
boolean merge = Math.abs(colorA-colorC) <= tolerance;
if( merge ) {
checkMerge(a,c);
}
}
}
}
}

/**
* Two pixels have been found to have regions of similar color and are not the same region. This will mark
* one region as being merged into the other. If one or both have already been previously merged then
* they will point to one of their end destinations.
*
* This procedure ensures that any merge reference will be at most one deep. E.g. nothing like A -> B -> C
*/
protected void checkMerge( int regionA , int regionB ) {
boolean alreadyA = mergeList.data[regionA] != -1;
boolean alreadyB = mergeList.data[regionB] != -1;

if( alreadyA && alreadyB ) {
// if they already point to the same one, do nothing
if( mergeList.data[regionB] != mergeList.data[regionA] ) {
// put B into A, arbitrary choice
// don't point to A instead point to A's destination
int dstA = mergeList.data[regionA];
int dstB = mergeList.data[regionB];
// look for all reference to B and change to A
for( int i = 0; i < mergeList.size; i++ ) {
if( mergeList.data[i] == dstB )
mergeList.data[i] = dstA;
}
}

} else if( alreadyA ) {
// have B link to the same one as A
mergeList.data[regionB] = mergeList.data[regionA];
} else if( alreadyB ) {
// have A link to the same one as B
mergeList.data[regionA] = mergeList.data[regionB];
} else {
// have B point to A, arbitrary choice
mergeList.data[regionB] = regionA;
}
}
}
Expand Up @@ -44,6 +44,42 @@ public class TestCannyEdge {

Random rand = new Random(234);

/**
* Image has no texture and the sadistic user and specified a threshold of zero. Exception should be
* thrown because the threshold is zero
*/
@Test(expected=IllegalArgumentException.class)
public void canHandleNoTexture_and_zeroThresh() {
ImageUInt8 input = new ImageUInt8(width,height);
ImageUInt8 output = new ImageUInt8(width,height);

CannyEdge<ImageUInt8,ImageSInt16> alg = createCanny(true);

alg.process(input,0,0,output);
}

/**
* Test a pathological case. The input image has a constant gradient
*/
@Test
public void constantGradient() {
ImageUInt8 input = new ImageUInt8(width,height);
ImageUInt8 output = new ImageUInt8(width,height);

// the whole image has a constant gradient
for( int i = 0; i < input.width; i++ ) {
for( int j = 0; j < input.height; j++ ) {
input.unsafe_set(i,j,i*2);
}
}

CannyEdge<ImageUInt8,ImageSInt16> alg = createCanny(true);

alg.process(input,1,2,output);

// just see if it blows up or freezes
}

@Test
public void basicTestPoints() {

Expand Down

0 comments on commit 2e30cc8

Please sign in to comment.