All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.alg.feature.detect.edge.HysteresisEdgeTraceMark Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

The newest version!
/*
 * 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.feature.detect.edge;

import boofcv.alg.InputSanityCheck;
import boofcv.alg.misc.ImageMiscOps;
import boofcv.struct.image.GrayF32;
import boofcv.struct.image.GrayS8;
import boofcv.struct.image.GrayU8;
import georegression.struct.point.Point2D_I32;
import org.ddogleg.struct.DogArray;

/**
 * Given the output from edge non-maximum suppression, perform hysteresis threshold along the edge and mark selected
 * pixels in a binary image. Points are first marked in direction tangential to the edge's
 * direction, if no matches are found then a match is searched for using an 8-connect rule. The direction
 * image must be the 4-direction type.
 *
 * @author Peter Abeles
 */
@SuppressWarnings({"NullAway.Init"})
public class HysteresisEdgeTraceMark {

	// after an edge has been traversed it is set to this value
	public static final float MARK_TRAVERSED = -1;

	// reference to input intensity and direction images
	private GrayF32 intensity; // intensity after edge non-maximum suppression
	private GrayS8 direction; // 4-direction
	// output binary image
	private GrayU8 output;

	// lower threshold
	private float lower;

	// list of points which have yet to be explored
	private final DogArray open = new DogArray<>(Point2D_I32::new);

	// point which is current being examined
	private final Point2D_I32 active = new Point2D_I32();

	/**
	 * Performs hysteresis thresholding using the provided lower and upper thresholds.
	 *
	 * @param intensity Intensity image after edge non-maximum suppression has been applied. Modified.
	 * @param direction 4-direction image. Not modified.
	 * @param lower Lower threshold.
	 * @param upper Upper threshold.
	 * @param output Output binary image. Modified.
	 */
	public void process( GrayF32 intensity, GrayS8 direction, float lower, float upper,
						 GrayU8 output ) {
		if (lower < 0)
			throw new IllegalArgumentException("Lower must be >= 0!");
		InputSanityCheck.checkSameShape(intensity, direction, output);

		// set up internal data structures
		this.intensity = intensity;
		this.direction = direction;
		this.output = output;
		this.lower = lower;
		ImageMiscOps.fill(output, 0);

		// step through each pixel in the image
		for (int y = 0; y < intensity.height; y++) {
			int indexInten = intensity.startIndex + y*intensity.stride;

			for (int x = 0; x < intensity.width; x++, indexInten++) {
				// start a search if a pixel is found that's above the threshold
				if (intensity.data[indexInten] >= upper) {
					trace(x, y, indexInten);
				}
			}
		}
	}

	/**
	 * Traces along object's contour starting at the specified seed. As it does so it will set the intensity of
	 * points which are below the lower threshold to zero and add points to contour.
	 */
	protected void trace( int x, int y, int indexInten ) {

		int dx, dy;

		int indexOut = output.getIndex(x, y);
		open.grow().setTo(x, y);
		output.data[indexOut] = 1;
		intensity.data[indexInten] = MARK_TRAVERSED;

		while (open.size() > 0) {
			active.setTo(open.removeTail());
			indexInten = intensity.getIndex(active.x, active.y);
			int indexDir = direction.getIndex(active.x, active.y);

			boolean first = true;

			while (true) {
				//----- First check along the direction of the edge. Only need to check 2 points this way
				switch (direction.data[indexDir]) {
					case 0 -> {
						dx = 0;
						dy = 1;
					}
					case 1 -> {
						dx = 1;
						dy = -1;
					}
					case 2 -> {
						dx = 1;
						dy = 0;
					}
					case -1 -> {
						dx = 1;
						dy = 1;
					}
					default -> throw new RuntimeException("Unknown direction: " + direction.data[indexDir]);
				}

				int indexForward = indexInten + dy*intensity.stride + dx;
				int indexBackward = indexInten - dy*intensity.stride - dx;

				int prevIndexDir = indexDir;

				boolean match = false;

				// pixel coordinate of forward and backward point
				x = active.x;
				y = active.y;
				int fx = active.x + dx, fy = active.y + dy;
				int bx = active.x - dx, by = active.y - dy;

				if (intensity.isInBounds(fx, fy) && intensity.data[indexForward] >= lower) {
					intensity.data[indexForward] = MARK_TRAVERSED;
					output.unsafe_set(fx, fy, 1);
					active.setTo(fx, fy);
					match = true;
					indexInten = indexForward;
					indexDir = prevIndexDir + dy*intensity.stride + dx;
				}
				if (intensity.isInBounds(bx, by) && intensity.data[indexBackward] >= lower) {
					intensity.data[indexBackward] = MARK_TRAVERSED;
					output.unsafe_set(bx, by, 1);
					if (match) {
						open.grow().setTo(bx, by);
					} else {
						active.setTo(bx, by);
						match = true;
						indexInten = indexBackward;
						indexDir = prevIndexDir - dy*intensity.stride - dx;
					}
				}

				if (first || !match) {
					boolean priorMatch = match;
					// Check local neighbors if its one of the end points, which would be the first point or
					// any point for which no matches were found
					match = checkAllNeighbors(x, y, match);

					if (!match)
						break;
					else {
						// if it was the first it's no longer the first
						first = false;

						// the point at the end was just added and is to be searched in the next iteration
						if (!priorMatch) {
							indexInten = intensity.getIndex(active.x, active.y);
							indexDir = direction.getIndex(active.x, active.y);
						}
					}
				}
			}
		}
	}

	private boolean checkAllNeighbors( int x, int y, boolean match ) {
		match |= check(x + 1, y, match);
		match |= check(x, y + 1, match);
		match |= check(x - 1, y, match);
		match |= check(x, y - 1, match);

		match |= check(x + 1, y + 1, match);
		match |= check(x + 1, y - 1, match);
		match |= check(x - 1, y + 1, match);
		match |= check(x - 1, y - 1, match);

		return match;
	}

	/**
	 * Checks to see if the given coordinate is above the lower threshold. If it is the point will be
	 * added to the current segment or be the start of a new segment.
	 *
	 * @param match Has a match to the current segment already been found?
	 * @return true if a match was found at this point
	 */
	private boolean check( int x, int y, boolean match ) {

		if (intensity.isInBounds(x, y)) {
			int index = intensity.getIndex(x, y);
			if (intensity.data[index] >= lower) {
				intensity.data[index] = MARK_TRAVERSED;
				output.unsafe_set(x, y, 1);

				if (match) {
					open.grow().setTo(x, y);
				} else {
					active.setTo(x, y);
				}
				return true;
			}
		}
		return false;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy