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.
Files changed (196) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +146 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3291 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  44. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  45. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  46. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  47. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  48. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  49. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  50. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  51. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  52. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  53. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  54. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  55. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  56. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  57. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  58. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  59. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  60. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  61. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  62. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  63. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  64. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  65. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  66. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  67. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  68. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  69. matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
  70. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  71. matrice_analytics/post_processing/post_processor.py +1175 -0
  72. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  73. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  74. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  75. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  76. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  77. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  78. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  79. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  80. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  81. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  82. matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
  83. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  84. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  85. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  86. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  87. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  88. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  89. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  90. matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
  91. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  92. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  93. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  94. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  95. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  96. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  97. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  98. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  99. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  100. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  101. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  102. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  103. matrice_analytics/post_processing/usecases/color/clip.py +660 -0
  104. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  105. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  106. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  107. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  108. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  109. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  110. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  111. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  112. matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
  113. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  114. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  115. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  116. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  117. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  118. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  119. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
  120. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  121. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  122. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  123. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  124. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  125. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  126. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  127. matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
  128. matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
  129. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  130. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  131. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  132. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  133. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  134. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  135. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  136. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  137. matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
  138. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
  139. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  140. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  141. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  142. matrice_analytics/post_processing/usecases/parking.py +787 -0
  143. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  144. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  145. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  146. matrice_analytics/post_processing/usecases/people_counting.py +706 -0
  147. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  148. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  149. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  150. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  151. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  152. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  153. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  154. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  155. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  156. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  157. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  158. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  159. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  160. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  161. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  162. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  163. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  164. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  165. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  166. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  167. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  168. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  169. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  170. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
  171. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  172. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  173. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  174. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  175. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  176. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  177. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  178. matrice_analytics/post_processing/utils/__init__.py +150 -0
  179. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  180. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  181. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  182. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  183. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  184. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  185. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  186. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  187. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  188. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  189. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  190. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  191. matrice_analytics/py.typed +0 -0
  192. matrice_analytics-0.1.60.dist-info/METADATA +481 -0
  193. matrice_analytics-0.1.60.dist-info/RECORD +196 -0
  194. matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
  195. matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
  196. matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
@@ -0,0 +1,94 @@
1
+ """
2
+ Category mapping utilities for post-processing operations.
3
+ """
4
+
5
+ from typing import List, Dict, Any
6
+ from .format_utils import match_results_structure
7
+
8
+
9
+ class CategoryMappingLibrary:
10
+ """Library class for handling category mapping operations."""
11
+
12
+ def __init__(self, index_to_category: Dict[int, str] = None):
13
+ self.index_to_category = index_to_category or {}
14
+
15
+ def map_results(self, results: Any) -> Any:
16
+ """Map category indices to category names in results."""
17
+ if not self.index_to_category:
18
+ return results
19
+
20
+ results_type = match_results_structure(results)
21
+
22
+ if results_type == "detection":
23
+ return self._map_detection_results(results)
24
+ elif results_type == "classification":
25
+ return self._map_classification_results(results)
26
+ elif results_type == "object_tracking":
27
+ return self._map_tracking_results(results)
28
+ elif results_type == "activity_recognition":
29
+ return self._map_activity_results(results)
30
+
31
+ return results
32
+
33
+ def _map_detection_results(self, results: List[Dict]) -> List[Dict]:
34
+ """Map categories in detection results."""
35
+ mapped_results = []
36
+ for result in results:
37
+ mapped_result = result.copy()
38
+ if "category" in result and isinstance(result["category"], int):
39
+ if result["category"] in self.index_to_category:
40
+ mapped_result["category"] = self.index_to_category[result["category"]]
41
+ mapped_results.append(mapped_result)
42
+ return mapped_results
43
+
44
+ def _map_classification_results(self, results: Dict) -> Dict:
45
+ """Map categories in classification results."""
46
+ mapped_results = {}
47
+ for key, value in results.items():
48
+ if isinstance(value, int) and value in self.index_to_category:
49
+ mapped_results[key] = self.index_to_category[value]
50
+ else:
51
+ mapped_results[key] = value
52
+ return mapped_results
53
+
54
+ def _map_tracking_results(self, results: Dict) -> Dict:
55
+ """Map categories in tracking results."""
56
+ mapped_results = {}
57
+ for frame_id, detections in results.items():
58
+ if isinstance(detections, list):
59
+ mapped_results[frame_id] = self._map_detection_results(detections)
60
+ else:
61
+ mapped_results[frame_id] = detections
62
+ return mapped_results
63
+
64
+ def _map_activity_results(self, results: List[Dict]) -> List[Dict]:
65
+ """Map categories in activity recognition results."""
66
+ return self._map_detection_results(results)
67
+
68
+
69
+ def apply_category_mapping(results: Any, index_to_category: Dict[int, str]) -> Any:
70
+ """
71
+ Convenience function to apply category mapping to results.
72
+
73
+ Args:
74
+ results: Raw results to map
75
+ index_to_category: Mapping from indices to category names
76
+
77
+ Returns:
78
+ Results with mapped categories
79
+ """
80
+ mapper = CategoryMappingLibrary(index_to_category)
81
+ return mapper.map_results(results)
82
+
83
+
84
+ def create_category_mapper(index_to_category: Dict[int, str]) -> CategoryMappingLibrary:
85
+ """
86
+ Create a category mapper instance.
87
+
88
+ Args:
89
+ index_to_category: Mapping from indices to category names
90
+
91
+ Returns:
92
+ CategoryMappingLibrary instance
93
+ """
94
+ return CategoryMappingLibrary(index_to_category)
@@ -0,0 +1,592 @@
1
+ """
2
+ Color processing utilities for extracting colors from detected objects in video frames.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ import json
8
+ import tempfile
9
+ import os
10
+ from typing import List, Dict, Any, Tuple, Optional
11
+ from collections import defaultdict
12
+ from datetime import datetime, timedelta
13
+ import logging
14
+ from sklearn.cluster import KMeans
15
+ from skimage import color
16
+ from matplotlib import colors as mcolors
17
+ import numpy as np
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Try to import sklearn at module level with fallback
21
+ try:
22
+ from sklearn.cluster import KMeans
23
+ SKLEARN_AVAILABLE = True
24
+ logger.debug("sklearn successfully imported for color clustering")
25
+ except ImportError:
26
+ SKLEARN_AVAILABLE = False
27
+ logger.warning("sklearn not available, using fallback color extraction method")
28
+ except RuntimeError as e:
29
+ # Handle the specific "can't register atexit after shutdown" error
30
+ if "atexit" in str(e):
31
+ SKLEARN_AVAILABLE = False
32
+ logger.warning(f"sklearn import failed due to shutdown race condition: {e}. Using fallback method.")
33
+ else:
34
+ raise
35
+
36
+ # Color extraction functions
37
+ def extract_major_colors(image: np.ndarray, k: int = 3) -> List[Tuple[str, str, float]]:
38
+ """
39
+ Extract the major colors from an image using K-means clustering.
40
+
41
+ Args:
42
+ image: Input image as numpy array (RGB format)
43
+ k: Number of dominant colors to extract
44
+
45
+ Returns:
46
+ List of tuples containing (color_name, hex_color, percentage)
47
+ """
48
+ if not SKLEARN_AVAILABLE:
49
+ logger.debug("Using OpenCV fallback method for color extraction")
50
+ return _extract_major_colors_opencv_fallback(image, k)
51
+
52
+ try:
53
+ # Use sklearn method
54
+ return _extract_major_colors_sklearn(image, k)
55
+ except Exception as e:
56
+ logger.warning(f"sklearn color extraction failed: {e}. Using OpenCV fallback.")
57
+ return _extract_major_colors_opencv_fallback(image, k)
58
+
59
+
60
+ def _extract_major_colors_sklearn(image: np.ndarray, k: int = 3) -> List[Tuple[str, str, float]]:
61
+ """Extract major colors using sklearn KMeans clustering."""
62
+ # Reshape image to be a list of pixels
63
+ data = image.reshape((-1, 3))
64
+ data = np.float32(data)
65
+
66
+ # Apply sklearn K-means clustering
67
+ kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
68
+ kmeans.fit(data)
69
+
70
+ # Get cluster centers and labels
71
+ centers = np.uint8(kmeans.cluster_centers_)
72
+ labels = kmeans.labels_
73
+
74
+ # Calculate percentages
75
+ unique_labels, counts = np.unique(labels, return_counts=True)
76
+ percentages = counts / len(labels)
77
+
78
+ # Convert to color names and hex
79
+ colors = []
80
+ for i, (center, percentage) in enumerate(zip(centers, percentages)):
81
+ hex_color = "#{:02x}{:02x}{:02x}".format(center[0], center[1], center[2])
82
+ color_name = _rgb_to_color_name(center)
83
+ colors.append((color_name, hex_color, float(percentage)))
84
+
85
+ # Sort by percentage (descending)
86
+ colors.sort(key=lambda x: x[2], reverse=True)
87
+
88
+ return colors
89
+
90
+
91
+ def _extract_major_colors_opencv_fallback(image: np.ndarray, k: int = 3) -> List[Tuple[str, str, float]]:
92
+ """Extract major colors using OpenCV's K-means clustering as fallback."""
93
+ try:
94
+ # Reshape image to be a list of pixels
95
+ data = image.reshape((-1, 3))
96
+ data = np.float32(data)
97
+
98
+ # Apply OpenCV K-means clustering
99
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
100
+ _, labels, centers = cv2.kmeans(data, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
101
+
102
+ # Convert centers to uint8
103
+ centers = np.uint8(centers)
104
+
105
+ # Calculate percentages
106
+ unique_labels, counts = np.unique(labels, return_counts=True)
107
+ percentages = counts / len(labels)
108
+
109
+ # Convert to color names and hex
110
+ colors = []
111
+ for i, (center, percentage) in enumerate(zip(centers, percentages)):
112
+ hex_color = "#{:02x}{:02x}{:02x}".format(center[0], center[1], center[2])
113
+ color_name = _rgb_to_color_name(center)
114
+ colors.append((color_name, hex_color, float(percentage)))
115
+
116
+ # Sort by percentage (descending)
117
+ colors.sort(key=lambda x: x[2], reverse=True)
118
+
119
+ return colors
120
+
121
+ except Exception as e:
122
+ logger.error(f"OpenCV color extraction failed: {e}. Using basic color analysis.")
123
+ return _extract_colors_basic_fallback(image, k)
124
+
125
+
126
+ def _extract_colors_basic_fallback(image: np.ndarray, k: int = 3) -> List[Tuple[str, str, float]]:
127
+ """Basic color extraction fallback using color channel analysis."""
128
+ try:
129
+ if image.size == 0:
130
+ return []
131
+
132
+ # Calculate average color
133
+ mean_color = np.mean(image.reshape(-1, 3), axis=0).astype(np.uint8)
134
+ hex_color = "#{:02x}{:02x}{:02x}".format(mean_color[0], mean_color[1], mean_color[2])
135
+ color_name = _rgb_to_color_name(mean_color)
136
+
137
+ # For simplicity, return the average color as the dominant color
138
+ return [(color_name, hex_color, 1.0)]
139
+
140
+ except Exception as e:
141
+ logger.error(f"Basic color extraction failed: {e}. Returning default.")
142
+ return [("unknown", "#808080", 1.0)] # Gray as default
143
+
144
+
145
+ # def _rgb_to_color_name(rgb: np.ndarray) -> str:
146
+ # """
147
+ # Convert RGB values to approximate color name.
148
+
149
+ # Args:
150
+ # rgb: RGB values as numpy array
151
+
152
+ # Returns:
153
+ # Color name as string
154
+ # """
155
+ # r, g, b = rgb
156
+
157
+ # # Simple color mapping based on dominant channel
158
+ # if r > g and r > b:
159
+ # if r > 200:
160
+ # return "red" if g < 100 and b < 100 else "pink"
161
+ # else:
162
+ # return "brown" if g > 100 or b > 100 else "dark_red"
163
+ # elif g > r and g > b:
164
+ # if g > 200:
165
+ # return "green" if r < 100 and b < 100 else "light_green"
166
+ # else:
167
+ # return "dark_green"
168
+ # elif b > r and b > g:
169
+ # if b > 200:
170
+ # return "blue" if r < 100 and g < 100 else "light_blue"
171
+ # else:
172
+ # return "dark_blue"
173
+ # else:
174
+ # # Similar values - grayscale or mixed
175
+ # avg = (r + g + b) / 3
176
+ # if avg > 200:
177
+ # return "white"
178
+ # elif avg < 50:
179
+ # return "black"
180
+ # else:
181
+ # return "gray"
182
+
183
+ XKCD_COLORS = {
184
+ name.replace("xkcd:", ""): mcolors.to_rgb(hex)
185
+ for name, hex in mcolors.XKCD_COLORS.items()
186
+ }
187
+
188
+ # Canonical colors you want to allow
189
+ CANONICAL_COLOR_NAMES = [
190
+ "brown", "red", "orange", "yellow", "green", "lime", "cyan",
191
+ "blue", "purple", "pink", "white", "grey", "black"
192
+ ]
193
+
194
+ # Canonical RGB values (you can adjust these if needed)
195
+ CANONICAL_COLOR_RGB = {
196
+ "brown": (150, 75, 0),
197
+ "red": (255, 0, 0),
198
+ "orange": (255, 165, 0),
199
+ "yellow": (255, 255, 0),
200
+ "green": (0, 128, 0),
201
+ "lime": (191, 255, 0),
202
+ "cyan": (0, 255, 255),
203
+ "blue": (0, 0, 255),
204
+ "purple": (128, 0, 128),
205
+ "pink": (255, 192, 203),
206
+ "white": (255, 255, 255),
207
+ "grey": (128, 128, 128),
208
+ "black": (0, 0, 0)
209
+ }
210
+
211
+ # Pre-convert to LAB for speed
212
+ CANONICAL_COLOR_LAB = {
213
+ name: color.rgb2lab([[np.array(rgb) / 255.0]])[0][0]
214
+ for name, rgb in CANONICAL_COLOR_RGB.items()
215
+ }
216
+
217
+ def _rgb_to_color_name(rgb: np.ndarray) -> str:
218
+ """
219
+ Convert an RGB color to the closest color in a fixed canonical set.
220
+
221
+ Args:
222
+ rgb: RGB triplet as np.ndarray or list (0–255)
223
+
224
+ Returns:
225
+ Closest canonical color name as string
226
+ """
227
+ rgb = np.array(rgb)
228
+ rgb_normalized = rgb / 255.0
229
+ input_lab = color.rgb2lab([[rgb_normalized]])[0][0]
230
+
231
+ min_dist = float('inf')
232
+ closest_name = "unknown"
233
+
234
+ for name, ref_lab in CANONICAL_COLOR_LAB.items():
235
+ dist = np.linalg.norm(input_lab - ref_lab)
236
+ if dist < min_dist:
237
+ min_dist = dist
238
+ closest_name = name
239
+
240
+ return closest_name
241
+
242
+
243
+
244
+
245
+ class VideoColorClassifier:
246
+ """
247
+ A comprehensive system for processing video frames with model predictions
248
+ and extracting color information from detected objects.
249
+ """
250
+
251
+ def __init__(self, top_k_colors: int = 3, min_confidence: float = 0.5):
252
+ """
253
+ Initialize the video color classifier.
254
+
255
+ Args:
256
+ top_k_colors: Number of top colors to extract per detection
257
+ min_confidence: Minimum confidence threshold for detections
258
+ """
259
+ self.top_k_colors = top_k_colors
260
+ self.min_confidence = min_confidence
261
+ self.detailed_results = []
262
+ self.summary_results = defaultdict(lambda: defaultdict(list))
263
+
264
+ def process_video_with_predictions(
265
+ self,
266
+ video_bytes: bytes,
267
+ predictions: Dict[str, List[Dict]],
268
+ output_dir: str = "./output",
269
+ fps: Optional[float] = None
270
+ ) -> Tuple[str, str]:
271
+ """
272
+ Main function to process video with model predictions and extract colors.
273
+
274
+ Args:
275
+ video_bytes: Raw video file bytes
276
+ predictions: Dict with frame_id -> list of detection dicts
277
+ output_dir: Directory to save output files
278
+ fps: Video FPS (will be auto-detected if not provided)
279
+
280
+ Returns:
281
+ Tuple of (detailed_results_path, summary_results_path)
282
+ """
283
+ # Create output directory
284
+ os.makedirs(output_dir, exist_ok=True)
285
+
286
+ # Create temporary video file
287
+ with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as temp_video:
288
+ temp_video.write(video_bytes)
289
+ temp_video_path = temp_video.name
290
+
291
+ try:
292
+ # Process video frame by frame
293
+ self._process_video_frames(temp_video_path, predictions, fps)
294
+
295
+ # Save detailed results
296
+ detailed_path = os.path.join(output_dir, "detailed_color_results.json")
297
+ self._save_detailed_results(detailed_path)
298
+
299
+ # Generate and save summary results
300
+ summary_path = os.path.join(output_dir, "color_summary_report.json")
301
+ self._generate_summary_report(summary_path, fps)
302
+
303
+ logger.info(f"Processing complete. Results saved to {output_dir}")
304
+ return detailed_path, summary_path
305
+
306
+ finally:
307
+ # Clean up temporary video file
308
+ if os.path.exists(temp_video_path):
309
+ os.unlink(temp_video_path)
310
+
311
+ def _process_video_frames(
312
+ self,
313
+ video_path: str,
314
+ predictions: Dict[str, List[Dict]],
315
+ fps: Optional[float] = None
316
+ ):
317
+ """
318
+ Process video frame by frame and extract colors from detections.
319
+ """
320
+ cap = cv2.VideoCapture(video_path)
321
+
322
+ if not cap.isOpened():
323
+ raise ValueError(f"Could not open video file: {video_path}")
324
+
325
+ # Get video properties
326
+ if fps is None:
327
+ fps = cap.get(cv2.CAP_PROP_FPS)
328
+
329
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
330
+
331
+ logger.info(f"Processing video: {total_frames} frames at {fps} FPS")
332
+
333
+ frame_count = 0
334
+
335
+ while True:
336
+ ret, frame = cap.read()
337
+ if not ret:
338
+ break
339
+
340
+ frame_id = str(frame_count)
341
+ timestamp = frame_count / fps
342
+
343
+ # Convert BGR to RGB
344
+ rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
345
+
346
+ # Process detections for this frame
347
+ if frame_id in predictions:
348
+ frame_detections = predictions[frame_id]
349
+ self._process_frame_detections(
350
+ rgb_frame, frame_detections, frame_id, timestamp
351
+ )
352
+
353
+ frame_count += 1
354
+
355
+ # Log progress
356
+ if frame_count % 100 == 0:
357
+ logger.info(f"Processed {frame_count}/{total_frames} frames")
358
+
359
+ cap.release()
360
+ logger.info(f"Completed processing {frame_count} frames")
361
+
362
+ def _process_frame_detections(
363
+ self,
364
+ frame: np.ndarray,
365
+ detections: List[Dict],
366
+ frame_id: str,
367
+ timestamp: float
368
+ ):
369
+ """
370
+ Process all detections in a single frame.
371
+ """
372
+ for detection in detections:
373
+ # Skip low confidence detections
374
+ if detection.get('confidence', 1.0) < self.min_confidence:
375
+ continue
376
+
377
+ # Extract detection information
378
+ bbox = detection.get('bounding_box', detection.get('bbox'))
379
+ category = detection.get('category', detection.get('class', 'unknown'))
380
+ track_id = detection.get('track_id', detection.get('id'))
381
+ confidence = detection.get('confidence', 1.0)
382
+
383
+ if bbox is None:
384
+ continue
385
+
386
+ # Crop object from frame
387
+ cropped_obj = self._crop_bbox(frame, bbox)
388
+
389
+ if cropped_obj.size == 0:
390
+ logger.warning(f"Empty crop for bbox: {bbox} in frame {frame_id}")
391
+ continue
392
+
393
+ # Extract colors
394
+ major_colors = extract_major_colors(cropped_obj, k=self.top_k_colors)
395
+ main_color = major_colors[0][0] if major_colors else "unknown"
396
+
397
+ # Create detailed result entry
398
+ detailed_entry = {
399
+ "frame_id": frame_id,
400
+ "timestamp": round(timestamp, 2),
401
+ "timestamp_formatted": self._format_timestamp(timestamp),
402
+ "track_id": track_id,
403
+ "category": category,
404
+ "confidence": round(confidence, 3),
405
+ "bbox": bbox,
406
+ "major_colors": major_colors,
407
+ "main_color": main_color,
408
+ "color_confidence": major_colors[0][2] if major_colors else 0.0
409
+ }
410
+
411
+ self.detailed_results.append(detailed_entry)
412
+
413
+ # Update summary data
414
+ self._update_summary_data(detailed_entry)
415
+
416
+ def _crop_bbox(self, image: np.ndarray, bbox: Dict[str, int]) -> np.ndarray:
417
+ """
418
+ Crop image using bbox coordinates with bounds checking.
419
+ """
420
+ h, w = image.shape[:2]
421
+
422
+ # Handle different bbox formats
423
+ if 'xmin' in bbox:
424
+ xmin = max(0, int(bbox["xmin"]))
425
+ ymin = max(0, int(bbox["ymin"]))
426
+ xmax = min(w, int(bbox["xmax"]))
427
+ ymax = min(h, int(bbox["ymax"]))
428
+ elif 'x1' in bbox:
429
+ xmin = max(0, int(bbox["x1"]))
430
+ ymin = max(0, int(bbox["y1"]))
431
+ xmax = min(w, int(bbox["x2"]))
432
+ ymax = min(h, int(bbox["y2"]))
433
+ else:
434
+ # Assume [x1, y1, x2, y2] format
435
+ values = list(bbox.values()) if isinstance(bbox, dict) else bbox
436
+ xmin = max(0, int(values[0]))
437
+ ymin = max(0, int(values[1]))
438
+ xmax = min(w, int(values[2]))
439
+ ymax = min(h, int(values[3]))
440
+
441
+ # Ensure valid crop region
442
+ if xmax <= xmin or ymax <= ymin:
443
+ return np.array([])
444
+
445
+ return image[ymin:ymax, xmin:xmax]
446
+
447
+ def _update_summary_data(self, detection_entry: Dict):
448
+ """
449
+ Update summary data with detection entry.
450
+ """
451
+ category = detection_entry["category"]
452
+ main_color = detection_entry["main_color"]
453
+ timestamp = detection_entry["timestamp"]
454
+
455
+ self.summary_results[category][main_color].append({
456
+ "timestamp": timestamp,
457
+ "confidence": detection_entry["confidence"],
458
+ "track_id": detection_entry["track_id"]
459
+ })
460
+
461
+ def _generate_summary_report(self, output_path: str, fps: float):
462
+ """
463
+ Generate and save summary report.
464
+ """
465
+ summary_report = {
466
+ "processing_info": {
467
+ "total_detections": len(self.detailed_results),
468
+ "fps": fps,
469
+ "processing_timestamp": datetime.now().isoformat()
470
+ },
471
+ "category_color_analysis": {},
472
+ "color_distribution": defaultdict(int),
473
+ "insights": []
474
+ }
475
+
476
+ # Analyze each category
477
+ for category, color_data in self.summary_results.items():
478
+ category_analysis = {
479
+ "total_detections": sum(len(detections) for detections in color_data.values()),
480
+ "color_breakdown": {},
481
+ "dominant_color": None,
482
+ "color_diversity": len(color_data)
483
+ }
484
+
485
+ # Calculate color breakdown
486
+ for color, detections in color_data.items():
487
+ category_analysis["color_breakdown"][color] = len(detections)
488
+ summary_report["color_distribution"][color] += len(detections)
489
+
490
+ # Find dominant color
491
+ if category_analysis["color_breakdown"]:
492
+ dominant_color = max(category_analysis["color_breakdown"].items(), key=lambda x: x[1])
493
+ category_analysis["dominant_color"] = {
494
+ "color": dominant_color[0],
495
+ "count": dominant_color[1],
496
+ "percentage": round(dominant_color[1] / category_analysis["total_detections"] * 100, 2)
497
+ }
498
+
499
+ summary_report["category_color_analysis"][category] = category_analysis
500
+
501
+ # Generate insights
502
+ summary_report["insights"] = self._generate_insights(summary_report)
503
+
504
+ # Save to file
505
+ with open(output_path, 'w') as f:
506
+ json.dump(summary_report, f, indent=2, default=str)
507
+
508
+ logger.info(f"Summary report saved to {output_path}")
509
+
510
+ def _generate_insights(self, summary_report: Dict) -> List[str]:
511
+ """
512
+ Generate insights from the summary report.
513
+ """
514
+ insights = []
515
+
516
+ total_detections = summary_report["processing_info"]["total_detections"]
517
+ insights.append(f"Processed {total_detections} total detections")
518
+
519
+ # Most common color overall
520
+ color_dist = summary_report["color_distribution"]
521
+ if color_dist:
522
+ most_common_color = max(color_dist.items(), key=lambda x: x[1])
523
+ insights.append(f"Most common color across all categories: {most_common_color[0]} ({most_common_color[1]} detections)")
524
+
525
+ # Category-specific insights
526
+ for category, analysis in summary_report["category_color_analysis"].items():
527
+ if analysis["dominant_color"]:
528
+ dominant = analysis["dominant_color"]
529
+ insights.append(f"{category.title()}: predominantly {dominant['color']} ({dominant['percentage']}%)")
530
+
531
+ if analysis["color_diversity"] > 5:
532
+ insights.append(f"{category.title()}: high color diversity ({analysis['color_diversity']} different colors)")
533
+
534
+ return insights
535
+
536
+ def _save_detailed_results(self, output_path: str):
537
+ """
538
+ Save detailed results to JSON file.
539
+ """
540
+ detailed_data = {
541
+ "processing_info": {
542
+ "total_detections": len(self.detailed_results),
543
+ "processing_timestamp": datetime.now().isoformat()
544
+ },
545
+ "detections": self.detailed_results
546
+ }
547
+
548
+ with open(output_path, 'w') as f:
549
+ json.dump(detailed_data, f, indent=2, default=str)
550
+
551
+ logger.info(f"Detailed results saved to {output_path}")
552
+
553
+ def _format_timestamp(self, seconds: float) -> str:
554
+ """
555
+ Format timestamp in MM:SS format.
556
+ """
557
+ minutes = int(seconds // 60)
558
+ seconds = int(seconds % 60)
559
+ return f"{minutes:02d}:{seconds:02d}"
560
+
561
+ def reset(self):
562
+ """
563
+ Reset the classifier state.
564
+ """
565
+ self.detailed_results = []
566
+ self.summary_results = defaultdict(lambda: defaultdict(list))
567
+
568
+
569
+ def process_video_with_color_detection(
570
+ video_bytes: bytes,
571
+ predictions: Dict[str, List[Dict]],
572
+ output_dir: str = "./output",
573
+ top_k_colors: int = 3,
574
+ min_confidence: float = 0.5,
575
+ fps: Optional[float] = None
576
+ ) -> Tuple[str, str]:
577
+ """
578
+ Convenience function to process video with color detection.
579
+
580
+ Args:
581
+ video_bytes: Raw video file bytes
582
+ predictions: Dict with frame_id -> list of detection dicts
583
+ output_dir: Directory to save output files
584
+ top_k_colors: Number of top colors to extract per detection
585
+ min_confidence: Minimum confidence threshold for detections
586
+ fps: Video FPS (will be auto-detected if not provided)
587
+
588
+ Returns:
589
+ Tuple of (detailed_results_path, summary_results_path)
590
+ """
591
+ classifier = VideoColorClassifier(top_k_colors=top_k_colors, min_confidence=min_confidence)
592
+ return classifier.process_video_with_predictions(video_bytes, predictions, output_dir, fps)