-
Notifications
You must be signed in to change notification settings - Fork 37
Add in classical cv #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add in classical cv #204
Changes from all commits
c8b6228
1692f6f
dd9a29a
bf8faf1
1a19e20
c609f60
15dff4e
6a16444
cf82cf4
da7b5af
c0e772d
4d105f1
74497e9
0742460
b3dcfca
9b62cfa
a6540e7
8def299
14fcc41
fa9b8d6
f873640
59c8d94
8ce749c
d423026
4b8aa3c
cc24a04
e522279
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
""" | ||
Detects objects using the provided model. | ||
""" | ||
|
||
import time | ||
|
||
import cv2 | ||
import numpy as np | ||
|
||
from . import base_detect_target | ||
from .. import image_and_time | ||
from .. import detections_and_time | ||
from ..common.modules.logger import logger | ||
|
||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
MIN_CONTOUR_AREA = 100 | ||
MAX_CIRCULARITY = 1.3 | ||
MIN_CIRCULARITY = 0.7 | ||
|
||
UPPER_BLUE = np.array([130, 255, 255]) | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
LOWER_BLUE = np.array([100, 50, 50]) | ||
|
||
CONFIDENCE = 1.0 | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
LABEL = 0 | ||
|
||
|
||
class DetectTargetContour(base_detect_target.BaseDetectTarget): | ||
""" | ||
Predicts and locates landing pads using the classical computer vision methodology. | ||
""" | ||
|
||
def __init__( | ||
self, image_logger: logger.Logger, show_annotations: bool = False, save_name: str = "" | ||
) -> None: | ||
""" | ||
image_logger: Log annotated images. | ||
show_annotations: Display annotated images. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
save_name: filename prefix for logging detections and annotated images. | ||
""" | ||
self.__counter = 0 | ||
self.__show_annotations = show_annotations | ||
self.__filename_prefix = "" | ||
self.__logger = image_logger | ||
|
||
if save_name != "": | ||
self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_" | ||
|
||
def detect_landing_pads_contours( | ||
self, image_and_time_data: image_and_time.ImageAndTime | ||
) -> tuple[True, detections_and_time.DetectionsAndTime, np.ndarray] | tuple[False, None, None]: | ||
""" | ||
Detects landing pads using contours/classical CV. | ||
|
||
image_and_time_data: Data for the current image and time. | ||
timestamp: Timestamp for the detections. | ||
|
||
Return: Success, the DetectionsAndTime object, and the annotated image. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
image = image_and_time_data.image | ||
timestamp = image_and_time_data.timestamp | ||
|
||
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) | ||
mask = cv2.inRange(hsv_image, LOWER_BLUE, UPPER_BLUE) | ||
|
||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | ||
|
||
if len(contours) == 0: | ||
return False, None, None | ||
|
||
result, detections = detections_and_time.DetectionsAndTime.create(timestamp) | ||
if not result: | ||
return False, None, None | ||
|
||
image_annotated = image | ||
for i, contour in enumerate(contours): | ||
contour_area = cv2.contourArea(contour) | ||
|
||
if contour_area < MIN_CONTOUR_AREA: | ||
continue | ||
|
||
(x, y), radius = cv2.minEnclosingCircle(contour) | ||
|
||
enclosing_area = np.pi * (radius**2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add spaces around |
||
circularity = contour_area / enclosing_area | ||
|
||
if circularity < MIN_CIRCULARITY or circularity > MAX_CIRCULARITY: | ||
continue | ||
|
||
x, y, w, h = cv2.boundingRect(contour) | ||
bounds = np.array([x, y, x + w, y + h]) | ||
|
||
# Create a Detection object and append it to detections | ||
result, detection = detections_and_time.Detection.create(bounds, LABEL, CONFIDENCE) | ||
|
||
if not result: | ||
return False, None, None | ||
|
||
detections.append(detection) | ||
|
||
# Annotate the image | ||
cv2.rectangle(image_annotated, (x, y), (x + w, y + h), (0, 0, 255), 2) | ||
cv2.putText( | ||
image_annotated, | ||
f"landing-pad {i+1}", | ||
(x, y - 10), | ||
cv2.FONT_HERSHEY_SIMPLEX, | ||
0.9, | ||
(0, 0, 255), | ||
2, | ||
) | ||
|
||
return True, detections, image_annotated | ||
|
||
def run( | ||
self, data: image_and_time.ImageAndTime | ||
) -> tuple[True, detections_and_time.DetectionsAndTime] | tuple[False, None]: | ||
""" | ||
Runs object detection on the provided image and returns the detections. | ||
|
||
data: Image with a timestamp. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Return: Success and the detections. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
|
||
result, detections, image_annotated = self.detect_landing_pads_contours(data) | ||
|
||
if not result: | ||
return False, None | ||
|
||
# Logging | ||
if self.__filename_prefix != "": | ||
filename = self.__filename_prefix + str(self.__counter) | ||
self.__logger.save_image(image_annotated, filename) | ||
self.__counter += 1 | ||
|
||
if self.__show_annotations: | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cv2.imshow("Annotated", image_annotated) # type: ignore | ||
|
||
return True, detections | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
""" | ||
Helper functions for `test_detect_target_contour.py`. | ||
|
||
""" | ||
|
||
import cv2 | ||
import math | ||
import numpy as np | ||
|
||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from modules import detections_and_time | ||
from modules import image_and_time | ||
|
||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
LANDING_PAD_COLOUR_BLUE = (100, 50, 50) # BGR | ||
|
||
|
||
class LandingPadImageConfig: | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Represents the data required to define and generate a landing pad. | ||
""" | ||
|
||
def __init__( | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self, | ||
centre: tuple[int, int], | ||
axis: tuple[int, int], | ||
blur: bool, | ||
angle: float, | ||
): | ||
""" | ||
centre: The pixel coordinates representing the centre of the landing pad. | ||
axis: The pixel lengths of the semi-major axes of the ellipse. | ||
blur: Indicates whether the landing pad should have a blur effect. | ||
angle: The rotation angle of the landing pad in degrees clockwise, where 0.0 degrees | ||
is where both semi major and minor are aligned with the x and y-axis respectively (0.0 <= angle <= 360.0). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention only the semi-major axis, since the orientation of the semi-minor axis is a derived value. |
||
""" | ||
self.centre = centre | ||
self.axis = axis | ||
self.blur = blur | ||
self.angle = angle | ||
|
||
|
||
class NumpyImage: | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Holds the numpy array which represents an image. | ||
""" | ||
|
||
def __init__(self, image: np.ndarray): | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
image: A numpy array that represents the image. | ||
""" | ||
self.image = image | ||
|
||
|
||
class BoundingBox: | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Holds the data that define the generated bounding boxes. | ||
""" | ||
|
||
def __init__(self, top_left: tuple[float, float], bottom_right: tuple[float, float]): | ||
""" | ||
top_left: The pixel coordinates representing the top left corner of the bounding box on an image. | ||
bottom_right: pixel coordinates representing the bottom right corner of the bounding box on an image. | ||
""" | ||
self.top_left = top_left | ||
self.bottom_right = bottom_right | ||
|
||
|
||
class InputImageAndTimeAndExpectedBoundingBoxes: | ||
""" | ||
Struct to hold the data needed to perform the tests. | ||
""" | ||
|
||
def __init__(self, image_and_time_data: image_and_time.ImageAndTime, bounding_box_list: list): | ||
""" | ||
image_and_time_data: ImageAndTime object containing the image and timestamp | ||
bounding_box_list: A list that holds expected bounding box coordinates. | ||
Given in the following format: | ||
[conf, label, top_left_x, top_left_y, bottom_right_x, bottom_right_y] | ||
""" | ||
self.image_and_time_data = image_and_time_data | ||
self.bounding_box_list = bounding_box_list | ||
|
||
|
||
def add_blurred_landing_pad( | ||
background: np.ndarray, landing_data: LandingPadImageConfig | ||
) -> NumpyImage: | ||
""" | ||
Blurs an image and adds a singular landing pad to the background. | ||
|
||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
background: A numpy image. | ||
landing_data: Landing pad data for the landing pad to be blurred and added. | ||
|
||
Returns: Image with the landing pad. | ||
""" | ||
x, y = background.shape[:2] | ||
|
||
mask = np.zeros((x, y), np.uint8) | ||
mask = cv2.ellipse( | ||
mask, | ||
landing_data.centre, | ||
landing_data.axis, | ||
landing_data.angle, | ||
0, | ||
360, | ||
255, | ||
-1, | ||
) | ||
|
||
mask = cv2.blur(mask, (25, 25), 7) | ||
|
||
alpha = mask[:, :, np.newaxis] / 255.0 | ||
# Brings the image back to its original colour. | ||
fg = np.full(background.shape, LANDING_PAD_COLOUR_BLUE, dtype=np.uint8) | ||
|
||
blended = (background * (1 - alpha) + fg * alpha).astype(np.uint8) | ||
return NumpyImage(blended) | ||
|
||
|
||
def draw_landing_pad( | ||
image: np.ndarray, landing_data: LandingPadImageConfig | ||
) -> tuple[NumpyImage, BoundingBox]: | ||
""" | ||
Draws a single landing pad on the provided image and saves the bounding box coordinates to a text file. | ||
|
||
image: The image to add a landing pad to. | ||
landing_data: Landing pad data for the landing pad to be added. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Returns: Image with landing pad and the bounding box for the drawn landing pad. | ||
""" | ||
centre_x, centre_y = landing_data.centre | ||
axis_x, axis_y = landing_data.axis | ||
angle_in_rad = math.radians(landing_data.angle) | ||
|
||
ux = axis_x * math.cos(angle_in_rad) | ||
uy = axis_x * math.sin(angle_in_rad) | ||
|
||
vx = axis_y * math.sin(angle_in_rad) | ||
vy = axis_y * math.cos(angle_in_rad) | ||
|
||
width = 2 * math.sqrt(ux**2 + vx**2) | ||
height = 2 * math.sqrt(uy**2 + vy**2) | ||
|
||
top_left = (max(centre_x - (0.5) * width, 0), max(centre_y - (0.5) * height, 0)) | ||
bottom_right = ( | ||
min(centre_x + (0.5) * width, image.shape[1]), | ||
min(centre_y + (0.5) * height, image.shape[0]), | ||
) | ||
|
||
bounding_box = BoundingBox(top_left, bottom_right) | ||
|
||
if landing_data.blur: | ||
image = add_blurred_landing_pad(image, landing_data) | ||
return image, bounding_box | ||
|
||
image = cv2.ellipse( | ||
image, | ||
landing_data.centre, | ||
landing_data.axis, | ||
landing_data.angle, | ||
0, | ||
360, | ||
LANDING_PAD_COLOUR_BLUE, | ||
-1, | ||
) | ||
return NumpyImage(image), bounding_box | ||
|
||
|
||
def create_test( | ||
landing_list: list[LandingPadImageConfig], | ||
) -> InputImageAndTimeAndExpectedBoundingBoxes: | ||
""" | ||
Generates test cases given a data set. | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
landing_list: List of landing pad data to be generated. | ||
|
||
Returns: The image and expected bounding box. | ||
""" | ||
image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.uint8) | ||
confidence_and_label = [1, 0] | ||
|
||
# List to hold the bounding boxes. | ||
# boxes_list = [confidence, label, top_left_x, top_left_y, bottom_right_x, bottom_right_y] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not true! |
||
boxes_list = [] | ||
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
Xierumeng marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
for landing_data in landing_list: | ||
image_wrapper, bounding_box = draw_landing_pad(image, landing_data) | ||
image = image_wrapper.image | ||
boxes_list.append( | ||
confidence_and_label + list(bounding_box.top_left + bounding_box.bottom_right) | ||
) | ||
|
||
# Sorts by the area of the bounding box | ||
boxes_list = sorted( | ||
boxes_list, reverse=True, key=lambda box: abs((box[4] - box[2]) * (box[5] - box[3])) | ||
) | ||
|
||
image = image.astype(np.uint8) | ||
result, image_and_time_data = image_and_time.ImageAndTime.create(image) | ||
|
||
assert result | ||
assert image_and_time_data is not None | ||
|
||
return InputImageAndTimeAndExpectedBoundingBoxes(image_and_time_data, boxes_list) |
Uh oh!
There was an error while loading. Please reload this page.