diff --git a/config.yaml b/config.yaml index ce511130..6ca2b687 100644 --- a/config.yaml +++ b/config.yaml @@ -27,6 +27,23 @@ detect_target: model_path: "tests/model_example/yolov8s_ultralytics_pretrained_default.pt" # See autonomy OneDrive for latest model save_prefix: "log_comp" +detect_brightspot: + brightspot_percentile_threshold: 99.9 + filter_by_color: True + blob_color: 255 + filter_by_circularity: False + min_circularity: 0.01 + max_circularity: 1 + filter_by_inertia: True + min_inertia_ratio: 0.2 + max_inertia_ratio: 1 + filter_by_convexity: False + min_convexity: 0.01 + max_convexity: 1 + filter_by_area: True + min_area_pixels: 50 + max_area_pixels: 640 + flight_interface: # Port 5762 connects directly to the simulated auto pilot, which is more realistic # than connecting to port 14550, which is the ground station @@ -53,6 +70,7 @@ geolocation: cluster_estimation: min_activation_threshold: 25 min_new_points_to_run: 5 + max_num_components: 10 random_state: 0 communications: diff --git a/main_2024.py b/main_2024.py index 7ce84c27..f927f220 100644 --- a/main_2024.py +++ b/main_2024.py @@ -137,6 +137,7 @@ def main() -> int: MIN_ACTIVATION_THRESHOLD = config["cluster_estimation"]["min_activation_threshold"] MIN_NEW_POINTS_TO_RUN = config["cluster_estimation"]["min_new_points_to_run"] + MAX_NUM_COMPONENTS = config["cluster_estimation"]["max_num_components"] RANDOM_STATE = config["cluster_estimation"]["random_state"] COMMUNICATIONS_TIMEOUT = config["communications"]["timeout"] @@ -327,7 +328,12 @@ def main() -> int: result, cluster_estimation_worker_properties = worker_manager.WorkerProperties.create( count=1, target=cluster_estimation_worker.cluster_estimation_worker, - work_arguments=(MIN_ACTIVATION_THRESHOLD, MIN_NEW_POINTS_TO_RUN, RANDOM_STATE), + work_arguments=( + MIN_ACTIVATION_THRESHOLD, + MIN_NEW_POINTS_TO_RUN, + MAX_NUM_COMPONENTS, + RANDOM_STATE, + ), input_queues=[geolocation_to_cluster_estimation_queue], output_queues=[cluster_estimation_to_communications_queue], controller=controller, diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index ae7ff0b3..10c7bb91 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -20,17 +20,6 @@ class ClusterEstimation: works by predicting 'cluster centres' from groups of closely placed landing pad detections. - ATTRIBUTES - ---------- - min_activation_threshold: int - Minimum total data points before model runs. - - min_new_points_to_run: int - Minimum number of new data points that must be collected before running model. - - random_state: int - Seed for randomizer, to get consistent results. - METHODS ------- run() @@ -62,9 +51,6 @@ class ClusterEstimation: __MEAN_PRECISION_PRIOR = 1e-6 __MAX_MODEL_ITERATIONS = 1000 - # Real-world scenario Hyperparameters - __MAX_NUM_COMPONENTS = 10 # assumed maximum number of real landing pads - # Hyperparameters to clean up model outputs __WEIGHT_DROP_THRESHOLD = 0.1 __MAX_COVARIANCE_THRESHOLD = 10 @@ -74,24 +60,48 @@ def create( cls, min_activation_threshold: int, min_new_points_to_run: int, + max_num_components: int, random_state: int, local_logger: logger.Logger, ) -> "tuple[bool, ClusterEstimation | None]": """ Data requirement conditions for estimation model to run. + + PARAMETERS: + min_activation_threshold: int + Minimum total data points before model runs. Must be at least max_num_components. + + min_new_points_to_run: int + Minimum number of new data points that must be collected before running model. Must be at least 0. + + max_num_components: int + Max number of real landing pads. Must be at least 1. + + random_state: int + Seed for randomizer, to get consistent results. Must be at least 0. + + local_logger: logger.Logger + The local logger to log this object's information. + + RETURNS: The ClusterEstimation object if all conditions pass, otherwise False, None """ - # These parameters must be positive - if min_new_points_to_run < 0 or random_state < 0: + if min_activation_threshold < max_num_components: + return False, None + + if min_new_points_to_run < 0: + return False, None + + if max_num_components < 1: return False, None - # At least 1 point for model to fit - if min_activation_threshold < 1: + if random_state < 0: return False, None return True, ClusterEstimation( cls.__create_key, min_activation_threshold, min_new_points_to_run, + max_num_components, random_state, local_logger, ) @@ -101,6 +111,7 @@ def __init__( class_private_create_key: object, min_activation_threshold: int, min_new_points_to_run: int, + max_num_components: int, random_state: int, local_logger: logger.Logger, ) -> None: @@ -112,7 +123,7 @@ def __init__( # Initializes VGMM self.__vgmm = sklearn.mixture.BayesianGaussianMixture( covariance_type=self.__COVAR_TYPE, - n_components=self.__MAX_NUM_COMPONENTS, + n_components=max_num_components, init_params=self.__MODEL_INIT_PARAM, weight_concentration_prior=self.__WEIGHT_CONCENTRATION_PRIOR, mean_precision_prior=self.__MEAN_PRECISION_PRIOR, diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index e5f7d223..cb193625 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -15,6 +15,7 @@ def cluster_estimation_worker( min_activation_threshold: int, min_new_points_to_run: int, + max_num_components: int, random_state: int, input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, @@ -31,6 +32,9 @@ def cluster_estimation_worker( min_new_points_to_run: int Minimum number of new data points that must be collected before running model. + max_num_components: int + Max number of real landing pads. + random_state: int Seed for randomizer, to get consistent results. @@ -57,6 +61,7 @@ def cluster_estimation_worker( result, estimator = cluster_estimation.ClusterEstimation.create( min_activation_threshold, min_new_points_to_run, + max_num_components, random_state, local_logger, ) diff --git a/modules/detect_target/detect_target_brightspot.py b/modules/detect_target/detect_target_brightspot.py index b868ae24..f236ffa7 100644 --- a/modules/detect_target/detect_target_brightspot.py +++ b/modules/detect_target/detect_target_brightspot.py @@ -13,14 +13,76 @@ from ..common.modules.logger import logger -BRIGHTSPOT_PERCENTILE = 99.9 - # Label for brightspots; is 1 since 0 is used for blue landing pads DETECTION_LABEL = 1 # SimpleBlobDetector is a binary detector, so a detection has confidence 1.0 by default CONFIDENCE = 1.0 +# Class has 15 attributes +# pylint: disable=too-many-instance-attributes +class DetectTargetBrightspotConfig: + """ + Configuration for DetectTargetBrightspot. + """ + + def __init__( + self, + brightspot_percentile_threshold: float, + filter_by_color: bool, + blob_color: int, + filter_by_circularity: bool, + min_circularity: float, + max_circularity: float, + filter_by_inertia: bool, + min_inertia_ratio: float, + max_inertia_ratio: float, + filter_by_convexity: bool, + min_convexity: float, + max_convexity: float, + filter_by_area: bool, + min_area_pixels: int, + max_area_pixels: int, + ) -> None: + """ + Initializes the configuration for DetectTargetBrightspot. + + brightspot_percentile_threshold: Percentile threshold for bright spots. + filter_by_color: Whether to filter by color. + blob_color: Color of the blob. + filter_by_circularity: Whether to filter by circularity. + min_circularity: Minimum circularity. + max_circularity: Maximum circularity. + filter_by_inertia: Whether to filter by inertia. + min_inertia_ratio: Minimum inertia ratio. + max_inertia_ratio: Maximum inertia ratio. + filter_by_convexity: Whether to filter by convexity. + min_convexity: Minimum convexity. + max_convexity: Maximum convexity. + filter_by_area: Whether to filter by area. + min_area_pixels: Minimum area in pixels. + max_area_pixels: Maximum area in pixels. + """ + self.brightspot_percentile_threshold = brightspot_percentile_threshold + self.filter_by_color = filter_by_color + self.blob_color = blob_color + self.filter_by_circularity = filter_by_circularity + self.min_circularity = min_circularity + self.max_circularity = max_circularity + self.filter_by_inertia = filter_by_inertia + self.min_inertia_ratio = min_inertia_ratio + self.max_inertia_ratio = max_inertia_ratio + self.filter_by_convexity = filter_by_convexity + self.min_convexity = min_convexity + self.max_convexity = max_convexity + self.filter_by_area = filter_by_area + self.min_area_pixels = min_area_pixels + self.max_area_pixels = max_area_pixels + + +# pylint: enable=too-many-instance-attributes + + class DetectTargetBrightspot(base_detect_target.BaseDetectTarget): """ Detects bright spots in images. @@ -28,6 +90,7 @@ class DetectTargetBrightspot(base_detect_target.BaseDetectTarget): def __init__( self, + config: DetectTargetBrightspotConfig, local_logger: logger.Logger, show_annotations: bool = False, save_name: str = "", @@ -38,6 +101,7 @@ def __init__( show_annotations: Display annotated images. save_name: Filename prefix for logging detections and annotated images. """ + self.__config = config self.__counter = 0 self.__local_logger = local_logger self.__show_annotations = show_annotations @@ -68,7 +132,9 @@ def run( ) return False, None - brightspot_threshold = np.percentile(grey_image, BRIGHTSPOT_PERCENTILE) + brightspot_threshold = np.percentile( + grey_image, self.__config.brightspot_percentile_threshold + ) # Apply thresholding to isolate bright spots threshold_used, bw_image = cv2.threshold( @@ -80,14 +146,20 @@ def run( # Set up SimpleBlobDetector params = cv2.SimpleBlobDetector_Params() - params.filterByColor = True - params.blobColor = 255 - params.filterByCircularity = False - params.filterByInertia = True - params.minInertiaRatio = 0.2 - params.filterByConvexity = False - params.filterByArea = True - params.minArea = 50 # pixels + params.filterByColor = self.__config.filter_by_color + params.blobColor = self.__config.blob_color + params.filterByCircularity = self.__config.filter_by_circularity + params.minCircularity = self.__config.min_circularity + params.maxCircularity = self.__config.max_circularity + params.filterByInertia = self.__config.filter_by_inertia + params.minInertiaRatio = self.__config.min_inertia_ratio + params.maxInertiaRatio = self.__config.max_inertia_ratio + params.filterByConvexity = self.__config.filter_by_convexity + params.minConvexity = self.__config.min_convexity + params.maxConvexity = self.__config.max_convexity + params.filterByArea = self.__config.filter_by_area + params.minArea = self.__config.min_area_pixels + params.maxArea = self.__config.max_area_pixels detector = cv2.SimpleBlobDetector_create(params) keypoints = detector.detect(bw_image) diff --git a/tests/brightspot_example/generate_expected.py b/tests/brightspot_example/generate_expected.py index c7af3096..a8fad523 100644 --- a/tests/brightspot_example/generate_expected.py +++ b/tests/brightspot_example/generate_expected.py @@ -32,6 +32,24 @@ pathlib.Path(f"ir_no_detections_{i}.png") for i in range(0, NUMBER_OF_IMAGES_NO_DETECTIONS) ] +DETECT_TARGET_BRIGHTSPOT_CONFIG = detect_target_brightspot.DetectTargetBrightspotConfig( + brightspot_percentile_threshold=99.9, + filter_by_color=True, + blob_color=255, + filter_by_circularity=False, + min_circularity=0.01, + max_circularity=1, + filter_by_inertia=True, + min_inertia_ratio=0.2, + max_inertia_ratio=1, + filter_by_convexity=False, + min_convexity=0.01, + max_convexity=1, + filter_by_area=True, + min_area_pixels=50, + max_area_pixels=640, +) + def main() -> int: """ @@ -43,7 +61,10 @@ def main() -> int: return 1 detector = detect_target_brightspot.DetectTargetBrightspot( - local_logger=temp_logger, show_annotations=False, save_name="" + config=DETECT_TARGET_BRIGHTSPOT_CONFIG, + local_logger=temp_logger, + show_annotations=False, + save_name="", ) for image_file, annotated_image_path, expected_detections_path in zip( diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 6f6da0f7..155cca06 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -13,10 +13,10 @@ MIN_TOTAL_POINTS_THRESHOLD = 100 MIN_NEW_POINTS_TO_RUN = 10 +MAX_NUM_COMPONENTS = 10 RNG_SEED = 0 CENTRE_BOX_SIZE = 500 - # Test functions use test fixture signature names and access class privates # No enable # pylint: disable=protected-access,redefined-outer-name @@ -34,6 +34,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore result, model = cluster_estimation.ClusterEstimation.create( MIN_TOTAL_POINTS_THRESHOLD, MIN_NEW_POINTS_TO_RUN, + MAX_NUM_COMPONENTS, RNG_SEED, test_logger, ) diff --git a/tests/unit/test_detect_target_brightspot.py b/tests/unit/test_detect_target_brightspot.py index a8f1a3c6..6e0ce99d 100644 --- a/tests/unit/test_detect_target_brightspot.py +++ b/tests/unit/test_detect_target_brightspot.py @@ -35,6 +35,24 @@ BOUNDING_BOX_PRECISION_TOLERANCE = 3 CONFIDENCE_PRECISION_TOLERANCE = 6 +DETECT_TARGET_BRIGHTSPOT_CONFIG = detect_target_brightspot.DetectTargetBrightspotConfig( + brightspot_percentile_threshold=99.9, + filter_by_color=True, + blob_color=255, + filter_by_circularity=False, + min_circularity=0.01, + max_circularity=1, + filter_by_inertia=True, + min_inertia_ratio=0.2, + max_inertia_ratio=1, + filter_by_convexity=False, + min_convexity=0.01, + max_convexity=1, + filter_by_area=True, + min_area_pixels=50, + max_area_pixels=640, +) + # Test functions use test fixture signature names and access class privates # No enable @@ -121,7 +139,9 @@ def detector() -> detect_target_brightspot.DetectTargetBrightspot: # type: igno assert result assert test_logger is not None - detection = detect_target_brightspot.DetectTargetBrightspot(test_logger) + detection = detect_target_brightspot.DetectTargetBrightspot( + DETECT_TARGET_BRIGHTSPOT_CONFIG, test_logger + ) yield detection # type: ignore