matrice-analytics 0.1.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +146 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3291 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +660 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for post processing utility functions.
|
|
3
|
+
|
|
4
|
+
This module tests all utility functions including geometry calculations,
|
|
5
|
+
format conversions, filtering, counting, and tracking utilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import math
|
|
10
|
+
from typing import Dict, List, Any
|
|
11
|
+
|
|
12
|
+
# Fix imports for proper module resolution
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src'))
|
|
16
|
+
|
|
17
|
+
from src.matrice_analytics.post_processing.utils import (
|
|
18
|
+
# Geometry utilities
|
|
19
|
+
point_in_polygon, get_bbox_center, calculate_distance, calculate_bbox_overlap,
|
|
20
|
+
calculate_iou, get_bbox_area, normalize_bbox, denormalize_bbox, line_segments_intersect,
|
|
21
|
+
|
|
22
|
+
# Format utilities
|
|
23
|
+
convert_to_coco_format, convert_to_yolo_format, convert_to_tracking_format,
|
|
24
|
+
convert_detection_to_tracking_format, convert_tracking_to_detection_format,
|
|
25
|
+
match_results_structure,
|
|
26
|
+
|
|
27
|
+
# Filter utilities
|
|
28
|
+
filter_by_confidence, filter_by_categories, calculate_bbox_fingerprint,
|
|
29
|
+
clean_expired_tracks, remove_duplicate_detections, apply_category_mapping,
|
|
30
|
+
filter_by_area,
|
|
31
|
+
|
|
32
|
+
# Counting utilities
|
|
33
|
+
count_objects_by_category, count_objects_in_zones, count_unique_tracks,
|
|
34
|
+
calculate_counting_summary,
|
|
35
|
+
|
|
36
|
+
# Tracking utilities
|
|
37
|
+
track_objects_in_zone, detect_line_crossings, analyze_track_movements,
|
|
38
|
+
filter_tracks_by_duration
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from .test_utilities import BasePostProcessingTest
|
|
42
|
+
from .test_data_generators import (
|
|
43
|
+
create_detection_results, create_tracking_results, create_zone_polygons,
|
|
44
|
+
create_line_crossing_data, create_edge_case_data
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestGeometryUtils(BasePostProcessingTest):
|
|
49
|
+
"""Test geometry utility functions."""
|
|
50
|
+
|
|
51
|
+
def test_point_in_polygon(self):
|
|
52
|
+
"""Test point in polygon detection."""
|
|
53
|
+
# Square polygon
|
|
54
|
+
square = [[0, 0], [100, 0], [100, 100], [0, 100]]
|
|
55
|
+
|
|
56
|
+
# Test points inside
|
|
57
|
+
self.assertTrue(point_in_polygon([50, 50], square))
|
|
58
|
+
self.assertTrue(point_in_polygon([10, 10], square))
|
|
59
|
+
self.assertTrue(point_in_polygon([90, 90], square))
|
|
60
|
+
|
|
61
|
+
# Test points outside
|
|
62
|
+
self.assertFalse(point_in_polygon([150, 50], square))
|
|
63
|
+
self.assertFalse(point_in_polygon([50, 150], square))
|
|
64
|
+
self.assertFalse(point_in_polygon([-10, 50], square))
|
|
65
|
+
|
|
66
|
+
# Test boundary points
|
|
67
|
+
self.assertTrue(point_in_polygon([0, 50], square)) # Edge case
|
|
68
|
+
self.assertTrue(point_in_polygon([50, 0], square)) # Edge case
|
|
69
|
+
|
|
70
|
+
def test_point_in_polygon_complex(self):
|
|
71
|
+
"""Test point in polygon with complex shapes."""
|
|
72
|
+
# Triangle
|
|
73
|
+
triangle = [[0, 0], [100, 0], [50, 100]]
|
|
74
|
+
|
|
75
|
+
self.assertTrue(point_in_polygon([50, 30], triangle))
|
|
76
|
+
self.assertFalse(point_in_polygon([10, 90], triangle))
|
|
77
|
+
|
|
78
|
+
# Concave polygon
|
|
79
|
+
concave = [[0, 0], [100, 0], [100, 50], [50, 50], [50, 100], [0, 100]]
|
|
80
|
+
|
|
81
|
+
self.assertTrue(point_in_polygon([25, 25], concave))
|
|
82
|
+
self.assertTrue(point_in_polygon([75, 25], concave))
|
|
83
|
+
self.assertFalse(point_in_polygon([75, 75], concave)) # In the "notch"
|
|
84
|
+
|
|
85
|
+
def test_get_bbox_center(self):
|
|
86
|
+
"""Test bounding box center calculation."""
|
|
87
|
+
# Simple rectangle
|
|
88
|
+
bbox = [10, 20, 50, 60]
|
|
89
|
+
center = get_bbox_center(bbox)
|
|
90
|
+
self.assertEqual(center, [30.0, 40.0])
|
|
91
|
+
|
|
92
|
+
# Square
|
|
93
|
+
bbox = [0, 0, 100, 100]
|
|
94
|
+
center = get_bbox_center(bbox)
|
|
95
|
+
self.assertEqual(center, [50.0, 50.0])
|
|
96
|
+
|
|
97
|
+
# Single point (degenerate case)
|
|
98
|
+
bbox = [50, 50, 50, 50]
|
|
99
|
+
center = get_bbox_center(bbox)
|
|
100
|
+
self.assertEqual(center, [50.0, 50.0])
|
|
101
|
+
|
|
102
|
+
def test_calculate_distance(self):
|
|
103
|
+
"""Test distance calculation between points."""
|
|
104
|
+
# Simple cases
|
|
105
|
+
self.assertEqual(calculate_distance([0, 0], [3, 4]), 5.0)
|
|
106
|
+
self.assertEqual(calculate_distance([0, 0], [0, 0]), 0.0)
|
|
107
|
+
self.assertEqual(calculate_distance([1, 1], [4, 5]), 5.0)
|
|
108
|
+
|
|
109
|
+
# Negative coordinates
|
|
110
|
+
self.assertEqual(calculate_distance([-3, -4], [0, 0]), 5.0)
|
|
111
|
+
|
|
112
|
+
# Floating point coordinates
|
|
113
|
+
dist = calculate_distance([1.5, 2.5], [4.5, 6.5])
|
|
114
|
+
self.assertAlmostEqual(dist, 5.0, places=5)
|
|
115
|
+
|
|
116
|
+
def test_calculate_bbox_overlap(self):
|
|
117
|
+
"""Test bounding box overlap calculation."""
|
|
118
|
+
# Overlapping boxes
|
|
119
|
+
bbox1 = [0, 0, 50, 50]
|
|
120
|
+
bbox2 = [25, 25, 75, 75]
|
|
121
|
+
overlap = calculate_bbox_overlap(bbox1, bbox2)
|
|
122
|
+
expected_overlap = 25 * 25 # 625
|
|
123
|
+
self.assertEqual(overlap, expected_overlap)
|
|
124
|
+
|
|
125
|
+
# Non-overlapping boxes
|
|
126
|
+
bbox1 = [0, 0, 50, 50]
|
|
127
|
+
bbox2 = [100, 100, 150, 150]
|
|
128
|
+
overlap = calculate_bbox_overlap(bbox1, bbox2)
|
|
129
|
+
self.assertEqual(overlap, 0)
|
|
130
|
+
|
|
131
|
+
# Identical boxes
|
|
132
|
+
bbox1 = [0, 0, 100, 100]
|
|
133
|
+
bbox2 = [0, 0, 100, 100]
|
|
134
|
+
overlap = calculate_bbox_overlap(bbox1, bbox2)
|
|
135
|
+
self.assertEqual(overlap, 10000) # 100 * 100
|
|
136
|
+
|
|
137
|
+
# Touching boxes (no overlap)
|
|
138
|
+
bbox1 = [0, 0, 50, 50]
|
|
139
|
+
bbox2 = [50, 0, 100, 50]
|
|
140
|
+
overlap = calculate_bbox_overlap(bbox1, bbox2)
|
|
141
|
+
self.assertEqual(overlap, 0)
|
|
142
|
+
|
|
143
|
+
def test_calculate_iou(self):
|
|
144
|
+
"""Test IoU (Intersection over Union) calculation."""
|
|
145
|
+
# Identical boxes
|
|
146
|
+
bbox1 = [0, 0, 100, 100]
|
|
147
|
+
bbox2 = [0, 0, 100, 100]
|
|
148
|
+
iou = calculate_iou(bbox1, bbox2)
|
|
149
|
+
self.assertEqual(iou, 1.0)
|
|
150
|
+
|
|
151
|
+
# Non-overlapping boxes
|
|
152
|
+
bbox1 = [0, 0, 50, 50]
|
|
153
|
+
bbox2 = [100, 100, 150, 150]
|
|
154
|
+
iou = calculate_iou(bbox1, bbox2)
|
|
155
|
+
self.assertEqual(iou, 0.0)
|
|
156
|
+
|
|
157
|
+
# Partially overlapping boxes
|
|
158
|
+
bbox1 = [0, 0, 100, 100]
|
|
159
|
+
bbox2 = [50, 50, 150, 150]
|
|
160
|
+
iou = calculate_iou(bbox1, bbox2)
|
|
161
|
+
# Intersection: 50*50 = 2500, Union: 10000 + 10000 - 2500 = 17500
|
|
162
|
+
expected_iou = 2500 / 17500
|
|
163
|
+
self.assertAlmostEqual(iou, expected_iou, places=5)
|
|
164
|
+
|
|
165
|
+
def test_get_bbox_area(self):
|
|
166
|
+
"""Test bounding box area calculation."""
|
|
167
|
+
# Rectangle
|
|
168
|
+
bbox = [0, 0, 100, 50]
|
|
169
|
+
area = get_bbox_area(bbox)
|
|
170
|
+
self.assertEqual(area, 5000)
|
|
171
|
+
|
|
172
|
+
# Square
|
|
173
|
+
bbox = [10, 10, 60, 60]
|
|
174
|
+
area = get_bbox_area(bbox)
|
|
175
|
+
self.assertEqual(area, 2500)
|
|
176
|
+
|
|
177
|
+
# Degenerate case (line)
|
|
178
|
+
bbox = [0, 0, 100, 0]
|
|
179
|
+
area = get_bbox_area(bbox)
|
|
180
|
+
self.assertEqual(area, 0)
|
|
181
|
+
|
|
182
|
+
# Point
|
|
183
|
+
bbox = [50, 50, 50, 50]
|
|
184
|
+
area = get_bbox_area(bbox)
|
|
185
|
+
self.assertEqual(area, 0)
|
|
186
|
+
|
|
187
|
+
def test_normalize_denormalize_bbox(self):
|
|
188
|
+
"""Test bbox normalization and denormalization."""
|
|
189
|
+
bbox = [100, 200, 300, 400]
|
|
190
|
+
image_size = (640, 480)
|
|
191
|
+
|
|
192
|
+
# Normalize
|
|
193
|
+
normalized = normalize_bbox(bbox, image_size)
|
|
194
|
+
expected = [100/640, 200/480, 300/640, 400/480]
|
|
195
|
+
self.assertEqual(normalized, expected)
|
|
196
|
+
|
|
197
|
+
# Denormalize
|
|
198
|
+
denormalized = denormalize_bbox(normalized, image_size)
|
|
199
|
+
self.assertEqual(denormalized, bbox)
|
|
200
|
+
|
|
201
|
+
# Round trip test
|
|
202
|
+
original = [50, 75, 150, 225]
|
|
203
|
+
normalized = normalize_bbox(original, image_size)
|
|
204
|
+
denormalized = denormalize_bbox(normalized, image_size)
|
|
205
|
+
self.assertEqual(denormalized, original)
|
|
206
|
+
|
|
207
|
+
def test_line_segments_intersect(self):
|
|
208
|
+
"""Test line segment intersection detection."""
|
|
209
|
+
# Intersecting lines
|
|
210
|
+
line1 = [[0, 0], [100, 100]]
|
|
211
|
+
line2 = [[0, 100], [100, 0]]
|
|
212
|
+
self.assertTrue(line_segments_intersect(line1, line2))
|
|
213
|
+
|
|
214
|
+
# Non-intersecting lines
|
|
215
|
+
line1 = [[0, 0], [50, 50]]
|
|
216
|
+
line2 = [[100, 100], [150, 150]]
|
|
217
|
+
self.assertFalse(line_segments_intersect(line1, line2))
|
|
218
|
+
|
|
219
|
+
# Parallel lines
|
|
220
|
+
line1 = [[0, 0], [100, 0]]
|
|
221
|
+
line2 = [[0, 50], [100, 50]]
|
|
222
|
+
self.assertFalse(line_segments_intersect(line1, line2))
|
|
223
|
+
|
|
224
|
+
# Touching at endpoint
|
|
225
|
+
line1 = [[0, 0], [50, 50]]
|
|
226
|
+
line2 = [[50, 50], [100, 100]]
|
|
227
|
+
self.assertTrue(line_segments_intersect(line1, line2))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class TestFormatUtils(BasePostProcessingTest):
|
|
231
|
+
"""Test format utility functions."""
|
|
232
|
+
|
|
233
|
+
def test_convert_to_coco_format(self):
|
|
234
|
+
"""Test conversion to COCO format."""
|
|
235
|
+
detections = [
|
|
236
|
+
{"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person"},
|
|
237
|
+
{"bbox": [100, 100, 150, 200], "confidence": 0.9, "category": "car"}
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
coco_format = convert_to_coco_format(detections)
|
|
241
|
+
|
|
242
|
+
# Check structure
|
|
243
|
+
self.assertIn("annotations", coco_format)
|
|
244
|
+
self.assertIn("categories", coco_format)
|
|
245
|
+
|
|
246
|
+
# Check annotations
|
|
247
|
+
annotations = coco_format["annotations"]
|
|
248
|
+
self.assertEqual(len(annotations), 2)
|
|
249
|
+
|
|
250
|
+
# Check first annotation
|
|
251
|
+
ann = annotations[0]
|
|
252
|
+
self.assertEqual(ann["bbox"], [10, 20, 40, 40]) # COCO format: [x, y, width, height]
|
|
253
|
+
self.assertEqual(ann["score"], 0.8)
|
|
254
|
+
self.assertEqual(ann["area"], 1600) # 40 * 40
|
|
255
|
+
|
|
256
|
+
def test_convert_to_yolo_format(self):
|
|
257
|
+
"""Test conversion to YOLO format."""
|
|
258
|
+
detections = [
|
|
259
|
+
{"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person"}
|
|
260
|
+
]
|
|
261
|
+
image_size = (640, 480)
|
|
262
|
+
|
|
263
|
+
yolo_format = convert_to_yolo_format(detections, image_size)
|
|
264
|
+
|
|
265
|
+
self.assertEqual(len(yolo_format), 1)
|
|
266
|
+
|
|
267
|
+
yolo_det = yolo_format[0]
|
|
268
|
+
# YOLO format: [class, confidence, center_x_norm, center_y_norm, width_norm, height_norm]
|
|
269
|
+
self.assertEqual(yolo_det["class"], 0) # Assuming first category gets index 0
|
|
270
|
+
self.assertEqual(yolo_det["confidence"], 0.8)
|
|
271
|
+
|
|
272
|
+
# Check normalized coordinates
|
|
273
|
+
expected_center_x = (10 + 50) / 2 / 640 # 30 / 640
|
|
274
|
+
expected_center_y = (20 + 60) / 2 / 480 # 40 / 480
|
|
275
|
+
expected_width = 40 / 640
|
|
276
|
+
expected_height = 40 / 480
|
|
277
|
+
|
|
278
|
+
self.assertAlmostEqual(yolo_det["bbox"][0], expected_center_x, places=5)
|
|
279
|
+
self.assertAlmostEqual(yolo_det["bbox"][1], expected_center_y, places=5)
|
|
280
|
+
self.assertAlmostEqual(yolo_det["bbox"][2], expected_width, places=5)
|
|
281
|
+
self.assertAlmostEqual(yolo_det["bbox"][3], expected_height, places=5)
|
|
282
|
+
|
|
283
|
+
def test_convert_to_tracking_format(self):
|
|
284
|
+
"""Test conversion to tracking format."""
|
|
285
|
+
detections = [
|
|
286
|
+
{"bbox": [10, 20, 50, 60], "confidence": 0.8, "category": "person", "detection_id": 1}
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
tracking_format = convert_to_tracking_format(detections)
|
|
290
|
+
|
|
291
|
+
self.assertEqual(len(tracking_format), 1)
|
|
292
|
+
|
|
293
|
+
track = tracking_format[0]
|
|
294
|
+
self.assertIn("track_id", track)
|
|
295
|
+
self.assertEqual(track["bbox"], [10, 20, 50, 60])
|
|
296
|
+
self.assertEqual(track["confidence"], 0.8)
|
|
297
|
+
self.assertEqual(track["category"], "person")
|
|
298
|
+
|
|
299
|
+
def test_match_results_structure(self):
|
|
300
|
+
"""Test result structure matching."""
|
|
301
|
+
# Detection format
|
|
302
|
+
detections = [{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"}]
|
|
303
|
+
format_type = match_results_structure(detections)
|
|
304
|
+
self.assertEqual(format_type.value, "detection")
|
|
305
|
+
|
|
306
|
+
# Tracking format
|
|
307
|
+
tracks = [{"track_id": 1, "bbox": [0, 0, 100, 100], "confidence": 0.8, "frame": 1}]
|
|
308
|
+
format_type = match_results_structure(tracks)
|
|
309
|
+
self.assertEqual(format_type.value, "tracking")
|
|
310
|
+
|
|
311
|
+
# Empty data
|
|
312
|
+
format_type = match_results_structure([])
|
|
313
|
+
self.assertEqual(format_type.value, "unknown")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestFilterUtils(BasePostProcessingTest):
|
|
317
|
+
"""Test filter utility functions."""
|
|
318
|
+
|
|
319
|
+
def test_filter_by_confidence(self):
|
|
320
|
+
"""Test confidence filtering."""
|
|
321
|
+
detections = [
|
|
322
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.3, "category": "person"},
|
|
323
|
+
{"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "car"},
|
|
324
|
+
{"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "person"}
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
# Filter with threshold 0.5
|
|
328
|
+
filtered = filter_by_confidence(detections, 0.5)
|
|
329
|
+
self.assertEqual(len(filtered), 2)
|
|
330
|
+
|
|
331
|
+
# Check that low confidence detection was removed
|
|
332
|
+
confidences = [det["confidence"] for det in filtered]
|
|
333
|
+
self.assertNotIn(0.3, confidences)
|
|
334
|
+
self.assertIn(0.7, confidences)
|
|
335
|
+
self.assertIn(0.9, confidences)
|
|
336
|
+
|
|
337
|
+
# Filter with high threshold
|
|
338
|
+
filtered = filter_by_confidence(detections, 0.8)
|
|
339
|
+
self.assertEqual(len(filtered), 1)
|
|
340
|
+
self.assertEqual(filtered[0]["confidence"], 0.9)
|
|
341
|
+
|
|
342
|
+
def test_filter_by_categories(self):
|
|
343
|
+
"""Test category filtering."""
|
|
344
|
+
detections = [
|
|
345
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
|
|
346
|
+
{"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "car"},
|
|
347
|
+
{"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "bike"}
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
# Filter for specific categories
|
|
351
|
+
filtered = filter_by_categories(detections, ["person", "bike"])
|
|
352
|
+
self.assertEqual(len(filtered), 2)
|
|
353
|
+
|
|
354
|
+
categories = [det["category"] for det in filtered]
|
|
355
|
+
self.assertIn("person", categories)
|
|
356
|
+
self.assertIn("bike", categories)
|
|
357
|
+
self.assertNotIn("car", categories)
|
|
358
|
+
|
|
359
|
+
# Filter for single category
|
|
360
|
+
filtered = filter_by_categories(detections, ["person"])
|
|
361
|
+
self.assertEqual(len(filtered), 1)
|
|
362
|
+
self.assertEqual(filtered[0]["category"], "person")
|
|
363
|
+
|
|
364
|
+
def test_filter_by_area(self):
|
|
365
|
+
"""Test area-based filtering."""
|
|
366
|
+
detections = [
|
|
367
|
+
{"bbox": [0, 0, 10, 10], "confidence": 0.8, "category": "person"}, # Area: 100
|
|
368
|
+
{"bbox": [0, 0, 50, 50], "confidence": 0.8, "category": "person"}, # Area: 2500
|
|
369
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"} # Area: 10000
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
# Filter by minimum area
|
|
373
|
+
filtered = filter_by_area(detections, min_area=1000)
|
|
374
|
+
self.assertEqual(len(filtered), 2) # Should exclude the smallest
|
|
375
|
+
|
|
376
|
+
# Filter by maximum area
|
|
377
|
+
filtered = filter_by_area(detections, max_area=5000)
|
|
378
|
+
self.assertEqual(len(filtered), 2) # Should exclude the largest
|
|
379
|
+
|
|
380
|
+
# Filter by area range
|
|
381
|
+
filtered = filter_by_area(detections, min_area=1000, max_area=5000)
|
|
382
|
+
self.assertEqual(len(filtered), 1) # Should only include middle one
|
|
383
|
+
self.assertEqual(get_bbox_area(filtered[0]["bbox"]), 2500)
|
|
384
|
+
|
|
385
|
+
def test_apply_category_mapping(self):
|
|
386
|
+
"""Test category mapping application."""
|
|
387
|
+
detections = [
|
|
388
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": 0},
|
|
389
|
+
{"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": 1},
|
|
390
|
+
{"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": 2}
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
# Apply mapping
|
|
394
|
+
mapping = {0: "person", 1: "car", 2: "bike"}
|
|
395
|
+
mapped = apply_category_mapping(detections, mapping)
|
|
396
|
+
|
|
397
|
+
self.assertEqual(len(mapped), 3)
|
|
398
|
+
categories = [det["category"] for det in mapped]
|
|
399
|
+
self.assertIn("person", categories)
|
|
400
|
+
self.assertIn("car", categories)
|
|
401
|
+
self.assertIn("bike", categories)
|
|
402
|
+
|
|
403
|
+
# Test with missing mapping
|
|
404
|
+
incomplete_mapping = {0: "person", 1: "car"}
|
|
405
|
+
mapped = apply_category_mapping(detections, incomplete_mapping)
|
|
406
|
+
|
|
407
|
+
# Should handle missing mapping gracefully
|
|
408
|
+
self.assertEqual(len(mapped), 3)
|
|
409
|
+
self.assertEqual(mapped[2]["category"], 2) # Unchanged
|
|
410
|
+
|
|
411
|
+
def test_remove_duplicate_detections(self):
|
|
412
|
+
"""Test duplicate detection removal."""
|
|
413
|
+
detections = [
|
|
414
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
|
|
415
|
+
{"bbox": [5, 5, 105, 105], "confidence": 0.7, "category": "person"}, # Similar bbox
|
|
416
|
+
{"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "car"}
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
# Remove duplicates with IoU threshold
|
|
420
|
+
unique = remove_duplicate_detections(detections, iou_threshold=0.5)
|
|
421
|
+
|
|
422
|
+
# Should remove one of the overlapping detections
|
|
423
|
+
self.assertLessEqual(len(unique), 2)
|
|
424
|
+
|
|
425
|
+
# Higher confidence detection should be kept
|
|
426
|
+
if len(unique) == 2:
|
|
427
|
+
confidences = [det["confidence"] for det in unique]
|
|
428
|
+
self.assertIn(0.8, confidences) # Higher confidence from overlapping pair
|
|
429
|
+
self.assertIn(0.9, confidences) # Non-overlapping detection
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestCountingUtils(BasePostProcessingTest):
|
|
433
|
+
"""Test counting utility functions."""
|
|
434
|
+
|
|
435
|
+
def test_count_objects_by_category(self):
|
|
436
|
+
"""Test object counting by category."""
|
|
437
|
+
detections = [
|
|
438
|
+
{"bbox": [0, 0, 100, 100], "confidence": 0.8, "category": "person"},
|
|
439
|
+
{"bbox": [100, 100, 200, 200], "confidence": 0.7, "category": "person"},
|
|
440
|
+
{"bbox": [200, 200, 300, 300], "confidence": 0.9, "category": "car"},
|
|
441
|
+
{"bbox": [300, 300, 400, 400], "confidence": 0.6, "category": "bike"}
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
counts = count_objects_by_category(detections)
|
|
445
|
+
|
|
446
|
+
self.assertEqual(counts["person"], 2)
|
|
447
|
+
self.assertEqual(counts["car"], 1)
|
|
448
|
+
self.assertEqual(counts["bike"], 1)
|
|
449
|
+
self.assertEqual(len(counts), 3)
|
|
450
|
+
|
|
451
|
+
def test_count_objects_in_zones(self):
|
|
452
|
+
"""Test zone-based object counting."""
|
|
453
|
+
detections = [
|
|
454
|
+
{"bbox": [25, 25, 75, 75], "confidence": 0.8, "category": "person"}, # In zone1
|
|
455
|
+
{"bbox": [125, 25, 175, 75], "confidence": 0.7, "category": "person"}, # In zone2
|
|
456
|
+
{"bbox": [225, 225, 275, 275], "confidence": 0.9, "category": "car"} # Outside zones
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
zones = {
|
|
460
|
+
"zone1": [[0, 0], [100, 0], [100, 100], [0, 100]],
|
|
461
|
+
"zone2": [[100, 0], [200, 0], [200, 100], [100, 100]]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
zone_counts = count_objects_in_zones(detections, zones)
|
|
465
|
+
|
|
466
|
+
self.assertEqual(zone_counts["zone1"]["total"], 1)
|
|
467
|
+
self.assertEqual(zone_counts["zone2"]["total"], 1)
|
|
468
|
+
self.assertEqual(zone_counts["zone1"]["by_category"]["person"], 1)
|
|
469
|
+
self.assertEqual(zone_counts["zone2"]["by_category"]["person"], 1)
|
|
470
|
+
|
|
471
|
+
def test_count_unique_tracks(self):
|
|
472
|
+
"""Test unique track counting."""
|
|
473
|
+
tracks = [
|
|
474
|
+
{"track_id": 1, "bbox": [0, 0, 100, 100], "confidence": 0.8, "frame": 1},
|
|
475
|
+
{"track_id": 1, "bbox": [10, 10, 110, 110], "confidence": 0.8, "frame": 2},
|
|
476
|
+
{"track_id": 2, "bbox": [200, 200, 300, 300], "confidence": 0.7, "frame": 1},
|
|
477
|
+
{"track_id": 2, "bbox": [210, 210, 310, 310], "confidence": 0.7, "frame": 2}
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
unique_count = count_unique_tracks(tracks)
|
|
481
|
+
self.assertEqual(unique_count, 2)
|
|
482
|
+
|
|
483
|
+
# Test with categories
|
|
484
|
+
tracks_with_categories = [
|
|
485
|
+
{"track_id": 1, "category": "person", "frame": 1},
|
|
486
|
+
{"track_id": 2, "category": "person", "frame": 1},
|
|
487
|
+
{"track_id": 3, "category": "car", "frame": 1}
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
category_counts = count_unique_tracks(tracks_with_categories, by_category=True)
|
|
491
|
+
self.assertEqual(category_counts["person"], 2)
|
|
492
|
+
self.assertEqual(category_counts["car"], 1)
|
|
493
|
+
|
|
494
|
+
def test_calculate_counting_summary(self):
|
|
495
|
+
"""Test comprehensive counting summary."""
|
|
496
|
+
detections = [
|
|
497
|
+
{"bbox": [25, 25, 75, 75], "confidence": 0.8, "category": "person"},
|
|
498
|
+
{"bbox": [125, 25, 175, 75], "confidence": 0.7, "category": "person"},
|
|
499
|
+
{"bbox": [225, 225, 275, 275], "confidence": 0.9, "category": "car"}
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
zones = {
|
|
503
|
+
"zone1": [[0, 0], [100, 0], [100, 100], [0, 100]]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
summary = calculate_counting_summary(detections, zones=zones)
|
|
507
|
+
|
|
508
|
+
# Check summary structure
|
|
509
|
+
self.assertIn("total_count", summary)
|
|
510
|
+
self.assertIn("category_counts", summary)
|
|
511
|
+
self.assertIn("zone_analysis", summary)
|
|
512
|
+
self.assertIn("confidence_stats", summary)
|
|
513
|
+
|
|
514
|
+
# Check values
|
|
515
|
+
self.assertEqual(summary["total_count"], 3)
|
|
516
|
+
self.assertEqual(summary["category_counts"]["person"], 2)
|
|
517
|
+
self.assertEqual(summary["category_counts"]["car"], 1)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class TestTrackingUtils(BasePostProcessingTest):
|
|
521
|
+
"""Test tracking utility functions."""
|
|
522
|
+
|
|
523
|
+
def test_track_objects_in_zone(self):
|
|
524
|
+
"""Test zone-based object tracking."""
|
|
525
|
+
tracks = [
|
|
526
|
+
{"track_id": 1, "bbox": [25, 25, 75, 75], "frame": 1}, # In zone
|
|
527
|
+
{"track_id": 1, "bbox": [125, 125, 175, 175], "frame": 2}, # Outside zone
|
|
528
|
+
{"track_id": 2, "bbox": [50, 50, 100, 100], "frame": 1} # In zone
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
zone = [[0, 0], [100, 0], [100, 100], [0, 100]]
|
|
532
|
+
|
|
533
|
+
zone_tracks = track_objects_in_zone(tracks, zone)
|
|
534
|
+
|
|
535
|
+
# Should track objects that entered the zone
|
|
536
|
+
self.assertGreater(len(zone_tracks), 0)
|
|
537
|
+
|
|
538
|
+
# Check that we have tracking information
|
|
539
|
+
for track_info in zone_tracks:
|
|
540
|
+
self.assertIn("track_id", track_info)
|
|
541
|
+
self.assertIn("enter_time", track_info)
|
|
542
|
+
|
|
543
|
+
def test_detect_line_crossings(self):
|
|
544
|
+
"""Test line crossing detection."""
|
|
545
|
+
# Create tracks that cross a line
|
|
546
|
+
line = [[100, 0], [100, 200]]
|
|
547
|
+
tracks = create_line_crossing_data({"test_line": line}, num_tracks=3, frames=10)
|
|
548
|
+
|
|
549
|
+
crossings = detect_line_crossings(tracks, {"test_line": line})
|
|
550
|
+
|
|
551
|
+
# Should detect crossings
|
|
552
|
+
self.assertIn("test_line", crossings)
|
|
553
|
+
self.assertGreater(len(crossings["test_line"]), 0)
|
|
554
|
+
|
|
555
|
+
# Check crossing information
|
|
556
|
+
for crossing in crossings["test_line"]:
|
|
557
|
+
self.assertIn("track_id", crossing)
|
|
558
|
+
self.assertIn("crossing_frame", crossing)
|
|
559
|
+
self.assertIn("direction", crossing)
|
|
560
|
+
|
|
561
|
+
def test_analyze_track_movements(self):
|
|
562
|
+
"""Test track movement analysis."""
|
|
563
|
+
tracks = [
|
|
564
|
+
{"track_id": 1, "bbox": [0, 0, 50, 50], "frame": 1, "timestamp": 1.0},
|
|
565
|
+
{"track_id": 1, "bbox": [10, 10, 60, 60], "frame": 2, "timestamp": 2.0},
|
|
566
|
+
{"track_id": 1, "bbox": [20, 20, 70, 70], "frame": 3, "timestamp": 3.0}
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
movements = analyze_track_movements(tracks)
|
|
570
|
+
|
|
571
|
+
# Should have movement analysis for track 1
|
|
572
|
+
self.assertIn(1, movements)
|
|
573
|
+
|
|
574
|
+
track_movement = movements[1]
|
|
575
|
+
self.assertIn("total_distance", track_movement)
|
|
576
|
+
self.assertIn("average_speed", track_movement)
|
|
577
|
+
self.assertIn("direction_changes", track_movement)
|
|
578
|
+
|
|
579
|
+
# Check calculated values
|
|
580
|
+
self.assertGreater(track_movement["total_distance"], 0)
|
|
581
|
+
self.assertGreater(track_movement["average_speed"], 0)
|
|
582
|
+
|
|
583
|
+
def test_filter_tracks_by_duration(self):
|
|
584
|
+
"""Test track filtering by duration."""
|
|
585
|
+
tracks = [
|
|
586
|
+
{"track_id": 1, "frame": 1, "timestamp": 1.0},
|
|
587
|
+
{"track_id": 1, "frame": 2, "timestamp": 2.0},
|
|
588
|
+
{"track_id": 1, "frame": 3, "timestamp": 3.0}, # Duration: 2 seconds
|
|
589
|
+
{"track_id": 2, "frame": 1, "timestamp": 1.0},
|
|
590
|
+
{"track_id": 2, "frame": 2, "timestamp": 6.0} # Duration: 5 seconds
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
# Filter tracks with minimum duration
|
|
594
|
+
long_tracks = filter_tracks_by_duration(tracks, min_duration=3.0)
|
|
595
|
+
|
|
596
|
+
# Should only include track 2
|
|
597
|
+
track_ids = set(track["track_id"] for track in long_tracks)
|
|
598
|
+
self.assertIn(2, track_ids)
|
|
599
|
+
self.assertNotIn(1, track_ids)
|
|
600
|
+
|
|
601
|
+
# Filter tracks with maximum duration
|
|
602
|
+
short_tracks = filter_tracks_by_duration(tracks, max_duration=3.0)
|
|
603
|
+
|
|
604
|
+
# Should only include track 1
|
|
605
|
+
track_ids = set(track["track_id"] for track in short_tracks)
|
|
606
|
+
self.assertIn(1, track_ids)
|
|
607
|
+
self.assertNotIn(2, track_ids)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class TestUtilsIntegration(BasePostProcessingTest):
|
|
611
|
+
"""Integration tests for utility functions."""
|
|
612
|
+
|
|
613
|
+
def test_detection_processing_pipeline(self):
|
|
614
|
+
"""Test complete detection processing pipeline."""
|
|
615
|
+
# Create test data
|
|
616
|
+
detections = create_detection_results(
|
|
617
|
+
num_detections=20,
|
|
618
|
+
categories=["person", "car", "bike"],
|
|
619
|
+
confidence_range=(0.3, 0.95)
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Step 1: Filter by confidence
|
|
623
|
+
filtered = filter_by_confidence(detections, 0.5)
|
|
624
|
+
|
|
625
|
+
# Step 2: Apply category mapping
|
|
626
|
+
mapping = {"person": "pedestrian", "car": "vehicle", "bike": "bicycle"}
|
|
627
|
+
mapped = []
|
|
628
|
+
for det in filtered:
|
|
629
|
+
if det["category"] in mapping:
|
|
630
|
+
det_copy = det.copy()
|
|
631
|
+
det_copy["category"] = mapping[det["category"]]
|
|
632
|
+
mapped.append(det_copy)
|
|
633
|
+
else:
|
|
634
|
+
mapped.append(det)
|
|
635
|
+
|
|
636
|
+
# Step 3: Remove duplicates
|
|
637
|
+
unique = remove_duplicate_detections(mapped, iou_threshold=0.5)
|
|
638
|
+
|
|
639
|
+
# Step 4: Count by category
|
|
640
|
+
counts = count_objects_by_category(unique)
|
|
641
|
+
|
|
642
|
+
# Verify pipeline
|
|
643
|
+
self.assertLessEqual(len(unique), len(detections)) # Should filter some
|
|
644
|
+
self.assertGreater(len(counts), 0) # Should have categories
|
|
645
|
+
|
|
646
|
+
# Check that mapping worked
|
|
647
|
+
original_categories = set(det["category"] for det in detections)
|
|
648
|
+
final_categories = set(det["category"] for det in unique)
|
|
649
|
+
|
|
650
|
+
# Should have some mapped categories
|
|
651
|
+
if "person" in original_categories:
|
|
652
|
+
self.assertIn("pedestrian", final_categories)
|
|
653
|
+
|
|
654
|
+
def test_tracking_analysis_pipeline(self):
|
|
655
|
+
"""Test complete tracking analysis pipeline."""
|
|
656
|
+
# Create tracking data
|
|
657
|
+
tracks = create_tracking_results(num_tracks=5, frames=15)
|
|
658
|
+
|
|
659
|
+
# Define zones and lines
|
|
660
|
+
zones = create_zone_polygons(["entrance", "lobby"])
|
|
661
|
+
lines = {"crossing_line": [[200, 0], [200, 400]]}
|
|
662
|
+
|
|
663
|
+
# Step 1: Filter by duration
|
|
664
|
+
long_tracks = filter_tracks_by_duration(tracks, min_duration=0.1)
|
|
665
|
+
|
|
666
|
+
# Step 2: Analyze movements
|
|
667
|
+
movements = analyze_track_movements(long_tracks)
|
|
668
|
+
|
|
669
|
+
# Step 3: Count in zones
|
|
670
|
+
zone_counts = count_objects_in_zones(long_tracks, zones)
|
|
671
|
+
|
|
672
|
+
# Step 4: Detect line crossings
|
|
673
|
+
crossings = detect_line_crossings(long_tracks, lines)
|
|
674
|
+
|
|
675
|
+
# Verify pipeline
|
|
676
|
+
self.assertGreater(len(movements), 0)
|
|
677
|
+
self.assertIn("entrance", zone_counts)
|
|
678
|
+
self.assertIn("lobby", zone_counts)
|
|
679
|
+
self.assertIn("crossing_line", crossings)
|
|
680
|
+
|
|
681
|
+
# Check movement analysis
|
|
682
|
+
for track_id, movement in movements.items():
|
|
683
|
+
self.assertIn("total_distance", movement)
|
|
684
|
+
self.assertIn("average_speed", movement)
|
|
685
|
+
|
|
686
|
+
def test_format_conversion_roundtrip(self):
|
|
687
|
+
"""Test format conversion round trip."""
|
|
688
|
+
# Create detection data
|
|
689
|
+
original_detections = create_detection_results(5)
|
|
690
|
+
|
|
691
|
+
# Convert to COCO format
|
|
692
|
+
coco_format = convert_to_coco_format(original_detections)
|
|
693
|
+
|
|
694
|
+
# Convert to tracking format
|
|
695
|
+
tracking_format = convert_detection_to_tracking_format(original_detections)
|
|
696
|
+
|
|
697
|
+
# Convert back to detection format
|
|
698
|
+
back_to_detection = convert_tracking_to_detection_format(tracking_format)
|
|
699
|
+
|
|
700
|
+
# Verify conversions
|
|
701
|
+
self.assertEqual(len(back_to_detection), len(original_detections))
|
|
702
|
+
|
|
703
|
+
# Check that essential information is preserved
|
|
704
|
+
for orig, converted in zip(original_detections, back_to_detection):
|
|
705
|
+
self.assertEqual(orig["bbox"], converted["bbox"])
|
|
706
|
+
self.assertEqual(orig["confidence"], converted["confidence"])
|
|
707
|
+
self.assertEqual(orig["category"], converted["category"])
|
|
708
|
+
|
|
709
|
+
def test_edge_cases_handling(self):
|
|
710
|
+
"""Test utility functions with edge cases."""
|
|
711
|
+
edge_cases = create_edge_case_data()
|
|
712
|
+
|
|
713
|
+
# Test with empty data
|
|
714
|
+
empty_data = edge_cases["empty_results"]
|
|
715
|
+
|
|
716
|
+
# All functions should handle empty data gracefully
|
|
717
|
+
self.assertEqual(filter_by_confidence(empty_data, 0.5), [])
|
|
718
|
+
self.assertEqual(count_objects_by_category(empty_data), {})
|
|
719
|
+
self.assertEqual(count_unique_tracks(empty_data), 0)
|
|
720
|
+
|
|
721
|
+
# Test with single detection
|
|
722
|
+
single_detection = edge_cases["single_detection"]
|
|
723
|
+
|
|
724
|
+
filtered = filter_by_confidence(single_detection, 0.3)
|
|
725
|
+
self.assertGreaterEqual(len(filtered), 0)
|
|
726
|
+
|
|
727
|
+
counts = count_objects_by_category(single_detection)
|
|
728
|
+
self.assertGreater(len(counts), 0)
|
|
729
|
+
|
|
730
|
+
# Test with boundary cases
|
|
731
|
+
boundary_data = edge_cases["boundary_bboxes"]
|
|
732
|
+
|
|
733
|
+
# Should handle boundary cases without errors
|
|
734
|
+
for bbox_data in boundary_data:
|
|
735
|
+
area = get_bbox_area(bbox_data["bbox"])
|
|
736
|
+
self.assertGreaterEqual(area, 0)
|
|
737
|
+
|
|
738
|
+
center = get_bbox_center(bbox_data["bbox"])
|
|
739
|
+
self.assertEqual(len(center), 2)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
if __name__ == "__main__":
|
|
743
|
+
unittest.main()
|