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,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)
|