Skip to content
This repository was archived by the owner on Nov 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ detect_target:
# min_circularity: 0.01
# max_circularity: 1
# filter_by_inertia: True
# min_inertia_ratio: 0.2
# min_inertia_ratio: 0.1
# max_inertia_ratio: 1
# filter_by_convexity: False
# min_convexity: 0.01
# max_convexity: 1
# filter_by_area: True
# min_area_pixels: 50
# max_area_pixels: 640
# min_area_pixels: 160
# max_area_pixels: 2000
# min_brightness_threshold: 50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make this a lot higher, like at least 200, probably 225

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make this a lot higher, like at least 200, probably 225

I originally had it at 200, but was failing the unit and the integration tests. It was able to detect the brightspots in the integration test at 100, and passed the unit test at 50. What should I do?

# min_average_brightness_threshold: 130

flight_interface:
# Port 5762 connects directly to the simulated auto pilot, which is more realistic
Expand Down
81 changes: 68 additions & 13 deletions modules/detect_target/detect_target_brightspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def __init__(
filter_by_area: bool,
min_area_pixels: int,
max_area_pixels: int,
min_brightness_threshold: int,
min_average_brightness_threshold: int,
) -> None:
"""
Initializes the configuration for DetectTargetBrightspot.
Expand All @@ -62,6 +64,8 @@ def __init__(
filter_by_area: Whether to filter by area.
min_area_pixels: Minimum area in pixels.
max_area_pixels: Maximum area in pixels.
min_brightness_threshold: Minimum brightness threshold for bright spots.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

min_brightness_threshold: Minimum absolute brightness of individual pixels to be considered
min_average_brightness_threshold: Minimum absolute average brightness of detected blobs

min_average_brightness_threshold: Minimum absolute average brightness of detected blobs.
"""
self.brightspot_percentile_threshold = brightspot_percentile_threshold
self.filter_by_color = filter_by_color
Expand All @@ -78,6 +82,8 @@ def __init__(
self.filter_by_area = filter_by_area
self.min_area_pixels = min_area_pixels
self.max_area_pixels = max_area_pixels
self.min_brightness_threshold = min_brightness_threshold
self.min_average_brightness_threshold = min_average_brightness_threshold


# pylint: enable=too-many-instance-attributes
Expand Down Expand Up @@ -128,20 +134,24 @@ def run(
# pylint: disable-next=broad-exception-caught
except Exception as exception:
self.__local_logger.error(
f"{time.time()}: Failed to convert to greyscale, exception: {exception}"
f"Failed to convert to greyscale, exception: {exception}"
)
return False, None

# Calculate the percentile threshold for bright spots
brightspot_threshold = np.percentile(
grey_image, self.__config.brightspot_percentile_threshold
)

# Apply thresholding to isolate bright spots
# Compute the maximum of the percentile threshold and the minimum brightness threshold
combined_threshold = max(brightspot_threshold, self.__config.min_brightness_threshold)

# Apply combined thresholding to isolate bright spots
threshold_used, bw_image = cv2.threshold(
grey_image, brightspot_threshold, 255, cv2.THRESH_BINARY
grey_image, combined_threshold, 255, cv2.THRESH_BINARY
)
if threshold_used == 0:
self.__local_logger.error(f"{time.time()}: Failed to threshold image.")
self.__local_logger.error("Failed to percentile threshold image.")
return False, None

# Set up SimpleBlobDetector
Expand All @@ -166,33 +176,69 @@ def run(

# A lack of detections is not an error, but should still not be forwarded
if len(keypoints) == 0:
self.__local_logger.info(f"{time.time()}: No brightspots detected.")
self.__local_logger.info("No brightspots detected (before blob average filter).")
return False, None

# Annotate the image (green circle) with detected keypoints
image_annotated = cv2.drawKeypoints(
image, keypoints, None, (0, 255, 0), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)
# Compute the average brightness of each blob
average_brightness_list = []

for i, keypoint in enumerate(keypoints):
x, y = keypoint.pt # Center of the blob
radius = keypoint.size / 2 # Radius of the blob

# Define a square region of interest (ROI) around the blob
x_min = int(max(0, x - radius))
x_max = int(min(grey_image.shape[1], x + radius))
y_min = int(max(0, y - radius))
y_max = int(min(grey_image.shape[0], y + radius))

# Create a circular mask for the blob
mask = np.zeros((y_max - y_min, x_max - x_min), dtype=np.uint8)
# Circle centered at middle of mask
cv2.circle(mask, (int(radius), int(radius)), int(radius), 255, -1)

# Extract the ROI from the grayscale image
roi = grey_image[y_min:y_max, x_min:x_max]

# Apply the mask to the ROI
masked_roi = cv2.bitwise_and(roi, roi, mask=mask)

# Calculate the mean brightness of the blob
mean_brightness = cv2.mean(masked_roi, mask=mask)[0]
# append index into list to keep track of associated keypoint
average_brightness_list.append((mean_brightness, i))

# filter the blobs by their average brightness
filtered_keypoints = []
for brightness, idx in average_brightness_list:
# Only append associated keypoint if the blob average is bright enough
if brightness >= self.__config.min_average_brightness_threshold:
filtered_keypoints.append(keypoints[idx])

# A lack of detections is not an error, but should still not be forwarded
if len(filtered_keypoints) == 0:
self.__local_logger.info("No brightspots detected (after blob average filter).")
return False, None

# Process bright spot detection
result, detections = detections_and_time.DetectionsAndTime.create(data.timestamp)
if not result:
self.__local_logger.error(f"{time.time()}: Failed to create detections for image.")
self.__local_logger.error("Failed to create detections for image.")
return False, None

# Get Pylance to stop complaining
assert detections is not None

# Draw bounding boxes around detected keypoints
for keypoint in keypoints:
for keypoint in filtered_keypoints:
x, y = keypoint.pt
size = keypoint.size
bounds = np.array([x - size / 2, y - size / 2, x + size / 2, y + size / 2])
result, detection = detections_and_time.Detection.create(
bounds, DETECTION_LABEL, CONFIDENCE
)
if not result:
self.__local_logger.error(f"{time.time()}: Failed to create bounding boxes.")
self.__local_logger.error("Failed to create bounding boxes.")
return False, None

# Get Pylance to stop complaining
Expand All @@ -206,12 +252,21 @@ def run(

# Logging
self.__local_logger.info(
f"{time.time()}: Count: {self.__counter}. Target detection took {end_time - start_time} seconds. Objects detected: {detections}."
f"Count: {self.__counter}. Target detection took {end_time - start_time} seconds. Objects detected: {detections}."
)

if self.__filename_prefix != "":
filename = self.__filename_prefix + str(self.__counter)

# Annotate the image (green circle) with detected keypoints
image_annotated = cv2.drawKeypoints(
image,
filtered_keypoints,
None,
(0, 255, 0),
cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS,
)

# Annotated image
cv2.imwrite(filename + ".png", image_annotated) # type: ignore

Expand Down
Binary file added tests/brightspot_example/ir_detections_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 7 additions & 5 deletions tests/integration/test_detect_target_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@


BRIGHTSPOT_TEST_PATH = pathlib.Path("tests", "brightspot_example")
IMAGE_BRIGHTSPOT_0_PATH = pathlib.Path(BRIGHTSPOT_TEST_PATH, "ir_detections_0.png")
IMAGE_BRIGHTSPOT_0_PATH = pathlib.Path(BRIGHTSPOT_TEST_PATH, "ir_detections_5.png")
IMAGE_BRIGHTSPOT_1_PATH = pathlib.Path(BRIGHTSPOT_TEST_PATH, "ir_detections_1.png")

BRIGHTSPOT_OPTION = detect_target_factory.DetectTargetOption.CV_BRIGHTSPOT
# Logging is identical to detect_target_ultralytics.py
# pylint: disable=duplicate-code
BRIGHTSPOT_CONFIG = detect_target_brightspot.DetectTargetBrightspotConfig(
brightspot_percentile_threshold=99.9,
brightspot_percentile_threshold=99.5,
filter_by_color=True,
blob_color=255,
filter_by_circularity=False,
Expand All @@ -40,8 +40,10 @@
min_convexity=0.01,
max_convexity=1,
filter_by_area=True,
min_area_pixels=50,
max_area_pixels=640,
min_area_pixels=100,
max_area_pixels=2000,
min_brightness_threshold=50,
min_average_brightness_threshold=120,
)
# pylint: enable=duplicate-code

Expand Down Expand Up @@ -160,7 +162,7 @@ def main() -> int:
Main function.
"""
run_worker(BRIGHTSPOT_OPTION, BRIGHTSPOT_CONFIG)
run_worker(ULTRALYTICS_OPTION, ULTRALYTICS_CONFIG)
# run_worker(ULTRALYTICS_OPTION, ULTRALYTICS_CONFIG)

return 0

Expand Down
8 changes: 5 additions & 3 deletions tests/unit/test_detect_target_brightspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
# Config is identical to test_detect_target_worker.py
# pylint: disable=duplicate-code
DETECT_TARGET_BRIGHTSPOT_CONFIG = detect_target_brightspot.DetectTargetBrightspotConfig(
brightspot_percentile_threshold=99.9,
brightspot_percentile_threshold=99.5,
filter_by_color=True,
blob_color=255,
filter_by_circularity=False,
Expand All @@ -51,8 +51,10 @@
min_convexity=0.01,
max_convexity=1,
filter_by_area=True,
min_area_pixels=50,
max_area_pixels=640,
min_area_pixels=100,
max_area_pixels=2000,
min_brightness_threshold=50,
min_average_brightness_threshold=120,
)
# pylint: enable=duplicate-code

Expand Down
Loading