From c8b6228f5cd9abbbbc813cafe47d82355d1a1728 Mon Sep 17 00:00:00 2001
From: Ashish Agrahari <shinyflame007@gmail.com>
Date: Sat, 28 Sep 2024 19:41:58 -0400
Subject: [PATCH 01/27] Added Dylans code

---
 main_detect_target.py                         | 215 ++++++++++++++++++
 .../detect_target/detect_target_contour.py    | 156 +++++++++++++
 .../detect_target/detect_target_factory.py    |   7 +
 3 files changed, 378 insertions(+)
 create mode 100644 main_detect_target.py
 create mode 100644 modules/detect_target/detect_target_contour.py

diff --git a/main_detect_target.py b/main_detect_target.py
new file mode 100644
index 00000000..3183c96f
--- /dev/null
+++ b/main_detect_target.py
@@ -0,0 +1,215 @@
+"""
+For 2022-2023 UAS competition.
+"""
+
+import argparse
+import multiprocessing as mp
+import pathlib
+
+from modules.detect_target import detect_target_factory
+from modules.detect_target import detect_target_worker
+from modules.video_input import video_input_worker
+from modules.common.logger.modules import logger
+from modules.common.logger.modules import logger_setup_main
+from modules.common.logger.read_yaml.modules import read_yaml
+from utilities.workers import queue_proxy_wrapper
+from utilities.workers import worker_controller
+from utilities.workers import worker_manager
+
+
+CONFIG_FILE_PATH = pathlib.Path("config.yaml")
+
+
+# Code copied into main_2024.py
+# pylint: disable=duplicate-code
+def main() -> int:
+    """
+    Main function.
+    """
+    # Parse whether or not to force cpu from command line
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--cpu", action="store_true", help="option to force cpu")
+    parser.add_argument("--full", action="store_true", help="option to force full precision")
+    parser.add_argument(
+        "--show-annotated",
+        action="store_true",
+        help="option to show annotated image",
+    )
+    args = parser.parse_args()
+
+    # Configuration settings
+    result, config = read_yaml.open_config(CONFIG_FILE_PATH)
+    if not result:
+        print("ERROR: Failed to load configuration file")
+        return -1
+
+    # Get Pylance to stop complaining
+    assert config is not None
+
+    # Logger configuration settings
+    result, config_logger = read_yaml.open_config(logger.CONFIG_FILE_PATH)
+    if not result:
+        print("ERROR: Failed to load configuration file")
+        return -1
+
+    # Get Pylance to stop complaining
+    assert config_logger is not None
+
+    # Setup main logger
+    result, main_logger, logging_path = logger_setup_main.setup_main_logger(config_logger)
+    if not result:
+        print("ERROR: Failed to create main logger")
+        return -1
+
+    # Get Pylance to stop complaining
+    assert main_logger is not None
+    assert logging_path is not None
+
+    # Get settings
+    try:
+        # Local constants
+        # pylint: disable=invalid-name
+        QUEUE_MAX_SIZE = config["queue_max_size"]
+
+        VIDEO_INPUT_CAMERA_NAME = config["video_input"]["camera_name"]
+        VIDEO_INPUT_WORKER_PERIOD = config["video_input"]["worker_period"]
+        VIDEO_INPUT_SAVE_NAME_PREFIX = config["video_input"]["save_prefix"]
+        VIDEO_INPUT_SAVE_PREFIX = str(pathlib.Path(logging_path, VIDEO_INPUT_SAVE_NAME_PREFIX))
+
+        DETECT_TARGET_WORKER_COUNT = config["detect_target"]["worker_count"]
+        detect_target_option_int = config["detect_target"]["option"]
+        DETECT_TARGET_OPTION = detect_target_factory.DetectTargetOption(detect_target_option_int)
+        DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"]
+        DETECT_TARGET_MODEL_PATH = config["detect_target"]["model_path"]
+        DETECT_TARGET_OVERRIDE_FULL_PRECISION = args.full
+        DETECT_TARGET_USE_CLASSICAL_CV = config["detect_target"]["use_classical_cv"]
+        DETECT_TARGET_SAVE_NAME_PREFIX = config["detect_target"]["save_prefix"]
+        DETECT_TARGET_SAVE_PREFIX = str(pathlib.Path(logging_path, DETECT_TARGET_SAVE_NAME_PREFIX))
+        DETECT_TARGET_SHOW_ANNOTATED = args.show_annotated
+        # pylint: enable=invalid-name
+    except KeyError as exception:
+        main_logger.error(f"ERROR: Config key(s) not found: {exception}", True)
+        return -1
+
+    # Setup
+    controller = worker_controller.WorkerController()
+
+    mp_manager = mp.Manager()
+    video_input_to_detect_target_queue = queue_proxy_wrapper.QueueProxyWrapper(
+        mp_manager,
+        QUEUE_MAX_SIZE,
+    )
+    detect_target_to_main_queue = queue_proxy_wrapper.QueueProxyWrapper(
+        mp_manager,
+        QUEUE_MAX_SIZE,
+    )
+
+    # Worker properties
+    result, video_input_worker_properties = worker_manager.WorkerProperties.create(
+        count=1,
+        target=video_input_worker.video_input_worker,
+        work_arguments=(
+            VIDEO_INPUT_CAMERA_NAME,
+            VIDEO_INPUT_WORKER_PERIOD,
+            VIDEO_INPUT_SAVE_PREFIX,
+        ),
+        input_queues=[],
+        output_queues=[video_input_to_detect_target_queue],
+        controller=controller,
+        local_logger=main_logger,
+    )
+    if not result:
+        main_logger.error("Failed to create arguments for Video Input", True)
+        return -1
+
+    # Get Pylance to stop complaining
+    assert video_input_worker_properties is not None
+
+    result, detect_target_worker_properties = worker_manager.WorkerProperties.create(
+        count=DETECT_TARGET_WORKER_COUNT,
+        target=detect_target_worker.detect_target_worker,
+        work_arguments=(
+            DETECT_TARGET_OPTION,
+            DETECT_TARGET_DEVICE,
+            DETECT_TARGET_MODEL_PATH,
+            DETECT_TARGET_OVERRIDE_FULL_PRECISION,
+            DETECT_TARGET_USE_CLASSICAL_CV,
+            DETECT_TARGET_SHOW_ANNOTATED,
+            DETECT_TARGET_SAVE_PREFIX,
+        ),
+        input_queues=[video_input_to_detect_target_queue],
+        output_queues=[detect_target_to_main_queue],
+        controller=controller,
+        local_logger=main_logger,
+    )
+    if not result:
+        main_logger.error("Failed to create arguments for Detect Target", True)
+        return -1
+
+    # Get Pylance to stop complaining
+    assert detect_target_worker_properties is not None
+
+    # Create managers
+    worker_managers = []
+
+    result, video_input_manager = worker_manager.WorkerManager.create(
+        worker_properties=video_input_worker_properties,
+        local_logger=main_logger,
+    )
+    if not result:
+        main_logger.error("Failed to create manager for Video Input", True)
+        return -1
+
+    # Get Pylance to stop complaining
+    assert video_input_manager is not None
+
+    worker_managers.append(video_input_manager)
+
+    result, detect_target_manager = worker_manager.WorkerManager.create(
+        worker_properties=detect_target_worker_properties,
+        local_logger=main_logger,
+    )
+    if not result:
+        main_logger.error("Failed to create manager for Detect Target", True)
+        return -1
+
+    # Get Pylance to stop complaining
+    assert detect_target_manager is not None
+
+    worker_managers.append(detect_target_manager)
+
+    # Run
+    for manager in worker_managers:
+        manager.start_workers()
+
+    while True:
+        # Use main_logger for debugging
+        detections_and_time = detect_target_to_main_queue.queue.get()
+        if detections_and_time is None:
+            break
+        main_logger.debug(f"Timestamp: {detections_and_time.timestamp}", True)
+        main_logger.debug(f"Num detections: {len(detections_and_time.detections)}", True)
+        for detection in detections_and_time.detections:
+            main_logger.debug(f"Detection: {detection}", True)
+
+    # Teardown
+    controller.request_exit()
+
+    video_input_to_detect_target_queue.fill_and_drain_queue()
+    detect_target_to_main_queue.fill_and_drain_queue()
+
+    for manager in worker_managers:
+        manager.join_workers()
+
+    return 0
+
+
+# pylint: enable=duplicate-code
+
+
+if __name__ == "__main__":
+    result_main = main()
+    if result_main < 0:
+        print(f"ERROR: Status code: {result_main}")
+
+    print("Done!")
diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
new file mode 100644
index 00000000..eb38a977
--- /dev/null
+++ b/modules/detect_target/detect_target_contour.py
@@ -0,0 +1,156 @@
+"""
+Detects objects using the provided model.
+"""
+
+import time
+
+import copy
+import cv2
+import numpy as np
+
+from . import base_detect_target
+from .. import image_and_time
+from .. import detections_and_time
+
+
+class DetectTargetContour(base_detect_target.BaseDetectTarget):
+    """
+    Contains the YOLOv8 model for prediction.
+    """
+
+    def __init__(
+        self,
+        show_annotations: bool = False,
+        save_name: str = "",
+    ) -> None:
+        """
+        device: name of target device to run inference on (i.e. "cpu" or cuda device 0, 1, 2, 3).
+        model_path: path to the YOLOv8 model.
+        override_full: Force full precision floating point calculations.
+        show_annotations: Display annotated images.
+        save_name: filename prefix for logging detections and annotated images.
+        """
+        self.__counter = 0
+        self.__show_annotations = show_annotations
+        self.__filename_prefix = ""
+        if save_name != "":
+            self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
+
+    @staticmethod
+    def is_contour_circular(contour: np.ndarray) -> bool:
+        """
+        Helper function for detect_landing_pads_contours.
+        Checks if the shape is close to circular.
+        Return: True is the shape is circular, false if it is not.
+        """
+        contour_minimum = 0.8
+        perimeter = cv2.arcLength(contour, True)
+        # Check if the perimeter is zero
+        if perimeter == 0.0:
+            return False
+
+        area = cv2.contourArea(contour)
+        circularity = 4 * np.pi * (area / (perimeter * perimeter))
+        return circularity > contour_minimum
+
+    @staticmethod
+    def is_contour_large_enough(contour: np.ndarray, min_diameter: float) -> bool:
+        """
+        Helper function for detect_landing_pads_contours.
+        Checks if the shape is larger than the provided diameter.
+        Return: True if it is, false if it not.
+        """
+        _, radius = cv2.minEnclosingCircle(contour)
+        diameter = radius * 2
+        return diameter >= min_diameter
+
+    def detect_landing_pads_contours(
+        self, image: "np.ndarray", timestamp: float
+    ) -> "tuple[bool, detections_and_time.DetectionsAndTime | None, np.ndarray]":
+        """
+        Detects landing pads using contours/classical cv.
+        image: Current image frame.
+        timestamp: Timestamp for the detections.
+        Return: Success, the DetectionsAndTime object, and the annotated image.
+        """
+        kernel = np.ones((2, 2), np.uint8)
+        gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+        threshold = 180
+        im_bw = cv2.threshold(gray_image, threshold, 255, cv2.THRESH_BINARY)[1]
+        im_dilation = cv2.dilate(im_bw, kernel, iterations=1)
+        contours, hierarchy = cv2.findContours(im_dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
+
+        if len(contours) == 0:
+            return False, None, image
+
+        contours_with_children = set(i for i, hier in enumerate(hierarchy[0]) if hier[2] != -1)
+        parent_circular_contours = [
+            cnt
+            for i, cnt in enumerate(contours)
+            if self.is_contour_circular(cnt)
+            and self.is_contour_large_enough(cnt, 7)
+            and i in contours_with_children
+        ]
+
+        largest_contour = max(parent_circular_contours, key=cv2.contourArea, default=None)
+        if largest_contour is None:
+            return False, None, image
+
+        # Create the DetectionsAndTime object
+        result, detections = detections_and_time.DetectionsAndTime.create(timestamp)
+        if not result:
+            return False, None, image
+
+        x, y, w, h = cv2.boundingRect(largest_contour)
+        bounds = np.array([x, y, x + w, y + h])
+        confidence = 1.0  # Confidence for classical CV is often set to a constant value
+        label = 0  # Label can be set to a constant or derived from some logic
+
+        # Create a Detection object and append it to detections
+        result, detection = detections_and_time.Detection.create(bounds, label, confidence)
+        if result:
+            detections.append(detection)
+
+        # Annotate the image
+        image_annotated = copy.deepcopy(image)
+        cv2.rectangle(image_annotated, (x, y), (x + w, y + h), (0, 0, 255), 2)
+        cv2.putText(
+            image_annotated,
+            "landing-pad",
+            (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[bool, detections_and_time.DetectionsAndTime | None]":
+        """
+        Runs object detection on the provided image and returns the detections.
+        data: Image with a timestamp.
+        Return: Success and the detections.
+        """
+        image = data.image
+        timestamp = data.timestamp
+
+        result, detections, image_annotated = self.detect_landing_pads_contours(image, timestamp)
+        if not result:
+            return False, None
+
+        # Logging
+        if self.__filename_prefix != "":
+            filename = self.__filename_prefix + str(self.__counter)
+            # Object detections
+            with open(filename + ".txt", "w", encoding="utf-8") as file:
+                # Use internal string representation
+                file.write(repr(detections))
+            # Annotated image
+            cv2.imwrite(filename + ".png", image_annotated)  # type: ignore
+            self.__counter += 1
+        if self.__show_annotations:
+            cv2.imshow("Annotated", image_annotated)  # type: ignore
+        return True, detections
diff --git a/modules/detect_target/detect_target_factory.py b/modules/detect_target/detect_target_factory.py
index 376456cb..a7a9cfab 100644
--- a/modules/detect_target/detect_target_factory.py
+++ b/modules/detect_target/detect_target_factory.py
@@ -6,6 +6,7 @@
 
 from . import base_detect_target
 from . import detect_target_brightspot
+from . import detect_target_contour
 from . import detect_target_ultralytics
 from ..common.modules.logger import logger
 
@@ -17,6 +18,7 @@ class DetectTargetOption(enum.Enum):
 
     ML_ULTRALYTICS = 0
     CV_BRIGHTSPOT = 1
+    C_CONTOUR = 2
 
 
 def create_detect_target(
@@ -47,5 +49,10 @@ def create_detect_target(
                 show_annotations,
                 save_name,
             )
+        case DetectTargetOption.C_CONTOUR:
+            return True, detect_target_contour.DetectTargetContour(
+                show_annotations,
+                save_name,
+            )
 
     return False, None

From 1692f6fd445106055f074f2e67c5497743853b55 Mon Sep 17 00:00:00 2001
From: Ashish Agrahari <shinyflame007@gmail.com>
Date: Sat, 28 Sep 2024 19:53:06 -0400
Subject: [PATCH 02/27] Made fixes

---
 main_detect_target.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/main_detect_target.py b/main_detect_target.py
index 3183c96f..7190386f 100644
--- a/main_detect_target.py
+++ b/main_detect_target.py
@@ -82,7 +82,6 @@ def main() -> int:
         DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"]
         DETECT_TARGET_MODEL_PATH = config["detect_target"]["model_path"]
         DETECT_TARGET_OVERRIDE_FULL_PRECISION = args.full
-        DETECT_TARGET_USE_CLASSICAL_CV = config["detect_target"]["use_classical_cv"]
         DETECT_TARGET_SAVE_NAME_PREFIX = config["detect_target"]["save_prefix"]
         DETECT_TARGET_SAVE_PREFIX = str(pathlib.Path(logging_path, DETECT_TARGET_SAVE_NAME_PREFIX))
         DETECT_TARGET_SHOW_ANNOTATED = args.show_annotated
@@ -133,7 +132,6 @@ def main() -> int:
             DETECT_TARGET_DEVICE,
             DETECT_TARGET_MODEL_PATH,
             DETECT_TARGET_OVERRIDE_FULL_PRECISION,
-            DETECT_TARGET_USE_CLASSICAL_CV,
             DETECT_TARGET_SHOW_ANNOTATED,
             DETECT_TARGET_SAVE_PREFIX,
         ),

From dd9a29a8ab805b453a7bf3431dc51fd091819037 Mon Sep 17 00:00:00 2001
From: Zenkqi <SSGSSAchita@gmail.com>
Date: Thu, 10 Oct 2024 21:28:02 -0400
Subject: [PATCH 03/27] Added test_detect_target_contour.py and edited
 detect_target_contour: Issues: detect_target_contour not detecting any
 contours(?)

---
 .../detect_target/detect_target_contour.py    |   7 -
 tests/model_example/background.png            | Bin 0 -> 16143 bytes
 tests/model_example/bounding_box1.txt         |   2 +
 tests/model_example/test_output_1.png         | Bin 0 -> 46145 bytes
 tests/unit/test_detect_target_contour.py      | 285 ++++++++++++++++++
 5 files changed, 287 insertions(+), 7 deletions(-)
 create mode 100644 tests/model_example/background.png
 create mode 100644 tests/model_example/bounding_box1.txt
 create mode 100644 tests/model_example/test_output_1.png
 create mode 100644 tests/unit/test_detect_target_contour.py

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index eb38a977..77c51d4a 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -14,19 +14,12 @@
 
 
 class DetectTargetContour(base_detect_target.BaseDetectTarget):
-    """
-    Contains the YOLOv8 model for prediction.
-    """
-
     def __init__(
         self,
         show_annotations: bool = False,
         save_name: str = "",
     ) -> None:
         """
-        device: name of target device to run inference on (i.e. "cpu" or cuda device 0, 1, 2, 3).
-        model_path: path to the YOLOv8 model.
-        override_full: Force full precision floating point calculations.
         show_annotations: Display annotated images.
         save_name: filename prefix for logging detections and annotated images.
         """
diff --git a/tests/model_example/background.png b/tests/model_example/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6f4067d8bad97f922582742aefdba473a64d7c2
GIT binary patch
literal 16143
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

literal 0
HcmV?d00001

diff --git a/tests/model_example/bounding_box1.txt b/tests/model_example/bounding_box1.txt
new file mode 100644
index 00000000..35857d70
--- /dev/null
+++ b/tests/model_example/bounding_box1.txt
@@ -0,0 +1,2 @@
+1 0 0 0 600 600
+1 1 1000 200 1920 1124
diff --git a/tests/model_example/test_output_1.png b/tests/model_example/test_output_1.png
new file mode 100644
index 0000000000000000000000000000000000000000..f37919759855825bd5a1add0a2b99b74f6bd0e46
GIT binary patch
literal 46145
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
G*Z&XeH28i1

literal 0
HcmV?d00001

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
new file mode 100644
index 00000000..dbcaa1ca
--- /dev/null
+++ b/tests/unit/test_detect_target_contour.py
@@ -0,0 +1,285 @@
+"""
+Test DetectTarget module.
+
+"""
+import pathlib
+
+import cv2
+import numpy as np
+import pytest
+import math
+
+from modules.detect_target import detect_target_contour
+from modules import image_and_time
+from modules import detections_and_time
+
+
+TEST_PATH = pathlib.Path("tests", "model_example")
+BG_PATH = pathlib.Path(TEST_PATH, "background.png")
+
+BOUNDING_BOX_PRECISION_TOLERANCE = 0
+CONFIDENCE_PRECISION_TOLERANCE = 2
+
+
+# Test functions use test fixture signature names and access class privates
+# No enable
+# pylint: disable=protected-access,redefined-outer-name
+
+
+class GenerateTest:
+    def __init__(self, circle_data: list[list], image_file: str, txt_file: str) -> None:
+        self.circle_data = circle_data[0:]
+        self.image_file = image_file
+        self.txt_file = txt_file
+        self.image_path = "tests/model_example/"
+
+    def save_bounding_box_annotation(self, test_case: int, boxes_list: int) -> None:
+        """
+        Save the bounding box annotation for the circle in the format:
+        format: conf class_label x_min y_min x_max y_max
+        """
+
+        txt_file = self.image_path + self.txt_file + str(test_case) + ".txt"
+        with open(txt_file, "w") as f:
+            for class_label, (top_left, bottom_right) in enumerate(boxes_list):
+                x_min, y_min = top_left
+                x_max, y_max = bottom_right
+
+                f.write(f"{1} {class_label} {x_min} {y_min} {x_max} {y_max}\n")
+        print(f"Bounding box annotation saved to {txt_file}")
+
+    def blur_img(
+        self,
+        bg: np.ndarray,
+        center: tuple[int, int],
+        radius: int = 0,
+        axis_length: tuple[int, int] = (0, 0),
+        angle: int = 0,
+        circle_check: bool = True,
+    ) -> np.ndarray:
+        """
+        Blurs an image a singular shape and adds it to the background.
+        """
+
+        bg_copy = bg.copy()
+        x, y = bg_copy.shape[:2]
+
+        mask = np.zeros((x, y), np.uint8)
+        if circle_check:
+            mask = cv2.circle(mask, center, radius, (215, 158, 115), -1, cv2.LINE_AA)
+        else:
+            mask = cv2.ellipse(mask, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+
+        mask = cv2.blur(mask, (25, 25), 7)
+
+        alpha = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
+        fg = np.zeros(bg.shape, np.uint8)
+        fg[:, :, :] = [200, 10, 200]
+
+        blended = cv2.convertScaleAbs(bg * (1 - alpha) + fg * alpha)
+        return blended
+
+    def draw_circle(
+        self, image: np.ndarray, center: tuple[int, int], radius: int, blur: bool
+    ) -> tuple[np.ndarray, int, int]:
+        """
+        Draws a circle on the provided image and saves the bounding box coordinates to a text file.
+        """
+        x, y = center
+        top_left = (max(x - radius, 0), max(y - radius, 0))
+        bottom_right = (min(x + radius, image.shape[1]), min(y + radius, image.shape[0]))
+
+        if blur:
+            image = self.blur_img(image, center, radius=radius, circle_check=True)
+            return image, top_left, bottom_right
+
+        cv2.circle(image, center, radius, (215, 158, 115), -1)
+        return image, top_left, bottom_right
+
+    def draw_ellipse(
+        self, image: np.ndarray, center: tuple[int, int], axis_length: tuple, angle: int, blur: bool
+    ) -> tuple[np.ndarray, int, int]:
+        """
+        Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
+        """
+
+        (h, k), (a, b) = center, axis_length
+        rad = math.pi / 180
+        ux, uy = a * math.cos(angle * rad), a * math.sin(angle * rad)  # first point on the ellipse
+        vx, vy = b * math.sin(angle * rad), b * math.cos(angle * rad)
+        width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
+
+        top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
+        bottom_right = (
+            int(min(h + (0.5) * width, image.shape[1])),
+            int(min(k + (0.5) * height, image.shape[0])),
+        )
+
+        if blur:
+            image = self.blur_img(
+                image, center, axis_length=axis_length, angle=angle, circle_check=False
+            )
+            return image, top_left, bottom_right
+
+        image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+        return image, top_left, bottom_right
+
+    def create_test_case(self, test_case: int) -> tuple[str, str]:
+        """
+        Genereates test cases given a data set.
+        """
+        image = cv2.imread(self.image_file)
+
+        boxes_list = []
+        for center, radius, blur, ellipse_data in self.circle_data:
+            if ellipse_data[0]:
+                _, axis_length, angle = ellipse_data
+                image, top_left, bottom_right = self.draw_ellipse(
+                    image, center, axis_length, angle, blur
+                )
+                boxes_list.append((top_left, bottom_right))
+                continue
+
+            image, top_left, bottom_right = self.draw_circle(image, center, radius, blur)
+            boxes_list.append((top_left, bottom_right))
+
+        self.save_bounding_box_annotation(test_case, boxes_list)
+
+        output_image_file = f"{self.image_path}test_output_{test_case}.png"
+        cv2.imwrite(output_image_file, image)
+        print(f"Image with bounding box saved as {output_image_file}")
+        return (output_image_file, self.image_path + self.txt_file + f"{test_case}.txt")
+
+
+# ---------------------------------------------------------------------------------------------------------------------
+
+
+def compare_detections(
+    actual: detections_and_time.DetectionsAndTime, expected: detections_and_time.DetectionsAndTime
+) -> None:
+    """
+    Compare expected and actual detections.
+    """
+    assert len(actual.detections) == len(expected.detections)
+
+    # Using integer indexing for both lists
+    # pylint: disable-next=consider-using-enumerate
+    for i in range(0, len(expected.detections)):
+        expected_detection = expected.detections[i]
+        actual_detection = actual.detections[i]
+
+        assert expected_detection.label == actual_detection.label
+        np.testing.assert_almost_equal(
+            expected_detection.confidence,
+            actual_detection.confidence,
+            decimal=CONFIDENCE_PRECISION_TOLERANCE,
+        )
+
+        np.testing.assert_almost_equal(
+            actual_detection.x_1,
+            expected_detection.x_1,
+            decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
+        )
+
+        np.testing.assert_almost_equal(
+            actual_detection.y_1,
+            expected_detection.y_1,
+            decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
+        )
+
+        np.testing.assert_almost_equal(
+            actual_detection.x_2,
+            expected_detection.x_2,
+            decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
+        )
+
+        np.testing.assert_almost_equal(
+            actual_detection.y_2,
+            expected_detection.y_2,
+            decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
+        )
+
+
+def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime:
+    """
+    Create DetectionsAndTime from expected.
+    Format: [confidence, label, x_1, y_1, x_2, y_2] .
+    """
+    assert detections_from_file.shape[1] == 6
+
+    result, detections = detections_and_time.DetectionsAndTime.create(0)
+    assert result
+    assert detections is not None
+
+    for i in range(0, detections_from_file.shape[0]):
+        result, detection = detections_and_time.Detection.create(
+            detections_from_file[i][2:],
+            int(detections_from_file[i][1]),
+            detections_from_file[i][0],
+        )
+        assert result
+        assert detection is not None
+        detections.append(detection)
+
+    return detections
+
+
+# ------------------------------------------------------------------------------------------------------------------
+@pytest.fixture()
+def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
+    """
+    Construct DetectTargetUltralytics.
+    """
+    detection = detect_target_contour.DetectTargetContour()
+    yield detection  # type: ignore
+
+
+# ---------------------------------------------------------------------------
+class TestDetector:
+    """
+    Tests `DetectTarget.run()` .
+    """
+
+    def test_multiple_landing_pads(
+        self,
+        detector: detect_target_contour.DetectTargetContour,
+    ) -> None:
+        """
+        Multiple images.
+        """
+
+        circle_data = [
+            [(200, 200), 400, False, [False, None, None]],
+            [(1500, 700), 500, False, [False, None, None]],
+        ]
+
+        actual_detections, expected_detections = [], []
+        circle_list = [circle_data]
+
+        for i, circle_data in enumerate(circle_list):
+            generate_test = GenerateTest(circle_data, BG_PATH, "bounding_box")
+            image_file, txt_file = generate_test.create_test_case(i + 1)
+            image = cv2.imread(image_file, 1)
+
+            result, actual = image_and_time.ImageAndTime.create(image)
+            assert result
+            assert actual is not None
+
+            expected = create_detections(np.loadtxt(txt_file))
+            actual_detections.append(actual)
+            expected_detections.append(expected)
+
+        # Run
+        outputs = []
+        for i in range(0, len(circle_list)):
+            output = detector.run(actual_detections[i])
+            outputs.append(output)
+
+        print(outputs)
+        # Test
+        for i in range(0, len(outputs)):
+            output: "tuple[bool, detections_and_time.DetectionsAndTime | None]" = outputs[i]
+            result, actual = output
+
+            print(actual)
+            compare_detections(actual, expected_detections[i])

From bf8faf19f5b2aece0fae35b180e3fd55b1d4daac Mon Sep 17 00:00:00 2001
From: Zenkqi <SSGSSAchita@gmail.com>
Date: Thu, 10 Oct 2024 23:48:37 -0400
Subject: [PATCH 04/27] linters: reformatting code

---
 tests/unit/test_detect_target_contour.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index dbcaa1ca..59471e97 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -2,6 +2,7 @@
 Test DetectTarget module.
 
 """
+
 import pathlib
 
 import cv2

From 1a19e20be1ae3c34327b6c3c271d31d42af75739 Mon Sep 17 00:00:00 2001
From: Zenkqi <SSGSSAchita@gmail.com>
Date: Mon, 14 Oct 2024 15:36:42 -0400
Subject: [PATCH 05/27] Added corrections(?): Class -> Functions (Some sort of
 issue with detect_target)contour, extra unit tests added after)

---
 tests/model_example/background.png       | Bin 16143 -> 0 bytes
 tests/model_example/bounding_box1.txt    |   2 -
 tests/model_example/test_output_1.png    | Bin 46145 -> 0 bytes
 tests/unit/test_detect_target_contour.py | 269 ++++++++++-------------
 4 files changed, 117 insertions(+), 154 deletions(-)
 delete mode 100644 tests/model_example/background.png
 delete mode 100644 tests/model_example/bounding_box1.txt
 delete mode 100644 tests/model_example/test_output_1.png

diff --git a/tests/model_example/background.png b/tests/model_example/background.png
deleted file mode 100644
index a6f4067d8bad97f922582742aefdba473a64d7c2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 16143
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

diff --git a/tests/model_example/bounding_box1.txt b/tests/model_example/bounding_box1.txt
deleted file mode 100644
index 35857d70..00000000
--- a/tests/model_example/bounding_box1.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-1 0 0 0 600 600
-1 1 1000 200 1920 1124
diff --git a/tests/model_example/test_output_1.png b/tests/model_example/test_output_1.png
deleted file mode 100644
index f37919759855825bd5a1add0a2b99b74f6bd0e46..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 46145
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
G*Z&XeH28i1

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 59471e97..ccdbb40d 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -3,156 +3,119 @@
 
 """
 
-import pathlib
-
+import math
 import cv2
 import numpy as np
 import pytest
-import math
 
 from modules.detect_target import detect_target_contour
 from modules import image_and_time
 from modules import detections_and_time
 
-
-TEST_PATH = pathlib.Path("tests", "model_example")
-BG_PATH = pathlib.Path(TEST_PATH, "background.png")
-
 BOUNDING_BOX_PRECISION_TOLERANCE = 0
 CONFIDENCE_PRECISION_TOLERANCE = 2
 
-
 # Test functions use test fixture signature names and access class privates
 # No enable
 # pylint: disable=protected-access,redefined-outer-name
 
 
-class GenerateTest:
-    def __init__(self, circle_data: list[list], image_file: str, txt_file: str) -> None:
-        self.circle_data = circle_data[0:]
-        self.image_file = image_file
-        self.txt_file = txt_file
-        self.image_path = "tests/model_example/"
-
-    def save_bounding_box_annotation(self, test_case: int, boxes_list: int) -> None:
-        """
-        Save the bounding box annotation for the circle in the format:
-        format: conf class_label x_min y_min x_max y_max
-        """
-
-        txt_file = self.image_path + self.txt_file + str(test_case) + ".txt"
-        with open(txt_file, "w") as f:
-            for class_label, (top_left, bottom_right) in enumerate(boxes_list):
-                x_min, y_min = top_left
-                x_max, y_max = bottom_right
-
-                f.write(f"{1} {class_label} {x_min} {y_min} {x_max} {y_max}\n")
-        print(f"Bounding box annotation saved to {txt_file}")
-
-    def blur_img(
-        self,
-        bg: np.ndarray,
-        center: tuple[int, int],
-        radius: int = 0,
-        axis_length: tuple[int, int] = (0, 0),
-        angle: int = 0,
-        circle_check: bool = True,
-    ) -> np.ndarray:
-        """
-        Blurs an image a singular shape and adds it to the background.
-        """
+def blur_img(
+    bg: np.ndarray,
+    center: tuple[int, int],
+    radius: int = 0,
+    axis_length: tuple[int, int] = (0, 0),
+    angle: int = 0,
+    circle_check: bool = True,
+) -> np.ndarray:
+    """
+    Blurs an image a singular shape and adds it to the background.
+    """
 
-        bg_copy = bg.copy()
-        x, y = bg_copy.shape[:2]
+    bg_copy = bg.copy()
+    x, y = bg_copy.shape[:2]
 
-        mask = np.zeros((x, y), np.uint8)
-        if circle_check:
-            mask = cv2.circle(mask, center, radius, (215, 158, 115), -1, cv2.LINE_AA)
-        else:
-            mask = cv2.ellipse(mask, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+    mask = np.zeros((x, y), np.uint8)
+    if circle_check:
+        mask = cv2.circle(mask, center, radius, (215, 158, 115), -1, cv2.LINE_AA)
+    else:
+        mask = cv2.ellipse(mask, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
 
-        mask = cv2.blur(mask, (25, 25), 7)
+    mask = cv2.blur(mask, (25, 25), 7)
 
-        alpha = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
-        fg = np.zeros(bg.shape, np.uint8)
-        fg[:, :, :] = [200, 10, 200]
+    alpha = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
+    fg = np.zeros(bg.shape, np.uint8)
+    fg[:, :, :] = [200, 10, 200]
 
-        blended = cv2.convertScaleAbs(bg * (1 - alpha) + fg * alpha)
-        return blended
+    blended = cv2.convertScaleAbs(bg * (1 - alpha) + fg * alpha)
+    return blended
 
-    def draw_circle(
-        self, image: np.ndarray, center: tuple[int, int], radius: int, blur: bool
-    ) -> tuple[np.ndarray, int, int]:
-        """
-        Draws a circle on the provided image and saves the bounding box coordinates to a text file.
-        """
-        x, y = center
-        top_left = (max(x - radius, 0), max(y - radius, 0))
-        bottom_right = (min(x + radius, image.shape[1]), min(y + radius, image.shape[0]))
 
-        if blur:
-            image = self.blur_img(image, center, radius=radius, circle_check=True)
-            return image, top_left, bottom_right
+def draw_circle(
+    image: np.ndarray, center: tuple[int, int], radius: int, blur: bool
+) -> tuple[np.ndarray, int, int]:
+    """
+    Draws a circle on the provided image and saves the bounding box coordinates to a text file.
+    """
+    x, y = center
+    top_left = (max(x - radius, 0), max(y - radius, 0))
+    bottom_right = (min(x + radius, image.shape[1]), min(y + radius, image.shape[0]))
 
-        cv2.circle(image, center, radius, (215, 158, 115), -1)
+    if blur:
+        image = blur_img(image, center, radius=radius, circle_check=True)
         return image, top_left, bottom_right
 
-    def draw_ellipse(
-        self, image: np.ndarray, center: tuple[int, int], axis_length: tuple, angle: int, blur: bool
-    ) -> tuple[np.ndarray, int, int]:
-        """
-        Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
-        """
+    cv2.circle(image, center, radius, (215, 158, 115), -1)
+    return image, top_left, bottom_right
 
-        (h, k), (a, b) = center, axis_length
-        rad = math.pi / 180
-        ux, uy = a * math.cos(angle * rad), a * math.sin(angle * rad)  # first point on the ellipse
-        vx, vy = b * math.sin(angle * rad), b * math.cos(angle * rad)
-        width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
-
-        top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
-        bottom_right = (
-            int(min(h + (0.5) * width, image.shape[1])),
-            int(min(k + (0.5) * height, image.shape[0])),
-        )
 
-        if blur:
-            image = self.blur_img(
-                image, center, axis_length=axis_length, angle=angle, circle_check=False
-            )
-            return image, top_left, bottom_right
-
-        image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+def draw_ellipse(
+    image: np.ndarray, center: tuple[int, int], axis_length: tuple, angle: int, blur: bool
+) -> tuple[np.ndarray, int, int]:
+    """
+    Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
+    """
+    (h, k), (a, b) = center, axis_length
+    rad = math.pi / 180
+    ux, uy = a * math.cos(angle * rad), a * math.sin(angle * rad)  # first point on the ellipse
+    vx, vy = b * math.sin(angle * rad), b * math.cos(angle * rad)
+    width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
+
+    top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
+    bottom_right = (
+        int(min(h + (0.5) * width, image.shape[1])),
+        int(min(k + (0.5) * height, image.shape[0])),
+    )
+
+    if blur:
+        image = blur_img(image, center, axis_length=axis_length, angle=angle, circle_check=False)
         return image, top_left, bottom_right
 
-    def create_test_case(self, test_case: int) -> tuple[str, str]:
-        """
-        Genereates test cases given a data set.
-        """
-        image = cv2.imread(self.image_file)
+    image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+    return image, top_left, bottom_right
 
-        boxes_list = []
-        for center, radius, blur, ellipse_data in self.circle_data:
-            if ellipse_data[0]:
-                _, axis_length, angle = ellipse_data
-                image, top_left, bottom_right = self.draw_ellipse(
-                    image, center, axis_length, angle, blur
-                )
-                boxes_list.append((top_left, bottom_right))
-                continue
 
-            image, top_left, bottom_right = self.draw_circle(image, center, radius, blur)
-            boxes_list.append((top_left, bottom_right))
-
-        self.save_bounding_box_annotation(test_case, boxes_list)
+def create_test_case(
+    circle_data: list[tuple[int, int], int, bool, list[bool, tuple[int, int] | None, int | None]]
+) -> tuple[np.ndarray, np.ndarray]:
+    """
+    Genereates test cases given a data set.
+    """
+    image = np.zeros(shape=(800, 1800, 3), dtype=np.int16)
 
-        output_image_file = f"{self.image_path}test_output_{test_case}.png"
-        cv2.imwrite(output_image_file, image)
-        print(f"Image with bounding box saved as {output_image_file}")
-        return (output_image_file, self.image_path + self.txt_file + f"{test_case}.txt")
+    boxes_list = []
+    for center, radius, blur, ellipse_data in circle_data:
+        if ellipse_data[0]:
+            _, axis_length, angle = ellipse_data
+            image, top_left, bottom_right = draw_ellipse(image, center, axis_length, angle, blur)
+            boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
+            continue
 
+        image, top_left, bottom_right = draw_circle(image, center, radius, blur)
+        boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
 
-# ---------------------------------------------------------------------------------------------------------------------
+    boxes_list = np.array(boxes_list)
+    return (image.astype(np.uint8), boxes_list)
 
 
 def compare_detections(
@@ -225,62 +188,64 @@ def create_detections(detections_from_file: np.ndarray) -> detections_and_time.D
     return detections
 
 
-# ------------------------------------------------------------------------------------------------------------------
 @pytest.fixture()
 def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
-    Construct DetectTargetUltralytics.
+    Construct DetectTargetContour.
     """
     detection = detect_target_contour.DetectTargetContour()
     yield detection  # type: ignore
 
 
-# ---------------------------------------------------------------------------
+@pytest.fixture()
+def image_easy() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load easy image.
+    """
+
+    circle_data = [[(1000, 400), 200, False, [False, None, None]]]
+
+    image = create_test_case(circle_data)
+    result, actual_image = image_and_time.ImageAndTime.create(image[0])
+    print((result, actual_image))
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
+
+@pytest.fixture()
+def expected_easy() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected an easy image detections.
+    """
+
+    circle_data = [
+        [(500, 1000), 400, False, [False, None, None]],
+    ]
+
+    _, expected = create_test_case(circle_data)
+    yield create_detections(expected)  # type: ignore
+
+
 class TestDetector:
     """
     Tests `DetectTarget.run()` .
     """
 
-    def test_multiple_landing_pads(
+    def test_single_circle(
         self,
         detector: detect_target_contour.DetectTargetContour,
+        image_easy: image_and_time.ImageAndTime,
+        expected_easy: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Multiple images.
+        Bus image.
         """
-
-        circle_data = [
-            [(200, 200), 400, False, [False, None, None]],
-            [(1500, 700), 500, False, [False, None, None]],
-        ]
-
-        actual_detections, expected_detections = [], []
-        circle_list = [circle_data]
-
-        for i, circle_data in enumerate(circle_list):
-            generate_test = GenerateTest(circle_data, BG_PATH, "bounding_box")
-            image_file, txt_file = generate_test.create_test_case(i + 1)
-            image = cv2.imread(image_file, 1)
-
-            result, actual = image_and_time.ImageAndTime.create(image)
-            assert result
-            assert actual is not None
-
-            expected = create_detections(np.loadtxt(txt_file))
-            actual_detections.append(actual)
-            expected_detections.append(expected)
-
         # Run
-        outputs = []
-        for i in range(0, len(circle_list)):
-            output = detector.run(actual_detections[i])
-            outputs.append(output)
+        result, actual = detector.run(image_easy)
 
-        print(outputs)
         # Test
-        for i in range(0, len(outputs)):
-            output: "tuple[bool, detections_and_time.DetectionsAndTime | None]" = outputs[i]
-            result, actual = output
+        assert result
+        assert actual is not None
 
-            print(actual)
-            compare_detections(actual, expected_detections[i])
+        compare_detections(actual, expected_easy)

From c609f60d779a5b56c003fa97d0e1bf53d9e09cc5 Mon Sep 17 00:00:00 2001
From: Zenkqi <SSGSSAchita@gmail.com>
Date: Sun, 10 Nov 2024 15:06:58 -0500
Subject: [PATCH 06/27] Updated code for the test (forgot to push earlier)

---
 tests/unit/test_detect_target_contour.py | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index ccdbb40d..be4f848f 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -45,7 +45,7 @@ def blur_img(
 
     alpha = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
     fg = np.zeros(bg.shape, np.uint8)
-    fg[:, :, :] = [200, 10, 200]
+    fg[:, :, :] = [0, 0, 0]
 
     blended = cv2.convertScaleAbs(bg * (1 - alpha) + fg * alpha)
     return blended
@@ -91,7 +91,7 @@ def draw_ellipse(
         image = blur_img(image, center, axis_length=axis_length, angle=angle, circle_check=False)
         return image, top_left, bottom_right
 
-    image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+    image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (0, 0, 0), -1)
     return image, top_left, bottom_right
 
 
@@ -101,18 +101,18 @@ def create_test_case(
     """
     Genereates test cases given a data set.
     """
-    image = np.zeros(shape=(800, 1800, 3), dtype=np.int16)
+    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
 
     boxes_list = []
     for center, radius, blur, ellipse_data in circle_data:
         if ellipse_data[0]:
             _, axis_length, angle = ellipse_data
             image, top_left, bottom_right = draw_ellipse(image, center, axis_length, angle, blur)
-            boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
+            boxes_list.append([1, 0] + list(top_left + bottom_right))
             continue
 
         image, top_left, bottom_right = draw_circle(image, center, radius, blur)
-        boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
+        boxes_list.append([1, 0] + list(top_left + bottom_right))
 
     boxes_list = np.array(boxes_list)
     return (image.astype(np.uint8), boxes_list)
@@ -193,7 +193,7 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
     Construct DetectTargetContour.
     """
-    detection = detect_target_contour.DetectTargetContour()
+    detection = detect_target_contour.DetectTargetContour(True, "bob")
     yield detection  # type: ignore
 
 
@@ -203,10 +203,10 @@ def image_easy() -> image_and_time.ImageAndTime:  # type: ignore
     Load easy image.
     """
 
-    circle_data = [[(1000, 400), 200, False, [False, None, None]]]
+    circle_data = [[(900, 500), 400, False, [False, None, None]]]
 
-    image = create_test_case(circle_data)
-    result, actual_image = image_and_time.ImageAndTime.create(image[0])
+    image, _ = create_test_case(circle_data)
+    result, actual_image = image_and_time.ImageAndTime.create(image)
     print((result, actual_image))
     assert result
     assert actual_image is not None
@@ -220,7 +220,7 @@ def expected_easy() -> image_and_time.ImageAndTime:  # type: ignore
     """
 
     circle_data = [
-        [(500, 1000), 400, False, [False, None, None]],
+        [(1000, 400), 200, False, [False, None, None]],
     ]
 
     _, expected = create_test_case(circle_data)

From 15dff4e57f55510dd2f35b76e310229e4d52d7af Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 21 Nov 2024 15:58:51 -0500
Subject: [PATCH 07/27] Detect Target Contour working and check now detect all
 circular contours + Working Tests

---
 .../detect_target/detect_target_contour.py    |  49 ++---
 tests/unit/test_detect_target_contour.py      | 185 ++++++++++++++++--
 2 files changed, 193 insertions(+), 41 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 77c51d4a..750b18a0 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -76,7 +76,7 @@ def detect_landing_pads_contours(
         if len(contours) == 0:
             return False, None, image
 
-        contours_with_children = set(i for i, hier in enumerate(hierarchy[0]) if hier[2] != -1)
+        contours_with_children = set(i for i, hier in enumerate(hierarchy[0]) if hier[3] != -1)
         parent_circular_contours = [
             cnt
             for i, cnt in enumerate(contours)
@@ -85,8 +85,7 @@ def detect_landing_pads_contours(
             and i in contours_with_children
         ]
 
-        largest_contour = max(parent_circular_contours, key=cv2.contourArea, default=None)
-        if largest_contour is None:
+        if not len(parent_circular_contours):
             return False, None, image
 
         # Create the DetectionsAndTime object
@@ -94,28 +93,30 @@ def detect_landing_pads_contours(
         if not result:
             return False, None, image
 
-        x, y, w, h = cv2.boundingRect(largest_contour)
-        bounds = np.array([x, y, x + w, y + h])
-        confidence = 1.0  # Confidence for classical CV is often set to a constant value
-        label = 0  # Label can be set to a constant or derived from some logic
-
-        # Create a Detection object and append it to detections
-        result, detection = detections_and_time.Detection.create(bounds, label, confidence)
-        if result:
-            detections.append(detection)
-
-        # Annotate the image
+        sorted_contour = sorted(parent_circular_contours, key=cv2.contourArea, reverse=True)
         image_annotated = copy.deepcopy(image)
-        cv2.rectangle(image_annotated, (x, y), (x + w, y + h), (0, 0, 255), 2)
-        cv2.putText(
-            image_annotated,
-            "landing-pad",
-            (x, y - 10),
-            cv2.FONT_HERSHEY_SIMPLEX,
-            0.9,
-            (0, 0, 255),
-            2,
-        )
+        for i, contour in enumerate(sorted_contour):
+            x, y, w, h = cv2.boundingRect(contour)
+            bounds = np.array([x, y, x + w, y + h])
+            confidence = 1.0  # Confidence for classical CV is often set to a constant value
+            label = 0  # Label can be set to a constant or derived from some logic
+
+            # Create a Detection object and append it to detections
+            result, detection = detections_and_time.Detection.create(bounds, label, confidence)
+            if result:
+                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
 
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index be4f848f..de82ac9b 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -8,11 +8,12 @@
 import numpy as np
 import pytest
 
+from typing import NamedTuple
 from modules.detect_target import detect_target_contour
 from modules import image_and_time
 from modules import detections_and_time
 
-BOUNDING_BOX_PRECISION_TOLERANCE = 0
+BOUNDING_BOX_PRECISION_TOLERANCE = -2 / 3
 CONFIDENCE_PRECISION_TOLERANCE = 2
 
 # Test functions use test fixture signature names and access class privates
@@ -20,6 +21,30 @@
 # pylint: disable=protected-access,redefined-outer-name
 
 
+class LandingPadData(NamedTuple):
+    center: tuple[int, int]
+    radius: int | tuple[int, int]
+    blur: bool = False
+    elipse: bool = False
+    angle: int = 0
+
+
+easy_data = [LandingPadData(center=(1000, 400), radius=200)]
+
+blurry_data = [
+    LandingPadData(center=(1000, 500), radius=423, blur=True),
+]
+
+stretched_data = [LandingPadData(center=(1000, 500), radius=(383, 405), elipse=True)]
+
+multiple_data = [
+    LandingPadData(center=(200, 500), radius=(50, 45), blur=True, elipse=True),
+    LandingPadData(center=(1590, 341), radius=250),
+    LandingPadData(center=(997, 600), radius=300),
+    LandingPadData(center=(401, 307), radius=(200, 150), blur=True, elipse=True),
+]
+
+
 def blur_img(
     bg: np.ndarray,
     center: tuple[int, int],
@@ -95,29 +120,39 @@ def draw_ellipse(
     return image, top_left, bottom_right
 
 
-def create_test_case(
-    circle_data: list[tuple[int, int], int, bool, list[bool, tuple[int, int] | None, int | None]]
-) -> tuple[np.ndarray, np.ndarray]:
+def create_test_case(landing_list: list[LandingPadData]) -> tuple[np.ndarray, np.ndarray]:
     """
     Genereates test cases given a data set.
     """
     image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
 
     boxes_list = []
-    for center, radius, blur, ellipse_data in circle_data:
-        if ellipse_data[0]:
-            _, axis_length, angle = ellipse_data
-            image, top_left, bottom_right = draw_ellipse(image, center, axis_length, angle, blur)
-            boxes_list.append([1, 0] + list(top_left + bottom_right))
+    for landing_data in landing_list:
+        if landing_data.elipse:
+            axis_length, angle = landing_data.radius, landing_data.angle
+            image, top_left, bottom_right = draw_ellipse(
+                image, landing_data.center, axis_length, angle, landing_data.blur
+            )
+            boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
             continue
 
-        image, top_left, bottom_right = draw_circle(image, center, radius, blur)
+        image, top_left, bottom_right = draw_circle(
+            image, landing_data.center, landing_data.radius, landing_data.blur
+        )
         boxes_list.append([1, 0] + list(top_left + bottom_right))
 
+    boxes_list = sorted(
+        boxes_list,
+        reverse=True,
+        key=lambda boxes_list: abs(
+            (boxes_list[4] - boxes_list[2]) * (boxes_list[5] - boxes_list[3])
+        ),
+    )
     boxes_list = np.array(boxes_list)
     return (image.astype(np.uint8), boxes_list)
 
 
+# pylint:disable=duplicate-code
 def compare_detections(
     actual: detections_and_time.DetectionsAndTime, expected: detections_and_time.DetectionsAndTime
 ) -> None:
@@ -203,11 +238,47 @@ def image_easy() -> image_and_time.ImageAndTime:  # type: ignore
     Load easy image.
     """
 
-    circle_data = [[(900, 500), 400, False, [False, None, None]]]
+    image, _ = create_test_case(easy_data)
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
+
+@pytest.fixture()
+def blurry_image() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load easy image.
+    """
+
+    image, _ = create_test_case(blurry_data)
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
+
+@pytest.fixture()
+def stretched_image() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load easy image.
+    """
+
+    image, _ = create_test_case(stretched_data)
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
+
+@pytest.fixture()
+def multiple_images() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load easy image.
+    """
 
-    image, _ = create_test_case(circle_data)
+    image, _ = create_test_case(multiple_data)
     result, actual_image = image_and_time.ImageAndTime.create(image)
-    print((result, actual_image))
     assert result
     assert actual_image is not None
     yield actual_image  # type: ignore
@@ -219,11 +290,37 @@ def expected_easy() -> image_and_time.ImageAndTime:  # type: ignore
     Load expected an easy image detections.
     """
 
-    circle_data = [
-        [(1000, 400), 200, False, [False, None, None]],
-    ]
+    _, expected = create_test_case(easy_data)
+    yield create_detections(expected)  # type: ignore
+
 
-    _, expected = create_test_case(circle_data)
+@pytest.fixture()
+def expected_blur() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected an easy image detections.
+    """
+
+    _, expected = create_test_case(blurry_data)
+    yield create_detections(expected)  # type: ignore
+
+
+@pytest.fixture()
+def expected_stretch() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected an easy image detections.
+    """
+
+    _, expected = create_test_case(stretched_data)
+    yield create_detections(expected)  # type: ignore
+
+
+@pytest.fixture()
+def expected_multiple() -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected an easy image detections.
+    """
+
+    _, expected = create_test_case(multiple_data)
     yield create_detections(expected)  # type: ignore
 
 
@@ -249,3 +346,57 @@ def test_single_circle(
         assert actual is not None
 
         compare_detections(actual, expected_easy)
+
+    def test_blurry_circle(
+        self,
+        detector: detect_target_contour.DetectTargetContour,
+        blurry_image: image_and_time.ImageAndTime,
+        expected_blur: detections_and_time.DetectionsAndTime,
+    ) -> None:
+        """
+        Bus image.
+        """
+        # Run
+        result, actual = detector.run(blurry_image)
+
+        # Test
+        assert result
+        assert actual is not None
+
+        compare_detections(actual, expected_blur)
+
+    def test_stretch(
+        self,
+        detector: detect_target_contour.DetectTargetContour,
+        stretched_image: image_and_time.ImageAndTime,
+        expected_stretch: detections_and_time.DetectionsAndTime,
+    ) -> None:
+        """
+        Bus image.
+        """
+        # Run
+        result, actual = detector.run(stretched_image)
+
+        # Test
+        assert result
+        assert actual is not None
+
+        compare_detections(actual, expected_stretch)
+
+    def test_multiple(
+        self,
+        detector: detect_target_contour.DetectTargetContour,
+        multiple_images: image_and_time.ImageAndTime,
+        expected_multiple: detections_and_time.DetectionsAndTime,
+    ) -> None:
+        """
+        Bus image.
+        """
+        # Run
+        result, actual = detector.run(multiple_images)
+
+        # Test
+        assert result
+        assert actual is not None
+
+        compare_detections(actual, expected_multiple)

From 6a16444026acb616d1bb245d5d7f9157f5132319 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 21 Nov 2024 16:03:46 -0500
Subject: [PATCH 08/27] black . change

---
 modules/detect_target/detect_target_contour.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 750b18a0..0c9f2848 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -115,7 +115,7 @@ def detect_landing_pads_contours(
                 cv2.FONT_HERSHEY_SIMPLEX,
                 0.9,
                 (0, 0, 255),
-                2
+                2,
             )
 
         return True, detections, image_annotated

From cf82cf4dec640daba28865362c57e1ed32ded006 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 21 Nov 2024 16:17:23 -0500
Subject: [PATCH 09/27] Linter Changes (again)

---
 modules/detect_target/detect_target_contour.py | 5 ++++-
 tests/unit/test_detect_target_contour.py       | 8 ++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 0c9f2848..9165db69 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -14,6 +14,9 @@
 
 
 class DetectTargetContour(base_detect_target.BaseDetectTarget):
+    """
+    Predicts annd locates landing pads using Classical Computer Vision
+    """
     def __init__(
         self,
         show_annotations: bool = False,
@@ -85,7 +88,7 @@ def detect_landing_pads_contours(
             and i in contours_with_children
         ]
 
-        if not len(parent_circular_contours):
+        if len(parent_circular_contours) == 0:
             return False, None, image
 
         # Create the DetectionsAndTime object
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index de82ac9b..ceefe2c4 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -3,12 +3,12 @@
 
 """
 
+from typing import NamedTuple
 import math
 import cv2
 import numpy as np
 import pytest
 
-from typing import NamedTuple
 from modules.detect_target import detect_target_contour
 from modules import image_and_time
 from modules import detections_and_time
@@ -22,6 +22,10 @@
 
 
 class LandingPadData(NamedTuple):
+    """
+    Landing Pad information struct
+    """
+
     center: tuple[int, int]
     radius: int | tuple[int, int]
     blur: bool = False
@@ -228,7 +232,7 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
     Construct DetectTargetContour.
     """
-    detection = detect_target_contour.DetectTargetContour(True, "bob")
+    detection = detect_target_contour.DetectTargetContour(True)
     yield detection  # type: ignore
 
 

From da7b5af5124793013b7ec22d0abf2ccc2a8f4b05 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Fri, 22 Nov 2024 01:03:19 -0500
Subject: [PATCH 10/27] linter???

---
 modules/detect_target/detect_target_contour.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 9165db69..56953cc4 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -17,6 +17,7 @@ class DetectTargetContour(base_detect_target.BaseDetectTarget):
     """
     Predicts annd locates landing pads using Classical Computer Vision
     """
+
     def __init__(
         self,
         show_annotations: bool = False,

From c0e772d613eaf02e6b846d68624ffdc4379ef93e Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Fri, 22 Nov 2024 01:20:22 -0500
Subject: [PATCH 11/27] linter please

---
 tests/unit/test_detect_target_contour.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index ceefe2c4..8b4618e0 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -137,7 +137,7 @@ def create_test_case(landing_list: list[LandingPadData]) -> tuple[np.ndarray, np
             image, top_left, bottom_right = draw_ellipse(
                 image, landing_data.center, axis_length, angle, landing_data.blur
             )
-            boxes_list.append([1, 0] + [point for point in top_left + bottom_right])
+            boxes_list.append([1, 0] + list(top_left + bottom_right))
             continue
 
         image, top_left, bottom_right = draw_circle(

From 4d105f114b8e89ddf59ccef288b868f4c4724c2f Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 28 Nov 2024 17:40:13 -0500
Subject: [PATCH 12/27] ??

---
 tests/unit/test_detect_target_contour.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 8b4618e0..150da75a 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -123,7 +123,6 @@ def draw_ellipse(
     image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (0, 0, 0), -1)
     return image, top_left, bottom_right
 
-
 def create_test_case(landing_list: list[LandingPadData]) -> tuple[np.ndarray, np.ndarray]:
     """
     Genereates test cases given a data set.

From 74497e98f9652f1710700cf46b77b6ff3325a0c7 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 28 Nov 2024 17:45:08 -0500
Subject: [PATCH 13/27] pls

---
 tests/unit/test_detect_target_contour.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 150da75a..8b4618e0 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -123,6 +123,7 @@ def draw_ellipse(
     image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (0, 0, 0), -1)
     return image, top_left, bottom_right
 
+
 def create_test_case(landing_list: list[LandingPadData]) -> tuple[np.ndarray, np.ndarray]:
     """
     Genereates test cases given a data set.

From 074246079c87487f95b4f96c7a30c40bee75ea8e Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Thu, 28 Nov 2024 17:53:41 -0500
Subject: [PATCH 14/27] Corrected main_detect_target paths

---
 main_detect_target.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/main_detect_target.py b/main_detect_target.py
index 7190386f..b1d44553 100644
--- a/main_detect_target.py
+++ b/main_detect_target.py
@@ -9,9 +9,9 @@
 from modules.detect_target import detect_target_factory
 from modules.detect_target import detect_target_worker
 from modules.video_input import video_input_worker
-from modules.common.logger.modules import logger
-from modules.common.logger.modules import logger_setup_main
-from modules.common.logger.read_yaml.modules import read_yaml
+from modules.common.modules.logger import logger
+from modules.common.modules.logger import logger_main_setup
+from modules.common.modules.read_yaml import read_yaml
 from utilities.workers import queue_proxy_wrapper
 from utilities.workers import worker_controller
 from utilities.workers import worker_manager
@@ -56,7 +56,7 @@ def main() -> int:
     assert config_logger is not None
 
     # Setup main logger
-    result, main_logger, logging_path = logger_setup_main.setup_main_logger(config_logger)
+    result, main_logger, logging_path = logger_main_setup.setup_main_logger(config_logger)
     if not result:
         print("ERROR: Failed to create main logger")
         return -1

From b3dcfca86b46b4e57c66a1ad980edee992507b4d Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Fri, 29 Nov 2024 17:24:46 -0500
Subject: [PATCH 15/27] removed window

---
 tests/unit/test_detect_target_contour.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 8b4618e0..23a0af2d 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -232,7 +232,7 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
     Construct DetectTargetContour.
     """
-    detection = detect_target_contour.DetectTargetContour(True)
+    detection = detect_target_contour.DetectTargetContour(False)
     yield detection  # type: ignore
 
 

From 9b62cfa09cc7950f5ee4e144cb90e5b83c6bac46 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Tue, 21 Jan 2025 01:08:35 -0500
Subject: [PATCH 16/27] Xierumeng changes (mostly?)

---
 .../detect_target/detect_target_contour.py    |  78 ++--
 tests/unit/generate_detect_target_contour.py  | 165 +++++++++
 tests/unit/test_detect_target_contour.py      | 335 +++++++-----------
 3 files changed, 319 insertions(+), 259 deletions(-)
 create mode 100644 tests/unit/generate_detect_target_contour.py

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 56953cc4..d80bcf40 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -12,10 +12,15 @@
 from .. import image_and_time
 from .. import detections_and_time
 
+MIN_CONTOUR_AREA = 100
+# some arbitrary value
+UPPER_BLUE = np.array([130, 255, 255])
+LOWER_BLUE = np.array([100, 50, 50])
+
 
 class DetectTargetContour(base_detect_target.BaseDetectTarget):
     """
-    Predicts annd locates landing pads using Classical Computer Vision
+    Predicts annd locates landing pads using the Classical Computer Vision methodology.
     """
 
     def __init__(
@@ -33,63 +38,21 @@ def __init__(
         if save_name != "":
             self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
 
-    @staticmethod
-    def is_contour_circular(contour: np.ndarray) -> bool:
-        """
-        Helper function for detect_landing_pads_contours.
-        Checks if the shape is close to circular.
-        Return: True is the shape is circular, false if it is not.
-        """
-        contour_minimum = 0.8
-        perimeter = cv2.arcLength(contour, True)
-        # Check if the perimeter is zero
-        if perimeter == 0.0:
-            return False
-
-        area = cv2.contourArea(contour)
-        circularity = 4 * np.pi * (area / (perimeter * perimeter))
-        return circularity > contour_minimum
-
-    @staticmethod
-    def is_contour_large_enough(contour: np.ndarray, min_diameter: float) -> bool:
-        """
-        Helper function for detect_landing_pads_contours.
-        Checks if the shape is larger than the provided diameter.
-        Return: True if it is, false if it not.
-        """
-        _, radius = cv2.minEnclosingCircle(contour)
-        diameter = radius * 2
-        return diameter >= min_diameter
-
     def detect_landing_pads_contours(
         self, image: "np.ndarray", timestamp: float
-    ) -> "tuple[bool, detections_and_time.DetectionsAndTime | None, np.ndarray]":
+    ) -> tuple[bool, detections_and_time.DetectionsAndTime | None, np.ndarray]:
         """
         Detects landing pads using contours/classical cv.
         image: Current image frame.
         timestamp: Timestamp for the detections.
         Return: Success, the DetectionsAndTime object, and the annotated image.
         """
-        kernel = np.ones((2, 2), np.uint8)
-        gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
-        threshold = 180
-        im_bw = cv2.threshold(gray_image, threshold, 255, cv2.THRESH_BINARY)[1]
-        im_dilation = cv2.dilate(im_bw, kernel, iterations=1)
-        contours, hierarchy = cv2.findContours(im_dilation, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
-
-        if len(contours) == 0:
-            return False, None, image
+        hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
+        mask = cv2.inRange(hsv_image, LOWER_BLUE, UPPER_BLUE)
 
-        contours_with_children = set(i for i, hier in enumerate(hierarchy[0]) if hier[3] != -1)
-        parent_circular_contours = [
-            cnt
-            for i, cnt in enumerate(contours)
-            if self.is_contour_circular(cnt)
-            and self.is_contour_large_enough(cnt, 7)
-            and i in contours_with_children
-        ]
+        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
-        if len(parent_circular_contours) == 0:
+        if len(contours) == 0:
             return False, None, image
 
         # Create the DetectionsAndTime object
@@ -97,13 +60,25 @@ def detect_landing_pads_contours(
         if not result:
             return False, None, image
 
-        sorted_contour = sorted(parent_circular_contours, key=cv2.contourArea, reverse=True)
+        sorted_contour = sorted(contours, key=cv2.contourArea, reverse=True)
         image_annotated = copy.deepcopy(image)
         for i, contour in enumerate(sorted_contour):
+            contour_area = cv2.contourArea(contour)
+
+            if contour_area < MIN_CONTOUR_AREA:
+                continue
+
+            (x, y), radius = cv2.minEnclosingCircle(contour)
+
+            enclosing_area = np.pi * (radius**2)
+            circularity = contour_area / enclosing_area
+
+            if circularity < 0.7 or circularity > 1.3:
+                continue
+
             x, y, w, h = cv2.boundingRect(contour)
             bounds = np.array([x, y, x + w, y + h])
-            confidence = 1.0  # Confidence for classical CV is often set to a constant value
-            label = 0  # Label can be set to a constant or derived from some logic
+            confidence, label = 1.0, 0
 
             # Create a Detection object and append it to detections
             result, detection = detections_and_time.Detection.create(bounds, label, confidence)
@@ -136,6 +111,7 @@ def run(
         timestamp = data.timestamp
 
         result, detections, image_annotated = self.detect_landing_pads_contours(image, timestamp)
+
         if not result:
             return False, None
 
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
new file mode 100644
index 00000000..c72b618f
--- /dev/null
+++ b/tests/unit/generate_detect_target_contour.py
@@ -0,0 +1,165 @@
+"""
+Helper functions for test_detect_target_contour.
+
+"""
+
+import cv2
+import math
+import numpy as np
+
+LANDING_PAD_COLOR = (100, 50, 50)  # blue color
+
+# Test functions use test fixture signature names and access class privates
+# No enable
+# pylint: disable=protected-access,redefined-outer-name
+
+
+class BoundingBox:
+    """
+    Holds the data that define the generated bounding boxes.
+
+    Attributes:
+        top_left: A tuple of integers that represents top left corner of bounding box.
+        bottom_right: A tuple of integers that represents bottom right corner of bounding box.
+    """
+
+    def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
+        self.top_left = top_left
+        self.bottom_right = bottom_right
+
+
+class NumpyImage:
+    """
+    Holds the Numpy Array which represents an image.
+    """
+
+    def __init__(self, image: np.ndarray):
+        self.image = image
+
+
+class LandingPadTestData:
+    """
+    Struct to hold the data needed to perform the tests.
+
+    Attributes:
+        image = A numpy array that represents the image needed to be tested
+        bounding_box_list: A numpy array that holds a list of expected bounding box coordinates
+    """
+
+    def __init__(self, image: NumpyImage, boxes_list: np.ndarray):
+        self.image = image.image
+        self.bounding_box_list = boxes_list
+
+
+class LandingPadData:
+    """
+    Represents the data required to define and generate a landing pad.
+
+    Attributes:
+        center: The (x, y) coordinates representing the center of the landing pad.
+        axis: The lengths of the semi-major and semi-minor axes of the ellipse.
+        blur: Indicates whether the landing pad should have a blur effect. default: False.
+        angle (int): The rotation angle of the landing pad in degrees. defaults: 0.
+    """
+
+    def __init__(
+        self,
+        center: tuple[int, int],
+        axis: tuple[int, int],
+        blur: bool = False,
+        angle: int = 0,
+    ):
+
+        self.center = center
+        self.axis = axis
+        self.blur = blur
+        self.angle = angle
+
+
+def blur_image(background: np.ndarray, landing_data: LandingPadData) -> NumpyImage:
+    """
+    Blurs an image a singular shape, adds it to the background, and returns an image.
+    """
+
+    background_copy = background.copy()
+    x, y = background_copy.shape[:2]
+
+    mask = np.zeros((x, y), np.uint8)
+    mask = cv2.ellipse(
+        mask,
+        landing_data.center,
+        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 color
+    fg = np.full(background.shape, LANDING_PAD_COLOR, dtype=np.uint8)
+
+    blended = (background * (1 - alpha) + fg * alpha).astype(np.uint8)
+    return NumpyImage(blended)
+
+
+def draw_landing_pad(
+    image: np.ndarray, landing_data: LandingPadData
+) -> tuple[NumpyImage, BoundingBox]:
+    """
+    Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
+    """
+    (h, k), (a, b) = landing_data.center, landing_data.axis
+    rad = math.pi / 180
+    ux, uy = a * math.cos(landing_data.angle * rad), a * math.sin(landing_data.angle * rad)
+    vx, vy = b * math.sin(landing_data.angle * rad), b * math.cos(landing_data.angle * rad)
+    width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
+
+    top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
+    bottom_right = (
+        int(min(h + (0.5) * width, image.shape[1])),
+        int(min(k + (0.5) * height, image.shape[0])),
+    )
+
+    if landing_data.blur:
+        image = blur_image(image, landing_data)
+        return image, BoundingBox(top_left, bottom_right)
+
+    image = cv2.ellipse(
+        image,
+        landing_data.center,
+        landing_data.axis,
+        landing_data.angle,
+        0,
+        360,
+        LANDING_PAD_COLOR,
+        -1,
+    )
+    return NumpyImage(image), BoundingBox(top_left, bottom_right)
+
+
+def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
+    """
+    Generates test cases given a data set.
+    """
+    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
+
+    boxes_list = []
+    for landing_data in landing_list:
+        np_image, bounding_box = draw_landing_pad(image, landing_data)
+        image = np_image.image
+
+        boxes_list.append([1, 0] + list(bounding_box.top_left + bounding_box.bottom_right))
+
+    boxes_list = sorted(
+        boxes_list,
+        reverse=True,
+        key=lambda box: abs((box[4] - box[2]) * (box[5] - box[3])),
+    )
+
+    boxes_list = np.array(boxes_list)
+    image = image.astype(np.uint8)
+    return LandingPadTestData(NumpyImage(image), boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 23a0af2d..796ffe67 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -1,19 +1,22 @@
 """
-Test DetectTarget module.
+Test Contour Detection module.
 
 """
 
-from typing import NamedTuple
-import math
-import cv2
 import numpy as np
 import pytest
 
-from modules.detect_target import detect_target_contour
-from modules import image_and_time
+from tests.unit.generate_detect_target_contour import (
+    LandingPadData,
+    LandingPadTestData,
+    create_test,
+)
 from modules import detections_and_time
+from modules import image_and_time
+from modules.detect_target import detect_target_contour
+
 
-BOUNDING_BOX_PRECISION_TOLERANCE = -2 / 3
+BOUNDING_BOX_PRECISION_TOLERANCE = -2 / 3  # Tolerance > 1
 CONFIDENCE_PRECISION_TOLERANCE = 2
 
 # Test functions use test fixture signature names and access class privates
@@ -21,139 +24,156 @@
 # pylint: disable=protected-access,redefined-outer-name
 
 
-class LandingPadData(NamedTuple):
+@pytest.fixture
+def single_circle() -> LandingPadTestData:  # type: ignore
     """
-    Landing Pad information struct
+    Loads the data for the single basic circle.
     """
+    options = [LandingPadData(center=(300, 400), axis=(200, 200))]
 
-    center: tuple[int, int]
-    radius: int | tuple[int, int]
-    blur: bool = False
-    elipse: bool = False
-    angle: int = 0
+    test_data = create_test(options)
+    yield test_data
 
 
-easy_data = [LandingPadData(center=(1000, 400), radius=200)]
+@pytest.fixture
+def single_blurry_circle() -> LandingPadTestData:  # type: ignore
+    """
+    Loads the data for the single blury circle.
+    """
+    options = [
+        LandingPadData(center=(1000, 500), axis=(423, 423), blur=True),
+    ]
 
-blurry_data = [
-    LandingPadData(center=(1000, 500), radius=423, blur=True),
-]
+    test_data = create_test(options)
+    yield test_data
 
-stretched_data = [LandingPadData(center=(1000, 500), radius=(383, 405), elipse=True)]
 
-multiple_data = [
-    LandingPadData(center=(200, 500), radius=(50, 45), blur=True, elipse=True),
-    LandingPadData(center=(1590, 341), radius=250),
-    LandingPadData(center=(997, 600), radius=300),
-    LandingPadData(center=(401, 307), radius=(200, 150), blur=True, elipse=True),
-]
+@pytest.fixture
+def single_stretched_circle() -> LandingPadTestData:  # type: ignore
+    """
+    Loads the data for the single stretched circle.
+    """
+    options = [LandingPadData(center=(1000, 500), axis=(383, 405))]
+
+    test_data = create_test(options)
+    yield test_data
 
 
-def blur_img(
-    bg: np.ndarray,
-    center: tuple[int, int],
-    radius: int = 0,
-    axis_length: tuple[int, int] = (0, 0),
-    angle: int = 0,
-    circle_check: bool = True,
-) -> np.ndarray:
+@pytest.fixture
+def multiple_circles() -> LandingPadTestData:  # type: ignore
     """
-    Blurs an image a singular shape and adds it to the background.
+    Loads the data for the multiple stretched circles.
     """
+    options = [
+        LandingPadData(center=(997, 600), axis=(300, 300)),
+        LandingPadData(center=(1590, 341), axis=(250, 250)),
+        LandingPadData(center=(200, 500), axis=(50, 45), blur=True),
+        LandingPadData(center=(401, 307), axis=(200, 150), blur=True),
+    ]
+
+    test_data = create_test(options)
+    yield test_data
 
-    bg_copy = bg.copy()
-    x, y = bg_copy.shape[:2]
 
-    mask = np.zeros((x, y), np.uint8)
-    if circle_check:
-        mask = cv2.circle(mask, center, radius, (215, 158, 115), -1, cv2.LINE_AA)
-    else:
-        mask = cv2.ellipse(mask, center, axis_length, angle, 0, 360, (215, 158, 115), -1)
+@pytest.fixture()
+def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
+    """
+    Construct DetectTargetContour.
+    """
+    detection = detect_target_contour.DetectTargetContour(False)
+    yield detection  # type: ignore
 
-    mask = cv2.blur(mask, (25, 25), 7)
 
-    alpha = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
-    fg = np.zeros(bg.shape, np.uint8)
-    fg[:, :, :] = [0, 0, 0]
+@pytest.fixture()
+def image_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load the single basic landing pad.
+    """
 
-    blended = cv2.convertScaleAbs(bg * (1 - alpha) + fg * alpha)
-    return blended
+    image = single_circle.image
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
 
 
-def draw_circle(
-    image: np.ndarray, center: tuple[int, int], radius: int, blur: bool
-) -> tuple[np.ndarray, int, int]:
+@pytest.fixture()
+def blurry_image(single_blurry_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
     """
-    Draws a circle on the provided image and saves the bounding box coordinates to a text file.
+    Load the single blurry landing pad.
     """
-    x, y = center
-    top_left = (max(x - radius, 0), max(y - radius, 0))
-    bottom_right = (min(x + radius, image.shape[1]), min(y + radius, image.shape[0]))
 
-    if blur:
-        image = blur_img(image, center, radius=radius, circle_check=True)
-        return image, top_left, bottom_right
+    image = single_blurry_circle.image
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
 
-    cv2.circle(image, center, radius, (215, 158, 115), -1)
-    return image, top_left, bottom_right
 
+@pytest.fixture()
+def stretched_image(single_stretched_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load the single stretched landing pad.
+    """
 
-def draw_ellipse(
-    image: np.ndarray, center: tuple[int, int], axis_length: tuple, angle: int, blur: bool
-) -> tuple[np.ndarray, int, int]:
+    image = single_stretched_circle.image
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
+
+@pytest.fixture()
+def multiple_images(multiple_circles: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
     """
-    Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
+    Load the multiple landing pads.
     """
-    (h, k), (a, b) = center, axis_length
-    rad = math.pi / 180
-    ux, uy = a * math.cos(angle * rad), a * math.sin(angle * rad)  # first point on the ellipse
-    vx, vy = b * math.sin(angle * rad), b * math.cos(angle * rad)
-    width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
 
-    top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
-    bottom_right = (
-        int(min(h + (0.5) * width, image.shape[1])),
-        int(min(k + (0.5) * height, image.shape[0])),
-    )
+    image = multiple_circles.image
+    result, actual_image = image_and_time.ImageAndTime.create(image)
+    assert result
+    assert actual_image is not None
+    yield actual_image  # type: ignore
+
 
-    if blur:
-        image = blur_img(image, center, axis_length=axis_length, angle=angle, circle_check=False)
-        return image, top_left, bottom_right
+@pytest.fixture()
+def expected_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected a basic image detections.
+    """
 
-    image = cv2.ellipse(image, center, axis_length, angle, 0, 360, (0, 0, 0), -1)
-    return image, top_left, bottom_right
+    expected = single_circle.bounding_box_list
+    yield create_detections(expected)  # type: ignore
 
 
-def create_test_case(landing_list: list[LandingPadData]) -> tuple[np.ndarray, np.ndarray]:
+@pytest.fixture()
+def expected_blur(single_blurry_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
     """
-    Genereates test cases given a data set.
+    Load expected the blured pad image detections.
     """
-    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
 
-    boxes_list = []
-    for landing_data in landing_list:
-        if landing_data.elipse:
-            axis_length, angle = landing_data.radius, landing_data.angle
-            image, top_left, bottom_right = draw_ellipse(
-                image, landing_data.center, axis_length, angle, landing_data.blur
-            )
-            boxes_list.append([1, 0] + list(top_left + bottom_right))
-            continue
+    expected = single_blurry_circle.bounding_box_list
+    yield create_detections(expected)  # type: ignore
 
-        image, top_left, bottom_right = draw_circle(
-            image, landing_data.center, landing_data.radius, landing_data.blur
-        )
-        boxes_list.append([1, 0] + list(top_left + bottom_right))
 
-    boxes_list = sorted(
-        boxes_list,
-        reverse=True,
-        key=lambda boxes_list: abs(
-            (boxes_list[4] - boxes_list[2]) * (boxes_list[5] - boxes_list[3])
-        ),
-    )
-    boxes_list = np.array(boxes_list)
-    return (image.astype(np.uint8), boxes_list)
+@pytest.fixture()
+def expected_stretch(single_stretched_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected a stretched pad image detections.
+    """
+
+    expected = single_stretched_circle.bounding_box_list
+    yield create_detections(expected)  # type: ignore
+
+
+@pytest.fixture()
+def expected_multiple(multiple_circles: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+    """
+    Load expected multiple pads image detections.
+    """
+
+    expected = multiple_circles.bounding_box_list
+    yield create_detections(expected)  # type: ignore
 
 
 # pylint:disable=duplicate-code
@@ -227,107 +247,6 @@ def create_detections(detections_from_file: np.ndarray) -> detections_and_time.D
     return detections
 
 
-@pytest.fixture()
-def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
-    """
-    Construct DetectTargetContour.
-    """
-    detection = detect_target_contour.DetectTargetContour(False)
-    yield detection  # type: ignore
-
-
-@pytest.fixture()
-def image_easy() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load easy image.
-    """
-
-    image, _ = create_test_case(easy_data)
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def blurry_image() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load easy image.
-    """
-
-    image, _ = create_test_case(blurry_data)
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def stretched_image() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load easy image.
-    """
-
-    image, _ = create_test_case(stretched_data)
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def multiple_images() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load easy image.
-    """
-
-    image, _ = create_test_case(multiple_data)
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def expected_easy() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected an easy image detections.
-    """
-
-    _, expected = create_test_case(easy_data)
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_blur() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected an easy image detections.
-    """
-
-    _, expected = create_test_case(blurry_data)
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_stretch() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected an easy image detections.
-    """
-
-    _, expected = create_test_case(stretched_data)
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_multiple() -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected an easy image detections.
-    """
-
-    _, expected = create_test_case(multiple_data)
-    yield create_detections(expected)  # type: ignore
-
-
 class TestDetector:
     """
     Tests `DetectTarget.run()` .
@@ -340,7 +259,7 @@ def test_single_circle(
         expected_easy: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Bus image.
+        Run the detection for the single landing pad.
         """
         # Run
         result, actual = detector.run(image_easy)
@@ -358,7 +277,7 @@ def test_blurry_circle(
         expected_blur: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Bus image.
+        Run the detection for the blury circle.
         """
         # Run
         result, actual = detector.run(blurry_image)
@@ -376,7 +295,7 @@ def test_stretch(
         expected_stretch: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Bus image.
+        Run the detection for the single stretched landing pad.
         """
         # Run
         result, actual = detector.run(stretched_image)
@@ -394,7 +313,7 @@ def test_multiple(
         expected_multiple: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Bus image.
+        Run the detection for the multiple landing pads.
         """
         # Run
         result, actual = detector.run(multiple_images)

From a6540e7f45d6e4315b8c21cad9d386d61b4a5c92 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@example.com>
Date: Tue, 21 Jan 2025 01:17:03 -0500
Subject: [PATCH 17/27] Last comma changes

---
 modules/detect_target/detect_target_contour.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index d80bcf40..8a4c2a4c 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -39,7 +39,7 @@ def __init__(
             self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
 
     def detect_landing_pads_contours(
-        self, image: "np.ndarray", timestamp: float
+        self, image: np.ndarray, timestamp: float
     ) -> tuple[bool, detections_and_time.DetectionsAndTime | None, np.ndarray]:
         """
         Detects landing pads using contours/classical cv.
@@ -101,7 +101,7 @@ def detect_landing_pads_contours(
 
     def run(
         self, data: image_and_time.ImageAndTime
-    ) -> "tuple[bool, detections_and_time.DetectionsAndTime | None]":
+    ) -> tuple[bool, detections_and_time.DetectionsAndTime] | tuple[False, None]:
         """
         Runs object detection on the provided image and returns the detections.
         data: Image with a timestamp.

From 8def299a91414b1eb0e7f2efd4bb8761a28f9185 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Tue, 4 Feb 2025 02:05:23 -0500
Subject: [PATCH 18/27] Xierumeng Chnages 2

---
 donke_1738651853_0.txt                        |   2 +
 main_detect_target.py                         | 213 ------------------
 .../detect_target/detect_target_contour.py    |  45 ++--
 tests/unit/generate_detect_target_contour.py  | 140 +++++++-----
 tests/unit/test_detect_target_contour.py      |  14 +-
 5 files changed, 112 insertions(+), 302 deletions(-)
 create mode 100644 donke_1738651853_0.txt
 delete mode 100644 main_detect_target.py

diff --git a/donke_1738651853_0.txt b/donke_1738651853_0.txt
new file mode 100644
index 00000000..204cf6a4
--- /dev/null
+++ b/donke_1738651853_0.txt
@@ -0,0 +1,2 @@
+<class 'modules.detections_and_time.DetectionsAndTime'>, time: 1738651853.983181, size: 1
+[cls: 0, conf: 1.0, bounds: 617 95 1384 906]
\ No newline at end of file
diff --git a/main_detect_target.py b/main_detect_target.py
deleted file mode 100644
index b1d44553..00000000
--- a/main_detect_target.py
+++ /dev/null
@@ -1,213 +0,0 @@
-"""
-For 2022-2023 UAS competition.
-"""
-
-import argparse
-import multiprocessing as mp
-import pathlib
-
-from modules.detect_target import detect_target_factory
-from modules.detect_target import detect_target_worker
-from modules.video_input import video_input_worker
-from modules.common.modules.logger import logger
-from modules.common.modules.logger import logger_main_setup
-from modules.common.modules.read_yaml import read_yaml
-from utilities.workers import queue_proxy_wrapper
-from utilities.workers import worker_controller
-from utilities.workers import worker_manager
-
-
-CONFIG_FILE_PATH = pathlib.Path("config.yaml")
-
-
-# Code copied into main_2024.py
-# pylint: disable=duplicate-code
-def main() -> int:
-    """
-    Main function.
-    """
-    # Parse whether or not to force cpu from command line
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--cpu", action="store_true", help="option to force cpu")
-    parser.add_argument("--full", action="store_true", help="option to force full precision")
-    parser.add_argument(
-        "--show-annotated",
-        action="store_true",
-        help="option to show annotated image",
-    )
-    args = parser.parse_args()
-
-    # Configuration settings
-    result, config = read_yaml.open_config(CONFIG_FILE_PATH)
-    if not result:
-        print("ERROR: Failed to load configuration file")
-        return -1
-
-    # Get Pylance to stop complaining
-    assert config is not None
-
-    # Logger configuration settings
-    result, config_logger = read_yaml.open_config(logger.CONFIG_FILE_PATH)
-    if not result:
-        print("ERROR: Failed to load configuration file")
-        return -1
-
-    # Get Pylance to stop complaining
-    assert config_logger is not None
-
-    # Setup main logger
-    result, main_logger, logging_path = logger_main_setup.setup_main_logger(config_logger)
-    if not result:
-        print("ERROR: Failed to create main logger")
-        return -1
-
-    # Get Pylance to stop complaining
-    assert main_logger is not None
-    assert logging_path is not None
-
-    # Get settings
-    try:
-        # Local constants
-        # pylint: disable=invalid-name
-        QUEUE_MAX_SIZE = config["queue_max_size"]
-
-        VIDEO_INPUT_CAMERA_NAME = config["video_input"]["camera_name"]
-        VIDEO_INPUT_WORKER_PERIOD = config["video_input"]["worker_period"]
-        VIDEO_INPUT_SAVE_NAME_PREFIX = config["video_input"]["save_prefix"]
-        VIDEO_INPUT_SAVE_PREFIX = str(pathlib.Path(logging_path, VIDEO_INPUT_SAVE_NAME_PREFIX))
-
-        DETECT_TARGET_WORKER_COUNT = config["detect_target"]["worker_count"]
-        detect_target_option_int = config["detect_target"]["option"]
-        DETECT_TARGET_OPTION = detect_target_factory.DetectTargetOption(detect_target_option_int)
-        DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"]
-        DETECT_TARGET_MODEL_PATH = config["detect_target"]["model_path"]
-        DETECT_TARGET_OVERRIDE_FULL_PRECISION = args.full
-        DETECT_TARGET_SAVE_NAME_PREFIX = config["detect_target"]["save_prefix"]
-        DETECT_TARGET_SAVE_PREFIX = str(pathlib.Path(logging_path, DETECT_TARGET_SAVE_NAME_PREFIX))
-        DETECT_TARGET_SHOW_ANNOTATED = args.show_annotated
-        # pylint: enable=invalid-name
-    except KeyError as exception:
-        main_logger.error(f"ERROR: Config key(s) not found: {exception}", True)
-        return -1
-
-    # Setup
-    controller = worker_controller.WorkerController()
-
-    mp_manager = mp.Manager()
-    video_input_to_detect_target_queue = queue_proxy_wrapper.QueueProxyWrapper(
-        mp_manager,
-        QUEUE_MAX_SIZE,
-    )
-    detect_target_to_main_queue = queue_proxy_wrapper.QueueProxyWrapper(
-        mp_manager,
-        QUEUE_MAX_SIZE,
-    )
-
-    # Worker properties
-    result, video_input_worker_properties = worker_manager.WorkerProperties.create(
-        count=1,
-        target=video_input_worker.video_input_worker,
-        work_arguments=(
-            VIDEO_INPUT_CAMERA_NAME,
-            VIDEO_INPUT_WORKER_PERIOD,
-            VIDEO_INPUT_SAVE_PREFIX,
-        ),
-        input_queues=[],
-        output_queues=[video_input_to_detect_target_queue],
-        controller=controller,
-        local_logger=main_logger,
-    )
-    if not result:
-        main_logger.error("Failed to create arguments for Video Input", True)
-        return -1
-
-    # Get Pylance to stop complaining
-    assert video_input_worker_properties is not None
-
-    result, detect_target_worker_properties = worker_manager.WorkerProperties.create(
-        count=DETECT_TARGET_WORKER_COUNT,
-        target=detect_target_worker.detect_target_worker,
-        work_arguments=(
-            DETECT_TARGET_OPTION,
-            DETECT_TARGET_DEVICE,
-            DETECT_TARGET_MODEL_PATH,
-            DETECT_TARGET_OVERRIDE_FULL_PRECISION,
-            DETECT_TARGET_SHOW_ANNOTATED,
-            DETECT_TARGET_SAVE_PREFIX,
-        ),
-        input_queues=[video_input_to_detect_target_queue],
-        output_queues=[detect_target_to_main_queue],
-        controller=controller,
-        local_logger=main_logger,
-    )
-    if not result:
-        main_logger.error("Failed to create arguments for Detect Target", True)
-        return -1
-
-    # Get Pylance to stop complaining
-    assert detect_target_worker_properties is not None
-
-    # Create managers
-    worker_managers = []
-
-    result, video_input_manager = worker_manager.WorkerManager.create(
-        worker_properties=video_input_worker_properties,
-        local_logger=main_logger,
-    )
-    if not result:
-        main_logger.error("Failed to create manager for Video Input", True)
-        return -1
-
-    # Get Pylance to stop complaining
-    assert video_input_manager is not None
-
-    worker_managers.append(video_input_manager)
-
-    result, detect_target_manager = worker_manager.WorkerManager.create(
-        worker_properties=detect_target_worker_properties,
-        local_logger=main_logger,
-    )
-    if not result:
-        main_logger.error("Failed to create manager for Detect Target", True)
-        return -1
-
-    # Get Pylance to stop complaining
-    assert detect_target_manager is not None
-
-    worker_managers.append(detect_target_manager)
-
-    # Run
-    for manager in worker_managers:
-        manager.start_workers()
-
-    while True:
-        # Use main_logger for debugging
-        detections_and_time = detect_target_to_main_queue.queue.get()
-        if detections_and_time is None:
-            break
-        main_logger.debug(f"Timestamp: {detections_and_time.timestamp}", True)
-        main_logger.debug(f"Num detections: {len(detections_and_time.detections)}", True)
-        for detection in detections_and_time.detections:
-            main_logger.debug(f"Detection: {detection}", True)
-
-    # Teardown
-    controller.request_exit()
-
-    video_input_to_detect_target_queue.fill_and_drain_queue()
-    detect_target_to_main_queue.fill_and_drain_queue()
-
-    for manager in worker_managers:
-        manager.join_workers()
-
-    return 0
-
-
-# pylint: enable=duplicate-code
-
-
-if __name__ == "__main__":
-    result_main = main()
-    if result_main < 0:
-        print(f"ERROR: Status code: {result_main}")
-
-    print("Done!")
diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 8a4c2a4c..946ec14a 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -4,29 +4,31 @@
 
 import time
 
-import copy
 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
+
+
 
 MIN_CONTOUR_AREA = 100
-# some arbitrary value
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
+LABEL = 0
 
 
 class DetectTargetContour(base_detect_target.BaseDetectTarget):
     """
-    Predicts annd locates landing pads using the Classical Computer Vision methodology.
+    Predicts annd locates landing pads using the classical computer vision methodology.
     """
 
     def __init__(
         self,
         show_annotations: bool = False,
-        save_name: str = "",
+        save_name: str = "donke"
     ) -> None:
         """
         show_annotations: Display annotated images.
@@ -35,12 +37,14 @@ def __init__(
         self.__counter = 0
         self.__show_annotations = show_annotations
         self.__filename_prefix = ""
+        _, self.__logger = logger.Logger.create(self.__filename_prefix, False)
+
         if save_name != "":
             self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
 
     def detect_landing_pads_contours(
         self, image: np.ndarray, timestamp: float
-    ) -> tuple[bool, detections_and_time.DetectionsAndTime | None, np.ndarray]:
+    ) -> tuple[True, detections_and_time.DetectionsAndTime, np.ndarray] | tuple[False, None, None]:
         """
         Detects landing pads using contours/classical cv.
         image: Current image frame.
@@ -53,15 +57,16 @@ def detect_landing_pads_contours(
         contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
         if len(contours) == 0:
-            return False, None, image
+            return False, None, None
 
         # Create the DetectionsAndTime object
         result, detections = detections_and_time.DetectionsAndTime.create(timestamp)
         if not result:
-            return False, None, image
+            return False, None, None
 
+        # Ordered for the mapping to the corresponding detections
         sorted_contour = sorted(contours, key=cv2.contourArea, reverse=True)
-        image_annotated = copy.deepcopy(image)
+        image_annotated = image
         for i, contour in enumerate(sorted_contour):
             contour_area = cv2.contourArea(contour)
 
@@ -78,12 +83,16 @@ def detect_landing_pads_contours(
 
             x, y, w, h = cv2.boundingRect(contour)
             bounds = np.array([x, y, x + w, y + h])
-            confidence, label = 1.0, 0
+            confidence = 1.0
+            label = LABEL
 
             # Create a Detection object and append it to detections
             result, detection = detections_and_time.Detection.create(bounds, label, confidence)
-            if result:
-                detections.append(detection)
+            
+            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)
@@ -101,7 +110,7 @@ def detect_landing_pads_contours(
 
     def run(
         self, data: image_and_time.ImageAndTime
-    ) -> tuple[bool, detections_and_time.DetectionsAndTime] | tuple[False, None]:
+    ) -> 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.
@@ -109,7 +118,7 @@ def run(
         """
         image = data.image
         timestamp = data.timestamp
-
+        
         result, detections, image_annotated = self.detect_landing_pads_contours(image, timestamp)
 
         if not result:
@@ -117,14 +126,10 @@ def run(
 
         # Logging
         if self.__filename_prefix != "":
-            filename = self.__filename_prefix + str(self.__counter)
-            # Object detections
-            with open(filename + ".txt", "w", encoding="utf-8") as file:
-                # Use internal string representation
-                file.write(repr(detections))
-            # Annotated image
-            cv2.imwrite(filename + ".png", image_annotated)  # type: ignore
+            self.__logger.save_image(image, self.__filename_prefix)
             self.__counter += 1
+        
         if self.__show_annotations:
             cv2.imshow("Annotated", image_annotated)  # type: ignore
+
         return True, detections
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index c72b618f..f0ca1056 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -1,5 +1,5 @@
 """
-Helper functions for test_detect_target_contour.
+Helper functions for `test_detect_target_contour.py`.
 
 """
 
@@ -7,82 +7,84 @@
 import math
 import numpy as np
 
-LANDING_PAD_COLOR = (100, 50, 50)  # blue color
+
+LANDING_PAD_COLOR_BLUE = (100, 50, 50)
+
 
 # Test functions use test fixture signature names and access class privates
 # No enable
 # pylint: disable=protected-access,redefined-outer-name
 
 
-class BoundingBox:
-    """
-    Holds the data that define the generated bounding boxes.
-
-    Attributes:
-        top_left: A tuple of integers that represents top left corner of bounding box.
-        bottom_right: A tuple of integers that represents bottom right corner of bounding box.
-    """
+class LandingPadData:
 
-    def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
-        self.top_left = top_left
-        self.bottom_right = bottom_right
+    def __init__(
+        self,
+        center: tuple[int, int],
+        axis: tuple[int, int],
+        blur: bool,
+        angle: int,
+    ):
+        """
+        Represents the data required to define and generate a landing pad.
+
+        Attributes:
+            center: The (x, y) coordinates representing the center of the landing pad.
+            axis: The lengths of the semi-major and semi-minor axes of the ellipse.
+            blur: Indicates whether the landing pad should have a blur effect. default: False.
+            angle (int): The rotation angle of the landing pad in degrees. defaults: 0.
+        """
+        self.center = center
+        self.axis = axis
+        self.blur = blur
+        self.angle = angle
 
 
 class NumpyImage:
-    """
-    Holds the Numpy Array which represents an image.
-    """
-
     def __init__(self, image: np.ndarray):
+        """
+        Holds the Numpy Array which represents an image.
+        """
         self.image = image
 
 
-class LandingPadTestData:
-    """
-    Struct to hold the data needed to perform the tests.
+class BoundingBox:
+    def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
+        """
+        Holds the data that define the generated bounding boxes.
 
-    Attributes:
-        image = A numpy array that represents the image needed to be tested
-        bounding_box_list: A numpy array that holds a list of expected bounding box coordinates
-    """
+        Attributes:
+            top_left: A tuple of integers that represents top left corner of bounding box.
+            bottom_right: A tuple of integers that represents bottom right corner of bounding box.
+        """
+        self.top_left = top_left
+        self.bottom_right = bottom_right
 
+
+class LandingPadTestData:
     def __init__(self, image: NumpyImage, boxes_list: np.ndarray):
+        """
+        Struct to hold the data needed to perform the tests.
+
+        Attributes:
+            image = A numpy array that represents the image needed to be tested
+            bounding_box_list: A numpy array that holds a list of expected bounding box coordinates
+        """
         self.image = image.image
         self.bounding_box_list = boxes_list
 
 
-class LandingPadData:
+def add_blurred_landing_pad(background: np.ndarray, landing_data: LandingPadData) -> NumpyImage:
     """
-    Represents the data required to define and generate a landing pad.
+    Blurs an image a singular lading pad, adds it to the background.
 
     Attributes:
-        center: The (x, y) coordinates representing the center of the landing pad.
-        axis: The lengths of the semi-major and semi-minor axes of the ellipse.
-        blur: Indicates whether the landing pad should have a blur effect. default: False.
-        angle (int): The rotation angle of the landing pad in degrees. defaults: 0.
-    """
-
-    def __init__(
-        self,
-        center: tuple[int, int],
-        axis: tuple[int, int],
-        blur: bool = False,
-        angle: int = 0,
-    ):
-
-        self.center = center
-        self.axis = axis
-        self.blur = blur
-        self.angle = angle
-
-
-def blur_image(background: np.ndarray, landing_data: LandingPadData) -> NumpyImage:
-    """
-    Blurs an image a singular shape, adds it to the background, and returns an image.
+        image = A numpy array that represents background.
+        landing_data = The landing pad which is to be blurred.
+    Returns:
+        NumpyImage: A numpy array of the new blured image.
     """
-
-    background_copy = background.copy()
-    x, y = background_copy.shape[:2]
+    x, y = background.shape[:2]
 
     mask = np.zeros((x, y), np.uint8)
     mask = cv2.ellipse(
@@ -100,7 +102,7 @@ def blur_image(background: np.ndarray, landing_data: LandingPadData) -> NumpyIma
 
     alpha = mask[:, :, np.newaxis] / 255.0
     # Brings the image back to its original color
-    fg = np.full(background.shape, LANDING_PAD_COLOR, dtype=np.uint8)
+    fg = np.full(background.shape, LANDING_PAD_COLOR_BLUE, dtype=np.uint8)
 
     blended = (background * (1 - alpha) + fg * alpha).astype(np.uint8)
     return NumpyImage(blended)
@@ -109,8 +111,17 @@ def blur_image(background: np.ndarray, landing_data: LandingPadData) -> NumpyIma
 def draw_landing_pad(
     image: np.ndarray, landing_data: LandingPadData
 ) -> tuple[NumpyImage, BoundingBox]:
+    print("asdasd")
+    print(image)
     """
-    Draws an ellipse on the provided image and saves the bounding box coordinates to a text file.
+    Draws an singular landing pad on the provided image and saves the bounding box coordinates to a text file.
+
+    Attributes:
+        image = A numpy array that represents background
+        landing_data = The landing pad which is to be placed
+    Returns:
+        NumpyImage: A numpy array of the new image .
+        BoundingBox: Bounding box of the newly placed bounding box.
     """
     (h, k), (a, b) = landing_data.center, landing_data.axis
     rad = math.pi / 180
@@ -124,9 +135,11 @@ def draw_landing_pad(
         int(min(k + (0.5) * height, image.shape[0])),
     )
 
+    bounding_box = BoundingBox(top_left, bottom_right)
+
     if landing_data.blur:
-        image = blur_image(image, landing_data)
-        return image, BoundingBox(top_left, bottom_right)
+        image = add_blurred_landing_pad(image, landing_data)
+        return image, bounding_box
 
     image = cv2.ellipse(
         image,
@@ -135,10 +148,10 @@ def draw_landing_pad(
         landing_data.angle,
         0,
         360,
-        LANDING_PAD_COLOR,
+        LANDING_PAD_COLOR_BLUE,
         -1,
     )
-    return NumpyImage(image), BoundingBox(top_left, bottom_right)
+    return NumpyImage(image), bounding_box
 
 
 def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
@@ -146,13 +159,15 @@ def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
     Generates test cases given a data set.
     """
     image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
+    confidence_and_label = [1, 0]
 
     boxes_list = []
-    for landing_data in landing_list:
-        np_image, bounding_box = draw_landing_pad(image, landing_data)
-        image = np_image.image
 
-        boxes_list.append([1, 0] + list(bounding_box.top_left + bounding_box.bottom_right))
+    for landing_data in landing_list:
+        print(image)
+        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))
 
     boxes_list = sorted(
         boxes_list,
@@ -162,4 +177,5 @@ def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
 
     boxes_list = np.array(boxes_list)
     image = image.astype(np.uint8)
+
     return LandingPadTestData(NumpyImage(image), boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 796ffe67..86554c82 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -29,7 +29,7 @@ def single_circle() -> LandingPadTestData:  # type: ignore
     """
     Loads the data for the single basic circle.
     """
-    options = [LandingPadData(center=(300, 400), axis=(200, 200))]
+    options = [LandingPadData(center=(300, 400), axis=(200, 200), blur=False, angle=0)]
 
     test_data = create_test(options)
     yield test_data
@@ -41,7 +41,7 @@ def single_blurry_circle() -> LandingPadTestData:  # type: ignore
     Loads the data for the single blury circle.
     """
     options = [
-        LandingPadData(center=(1000, 500), axis=(423, 423), blur=True),
+        LandingPadData(center=(1000, 500), axis=(423, 423), blur=True, angle=0),
     ]
 
     test_data = create_test(options)
@@ -53,7 +53,7 @@ def single_stretched_circle() -> LandingPadTestData:  # type: ignore
     """
     Loads the data for the single stretched circle.
     """
-    options = [LandingPadData(center=(1000, 500), axis=(383, 405))]
+    options = [LandingPadData(center=(1000, 500), axis=(383, 405), blur=False, angle=0)]
 
     test_data = create_test(options)
     yield test_data
@@ -65,10 +65,10 @@ def multiple_circles() -> LandingPadTestData:  # type: ignore
     Loads the data for the multiple stretched circles.
     """
     options = [
-        LandingPadData(center=(997, 600), axis=(300, 300)),
-        LandingPadData(center=(1590, 341), axis=(250, 250)),
-        LandingPadData(center=(200, 500), axis=(50, 45), blur=True),
-        LandingPadData(center=(401, 307), axis=(200, 150), blur=True),
+        LandingPadData(center=(997, 600), axis=(300, 300), blur=False, angle=0),
+        LandingPadData(center=(1590, 341), axis=(250, 250), blur=False, angle=0),
+        LandingPadData(center=(200, 500), axis=(50, 45), blur=True, angle=0),
+        LandingPadData(center=(401, 307), axis=(200, 150), blur=True, angle=0),
     ]
 
     test_data = create_test(options)

From 14fcc41a2de20bab72038f4784b68fcb0cd42565 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Tue, 4 Feb 2025 02:08:21 -0500
Subject: [PATCH 19/27] small pylint changes

---
 modules/detect_target/detect_target_contour.py | 18 +++++++-----------
 tests/unit/generate_detect_target_contour.py   |  4 +++-
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 946ec14a..4d343f0b 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -13,7 +13,6 @@
 from ..common.modules.logger import logger
 
 
-
 MIN_CONTOUR_AREA = 100
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
@@ -25,11 +24,7 @@ class DetectTargetContour(base_detect_target.BaseDetectTarget):
     Predicts annd locates landing pads using the classical computer vision methodology.
     """
 
-    def __init__(
-        self,
-        show_annotations: bool = False,
-        save_name: str = "donke"
-    ) -> None:
+    def __init__(self, show_annotations: bool = False, save_name: str = "donke") -> None:
         """
         show_annotations: Display annotated images.
         save_name: filename prefix for logging detections and annotated images.
@@ -88,10 +83,10 @@ def detect_landing_pads_contours(
 
             # 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
@@ -118,7 +113,7 @@ def run(
         """
         image = data.image
         timestamp = data.timestamp
-        
+
         result, detections, image_annotated = self.detect_landing_pads_contours(image, timestamp)
 
         if not result:
@@ -126,9 +121,10 @@ def run(
 
         # Logging
         if self.__filename_prefix != "":
-            self.__logger.save_image(image, self.__filename_prefix)
+            filename = self.__filename_prefix + str(self.__counter)
+            self.__logger.save_image(image, filename)
             self.__counter += 1
-        
+
         if self.__show_annotations:
             cv2.imshow("Annotated", image_annotated)  # type: ignore
 
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index f0ca1056..524a0fec 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -167,7 +167,9 @@ def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
         print(image)
         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))
+        boxes_list.append(
+            confidence_and_label + list(bounding_box.top_left + bounding_box.bottom_right)
+        )
 
     boxes_list = sorted(
         boxes_list,

From fa9b8d66410ac7f605b12b7f2b99fcaea066c5b2 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Thu, 6 Feb 2025 14:56:58 -0500
Subject: [PATCH 20/27] xierumeng changes v3

---
 donke_1738651853_0.txt                        |  2 -
 .../detect_target/detect_target_contour.py    | 18 +++--
 tests/unit/generate_detect_target_contour.py  | 65 ++++++++++---------
 tests/unit/test_detect_target_contour.py      | 48 +++++++-------
 4 files changed, 72 insertions(+), 61 deletions(-)
 delete mode 100644 donke_1738651853_0.txt

diff --git a/donke_1738651853_0.txt b/donke_1738651853_0.txt
deleted file mode 100644
index 204cf6a4..00000000
--- a/donke_1738651853_0.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-<class 'modules.detections_and_time.DetectionsAndTime'>, time: 1738651853.983181, size: 1
-[cls: 0, conf: 1.0, bounds: 617 95 1384 906]
\ No newline at end of file
diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 4d343f0b..f9fb4192 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -12,7 +12,7 @@
 from .. import detections_and_time
 from ..common.modules.logger import logger
 
-
+CONFIDENCE = 1.0
 MIN_CONTOUR_AREA = 100
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
@@ -24,7 +24,9 @@ class DetectTargetContour(base_detect_target.BaseDetectTarget):
     Predicts annd locates landing pads using the classical computer vision methodology.
     """
 
-    def __init__(self, show_annotations: bool = False, save_name: str = "donke") -> None:
+    def __init__(
+        self, image_logger: logger.Logger, show_annotations: bool = False, save_name: str = ""
+    ) -> None:
         """
         show_annotations: Display annotated images.
         save_name: filename prefix for logging detections and annotated images.
@@ -32,7 +34,7 @@ def __init__(self, show_annotations: bool = False, save_name: str = "donke") ->
         self.__counter = 0
         self.__show_annotations = show_annotations
         self.__filename_prefix = ""
-        _, self.__logger = logger.Logger.create(self.__filename_prefix, False)
+        self.__logger = image_logger
 
         if save_name != "":
             self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
@@ -41,9 +43,11 @@ def detect_landing_pads_contours(
         self, image: np.ndarray, timestamp: float
     ) -> tuple[True, detections_and_time.DetectionsAndTime, np.ndarray] | tuple[False, None, None]:
         """
-        Detects landing pads using contours/classical cv.
+        Detects landing pads using contours/classical CV.
+
         image: Current image frame.
         timestamp: Timestamp for the detections.
+
         Return: Success, the DetectionsAndTime object, and the annotated image.
         """
         hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
@@ -78,11 +82,9 @@ def detect_landing_pads_contours(
 
             x, y, w, h = cv2.boundingRect(contour)
             bounds = np.array([x, y, x + w, y + h])
-            confidence = 1.0
-            label = LABEL
 
             # Create a Detection object and append it to detections
-            result, detection = detections_and_time.Detection.create(bounds, label, confidence)
+            result, detection = detections_and_time.Detection.create(bounds, LABEL, CONFIDENCE)
 
             if not result:
                 return False, None, None
@@ -108,7 +110,9 @@ def run(
     ) -> 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.
+
         Return: Success and the detections.
         """
         image = data.image
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index 524a0fec..f2031386 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -8,7 +8,7 @@
 import numpy as np
 
 
-LANDING_PAD_COLOR_BLUE = (100, 50, 50)
+LANDING_PAD_COLOUR_BLUE = (100, 50, 50)  # BGR
 
 
 # Test functions use test fixture signature names and access class privates
@@ -16,23 +16,22 @@
 # pylint: disable=protected-access,redefined-outer-name
 
 
-class LandingPadData:
+class LandingPadImageConfig:
 
     def __init__(
         self,
         center: tuple[int, int],
         axis: tuple[int, int],
         blur: bool,
-        angle: int,
+        angle: float,
     ):
         """
         Represents the data required to define and generate a landing pad.
 
-        Attributes:
-            center: The (x, y) coordinates representing the center of the landing pad.
-            axis: The lengths of the semi-major and semi-minor axes of the ellipse.
-            blur: Indicates whether the landing pad should have a blur effect. default: False.
-            angle (int): The rotation angle of the landing pad in degrees. defaults: 0.
+        center: The (x, y) coordinates representing the center of the landing pad.
+        axis: The pixel lengths of the semi-major and semi-minor axes of the ellipse.
+        blur: Indicates whether the landing pad should have a blur effect. default: False.
+        angle: The rotation angle of the landing pad in degrees clockwise (0 < angle < 360).
         """
         self.center = center
         self.axis = axis
@@ -61,7 +60,7 @@ def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
         self.bottom_right = bottom_right
 
 
-class LandingPadTestData:
+class InputImageAndExpectedBoundingBoxes:
     def __init__(self, image: NumpyImage, boxes_list: np.ndarray):
         """
         Struct to hold the data needed to perform the tests.
@@ -74,15 +73,16 @@ def __init__(self, image: NumpyImage, boxes_list: np.ndarray):
         self.bounding_box_list = boxes_list
 
 
-def add_blurred_landing_pad(background: np.ndarray, landing_data: LandingPadData) -> NumpyImage:
+def add_blurred_landing_pad(
+    background: np.ndarray, landing_data: LandingPadImageConfig
+) -> NumpyImage:
     """
     Blurs an image a singular lading pad, adds it to the background.
 
-    Attributes:
-        image = A numpy array that represents background.
-        landing_data = The landing pad which is to be blurred.
-    Returns:
-        NumpyImage: A numpy array of the new blured image.
+    background: A numpy image.
+    landing_data = The landing pad which is to be blurred.
+
+    Returns: Image with the landing pad.
     """
     x, y = background.shape[:2]
 
@@ -101,27 +101,24 @@ def add_blurred_landing_pad(background: np.ndarray, landing_data: LandingPadData
     mask = cv2.blur(mask, (25, 25), 7)
 
     alpha = mask[:, :, np.newaxis] / 255.0
-    # Brings the image back to its original color
-    fg = np.full(background.shape, LANDING_PAD_COLOR_BLUE, dtype=np.uint8)
+    # 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: LandingPadData
+    image: np.ndarray, landing_data: LandingPadImageConfig
 ) -> tuple[NumpyImage, BoundingBox]:
     print("asdasd")
     print(image)
     """
-    Draws an singular landing pad on the provided image and saves the bounding box coordinates to a text file.
-
-    Attributes:
-        image = A numpy array that represents background
-        landing_data = The landing pad which is to be placed
-    Returns:
-        NumpyImage: A numpy array of the new image .
-        BoundingBox: Bounding box of the newly placed bounding box.
+    Draws a single landing pad on the provided image and saves the bounding box coordinates to a text file.
+
+    landing_data: Landing pad data for the landing pad to be added.
+
+    Returns: Image with landing pad and the bounding box for the drawn landing pad.
     """
     (h, k), (a, b) = landing_data.center, landing_data.axis
     rad = math.pi / 180
@@ -148,17 +145,24 @@ def draw_landing_pad(
         landing_data.angle,
         0,
         360,
-        LANDING_PAD_COLOR_BLUE,
+        LANDING_PAD_COLOUR_BLUE,
         -1,
     )
     return NumpyImage(image), bounding_box
 
 
-def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
+def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpectedBoundingBoxes:
     """
     Generates test cases given a data set.
+
+    landing_data: Landing pad data for the landing pad to be added.
+
+    Returns: The image and expected bounding box.
+
     """
-    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int16)
+    image = np.full(
+        shape=(1000, 2000, 3), fill_value=255, dtype=np.int16
+    )  # shape: size of the screen
     confidence_and_label = [1, 0]
 
     boxes_list = []
@@ -175,9 +179,10 @@ def create_test(landing_list: list[LandingPadData]) -> LandingPadTestData:
         boxes_list,
         reverse=True,
         key=lambda box: abs((box[4] - box[2]) * (box[5] - box[3])),
+        # calculates the absolute value of area of the bounding box
     )
 
     boxes_list = np.array(boxes_list)
     image = image.astype(np.uint8)
 
-    return LandingPadTestData(NumpyImage(image), boxes_list)
+    return InputImageAndExpectedBoundingBoxes(NumpyImage(image), boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 86554c82..934847ca 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -7,17 +7,21 @@
 import pytest
 
 from tests.unit.generate_detect_target_contour import (
-    LandingPadData,
-    LandingPadTestData,
+    LandingPadImageConfig,
+    InputImageAndExpectedBoundingBoxes,
     create_test,
 )
 from modules import detections_and_time
 from modules import image_and_time
 from modules.detect_target import detect_target_contour
+from modules.common.modules.logger import logger  # Changed from relative to absolute import
 
 
 BOUNDING_BOX_PRECISION_TOLERANCE = -2 / 3  # Tolerance > 1
 CONFIDENCE_PRECISION_TOLERANCE = 2
+LOGGER_NAME = ""
+
+_, test_logger = logger.Logger.create(LOGGER_NAME, False)
 
 # Test functions use test fixture signature names and access class privates
 # No enable
@@ -25,23 +29,23 @@
 
 
 @pytest.fixture
-def single_circle() -> LandingPadTestData:  # type: ignore
+def single_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single basic circle.
     """
-    options = [LandingPadData(center=(300, 400), axis=(200, 200), blur=False, angle=0)]
+    options = [LandingPadImageConfig(center=(300, 400), axis=(200, 200), blur=False, angle=0)]
 
     test_data = create_test(options)
     yield test_data
 
 
 @pytest.fixture
-def single_blurry_circle() -> LandingPadTestData:  # type: ignore
+def single_blurry_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single blury circle.
     """
     options = [
-        LandingPadData(center=(1000, 500), axis=(423, 423), blur=True, angle=0),
+        LandingPadImageConfig(center=(1000, 500), axis=(423, 423), blur=True, angle=0),
     ]
 
     test_data = create_test(options)
@@ -49,26 +53,26 @@ def single_blurry_circle() -> LandingPadTestData:  # type: ignore
 
 
 @pytest.fixture
-def single_stretched_circle() -> LandingPadTestData:  # type: ignore
+def single_stretched_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single stretched circle.
     """
-    options = [LandingPadData(center=(1000, 500), axis=(383, 405), blur=False, angle=0)]
+    options = [LandingPadImageConfig(center=(1000, 500), axis=(383, 405), blur=False, angle=0)]
 
     test_data = create_test(options)
     yield test_data
 
 
 @pytest.fixture
-def multiple_circles() -> LandingPadTestData:  # type: ignore
+def multiple_circles() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the multiple stretched circles.
     """
     options = [
-        LandingPadData(center=(997, 600), axis=(300, 300), blur=False, angle=0),
-        LandingPadData(center=(1590, 341), axis=(250, 250), blur=False, angle=0),
-        LandingPadData(center=(200, 500), axis=(50, 45), blur=True, angle=0),
-        LandingPadData(center=(401, 307), axis=(200, 150), blur=True, angle=0),
+        LandingPadImageConfig(center=(997, 600), axis=(300, 300), blur=False, angle=0),
+        LandingPadImageConfig(center=(1590, 341), axis=(250, 250), blur=False, angle=0),
+        LandingPadImageConfig(center=(200, 500), axis=(50, 45), blur=True, angle=0),
+        LandingPadImageConfig(center=(401, 307), axis=(200, 150), blur=True, angle=0),
     ]
 
     test_data = create_test(options)
@@ -80,12 +84,12 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
     Construct DetectTargetContour.
     """
-    detection = detect_target_contour.DetectTargetContour(False)
+    detection = detect_target_contour.DetectTargetContour(test_logger, False)
     yield detection  # type: ignore
 
 
 @pytest.fixture()
-def image_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def image_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single basic landing pad.
     """
@@ -98,7 +102,7 @@ def image_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndTime
 
 
 @pytest.fixture()
-def blurry_image(single_blurry_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def blurry_image(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single blurry landing pad.
     """
@@ -111,7 +115,7 @@ def blurry_image(single_blurry_circle: LandingPadTestData) -> image_and_time.Ima
 
 
 @pytest.fixture()
-def stretched_image(single_stretched_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def stretched_image(single_stretched_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single stretched landing pad.
     """
@@ -124,7 +128,7 @@ def stretched_image(single_stretched_circle: LandingPadTestData) -> image_and_ti
 
 
 @pytest.fixture()
-def multiple_images(multiple_circles: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def multiple_images(multiple_circles: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the multiple landing pads.
     """
@@ -137,7 +141,7 @@ def multiple_images(multiple_circles: LandingPadTestData) -> image_and_time.Imag
 
 
 @pytest.fixture()
-def expected_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected a basic image detections.
     """
@@ -147,7 +151,7 @@ def expected_easy(single_circle: LandingPadTestData) -> image_and_time.ImageAndT
 
 
 @pytest.fixture()
-def expected_blur(single_blurry_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_blur(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected the blured pad image detections.
     """
@@ -157,7 +161,7 @@ def expected_blur(single_blurry_circle: LandingPadTestData) -> image_and_time.Im
 
 
 @pytest.fixture()
-def expected_stretch(single_stretched_circle: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_stretch(single_stretched_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected a stretched pad image detections.
     """
@@ -167,7 +171,7 @@ def expected_stretch(single_stretched_circle: LandingPadTestData) -> image_and_t
 
 
 @pytest.fixture()
-def expected_multiple(multiple_circles: LandingPadTestData) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_multiple(multiple_circles: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected multiple pads image detections.
     """

From f87364060353c39bfa1d2aadbea647b595be7184 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Sun, 9 Feb 2025 18:58:29 -0500
Subject: [PATCH 21/27] logger changes

---
 modules/detect_target/detect_target_contour.py | 12 +++++++-----
 tests/unit/generate_detect_target_contour.py   |  1 -
 tests/unit/test_detect_target_contour.py       |  7 +++++--
 3 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index f9fb4192..3f4e86e4 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -40,7 +40,7 @@ def __init__(
             self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_"
 
     def detect_landing_pads_contours(
-        self, image: np.ndarray, timestamp: float
+        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.
@@ -50,6 +50,10 @@ def detect_landing_pads_contours(
 
         Return: Success, the DetectionsAndTime object, and the annotated image.
         """
+
+        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)
 
@@ -115,10 +119,8 @@ def run(
 
         Return: Success and the detections.
         """
-        image = data.image
-        timestamp = data.timestamp
 
-        result, detections, image_annotated = self.detect_landing_pads_contours(image, timestamp)
+        result, detections, image_annotated = self.detect_landing_pads_contours(data)
 
         if not result:
             return False, None
@@ -126,7 +128,7 @@ def run(
         # Logging
         if self.__filename_prefix != "":
             filename = self.__filename_prefix + str(self.__counter)
-            self.__logger.save_image(image, filename)
+            self.__logger.save_image(image_annotated, filename)
             self.__counter += 1
 
         if self.__show_annotations:
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index f2031386..9d64ff4b 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -17,7 +17,6 @@
 
 
 class LandingPadImageConfig:
-
     def __init__(
         self,
         center: tuple[int, int],
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 934847ca..92e54cb9 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -21,8 +21,6 @@
 CONFIDENCE_PRECISION_TOLERANCE = 2
 LOGGER_NAME = ""
 
-_, test_logger = logger.Logger.create(LOGGER_NAME, False)
-
 # Test functions use test fixture signature names and access class privates
 # No enable
 # pylint: disable=protected-access,redefined-outer-name
@@ -84,6 +82,11 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     """
     Construct DetectTargetContour.
     """
+    result, test_logger = logger.Logger.create(LOGGER_NAME, False)
+
+    assert result
+    assert test_logger is not None
+
     detection = detect_target_contour.DetectTargetContour(test_logger, False)
     yield detection  # type: ignore
 

From 59c8d9446c333ef5a580c36c83f0d6b92d5f28d4 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Sun, 16 Feb 2025 16:00:09 -0500
Subject: [PATCH 22/27] Xierumeng Changes

---
 .../detect_target/detect_target_contour.py    | 10 ++-
 tests/unit/generate_detect_target_contour.py  | 65 ++++++++++---------
 tests/unit/test_detect_target_contour.py      | 12 +++-
 3 files changed, 49 insertions(+), 38 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 3f4e86e4..1bb2cf67 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -12,10 +12,11 @@
 from .. import detections_and_time
 from ..common.modules.logger import logger
 
-CONFIDENCE = 1.0
+
 MIN_CONTOUR_AREA = 100
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
+CONFIDENCE = 1.0
 LABEL = 0
 
 
@@ -28,6 +29,7 @@ def __init__(
         self, image_logger: logger.Logger, show_annotations: bool = False, save_name: str = ""
     ) -> None:
         """
+        image_logger: Log annotated iamges.
         show_annotations: Display annotated images.
         save_name: filename prefix for logging detections and annotated images.
         """
@@ -50,7 +52,6 @@ def detect_landing_pads_contours(
 
         Return: Success, the DetectionsAndTime object, and the annotated image.
         """
-
         image = image_and_time_data.image
         timestamp = image_and_time_data.timestamp
 
@@ -62,15 +63,12 @@ def detect_landing_pads_contours(
         if len(contours) == 0:
             return False, None, None
 
-        # Create the DetectionsAndTime object
         result, detections = detections_and_time.DetectionsAndTime.create(timestamp)
         if not result:
             return False, None, None
 
-        # Ordered for the mapping to the corresponding detections
-        sorted_contour = sorted(contours, key=cv2.contourArea, reverse=True)
         image_annotated = image
-        for i, contour in enumerate(sorted_contour):
+        for i, contour in enumerate(contours):
             contour_area = cv2.contourArea(contour)
 
             if contour_area < MIN_CONTOUR_AREA:
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index 9d64ff4b..507a6635 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -17,6 +17,9 @@
 
 
 class LandingPadImageConfig:
+    """
+    Represents the data required to define and generate a landing pad.
+    """
     def __init__(
         self,
         center: tuple[int, int],
@@ -25,12 +28,12 @@ def __init__(
         angle: float,
     ):
         """
-        Represents the data required to define and generate a landing pad.
 
         center: The (x, y) coordinates representing the center of the landing pad.
         axis: The pixel lengths of the semi-major and semi-minor axes of the ellipse.
-        blur: Indicates whether the landing pad should have a blur effect. default: False.
-        angle: The rotation angle of the landing pad in degrees clockwise (0 < angle < 360).
+        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).
         """
         self.center = center
         self.axis = axis
@@ -39,36 +42,39 @@ def __init__(
 
 
 class NumpyImage:
+    """
+    Holds the Numpy Array which represents an image.
+    """
     def __init__(self, image: np.ndarray):
         """
-        Holds the Numpy Array which represents an image.
+        image: A numpy array that represents the image.
         """
         self.image = image
 
 
 class BoundingBox:
+    """
+    Holds the data that define the generated bounding boxes.
+    """
     def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
         """
-        Holds the data that define the generated bounding boxes.
-
-        Attributes:
-            top_left: A tuple of integers that represents top left corner of bounding box.
-            bottom_right: A tuple of integers that represents bottom right corner of bounding box.
+        top_left: x, y coordinates representing the top left corner of the bounding box on an image.
+        bottom_right: x, y coordinates representing the bottom right corner of the bounding box on an image.
         """
         self.top_left = top_left
         self.bottom_right = bottom_right
 
 
 class InputImageAndExpectedBoundingBoxes:
-    def __init__(self, image: NumpyImage, boxes_list: np.ndarray):
+    '''
+    Struct to hold the data needed to perform the tests.
+    '''
+    def __init__(self, image: np.ndarray, boxes_list: np.ndarray):
         """
-        Struct to hold the data needed to perform the tests.
-
-        Attributes:
-            image = A numpy array that represents the image needed to be tested
-            bounding_box_list: A numpy array that holds a list of expected bounding box coordinates
+        image = A numpy array that represents the image needed to be tested.
+        bounding_box_list: A numpy array that holds a list of expected bounding box coordinates.
         """
-        self.image = image.image
+        self.image = image
         self.bounding_box_list = boxes_list
 
 
@@ -76,10 +82,11 @@ def add_blurred_landing_pad(
     background: np.ndarray, landing_data: LandingPadImageConfig
 ) -> NumpyImage:
     """
-    Blurs an image a singular lading pad, adds it to the background.
+    Blurs an image and adds a singular lading pad to the background.
 
     background: A numpy image.
-    landing_data = The landing pad which is to be blurred.
+    landing_data: Landing pad data for the landing pad to be blurred and added.
+
 
     Returns: Image with the landing pad.
     """
@@ -100,7 +107,7 @@ def add_blurred_landing_pad(
     mask = cv2.blur(mask, (25, 25), 7)
 
     alpha = mask[:, :, np.newaxis] / 255.0
-    # Brings the image back to its original colour
+    # 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)
@@ -110,19 +117,18 @@ def add_blurred_landing_pad(
 def draw_landing_pad(
     image: np.ndarray, landing_data: LandingPadImageConfig
 ) -> tuple[NumpyImage, BoundingBox]:
-    print("asdasd")
-    print(image)
     """
     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.
 
     Returns: Image with landing pad and the bounding box for the drawn landing pad.
     """
     (h, k), (a, b) = landing_data.center, landing_data.axis
-    rad = math.pi / 180
-    ux, uy = a * math.cos(landing_data.angle * rad), a * math.sin(landing_data.angle * rad)
-    vx, vy = b * math.sin(landing_data.angle * rad), b * math.cos(landing_data.angle * rad)
+    angle_in_rad = math.radians(landing_data.angle)
+    ux, uy = a * math.cos(angle_in_rad), a * math.sin(angle_in_rad)
+    vx, vy = b * math.sin(angle_in_rad), b * math.cos(angle_in_rad)
     width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
 
     top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
@@ -154,34 +160,33 @@ def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpec
     """
     Generates test cases given a data set.
 
-    landing_data: Landing pad data for the landing pad to be added.
+    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.int16
-    )  # shape: size of the screen
+    )
     confidence_and_label = [1, 0]
 
+    # List to hold the bounding boxes.
     boxes_list = []
 
     for landing_data in landing_list:
-        print(image)
         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)
         )
 
+    # Calculates 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])),
-        # calculates the absolute value of area of the bounding box
     )
 
     boxes_list = np.array(boxes_list)
     image = image.astype(np.uint8)
 
-    return InputImageAndExpectedBoundingBoxes(NumpyImage(image), boxes_list)
+    return InputImageAndExpectedBoundingBoxes(image, boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 92e54cb9..413f7076 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -2,7 +2,7 @@
 Test Contour Detection module.
 
 """
-
+import cv2
 import numpy as np
 import pytest
 
@@ -194,9 +194,17 @@ def compare_detections(
 
     # Using integer indexing for both lists
     # pylint: disable-next=consider-using-enumerate
+
+    # Ordered for the mapping to the corresponding detections
+    sorted_actual_detections = sorted(
+        actual.detections,
+        reverse=True,
+        key=lambda box: abs((box.x_1 - box.x_2) * (box.y_1 - box.y_2)),
+    )
+
     for i in range(0, len(expected.detections)):
         expected_detection = expected.detections[i]
-        actual_detection = actual.detections[i]
+        actual_detection = sorted_actual_detections[i]
 
         assert expected_detection.label == actual_detection.label
         np.testing.assert_almost_equal(

From 8ce749c0f2e643978431ba9a956cc56ab39da99f Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Thu, 6 Mar 2025 00:42:14 -0500
Subject: [PATCH 23/27] Most changes made

---
 .../detect_target/detect_target_contour.py    |   6 +-
 .../detect_target/detect_target_factory.py    |   5 +-
 tests/unit/generate_detect_target_contour.py  | 104 ++++++++++++------
 tests/unit/test_detect_target_contour.py      |  82 ++++++++------
 4 files changed, 124 insertions(+), 73 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 1bb2cf67..32814ffb 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -14,6 +14,8 @@
 
 
 MIN_CONTOUR_AREA = 100
+MAX_CIRCULARITY = 1.3
+MIN_CIRCULARITY = 0.7
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
 CONFIDENCE = 1.0
@@ -29,7 +31,7 @@ def __init__(
         self, image_logger: logger.Logger, show_annotations: bool = False, save_name: str = ""
     ) -> None:
         """
-        image_logger: Log annotated iamges.
+        image_logger: Log annotated images.
         show_annotations: Display annotated images.
         save_name: filename prefix for logging detections and annotated images.
         """
@@ -79,7 +81,7 @@ def detect_landing_pads_contours(
             enclosing_area = np.pi * (radius**2)
             circularity = contour_area / enclosing_area
 
-            if circularity < 0.7 or circularity > 1.3:
+            if circularity < MIN_CIRCULARITY or circularity > MAX_CIRCULARITY:
                 continue
 
             x, y, w, h = cv2.boundingRect(contour)
diff --git a/modules/detect_target/detect_target_factory.py b/modules/detect_target/detect_target_factory.py
index a7a9cfab..b241cdaa 100644
--- a/modules/detect_target/detect_target_factory.py
+++ b/modules/detect_target/detect_target_factory.py
@@ -18,7 +18,7 @@ class DetectTargetOption(enum.Enum):
 
     ML_ULTRALYTICS = 0
     CV_BRIGHTSPOT = 1
-    C_CONTOUR = 2
+    CV_CONTOUR = 2
 
 
 def create_detect_target(
@@ -49,8 +49,9 @@ def create_detect_target(
                 show_annotations,
                 save_name,
             )
-        case DetectTargetOption.C_CONTOUR:
+        case DetectTargetOption.CV_CONTOUR:
             return True, detect_target_contour.DetectTargetContour(
+                local_logger,
                 show_annotations,
                 save_name,
             )
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index 507a6635..6a097862 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -7,35 +7,32 @@
 import math
 import numpy as np
 
-
-LANDING_PAD_COLOUR_BLUE = (100, 50, 50)  # BGR
+from modules import detections_and_time
 
 
-# Test functions use test fixture signature names and access class privates
-# No enable
-# pylint: disable=protected-access,redefined-outer-name
+LANDING_PAD_COLOUR_BLUE = (100, 50, 50)  # BGR
 
 
 class LandingPadImageConfig:
     """
     Represents the data required to define and generate a landing pad.
     """
+
     def __init__(
         self,
-        center: tuple[int, int],
+        centre: tuple[int, int],
         axis: tuple[int, int],
         blur: bool,
         angle: float,
     ):
         """
-
-        center: The (x, y) coordinates representing the center of the landing pad.
-        axis: The pixel lengths of the semi-major and semi-minor axes of the ellipse.
+        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).
         """
-        self.center = center
+        self.centre = centre
         self.axis = axis
         self.blur = blur
         self.angle = angle
@@ -43,8 +40,9 @@ def __init__(
 
 class NumpyImage:
     """
-    Holds the Numpy Array which represents an image.
+    Holds the numpy array which represents an image.
     """
+
     def __init__(self, image: np.ndarray):
         """
         image: A numpy array that represents the image.
@@ -56,38 +54,70 @@ class BoundingBox:
     """
     Holds the data that define the generated bounding boxes.
     """
-    def __init__(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
+
+    def __init__(self, top_left: tuple[float, float], bottom_right: tuple[float, float]):
         """
-        top_left: x, y coordinates representing the top left corner of the bounding box on an image.
-        bottom_right: x, y coordinates representing the bottom right corner of the bounding box on an image.
+        top_left: 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 InputImageAndExpectedBoundingBoxes:
-    '''
+    """
     Struct to hold the data needed to perform the tests.
-    '''
+    """
+
     def __init__(self, image: np.ndarray, boxes_list: np.ndarray):
         """
-        image = A numpy array that represents the image needed to be tested.
+        image: A numpy array that represents the image needed to be tested.
         bounding_box_list: A numpy array that holds a list of expected bounding box coordinates.
+
+        The bounding box coordinates are in the following format:
+            top_left_x = boxes_list[0]
+            top_left_y = boxes_list[1]
+
+            bottom_right_x = boxes_list[2]
+            bottom_right_x = boxes_list[3]
         """
         self.image = image
         self.bounding_box_list = boxes_list
 
 
+def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime:
+    """
+    Create DetectionsAndTime from expected.
+    Format: [confidence, label, x_1, y_1, x_2, y_2] .
+    """
+    assert detections_from_file.shape[1] == 6
+
+    result, detections = detections_and_time.DetectionsAndTime.create(0)
+    assert result
+    assert detections is not None
+
+    for i in range(0, detections_from_file.shape[0]):
+        result, detection = detections_and_time.Detection.create(
+            detections_from_file[i][2:],
+            int(detections_from_file[i][1]),
+            detections_from_file[i][0],
+        )
+        assert result
+        assert detection is not None
+        detections.append(detection)
+
+    return detections
+
+
 def add_blurred_landing_pad(
     background: np.ndarray, landing_data: LandingPadImageConfig
 ) -> NumpyImage:
     """
-    Blurs an image and adds a singular lading pad to the background.
+    Blurs an image and adds a singular landing pad to the background.
 
     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]
@@ -95,7 +125,7 @@ def add_blurred_landing_pad(
     mask = np.zeros((x, y), np.uint8)
     mask = cv2.ellipse(
         mask,
-        landing_data.center,
+        landing_data.centre,
         landing_data.axis,
         landing_data.angle,
         0,
@@ -125,16 +155,23 @@ def draw_landing_pad(
 
     Returns: Image with landing pad and the bounding box for the drawn landing pad.
     """
-    (h, k), (a, b) = landing_data.center, landing_data.axis
+    centre_x, centre_y = landing_data.centre
+    axis_x, axis_y = landing_data.axis
     angle_in_rad = math.radians(landing_data.angle)
-    ux, uy = a * math.cos(angle_in_rad), a * math.sin(angle_in_rad)
-    vx, vy = b * math.sin(angle_in_rad), b * math.cos(angle_in_rad)
-    width, height = 2 * math.sqrt(ux**2 + vx**2), 2 * math.sqrt(uy**2 + vy**2)
 
-    top_left = (int(max(h - (0.5) * width, 0)), int(max(k - (0.5) * height, 0)))
+    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 = (int(max(centre_x - (0.5) * width, 0)), int(max(centre_y - (0.5) * height, 0)))
     bottom_right = (
-        int(min(h + (0.5) * width, image.shape[1])),
-        int(min(k + (0.5) * height, image.shape[0])),
+        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)
@@ -145,7 +182,7 @@ def draw_landing_pad(
 
     image = cv2.ellipse(
         image,
-        landing_data.center,
+        landing_data.centre,
         landing_data.axis,
         landing_data.angle,
         0,
@@ -164,12 +201,11 @@ def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpec
 
     Returns: The image and expected bounding box.
     """
-    image = np.full(
-        shape=(1000, 2000, 3), fill_value=255, dtype=np.int16
-    )
+    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int8)
     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]
     boxes_list = []
 
     for landing_data in landing_list:
@@ -179,11 +215,9 @@ def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpec
             confidence_and_label + list(bounding_box.top_left + bounding_box.bottom_right)
         )
 
-    # Calculates the area of the bounding box.
+    # 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])),
+        boxes_list, reverse=True, key=lambda box: abs((box[4] - box[2]) * (box[5] - box[3]))
     )
 
     boxes_list = np.array(boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 413f7076..95a299ad 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -1,79 +1,93 @@
 """
-Test Contour Detection module.
-
+Test contour detection module.
 """
-import cv2
+
 import numpy as np
 import pytest
 
-from tests.unit.generate_detect_target_contour import (
-    LandingPadImageConfig,
-    InputImageAndExpectedBoundingBoxes,
-    create_test,
-)
+from tests.unit import generate_detect_target_contour
 from modules import detections_and_time
 from modules import image_and_time
 from modules.detect_target import detect_target_contour
 from modules.common.modules.logger import logger  # Changed from relative to absolute import
 
 
-BOUNDING_BOX_PRECISION_TOLERANCE = -2 / 3  # Tolerance > 1
+BOUNDING_BOX_PRECISION_TOLERANCE = -1  # Tolerance > 1
 CONFIDENCE_PRECISION_TOLERANCE = 2
 LOGGER_NAME = ""
 
+
 # Test functions use test fixture signature names and access class privates
 # No enable
 # pylint: disable=protected-access,redefined-outer-name
 
 
 @pytest.fixture
-def single_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
+def single_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single basic circle.
     """
-    options = [LandingPadImageConfig(center=(300, 400), axis=(200, 200), blur=False, angle=0)]
+    options = [
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(300, 400), axis=(200, 200), blur=False, angle=0
+        )
+    ]
 
-    test_data = create_test(options)
+    test_data = generate_detect_target_contour.create_test(options)
     yield test_data
 
 
 @pytest.fixture
-def single_blurry_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
+def single_blurry_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single blury circle.
     """
     options = [
-        LandingPadImageConfig(center=(1000, 500), axis=(423, 423), blur=True, angle=0),
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(1000, 500), axis=(423, 423), blur=True, angle=0
+        ),
     ]
 
-    test_data = create_test(options)
+    test_data = generate_detect_target_contour.create_test(options)
     yield test_data
 
 
 @pytest.fixture
-def single_stretched_circle() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
+def single_stretched_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the single stretched circle.
     """
-    options = [LandingPadImageConfig(center=(1000, 500), axis=(383, 405), blur=False, angle=0)]
+    options = [
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(1000, 500), axis=(383, 405), blur=False, angle=0
+        )
+    ]
 
-    test_data = create_test(options)
+    test_data = generate_detect_target_contour.create_test(options)
     yield test_data
 
 
 @pytest.fixture
-def multiple_circles() -> InputImageAndExpectedBoundingBoxes:  # type: ignore
+def multiple_circles() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
     """
     Loads the data for the multiple stretched circles.
     """
     options = [
-        LandingPadImageConfig(center=(997, 600), axis=(300, 300), blur=False, angle=0),
-        LandingPadImageConfig(center=(1590, 341), axis=(250, 250), blur=False, angle=0),
-        LandingPadImageConfig(center=(200, 500), axis=(50, 45), blur=True, angle=0),
-        LandingPadImageConfig(center=(401, 307), axis=(200, 150), blur=True, angle=0),
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(997, 600), axis=(300, 300), blur=False, angle=0
+        ),
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(1590, 341), axis=(250, 250), blur=False, angle=0
+        ),
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(200, 500), axis=(50, 45), blur=True, angle=0
+        ),
+        generate_detect_target_contour.LandingPadImageConfig(
+            centre=(401, 307), axis=(200, 150), blur=True, angle=0
+        ),
     ]
 
-    test_data = create_test(options)
+    test_data = generate_detect_target_contour.create_test(options)
     yield test_data
 
 
@@ -92,7 +106,7 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
 
 
 @pytest.fixture()
-def image_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def image_easy(single_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single basic landing pad.
     """
@@ -105,7 +119,7 @@ def image_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_and_t
 
 
 @pytest.fixture()
-def blurry_image(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def blurry_image(single_blurry_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single blurry landing pad.
     """
@@ -118,7 +132,7 @@ def blurry_image(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> im
 
 
 @pytest.fixture()
-def stretched_image(single_stretched_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def stretched_image(single_stretched_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the single stretched landing pad.
     """
@@ -131,7 +145,7 @@ def stretched_image(single_stretched_circle: InputImageAndExpectedBoundingBoxes)
 
 
 @pytest.fixture()
-def multiple_images(multiple_circles: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def multiple_images(multiple_circles: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load the multiple landing pads.
     """
@@ -144,7 +158,7 @@ def multiple_images(multiple_circles: InputImageAndExpectedBoundingBoxes) -> ima
 
 
 @pytest.fixture()
-def expected_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_easy(single_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected a basic image detections.
     """
@@ -154,7 +168,7 @@ def expected_easy(single_circle: InputImageAndExpectedBoundingBoxes) -> image_an
 
 
 @pytest.fixture()
-def expected_blur(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_blur(single_blurry_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected the blured pad image detections.
     """
@@ -164,7 +178,7 @@ def expected_blur(single_blurry_circle: InputImageAndExpectedBoundingBoxes) -> i
 
 
 @pytest.fixture()
-def expected_stretch(single_stretched_circle: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_stretch(single_stretched_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected a stretched pad image detections.
     """
@@ -174,7 +188,7 @@ def expected_stretch(single_stretched_circle: InputImageAndExpectedBoundingBoxes
 
 
 @pytest.fixture()
-def expected_multiple(multiple_circles: InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
+def expected_multiple(multiple_circles: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
     """
     Load expected multiple pads image detections.
     """
@@ -274,7 +288,7 @@ def test_single_circle(
         expected_easy: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Run the detection for the single landing pad.
+        Run the detection for the single cirucluar landing pad.
         """
         # Run
         result, actual = detector.run(image_easy)
@@ -292,7 +306,7 @@ def test_blurry_circle(
         expected_blur: detections_and_time.DetectionsAndTime,
     ) -> None:
         """
-        Run the detection for the blury circle.
+        Run the detection for the blury cicular circle.
         """
         # Run
         result, actual = detector.run(blurry_image)

From d423026ba31d5b209b48f8e63d45917d388c3283 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Mon, 17 Mar 2025 21:59:35 -0400
Subject: [PATCH 24/27] Refactored + Rebase

---
 .../detect_target/detect_target_contour.py    |   6 +-
 tests/unit/generate_detect_target_contour.py  |  39 ++--
 tests/unit/test_detect_target_contour.py      | 197 ++++--------------
 3 files changed, 59 insertions(+), 183 deletions(-)

diff --git a/modules/detect_target/detect_target_contour.py b/modules/detect_target/detect_target_contour.py
index 32814ffb..c283a69a 100644
--- a/modules/detect_target/detect_target_contour.py
+++ b/modules/detect_target/detect_target_contour.py
@@ -16,15 +16,17 @@
 MIN_CONTOUR_AREA = 100
 MAX_CIRCULARITY = 1.3
 MIN_CIRCULARITY = 0.7
+
 UPPER_BLUE = np.array([130, 255, 255])
 LOWER_BLUE = np.array([100, 50, 50])
+
 CONFIDENCE = 1.0
 LABEL = 0
 
 
 class DetectTargetContour(base_detect_target.BaseDetectTarget):
     """
-    Predicts annd locates landing pads using the classical computer vision methodology.
+    Predicts and locates landing pads using the classical computer vision methodology.
     """
 
     def __init__(
@@ -49,7 +51,7 @@ def detect_landing_pads_contours(
         """
         Detects landing pads using contours/classical CV.
 
-        image: Current image frame.
+        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.
diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index 6a097862..9f84e9ec 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -8,6 +8,7 @@
 import numpy as np
 
 from modules import detections_and_time
+from modules import image_and_time
 
 
 LANDING_PAD_COLOUR_BLUE = (100, 50, 50)  # BGR
@@ -57,32 +58,27 @@ class BoundingBox:
 
     def __init__(self, top_left: tuple[float, float], bottom_right: tuple[float, float]):
         """
-        top_left: pixel coordinates representing the top left corner of the bounding box on an image.
+        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 InputImageAndExpectedBoundingBoxes:
+class InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Struct to hold the data needed to perform the tests.
     """
 
-    def __init__(self, image: np.ndarray, boxes_list: np.ndarray):
+    def __init__(self, image_and_time_data: image_and_time.ImageAndTime, bounding_box_list: list):
         """
-        image: A numpy array that represents the image needed to be tested.
-        bounding_box_list: A numpy array that holds a list of expected bounding box coordinates.
-
-        The bounding box coordinates are in the following format:
-            top_left_x = boxes_list[0]
-            top_left_y = boxes_list[1]
-
-            bottom_right_x = boxes_list[2]
-            bottom_right_x = boxes_list[3]
+        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 = image
-        self.bounding_box_list = boxes_list
+        self.image_and_time_data = image_and_time_data
+        self.bounding_box_list = bounding_box_list
 
 
 def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime:
@@ -168,7 +164,7 @@ def draw_landing_pad(
     width = 2 * math.sqrt(ux**2 + vx**2)
     height = 2 * math.sqrt(uy**2 + vy**2)
 
-    top_left = (int(max(centre_x - (0.5) * width, 0)), int(max(centre_y - (0.5) * height, 0)))
+    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]),
@@ -193,7 +189,9 @@ def draw_landing_pad(
     return NumpyImage(image), bounding_box
 
 
-def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpectedBoundingBoxes:
+def create_test(
+    landing_list: list[LandingPadImageConfig],
+) -> InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Generates test cases given a data set.
 
@@ -201,7 +199,7 @@ def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpec
 
     Returns: The image and expected bounding box.
     """
-    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.int8)
+    image = np.full(shape=(1000, 2000, 3), fill_value=255, dtype=np.uint8)
     confidence_and_label = [1, 0]
 
     # List to hold the bounding boxes.
@@ -220,7 +218,10 @@ def create_test(landing_list: list[LandingPadImageConfig]) -> InputImageAndExpec
         boxes_list, reverse=True, key=lambda box: abs((box[4] - box[2]) * (box[5] - box[3]))
     )
 
-    boxes_list = np.array(boxes_list)
     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 InputImageAndExpectedBoundingBoxes(image, boxes_list)
+    return InputImageAndTimeAndExpectedBoundingBoxes(image_and_time_data, boxes_list)
diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 95a299ad..cc093cc5 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -5,12 +5,10 @@
 import numpy as np
 import pytest
 
-from tests.unit import generate_detect_target_contour
 from modules import detections_and_time
-from modules import image_and_time
 from modules.detect_target import detect_target_contour
-from modules.common.modules.logger import logger  # Changed from relative to absolute import
-
+from modules.common.modules.logger import logger
+from tests.unit import generate_detect_target_contour
 
 BOUNDING_BOX_PRECISION_TOLERANCE = -1  # Tolerance > 1
 CONFIDENCE_PRECISION_TOLERANCE = 2
@@ -18,12 +16,10 @@
 
 
 # Test functions use test fixture signature names and access class privates
-# No enable
-# pylint: disable=protected-access,redefined-outer-name
-
+# pylint: disable=protected-access,redefined-outer-name, duplicate-code
 
 @pytest.fixture
-def single_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
+def single_circle() -> generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Loads the data for the single basic circle.
     """
@@ -38,7 +34,7 @@ def single_circle() -> generate_detect_target_contour.InputImageAndExpectedBound
 
 
 @pytest.fixture
-def single_blurry_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
+def blurry_circle() -> generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Loads the data for the single blury circle.
     """
@@ -53,7 +49,7 @@ def single_blurry_circle() -> generate_detect_target_contour.InputImageAndExpect
 
 
 @pytest.fixture
-def single_stretched_circle() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
+def stretched_circle() -> generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Loads the data for the single stretched circle.
     """
@@ -68,7 +64,7 @@ def single_stretched_circle() -> generate_detect_target_contour.InputImageAndExp
 
 
 @pytest.fixture
-def multiple_circles() -> generate_detect_target_contour.InputImageAndExpectedBoundingBoxes:  # type: ignore
+def multiple_circles() -> generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes:
     """
     Loads the data for the multiple stretched circles.
     """
@@ -105,177 +101,58 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
     yield detection  # type: ignore
 
 
-@pytest.fixture()
-def image_easy(single_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load the single basic landing pad.
-    """
-
-    image = single_circle.image
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def blurry_image(single_blurry_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load the single blurry landing pad.
-    """
-
-    image = single_blurry_circle.image
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def stretched_image(single_stretched_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load the single stretched landing pad.
-    """
-
-    image = single_stretched_circle.image
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def multiple_images(multiple_circles: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load the multiple landing pads.
-    """
-
-    image = multiple_circles.image
-    result, actual_image = image_and_time.ImageAndTime.create(image)
-    assert result
-    assert actual_image is not None
-    yield actual_image  # type: ignore
-
-
-@pytest.fixture()
-def expected_easy(single_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected a basic image detections.
-    """
-
-    expected = single_circle.bounding_box_list
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_blur(single_blurry_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected the blured pad image detections.
-    """
-
-    expected = single_blurry_circle.bounding_box_list
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_stretch(single_stretched_circle: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected a stretched pad image detections.
-    """
-
-    expected = single_stretched_circle.bounding_box_list
-    yield create_detections(expected)  # type: ignore
-
-
-@pytest.fixture()
-def expected_multiple(multiple_circles: generate_detect_target_contour.InputImageAndExpectedBoundingBoxes) -> image_and_time.ImageAndTime:  # type: ignore
-    """
-    Load expected multiple pads image detections.
-    """
-
-    expected = multiple_circles.bounding_box_list
-    yield create_detections(expected)  # type: ignore
-
-
-# pylint:disable=duplicate-code
 def compare_detections(
-    actual: detections_and_time.DetectionsAndTime, expected: detections_and_time.DetectionsAndTime
+    actual: list[detections_and_time.Detection], expected: list[list[float]]
 ) -> None:
     """
     Compare expected and actual detections.
     """
-    assert len(actual.detections) == len(expected.detections)
-
-    # Using integer indexing for both lists
-    # pylint: disable-next=consider-using-enumerate
+    assert len(actual) == len(expected)
 
     # Ordered for the mapping to the corresponding detections
     sorted_actual_detections = sorted(
-        actual.detections,
+        actual,
         reverse=True,
-        key=lambda box: abs((box.x_1 - box.x_2) * (box.y_1 - box.y_2)),
+        key=lambda box: abs((box.x_2 - box.x_1) * (box.y_2 - box.y_1)),
     )
 
-    for i in range(0, len(expected.detections)):
-        expected_detection = expected.detections[i]
+    for i, expected_detection in enumerate(expected):
         actual_detection = sorted_actual_detections[i]
 
-        assert expected_detection.label == actual_detection.label
+        # Check label and confidence
+        assert actual_detection.label == expected_detection[1]
         np.testing.assert_almost_equal(
-            expected_detection.confidence,
             actual_detection.confidence,
+            expected_detection[0],
             decimal=CONFIDENCE_PRECISION_TOLERANCE,
         )
 
+        # Check bounding box coordinates
         np.testing.assert_almost_equal(
             actual_detection.x_1,
-            expected_detection.x_1,
+            expected_detection[2],
             decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
         )
 
         np.testing.assert_almost_equal(
             actual_detection.y_1,
-            expected_detection.y_1,
+            expected_detection[3],
             decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
         )
 
         np.testing.assert_almost_equal(
             actual_detection.x_2,
-            expected_detection.x_2,
+            expected_detection[4],
             decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
         )
 
         np.testing.assert_almost_equal(
             actual_detection.y_2,
-            expected_detection.y_2,
+            expected_detection[5],
             decimal=BOUNDING_BOX_PRECISION_TOLERANCE,
         )
 
 
-def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime:
-    """
-    Create DetectionsAndTime from expected.
-    Format: [confidence, label, x_1, y_1, x_2, y_2] .
-    """
-    assert detections_from_file.shape[1] == 6
-
-    result, detections = detections_and_time.DetectionsAndTime.create(0)
-    assert result
-    assert detections is not None
-
-    for i in range(0, detections_from_file.shape[0]):
-        result, detection = detections_and_time.Detection.create(
-            detections_from_file[i][2:],
-            int(detections_from_file[i][1]),
-            detections_from_file[i][0],
-        )
-        assert result
-        assert detection is not None
-        detections.append(detection)
-
-    return detections
-
-
 class TestDetector:
     """
     Tests `DetectTarget.run()` .
@@ -284,71 +161,67 @@ class TestDetector:
     def test_single_circle(
         self,
         detector: detect_target_contour.DetectTargetContour,
-        image_easy: image_and_time.ImageAndTime,
-        expected_easy: detections_and_time.DetectionsAndTime,
+        single_circle: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
     ) -> None:
         """
         Run the detection for the single cirucluar landing pad.
         """
         # Run
-        result, actual = detector.run(image_easy)
+        result, actual = detector.run(single_circle.image_and_time_data)
 
         # Test
         assert result
         assert actual is not None
 
-        compare_detections(actual, expected_easy)
+        compare_detections(actual.detections, single_circle.bounding_box_list)
 
     def test_blurry_circle(
         self,
         detector: detect_target_contour.DetectTargetContour,
-        blurry_image: image_and_time.ImageAndTime,
-        expected_blur: detections_and_time.DetectionsAndTime,
+        blurry_circle: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
     ) -> None:
         """
         Run the detection for the blury cicular circle.
         """
         # Run
-        result, actual = detector.run(blurry_image)
+        result, actual = detector.run(blurry_circle.image_and_time_data)
 
         # Test
         assert result
         assert actual is not None
 
-        compare_detections(actual, expected_blur)
+        compare_detections(actual.detections, blurry_circle.bounding_box_list)
 
-    def test_stretch(
+    def test_stretched_circle(
         self,
         detector: detect_target_contour.DetectTargetContour,
-        stretched_image: image_and_time.ImageAndTime,
-        expected_stretch: detections_and_time.DetectionsAndTime,
+        stretched_circle: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
     ) -> None:
         """
-        Run the detection for the single stretched landing pad.
+        Run the detection for a single stretched circular landing pad.
         """
         # Run
-        result, actual = detector.run(stretched_image)
+        result, actual = detector.run(stretched_circle.image_and_time_data)
 
         # Test
         assert result
         assert actual is not None
 
-        compare_detections(actual, expected_stretch)
+        compare_detections(actual.detections, stretched_circle.bounding_box_list)
 
-    def test_multiple(
+    def test_multiple_circles(
         self,
         detector: detect_target_contour.DetectTargetContour,
-        multiple_images: image_and_time.ImageAndTime,
-        expected_multiple: detections_and_time.DetectionsAndTime,
+        multiple_circles: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
     ) -> None:
         """
         Run the detection for the multiple landing pads.
         """
         # Run
-        result, actual = detector.run(multiple_images)
+        result, actual = detector.run(multiple_circles.image_and_time_data)
 
         # Test
         assert result
         assert actual is not None
 
-        compare_detections(actual, expected_multiple)
+        compare_detections(actual.detections, multiple_circles.bounding_box_list)

From 4b8aa3c3a360ff847c9b315e24c6f8a73c8ca839 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Mon, 17 Mar 2025 22:00:24 -0400
Subject: [PATCH 25/27] Removed create_detections

---
 tests/unit/generate_detect_target_contour.py | 24 --------------------
 1 file changed, 24 deletions(-)

diff --git a/tests/unit/generate_detect_target_contour.py b/tests/unit/generate_detect_target_contour.py
index 9f84e9ec..c1677f4a 100644
--- a/tests/unit/generate_detect_target_contour.py
+++ b/tests/unit/generate_detect_target_contour.py
@@ -81,30 +81,6 @@ def __init__(self, image_and_time_data: image_and_time.ImageAndTime, bounding_bo
         self.bounding_box_list = bounding_box_list
 
 
-def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime:
-    """
-    Create DetectionsAndTime from expected.
-    Format: [confidence, label, x_1, y_1, x_2, y_2] .
-    """
-    assert detections_from_file.shape[1] == 6
-
-    result, detections = detections_and_time.DetectionsAndTime.create(0)
-    assert result
-    assert detections is not None
-
-    for i in range(0, detections_from_file.shape[0]):
-        result, detection = detections_and_time.Detection.create(
-            detections_from_file[i][2:],
-            int(detections_from_file[i][1]),
-            detections_from_file[i][0],
-        )
-        assert result
-        assert detection is not None
-        detections.append(detection)
-
-    return detections
-
-
 def add_blurred_landing_pad(
     background: np.ndarray, landing_data: LandingPadImageConfig
 ) -> NumpyImage:

From cc24a0471d1908b02eeb0d86d61bdcd9389d458b Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Mon, 17 Mar 2025 22:13:42 -0400
Subject: [PATCH 26/27] Updated compare_detections

---
 tests/unit/test_detect_target_contour.py | 24 +++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index cc093cc5..14c71ba6 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -102,11 +102,16 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
 
 
 def compare_detections(
-    actual: list[detections_and_time.Detection], expected: list[list[float]]
+    actual_and_expected_detections: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes
 ) -> None:
     """
     Compare expected and actual detections.
+    
+    actual_and_expected_detections: Test data containing both actual image and time and expected bounding boxes.
     """
+    actual = actual_and_expected_detections.image_and_time_data.detector  
+    expected = actual_and_expected_detections.bounding_box_list
+
     assert len(actual) == len(expected)
 
     # Ordered for the mapping to the corresponding detections
@@ -119,7 +124,7 @@ def compare_detections(
     for i, expected_detection in enumerate(expected):
         actual_detection = sorted_actual_detections[i]
 
-        # Check label and confidence
+        # Check label and confidence 
         assert actual_detection.label == expected_detection[1]
         np.testing.assert_almost_equal(
             actual_detection.confidence,
@@ -164,7 +169,7 @@ def test_single_circle(
         single_circle: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
     ) -> None:
         """
-        Run the detection for the single cirucluar landing pad.
+        Run the detection for the single circular landing pad.
         """
         # Run
         result, actual = detector.run(single_circle.image_and_time_data)
@@ -173,7 +178,12 @@ def test_single_circle(
         assert result
         assert actual is not None
 
-        compare_detections(actual.detections, single_circle.bounding_box_list)
+        # Create new object with actual detections
+        test_data = generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes(
+            actual,
+            single_circle.bounding_box_list
+        )
+        compare_detections(test_data)
 
     def test_blurry_circle(
         self,
@@ -190,7 +200,7 @@ def test_blurry_circle(
         assert result
         assert actual is not None
 
-        compare_detections(actual.detections, blurry_circle.bounding_box_list)
+        compare_detections(blurry_circle)
 
     def test_stretched_circle(
         self,
@@ -207,7 +217,7 @@ def test_stretched_circle(
         assert result
         assert actual is not None
 
-        compare_detections(actual.detections, stretched_circle.bounding_box_list)
+        compare_detections(stretched_circle)
 
     def test_multiple_circles(
         self,
@@ -224,4 +234,4 @@ def test_multiple_circles(
         assert result
         assert actual is not None
 
-        compare_detections(actual.detections, multiple_circles.bounding_box_list)
+        compare_detections(multiple_circles.bounding_box_list)

From e52227943ef8fd0093baf5f17cad8a05d3957ac1 Mon Sep 17 00:00:00 2001
From: Achita <ssgssachita@gmail.com>
Date: Mon, 17 Mar 2025 22:18:47 -0400
Subject: [PATCH 27/27] black . space??

---
 tests/unit/test_detect_target_contour.py | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/tests/unit/test_detect_target_contour.py b/tests/unit/test_detect_target_contour.py
index 14c71ba6..3f0c7d39 100644
--- a/tests/unit/test_detect_target_contour.py
+++ b/tests/unit/test_detect_target_contour.py
@@ -18,6 +18,7 @@
 # Test functions use test fixture signature names and access class privates
 # pylint: disable=protected-access,redefined-outer-name, duplicate-code
 
+
 @pytest.fixture
 def single_circle() -> generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes:
     """
@@ -102,14 +103,14 @@ def detector() -> detect_target_contour.DetectTargetContour:  # type: ignore
 
 
 def compare_detections(
-    actual_and_expected_detections: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes
+    actual_and_expected_detections: generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes,
 ) -> None:
     """
     Compare expected and actual detections.
-    
+
     actual_and_expected_detections: Test data containing both actual image and time and expected bounding boxes.
     """
-    actual = actual_and_expected_detections.image_and_time_data.detector  
+    actual = actual_and_expected_detections.image_and_time_data.detector
     expected = actual_and_expected_detections.bounding_box_list
 
     assert len(actual) == len(expected)
@@ -124,7 +125,7 @@ def compare_detections(
     for i, expected_detection in enumerate(expected):
         actual_detection = sorted_actual_detections[i]
 
-        # Check label and confidence 
+        # Check label and confidence
         assert actual_detection.label == expected_detection[1]
         np.testing.assert_almost_equal(
             actual_detection.confidence,
@@ -180,8 +181,7 @@ def test_single_circle(
 
         # Create new object with actual detections
         test_data = generate_detect_target_contour.InputImageAndTimeAndExpectedBoundingBoxes(
-            actual,
-            single_circle.bounding_box_list
+            actual, single_circle.bounding_box_list
         )
         compare_detections(test_data)