Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented label in cluster estimation #231

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions modules/cluster_estimation/cluster_estimation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ 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.

local_logger: Logger
For logging error and debug messages.

METHODS
-------
run()
Expand Down Expand Up @@ -85,16 +99,11 @@ def create(

RETURNS: The ClusterEstimation object if all conditions pass, otherwise False, None
"""
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
is_valid_arguments = ClusterEstimation.check_create_arguments(
min_activation_threshold, min_new_points_to_run, max_num_components, random_state
)

if random_state < 0:
if not is_valid_arguments:
return False, None

return True, ClusterEstimation(
Expand Down Expand Up @@ -211,21 +220,46 @@ def run(
model_output = self.__filter_by_covariances(model_output)

# Create output list of remaining valid clusters
detections_in_world = []
objects_in_world = []
for cluster in model_output:
result, landing_pad = object_in_world.ObjectInWorld.create(
cluster[0][0],
cluster[0][1],
cluster[2],
cluster[0][0], cluster[0][1], cluster[2]
)

if result:
detections_in_world.append(landing_pad)
objects_in_world.append(landing_pad)
else:
self.__logger.warning("Failed to create ObjectInWorld object")
self.__logger.error("Failed to create ObjectInWorld object")
return False, None

self.__logger.info(objects_in_world)
return True, objects_in_world

@staticmethod
def check_create_arguments(
min_activation_threshold: int,
min_new_points_to_run: int,
max_num_components: int,
random_state: int,
):
"""
Checks if a valid cluster estimation object can be constructed.

See `ClusterEstimation` for parameter descriptions.
"""
if min_activation_threshold < max_num_components:
return False

if min_new_points_to_run < 0:
return False

if max_num_components < 1:
return False

self.__logger.info(detections_in_world)
return True, detections_in_world
if random_state < 0:
return False

return True

def __decide_to_run(self, run_override: bool) -> bool:
"""
Expand Down
158 changes: 158 additions & 0 deletions modules/cluster_estimation/cluster_estimation_by_label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""
Cluster estimation by label.
"""

from . import cluster_estimation
from .. import detection_in_world
from .. import object_in_world
from ..common.modules.logger import logger


class ClusterEstimationByLabel:
"""
Cluster estimation filtered on label.

ATTRIBUTES
----------
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.

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.

local_logger: Logger
For logging error and debug messages.

METHODS
-------
run()
Cluster estimation filtered by label.
Comment on lines +14 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove this.

"""

__create_key = object()

@classmethod
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[True, ClusterEstimationByLabel] | tuple[False, None]":
"""
See `ClusterEstimation` for parameter descriptions.
"""

is_valid_arguments = cluster_estimation.ClusterEstimation.check_create_arguments(
min_activation_threshold, min_new_points_to_run, max_num_components, random_state
)

if not is_valid_arguments:
return False, None

return True, ClusterEstimationByLabel(
Copy link
Member

Choose a reason for hiding this comment

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

Please check ClusterEstimation's restrictions. Either apply them again here or by invoking creating cluster estimation

cls.__create_key,
min_activation_threshold,
min_new_points_to_run,
max_num_components,
random_state,
local_logger,
)

def __init__(
self,
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:
"""
Private constructor, use create() method.
"""
assert (
class_private_create_key is ClusterEstimationByLabel.__create_key
), "Use create() method"

# Construction arguments for `ClusterEstimation`
self.__min_activation_threshold = min_activation_threshold
self.__min_new_points_to_run = min_new_points_to_run
self.__max_num_components = max_num_components
self.__random_state = random_state
self.__local_logger = local_logger

# Cluster model corresponding to each label
# Each cluster estimation object stores the detections given to in its __all_points bucket across runs
self.__label_to_cluster_estimation_model: dict[
int, cluster_estimation.ClusterEstimation
] = {}

def run(
self,
input_detections: list[detection_in_world.DetectionInWorld],
run_override: bool,
) -> tuple[True, dict[int, list[object_in_world.ObjectInWorld]]] | tuple[False, None]:
"""
See `ClusterEstimation` for parameter descriptions.

RETURNS
-------
model_ran: bool
True if ClusterEstimation object successfully ran its estimation model, False otherwise.

labels_to_objects: dict[int, list[object_in_world.ObjectInWorld] or None.
Dictionary where the key is a label and the value is a list of all cluster detections with that label.
ObjectInWorld objects don't have a label property, but they are sorted into label categories in the dictionary.
Comment on lines +108 to +115
Copy link
Collaborator

@Xierumeng Xierumeng Mar 7, 2025

Choose a reason for hiding this comment

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

Simplify:

Return: Success, labels and their associated objects.

"""
label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {}
Copy link
Member

Choose a reason for hiding this comment

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

Add comment saying sorting detections by label


# Sorting detections by label
for detection in input_detections:
if not detection.label in label_to_detections:
label_to_detections[detection.label] = []
label_to_detections[detection.label].append(detection)

labels_to_objects: dict[int, list[object_in_world.ObjectInWorld]] = {}

for label, detections in label_to_detections.items():
# Create cluster estimation for label if it doesn't exist
if not label in self.__label_to_cluster_estimation_model:
result, cluster_model = cluster_estimation.ClusterEstimation.create(
self.__min_activation_threshold,
self.__min_new_points_to_run,
self.__max_num_components,
self.__random_state,
self.__local_logger,
)
if not result:
self.__local_logger.error(
f"Failed to create cluster estimation for label {label}"
)
return False, None
self.__label_to_cluster_estimation_model[label] = cluster_model

# runs cluster estimation for specific label
result, clusters = self.__label_to_cluster_estimation_model[label].run(
detections,
run_override,
)

if not result:
self.__local_logger.error(
f"Failed to run cluster estimation model for label {label}"
)
return False, None

if not label in labels_to_objects:
labels_to_objects[label] = []
labels_to_objects[label] += clusters
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add empty line above this line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this the desired behaviour? The cluster estimation objects already hold a record of all points, so they will always generate an updated version of the cluster centres. I think this should be an unconditional assignment instead.


return True, labels_to_objects
27 changes: 18 additions & 9 deletions modules/cluster_estimation/cluster_estimation_worker.py
Copy link
Collaborator

Choose a reason for hiding this comment

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

Change this worker to use the new ClusterEstimationByLabel class.

Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,29 @@ 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.

input_queue: queue_proxy_wrapper.QueuePRoxyWrapper
Data queue.
METHODS
-------
run()
Take in list of landing pad detections and return list of estimated landing pad locations
if number of detections is sufficient, or if manually forced to run.

__decide_to_run()
Decide when to run cluster estimation model.

__sort_by_weights()
Sort input model output list by weights in descending order.

__convert_detections_to_point()
Convert DetectionInWorld input object to a [x,y] position to store.

output_queue: queue_proxy_wrapper.QueuePRoxyWrapper
Data queue.
__filter_by_points_ownership()
Removes any clusters that don't have any points belonging to it.

worker_controller: worker_controller.WorkerController
How the main process communicates to this worker process.
__filter_by_covariances()
Removes any cluster with covariances much higher than the lowest covariance value.
"""
worker_name = pathlib.Path(__file__).stem
process_id = os.getpid()
Expand Down
7 changes: 6 additions & 1 deletion modules/object_in_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ def create(
if spherical_variance < 0.0:
return False, None

return True, ObjectInWorld(cls.__create_key, location_x, location_y, spherical_variance)
return True, ObjectInWorld(
cls.__create_key,
location_x,
location_y,
spherical_variance,
)

def __init__(
self,
Expand Down
Loading