diff --git a/panoptes_aggregation/reducers/shape_metric_IoU.py b/panoptes_aggregation/reducers/shape_metric_IoU.py index 57b20747..6b89a957 100644 --- a/panoptes_aggregation/reducers/shape_metric_IoU.py +++ b/panoptes_aggregation/reducers/shape_metric_IoU.py @@ -80,7 +80,7 @@ def panoptes_to_geometry(params, shape): raise ValueError('The IoU metric only works with the following shapes: rectangle, rotateing rectangle, circle, ellipse, or triangle') -def IoU_metric(params1, params2, shape): +def IoU_metric(params1, params2, shape, eps_t=None): '''Find the Intersection of Union distance between two shapes. Parameters @@ -92,6 +92,9 @@ def IoU_metric(params1, params2, shape): shape : string The shape these parameters belong to (see :meth:`panoptes_to_geometry` for supported shapes) + eps_t : float + For temporal tools, this defines the temporal width of the rectangle. + Two shapes are connected if the displayTime parameters are within eps_t. Returns ------- @@ -105,16 +108,29 @@ def IoU_metric(params1, params2, shape): intersection = 0 if geo1.intersects(geo2): intersection = geo1.intersection(geo2).area - union = geo1.union(geo2).area - if union == 0: - # catch divide by zero (i.e. cases when neither shape has an area) - return numpy.inf if 'temporal' in shape: # combine the shape IoU with the time difference and normalize - return 0.5 * ((1 - intersection / union) + numpy.abs(params1[-1] - params2[-1])) + # build two boxes in the time domain with width eps_t and height 1 + # centered at (t - eps_t / 2, 0.5) and calculate the intersection in time + time_params1 = (params1[-1] - eps_t, 0, eps_t, 1) + time_params2 = (params2[-1] - eps_t, 0, eps_t, 1) + time_geo1 = panoptes_to_geometry(time_params1, 'rectangle') + time_geo2 = panoptes_to_geometry(time_params2, 'rectangle') + time_intersection = 0 + if time_geo1.intersects(time_geo2): + time_intersection = time_geo1.intersection(time_geo2).area + + intersection = intersection * time_intersection + union = ((geo1.area + geo2.area) * eps_t - intersection) else: - return 1 - intersection / union + union = geo1.union(geo2).area + + if union == 0: + # catch divide by zero (i.e. cases when neither shape has an area) + return numpy.inf + + return 1 - intersection / union def average_bounds(params_list, shape): @@ -252,7 +268,7 @@ def scale_shape(params, shape, gamma): raise ValueError('The IoU metric only works with the following shapes: rectangle, rotateing rectangle, circle, ellipse, or triangle') -def average_shape_IoU(params_list, shape): +def average_shape_IoU(params_list, shape, eps_t=None): '''Find the average shape and standard deviation from a list of parameters with respect to the IoU metric. @@ -273,11 +289,11 @@ def average_shape_IoU(params_list, shape): The standard deviation of the input shapes with respect to the IoU metric ''' def sum_distance(x): - return sum([IoU_metric(x, p, shape)**2 for p in params_list]) + return sum([IoU_metric(x, p, shape, eps_t)**2 for p in params_list]) # find shape that minimizes the variance in the IoU metric using bounds - m = scipy.optimize.shgo( + m = scipy.optimize.direct( sum_distance, - sampling_method='sobol', + locally_biased=False, bounds=average_bounds(params_list, shape) ) # find the 1-sigma value diff --git a/panoptes_aggregation/reducers/shape_reducer_dbscan.py b/panoptes_aggregation/reducers/shape_reducer_dbscan.py index 2bf7a743..bc13f5ad 100644 --- a/panoptes_aggregation/reducers/shape_reducer_dbscan.py +++ b/panoptes_aggregation/reducers/shape_reducer_dbscan.py @@ -14,9 +14,9 @@ from .shape_metric import get_shape_metric_and_avg from .shape_metric_IoU import IoU_metric, average_shape_IoU - DEFAULTS = { 'eps': {'default': 5.0, 'type': float}, + 'eps_t': {'default': 0.5, 'type': float}, 'min_samples': {'default': 3, 'type': int}, 'algorithm': {'default': 'auto', 'type': str}, 'leaf_size': {'default': 30, 'type': int}, @@ -67,6 +67,7 @@ def shape_reducer_dbscan(data_by_tool, **kwargs): * `tool*_clusters_sigma` : The standard deviation of the average shape under the IoU metric ''' shape = data_by_tool.pop('shape') + eps_t = kwargs.pop('eps_t', None) shape_params = SHAPE_LUT[shape] metric_type = kwargs.pop('metric_type', 'euclidean').lower() symmetric = data_by_tool.pop('symmetric') @@ -75,7 +76,7 @@ def shape_reducer_dbscan(data_by_tool, **kwargs): kwargs['metric'] = metric elif metric_type == 'iou': kwargs['metric'] = IoU_metric - kwargs['metric_params'] = {'shape': shape} + kwargs['metric_params'] = {'shape': shape, 'eps_t': eps_t} avg = average_shape_IoU else: raise ValueError('metric_type must be either "euclidean" or "IoU".') @@ -104,7 +105,7 @@ def shape_reducer_dbscan(data_by_tool, **kwargs): if metric_type == 'euclidean': k_loc = avg(loc[idx]) elif metric_type == 'iou': - k_loc, sigma = avg(loc[idx], shape) + k_loc, sigma = avg(loc[idx], shape, eps_t) clusters[frame].setdefault('{0}_clusters_sigma'.format(tool), []).append(float(sigma)) for pdx, param in enumerate(shape_params): clusters[frame].setdefault('{0}_clusters_{1}'.format(tool, param), []).append(float(k_loc[pdx])) diff --git a/panoptes_aggregation/reducers/shape_reducer_hdbscan.py b/panoptes_aggregation/reducers/shape_reducer_hdbscan.py index 26af2fda..71d8fee6 100644 --- a/panoptes_aggregation/reducers/shape_reducer_hdbscan.py +++ b/panoptes_aggregation/reducers/shape_reducer_hdbscan.py @@ -19,6 +19,7 @@ DEFAULTS = { 'min_cluster_size': {'default': 5, 'type': int}, 'min_samples': {'default': 3, 'type': int}, + 'eps_t': {'default': 0.5, 'type': float}, 'algorithm': {'default': 'best', 'type': str}, 'leaf_size': {'default': 40, 'type': int}, 'p': {'default': None, 'type': float}, @@ -72,6 +73,7 @@ def shape_reducer_hdbscan(data_by_tool, **kwargs): * `tool*_clusters_sigma` : The standard deviation of the average shape under the IoU metric ''' shape = data_by_tool.pop('shape') + eps_t = kwargs.pop('eps_t', None) shape_params = SHAPE_LUT[shape] metric_type = kwargs.pop('metric_type', 'euclidean').lower() symmetric = data_by_tool.pop('symmetric') @@ -81,6 +83,7 @@ def shape_reducer_hdbscan(data_by_tool, **kwargs): elif metric_type == 'iou': kwargs['metric'] = IoU_metric kwargs['shape'] = shape + kwargs['eps_t'] = eps_t avg = average_shape_IoU else: raise ValueError('metric_type must be either "euclidean" or "IoU".') @@ -112,7 +115,7 @@ def shape_reducer_hdbscan(data_by_tool, **kwargs): if metric_type == 'euclidean': k_loc = avg(loc[idx]) elif metric_type == 'iou': - k_loc, sigma = avg(loc[idx], shape) + k_loc, sigma = avg(loc[idx], shape, eps_t) clusters[frame].setdefault('{0}_clusters_sigma'.format(tool), []).append(float(sigma)) for pdx, param in enumerate(shape_params): clusters[frame].setdefault('{0}_clusters_{1}'.format(tool, param), []).append(float(k_loc[pdx])) diff --git a/panoptes_aggregation/reducers/shape_reducer_optics.py b/panoptes_aggregation/reducers/shape_reducer_optics.py index 6dc5bb76..3f4bb8dd 100644 --- a/panoptes_aggregation/reducers/shape_reducer_optics.py +++ b/panoptes_aggregation/reducers/shape_reducer_optics.py @@ -21,6 +21,7 @@ DEFAULTS = { 'min_samples': {'default': 3, 'type': int}, + 'eps_t': {'default': 0.5, 'type': float}, 'min_cluster_size': {'default': 2, 'type': int}, 'algorithm': {'default': 'auto', 'type': str}, 'leaf_size': {'default': 30, 'type': int}, @@ -71,6 +72,7 @@ def shape_reducer_optics(data_by_tool, **kwargs): * `tool*_clusters_sigma` : The standard deviation of the average shape under the IoU metric ''' shape = data_by_tool.pop('shape') + eps_t = kwargs.pop('eps_t', None) shape_params = SHAPE_LUT[shape] metric_type = kwargs.pop('metric_type', 'euclidean').lower() symmetric = data_by_tool.pop('symmetric') @@ -79,7 +81,7 @@ def shape_reducer_optics(data_by_tool, **kwargs): kwargs['metric'] = metric elif metric_type == 'iou': kwargs['metric'] = IoU_metric - kwargs['metric_params'] = {'shape': shape} + kwargs['metric_params'] = {'shape': shape, 'eps_t': eps_t} avg = average_shape_IoU else: raise ValueError('metric_type must be either "euclidean" or "IoU".') @@ -110,7 +112,7 @@ def shape_reducer_optics(data_by_tool, **kwargs): if metric_type == 'euclidean': k_loc = avg(loc[idx]) elif metric_type == 'iou': - k_loc, sigma = avg(loc[idx], shape) + k_loc, sigma = avg(loc[idx], shape, eps_t) clusters[frame].setdefault('{0}_clusters_sigma'.format(tool), []).append(float(sigma)) for pdx, param in enumerate(shape_params): clusters[frame].setdefault('{0}_clusters_{1}'.format(tool, param), []).append(float(k_loc[pdx])) diff --git a/panoptes_aggregation/tests/reducer_tests/test_shape_reducer_temporal_rotate_rectangle.py b/panoptes_aggregation/tests/reducer_tests/test_shape_reducer_temporal_rotate_rectangle.py index 00da23b9..ab76f398 100644 --- a/panoptes_aggregation/tests/reducer_tests/test_shape_reducer_temporal_rotate_rectangle.py +++ b/panoptes_aggregation/tests/reducer_tests/test_shape_reducer_temporal_rotate_rectangle.py @@ -95,6 +95,16 @@ "T0_toolIndex0_y_center": [580.0], } }, + { + 'frame0': { + 'T0_toolIndex0_angle': [50], + 'T0_toolIndex0_displayTime': [0.5], + 'T0_toolIndex0_height': [100], + 'T0_toolIndex0_width': [80], + 'T0_toolIndex0_x_center': [500], + 'T0_toolIndex0_y_center': [580] + }, + } ] kwargs_extra_data = { @@ -107,7 +117,8 @@ 6, 7, 8, - 9 + 9, + 10 ] } @@ -122,7 +133,8 @@ (520.0, 510.0, 120.0, 50.0, 10.0, 1.0), (530.0, 500.0, 150.0, 50.0, 12.0, 0.9), (350.0, 620.0, 100.0, 80.0, 25.0, 0.6), - (350.0, 580.0, 80.0, 140.0, 20.0, 0.9) + (350.0, 580.0, 80.0, 140.0, 20.0, 0.9), + (500.0, 580.0, 80.0, 100.0, 50.0, 0.5) ] }, 'shape': 'temporalRotateRectangle', @@ -130,22 +142,22 @@ } reduced_data_dbscan = { - "frame0": { - "T0_toolIndex0_cluster_labels": [0, 0, 0, 1, 1, 1, 1, 1, 1], - "T0_toolIndex0_clusters_count": [3, 6], - "T0_toolIndex0_clusters_angle": [9.4, 21.5], - "T0_toolIndex0_clusters_displayTime": [0.1, 0.9], - "T0_toolIndex0_clusters_height": [60.7, 104.5], - "T0_toolIndex0_clusters_sigma": [0.2, 0.5], - "T0_toolIndex0_clusters_width": [138.0, 102.8], - "T0_toolIndex0_clusters_x_center": [502.2, 352.3], - "T0_toolIndex0_clusters_y_center": [503.9, 604.9], - "T0_toolIndex0_temporalRotateRectangle_angle": [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0], - "T0_toolIndex0_temporalRotateRectangle_displayTime": [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9], - "T0_toolIndex0_temporalRotateRectangle_height": [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0], - "T0_toolIndex0_temporalRotateRectangle_width": [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0], - "T0_toolIndex0_temporalRotateRectangle_x_center": [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0], - "T0_toolIndex0_temporalRotateRectangle_y_center": [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0] + 'frame0': { + 'T0_toolIndex0_cluster_labels': [0, 0, 0, 1, 2, 2, 2, 1, 1, -1], + 'T0_toolIndex0_clusters_angle': [10.0, 110.0, 10.0], + 'T0_toolIndex0_clusters_count': [3, 3, 3], + 'T0_toolIndex0_clusters_displayTime': [0.1, 0.7, 0.9], + 'T0_toolIndex0_clusters_height': [61.0, 98.5, 54.5], + 'T0_toolIndex0_clusters_sigma': [0.4, 0.7, 0.4], + 'T0_toolIndex0_clusters_width': [137.5, 143.3, 136.1], + 'T0_toolIndex0_clusters_x_center': [502.9, 358.1, 522.3], + 'T0_toolIndex0_clusters_y_center': [504.3, 584.7, 508.1], + 'T0_toolIndex0_temporalRotateRectangle_angle': [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0, 50.0], + 'T0_toolIndex0_temporalRotateRectangle_displayTime': [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9, 0.5], + 'T0_toolIndex0_temporalRotateRectangle_height': [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0, 100.0], + 'T0_toolIndex0_temporalRotateRectangle_width': [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0, 80.0], + 'T0_toolIndex0_temporalRotateRectangle_x_center': [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0, 500.0], + 'T0_toolIndex0_temporalRotateRectangle_y_center': [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0, 580.0] } } @@ -159,8 +171,9 @@ network_kwargs=kwargs_extra_data, pkwargs={'shape': 'temporalRotateRectangle'}, kwargs={ - 'eps': 0.5, + 'eps': 0.8, 'min_samples': 2, + 'eps_t': 0.5, 'metric_type': 'IoU', }, test_name='TestShapeReducerTemporalRotateRectangleDbscan', @@ -168,22 +181,22 @@ ) reduced_data_optics = { - "frame0": { - "T0_toolIndex0_cluster_labels": [0, 0, 0, 2, 1, 1, 1, 2, 2], - "T0_toolIndex0_clusters_angle": [9.4, 9.4, 20.0], - "T0_toolIndex0_clusters_count": [3, 3, 3], - "T0_toolIndex0_clusters_displayTime": [0.1, 0.9, 0.7], - "T0_toolIndex0_clusters_height": [60.7, 51.4, 143.7], - "T0_toolIndex0_clusters_sigma": [0.2, 0.2, 0.3], - "T0_toolIndex0_clusters_width": [138.0, 137.4, 92.1], - "T0_toolIndex0_clusters_x_center": [502.2, 522.5, 355.0], - "T0_toolIndex0_clusters_y_center": [503.9, 509.1, 583.8], - "T0_toolIndex0_temporalRotateRectangle_angle": [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0], - "T0_toolIndex0_temporalRotateRectangle_displayTime": [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9], - "T0_toolIndex0_temporalRotateRectangle_height": [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0], - "T0_toolIndex0_temporalRotateRectangle_width": [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0], - "T0_toolIndex0_temporalRotateRectangle_x_center": [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0], - "T0_toolIndex0_temporalRotateRectangle_y_center": [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0] + 'frame0': { + 'T0_toolIndex0_cluster_labels': [0, 0, 0, 2, 1, 1, 1, 2, 2, -1], + 'T0_toolIndex0_clusters_angle': [10.0, 10.0, 110.0], + 'T0_toolIndex0_clusters_count': [3, 3, 3], + 'T0_toolIndex0_clusters_displayTime': [0.1, 0.9, 0.7], + 'T0_toolIndex0_clusters_height': [61.0, 54.5, 98.5], + 'T0_toolIndex0_clusters_sigma': [0.4, 0.4, 0.7], + 'T0_toolIndex0_clusters_width': [137.5, 136.1, 143.3], + 'T0_toolIndex0_clusters_x_center': [502.9, 522.3, 358.1], + 'T0_toolIndex0_clusters_y_center': [504.3, 508.1, 584.7], + 'T0_toolIndex0_temporalRotateRectangle_angle': [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0, 50.0], + 'T0_toolIndex0_temporalRotateRectangle_displayTime': [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9, 0.5], + 'T0_toolIndex0_temporalRotateRectangle_height': [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0, 100.0], + 'T0_toolIndex0_temporalRotateRectangle_width': [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0, 80.0], + 'T0_toolIndex0_temporalRotateRectangle_x_center': [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0, 500.0], + 'T0_toolIndex0_temporalRotateRectangle_y_center': [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0, 580.0] } } @@ -200,6 +213,7 @@ kwargs={ 'min_samples': 2, 'metric_type': 'IoU', + 'eps_t': 0.5 }, test_name='TestShapeReducerTemporalRotateRectangleOptics', round=1 @@ -207,23 +221,23 @@ reduced_data_hdbscan = { "frame0": { - "T0_toolIndex0_cluster_labels": [0, 0, 0, 2, 1, 1, 1, 2, 2], - "T0_toolIndex0_clusters_count": [3, 3, 3], - "T0_toolIndex0_cluster_probabilities": [1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 0.6, 0.7, 1.0], - "T0_toolIndex0_clusters_angle": [9.4, 9.4, 20.0], - "T0_toolIndex0_clusters_displayTime": [0.1, 0.9, 0.7], - "T0_toolIndex0_clusters_height": [60.7, 51.4, 143.7], - "T0_toolIndex0_clusters_persistance": [0.5, 0.6, 0.1], - "T0_toolIndex0_clusters_sigma": [0.2, 0.2, 0.3], - "T0_toolIndex0_clusters_width": [138.0, 137.4, 92.1], - "T0_toolIndex0_clusters_x_center": [502.2, 522.5, 355.0], - "T0_toolIndex0_clusters_y_center": [503.9, 509.1, 583.8], - "T0_toolIndex0_temporalRotateRectangle_angle": [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0], - "T0_toolIndex0_temporalRotateRectangle_displayTime": [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9], - "T0_toolIndex0_temporalRotateRectangle_height": [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0], - "T0_toolIndex0_temporalRotateRectangle_width": [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0], - "T0_toolIndex0_temporalRotateRectangle_x_center": [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0], - "T0_toolIndex0_temporalRotateRectangle_y_center": [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0] + 'T0_toolIndex0_cluster_labels': [1, 1, 1, 0, 2, 2, 2, 0, 0, 1], + 'T0_toolIndex0_cluster_probabilities': [1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 1.0, 0.3], + 'T0_toolIndex0_clusters_angle': [110.0, 100.1, 10.0], + 'T0_toolIndex0_clusters_count': [3, 4, 3], + 'T0_toolIndex0_clusters_displayTime': [0.7, 0.1, 0.9], + 'T0_toolIndex0_clusters_height': [98.5, 137.8, 54.5], + 'T0_toolIndex0_clusters_persistance': [0.1, 0.4, 0.4], + 'T0_toolIndex0_clusters_sigma': [0.7, 0.7, 0.4], + 'T0_toolIndex0_clusters_width': [143.3, 62.7, 136.1], + 'T0_toolIndex0_clusters_x_center': [358.1, 502.9, 522.3], + 'T0_toolIndex0_clusters_y_center': [584.7, 505.2, 508.1], + 'T0_toolIndex0_temporalRotateRectangle_angle': [10.0, 12.0, 15.0, 30.0, 10.0, 10.0, 12.0, 25.0, 20.0, 50.0], + 'T0_toolIndex0_temporalRotateRectangle_displayTime': [0.1, 0.1, 0.1, 0.7, 0.9, 1.0, 0.9, 0.6, 0.9, 0.5], + 'T0_toolIndex0_temporalRotateRectangle_height': [50.0, 60.0, 60.0, 120.0, 50.0, 50.0, 50.0, 80.0, 140.0, 100.0], + 'T0_toolIndex0_temporalRotateRectangle_width': [150.0, 160.0, 120.0, 110.0, 140.0, 120.0, 150.0, 100.0, 80.0, 80.0], + 'T0_toolIndex0_temporalRotateRectangle_x_center': [510.0, 490.0, 510.0, 360.0, 520.0, 520.0, 530.0, 350.0, 350.0, 500.0], + 'T0_toolIndex0_temporalRotateRectangle_y_center': [500.0, 515.0, 500.0, 570.0, 510.0, 510.0, 500.0, 620.0, 580.0, 580.0] } } @@ -242,6 +256,7 @@ 'allow_single_cluster': True, 'metric_type': 'IoU', 'min_samples': 1, + 'eps_t': 0.5 }, test_name='TestShapeReducerTemporalRotateRectangleHdbscan', round=1,