matrice-analytics 0.1.70__py3-none-any.whl → 0.1.96__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/post_processing/__init__.py +8 -2
- matrice_analytics/post_processing/config.py +4 -2
- matrice_analytics/post_processing/core/base.py +1 -1
- matrice_analytics/post_processing/core/config.py +40 -3
- matrice_analytics/post_processing/face_reg/face_recognition.py +1014 -201
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +171 -29
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +19 -0
- matrice_analytics/post_processing/post_processor.py +4 -0
- matrice_analytics/post_processing/usecases/__init__.py +4 -1
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +913 -500
- matrice_analytics/post_processing/usecases/color_detection.py +19 -18
- matrice_analytics/post_processing/usecases/customer_service.py +356 -9
- matrice_analytics/post_processing/usecases/fire_detection.py +241 -23
- matrice_analytics/post_processing/usecases/footfall.py +750 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +638 -40
- matrice_analytics/post_processing/usecases/people_counting.py +66 -33
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +35 -34
- matrice_analytics/post_processing/usecases/weapon_detection.py +2 -1
- matrice_analytics/post_processing/utils/alert_instance_utils.py +1018 -0
- matrice_analytics/post_processing/utils/business_metrics_manager_utils.py +1338 -0
- matrice_analytics/post_processing/utils/incident_manager_utils.py +1754 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/METADATA +1 -1
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/RECORD +26 -22
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/WHEEL +0 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/licenses/LICENSE.txt +0 -0
- {matrice_analytics-0.1.70.dist-info → matrice_analytics-0.1.96.dist-info}/top_level.txt +0 -0
|
@@ -27,6 +27,8 @@ from ..utils import (
|
|
|
27
27
|
BBoxSmoothingConfig,
|
|
28
28
|
BBoxSmoothingTracker
|
|
29
29
|
)
|
|
30
|
+
# Import incident manager for publishing incidents on level change
|
|
31
|
+
from ..utils.incident_manager_utils import INCIDENT_MANAGER, IncidentManagerFactory
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
# ======================
|
|
@@ -75,6 +77,10 @@ class FireSmokeConfig(BaseConfig):
|
|
|
75
77
|
smoothing_cooldown_frames: int = 10
|
|
76
78
|
smoothing_confidence_range_factor: float = 0.2
|
|
77
79
|
threshold_area: Optional[float] = 250200.0
|
|
80
|
+
|
|
81
|
+
# Session and server configuration for incident manager
|
|
82
|
+
session: Optional[Any] = None # Matrice session for Redis/Kafka initialization
|
|
83
|
+
server_id: Optional[str] = None # Server ID for localhost/cloud detection
|
|
78
84
|
|
|
79
85
|
def __post_init__(self):
|
|
80
86
|
if not (0.0 <= self.confidence_threshold <= 1.0):
|
|
@@ -101,7 +107,7 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
101
107
|
|
|
102
108
|
self.smoothing_tracker = None # Required for bbox smoothing
|
|
103
109
|
self._fire_smoke_recent_history = []
|
|
104
|
-
self.target_categories=['fire']
|
|
110
|
+
self.target_categories=['fire', 'smoke'] # Lowercase to match filtering logic at line 276
|
|
105
111
|
|
|
106
112
|
self._ascending_alert_list: List[str] = []
|
|
107
113
|
self.current_incident_end_timestamp: str = "N/A"
|
|
@@ -112,6 +118,114 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
112
118
|
self.return_id_counter = 1
|
|
113
119
|
self.start_timer = None
|
|
114
120
|
self._tracking_start_time = None
|
|
121
|
+
|
|
122
|
+
# Incident manager for publishing incidents on severity level change
|
|
123
|
+
self._incident_manager_factory: Optional[IncidentManagerFactory] = None
|
|
124
|
+
self._incident_manager: Optional[INCIDENT_MANAGER] = None
|
|
125
|
+
self._incident_manager_initialized: bool = False
|
|
126
|
+
|
|
127
|
+
def _initialize_incident_manager_once(self, config: FireSmokeConfig) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Initialize incident manager ONCE with Redis OR Kafka clients (Environment based).
|
|
130
|
+
Called from process() on first invocation.
|
|
131
|
+
Uses config.session (existing session from pipeline) or creates from environment.
|
|
132
|
+
"""
|
|
133
|
+
if self._incident_manager_initialized:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
self.logger.info("[INCIDENT_MANAGER] Starting incident manager initialization for fire detection...")
|
|
138
|
+
|
|
139
|
+
# Create factory if not exists
|
|
140
|
+
if self._incident_manager_factory is None:
|
|
141
|
+
self._incident_manager_factory = IncidentManagerFactory(logger=self.logger)
|
|
142
|
+
|
|
143
|
+
# Initialize using factory (handles session creation, Redis/Kafka setup)
|
|
144
|
+
self._incident_manager = self._incident_manager_factory.initialize(config)
|
|
145
|
+
|
|
146
|
+
if self._incident_manager:
|
|
147
|
+
self.logger.info("[INCIDENT_MANAGER] ✓ Incident manager initialized successfully for fire detection")
|
|
148
|
+
else:
|
|
149
|
+
self.logger.warning("[INCIDENT_MANAGER] Incident manager not available, incidents won't be published")
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
self.logger.error(f"[INCIDENT_MANAGER] Incident manager initialization failed: {e}", exc_info=True)
|
|
153
|
+
finally:
|
|
154
|
+
self._incident_manager_initialized = True # Mark as initialized (don't retry every frame)
|
|
155
|
+
|
|
156
|
+
def _send_incident_to_manager(
|
|
157
|
+
self,
|
|
158
|
+
incident: Dict,
|
|
159
|
+
stream_info: Optional[Dict[str, Any]] = None
|
|
160
|
+
) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Send incident to incident manager for level tracking and publishing.
|
|
163
|
+
|
|
164
|
+
The incident manager will:
|
|
165
|
+
1. Track the severity level
|
|
166
|
+
2. Wait for consecutive frames before publishing:
|
|
167
|
+
- 5 frames for medium/significant/critical
|
|
168
|
+
- 10 frames for low (stricter to avoid false positives)
|
|
169
|
+
3. Publish only when level changes
|
|
170
|
+
4. Send 'info' level after 101 consecutive empty frames (incident ended)
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
incident: Incident dictionary from _generate_incidents
|
|
174
|
+
stream_info: Stream metadata containing camera info
|
|
175
|
+
"""
|
|
176
|
+
if not self._incident_manager:
|
|
177
|
+
self.logger.debug("[INCIDENT_MANAGER] No incident manager available, skipping")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Extract camera_id from stream_info
|
|
181
|
+
# Priority: camera_info.camera_id > stream_info.camera_id > extract from topic
|
|
182
|
+
camera_id = ""
|
|
183
|
+
if stream_info:
|
|
184
|
+
camera_info = stream_info.get("camera_info", {}) or {}
|
|
185
|
+
camera_id = camera_info.get("camera_id", "") or camera_info.get("cameraId", "")
|
|
186
|
+
|
|
187
|
+
if not camera_id:
|
|
188
|
+
camera_id = stream_info.get("camera_id", "") or stream_info.get("cameraId", "")
|
|
189
|
+
|
|
190
|
+
# Extract camera_id from topic if not found elsewhere
|
|
191
|
+
# Topic format: {camera_id}_input_topic
|
|
192
|
+
if not camera_id:
|
|
193
|
+
topic = stream_info.get("topic", "")
|
|
194
|
+
if topic:
|
|
195
|
+
if topic.endswith("_input_topic"):
|
|
196
|
+
camera_id = topic[: -len("_input_topic")]
|
|
197
|
+
self.logger.debug(f"[INCIDENT_MANAGER] Extracted camera_id from topic (underscore): {camera_id}")
|
|
198
|
+
elif topic.endswith("_input-topic"):
|
|
199
|
+
camera_id = topic[: -len("_input-topic")]
|
|
200
|
+
self.logger.debug(f"[INCIDENT_MANAGER] Extracted camera_id from topic (hyphen): {camera_id}")
|
|
201
|
+
else:
|
|
202
|
+
# Fallback: split on known markers if not strictly at the end
|
|
203
|
+
if "_input_topic" in topic:
|
|
204
|
+
camera_id = topic.split("_input_topic")[0]
|
|
205
|
+
self.logger.debug(f"[INCIDENT_MANAGER] Extracted camera_id from topic split (underscore): {camera_id}")
|
|
206
|
+
elif "_input-topic" in topic:
|
|
207
|
+
camera_id = topic.split("_input-topic")[0]
|
|
208
|
+
self.logger.debug(f"[INCIDENT_MANAGER] Extracted camera_id from topic split (hyphen): {camera_id}")
|
|
209
|
+
|
|
210
|
+
if not camera_id:
|
|
211
|
+
# Fallback to a default identifier
|
|
212
|
+
camera_id = "default_camera"
|
|
213
|
+
self.logger.debug(f"[INCIDENT_MANAGER] No camera_id found, using default: {camera_id}")
|
|
214
|
+
else:
|
|
215
|
+
self.logger.debug(f"[INCIDENT_MANAGER] Using camera_id: {camera_id}")
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Process the incident through the manager
|
|
219
|
+
published = self._incident_manager.process_incident(
|
|
220
|
+
camera_id=camera_id,
|
|
221
|
+
incident_data=incident,
|
|
222
|
+
stream_info=stream_info
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if published:
|
|
226
|
+
self.logger.info(f"[INCIDENT_MANAGER] Incident published for camera: {camera_id}")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self.logger.error(f"[INCIDENT_MANAGER] Error sending incident to manager: {e}", exc_info=True)
|
|
115
229
|
|
|
116
230
|
def process(
|
|
117
231
|
self,
|
|
@@ -135,6 +249,10 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
135
249
|
context=context,
|
|
136
250
|
)
|
|
137
251
|
|
|
252
|
+
# Step 0.5: Initialize incident manager once (for publishing incidents on level change)
|
|
253
|
+
if not self._incident_manager_initialized:
|
|
254
|
+
self._initialize_incident_manager_once(config)
|
|
255
|
+
|
|
138
256
|
# Step 1: Init context
|
|
139
257
|
if context is None:
|
|
140
258
|
context = ProcessingContext()
|
|
@@ -225,6 +343,18 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
225
343
|
stream_info=stream_info
|
|
226
344
|
)
|
|
227
345
|
business_analytics_list = self._generate_business_analytics(fire_smoke_summary, alerts, config, stream_info, is_empty=True)
|
|
346
|
+
|
|
347
|
+
# Step 8.5: Send incident to incident manager for level tracking and publishing
|
|
348
|
+
# The incident manager handles:
|
|
349
|
+
# - 5-consecutive-frame validation
|
|
350
|
+
# - Publishing only on level change
|
|
351
|
+
# - Skipping "low" level incidents
|
|
352
|
+
incidents = incidents_list[0] if incidents_list else {}
|
|
353
|
+
self._send_incident_to_manager(incidents, stream_info)
|
|
354
|
+
# if incidents_list and len(incidents_list) > 0:
|
|
355
|
+
# incident = incidents_list[0]
|
|
356
|
+
# if incident and incident != {}:
|
|
357
|
+
# self._send_incident_to_manager(incident, stream_info)
|
|
228
358
|
|
|
229
359
|
# Step 9: Human-readable summary
|
|
230
360
|
summary_list = self._generate_summary(fire_smoke_summary, general_summary, incidents_list, tracking_stats_list, business_analytics_list, alerts)
|
|
@@ -232,7 +362,7 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
232
362
|
# Finalize context and return result
|
|
233
363
|
context.processing_time = time.time() - start_time
|
|
234
364
|
|
|
235
|
-
|
|
365
|
+
|
|
236
366
|
tracking_stats = tracking_stats_list[0] if tracking_stats_list else {}
|
|
237
367
|
#EVENT ENDED SIGNAL
|
|
238
368
|
|
|
@@ -363,6 +493,9 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
363
493
|
) -> Dict:
|
|
364
494
|
"""Generate structured events for fire and smoke detection output with frame-aware keys."""
|
|
365
495
|
|
|
496
|
+
level_params=[{"level":"low","percentage":0.0001},{"level":"medium","percentage":3},
|
|
497
|
+
{"level":"significant","percentage":13},{"level":"critical","percentage":30}]
|
|
498
|
+
|
|
366
499
|
def get_trend_incident(data, lookback=23, prior=14):
|
|
367
500
|
'''
|
|
368
501
|
Determine if the trend is ascending or descending based on actual value progression.
|
|
@@ -405,9 +538,21 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
405
538
|
count_thresholds = {}
|
|
406
539
|
if config.alert_config and hasattr(config.alert_config, "count_thresholds"):
|
|
407
540
|
count_thresholds = config.alert_config.count_thresholds or {}
|
|
541
|
+
|
|
542
|
+
# CRITICAL FIX: Ensure we have at least one category to process
|
|
543
|
+
# If count_thresholds is empty, use detected categories from per_category_count
|
|
544
|
+
# This ensures incidents are always generated when detections exist
|
|
545
|
+
per_category_count = summary.get("per_category_count", {})
|
|
546
|
+
if not count_thresholds and per_category_count:
|
|
547
|
+
# Create thresholds for all detected categories with threshold=0 (always trigger)
|
|
548
|
+
count_thresholds = {cat: 0 for cat in per_category_count.keys()}
|
|
549
|
+
self.logger.debug(f"[INCIDENT] count_thresholds was empty, using detected categories: {count_thresholds}")
|
|
550
|
+
|
|
551
|
+
# Flag to track if we generated any incident
|
|
552
|
+
incident_generated = False
|
|
408
553
|
|
|
409
554
|
for category, threshold in count_thresholds.items():
|
|
410
|
-
if category in
|
|
555
|
+
if category in per_category_count:
|
|
411
556
|
|
|
412
557
|
#count = summary.get("per_category_count", {})[category]
|
|
413
558
|
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
@@ -474,7 +619,11 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
474
619
|
|
|
475
620
|
# Generate human text in new format
|
|
476
621
|
human_text_lines = [f"INCIDENTS DETECTED @ {current_timestamp}:"]
|
|
477
|
-
|
|
622
|
+
if level=='significant':
|
|
623
|
+
print_level = "high"
|
|
624
|
+
else:
|
|
625
|
+
print_level = level
|
|
626
|
+
human_text_lines.append(f"\tSeverity Level: {(self.CASE_TYPE,print_level)}")
|
|
478
627
|
human_text = "\n".join(human_text_lines)
|
|
479
628
|
|
|
480
629
|
# Pass the last severity level **value** instead of a single-element list
|
|
@@ -504,7 +653,57 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
504
653
|
start_time=start_timestamp, end_time=self.current_incident_end_timestamp,
|
|
505
654
|
level_settings= {"low": 3, "medium": 5, "significant":15, "critical": 30})
|
|
506
655
|
event['duration'] = self.get_duration_seconds(start_timestamp, self.current_incident_end_timestamp)
|
|
656
|
+
event['incident_quant'] = intensity_pct
|
|
507
657
|
incidents.append(event)
|
|
658
|
+
incident_generated = True
|
|
659
|
+
|
|
660
|
+
# CRITICAL FALLBACK: If no incident was generated despite having detections,
|
|
661
|
+
# generate a basic incident to ensure the incident manager receives data
|
|
662
|
+
if not incident_generated and total > 0:
|
|
663
|
+
self.logger.warning(f"[INCIDENT] No incident generated despite {total} detections. Generating fallback incident.")
|
|
664
|
+
# Calculate area and intensity for fallback
|
|
665
|
+
for det in detections:
|
|
666
|
+
bbox = det.get("bounding_box") or det.get("bbox")
|
|
667
|
+
if bbox:
|
|
668
|
+
xmin, ymin = bbox.get("xmin"), bbox.get("ymin")
|
|
669
|
+
xmax, ymax = bbox.get("xmax"), bbox.get("ymax")
|
|
670
|
+
if None not in (xmin, ymin, xmax, ymax):
|
|
671
|
+
width, height = xmax - xmin, ymax - ymin
|
|
672
|
+
if width > 0 and height > 0:
|
|
673
|
+
total_area += width * height
|
|
674
|
+
|
|
675
|
+
threshold_area = config.threshold_area or 250200.0
|
|
676
|
+
intensity_pct = min(100.0, (total_area / threshold_area) * 100)
|
|
677
|
+
|
|
678
|
+
# Determine severity level
|
|
679
|
+
if intensity_pct >= 30:
|
|
680
|
+
level = "critical"
|
|
681
|
+
elif intensity_pct >= 13:
|
|
682
|
+
level = "significant"
|
|
683
|
+
elif intensity_pct >= 3:
|
|
684
|
+
level = "medium"
|
|
685
|
+
else:
|
|
686
|
+
level = "low"
|
|
687
|
+
self._ascending_alert_list.append(level)
|
|
688
|
+
|
|
689
|
+
start_timestamp = self._get_start_timestamp_str(stream_info)
|
|
690
|
+
human_text = f"INCIDENTS DETECTED @ {current_timestamp}:\n\tSeverity Level: {(self.CASE_TYPE, level)}"
|
|
691
|
+
|
|
692
|
+
event = self.create_incident(
|
|
693
|
+
incident_id='incident_' + self.CASE_TYPE + '_fallback',
|
|
694
|
+
incident_type=self.CASE_TYPE,
|
|
695
|
+
severity_level=level,
|
|
696
|
+
human_text=human_text,
|
|
697
|
+
camera_info=camera_info,
|
|
698
|
+
alerts=alerts,
|
|
699
|
+
alert_settings=[],
|
|
700
|
+
start_time=start_timestamp,
|
|
701
|
+
end_time='Incident still active',
|
|
702
|
+
level_settings={"low": 3, "medium": 5, "significant": 15, "critical": 30}
|
|
703
|
+
)
|
|
704
|
+
event['incident_quant'] = intensity_pct
|
|
705
|
+
incidents.append(event)
|
|
706
|
+
self.logger.info(f"[INCIDENT] Generated fallback incident with level={level}, intensity={intensity_pct:.2f}%")
|
|
508
707
|
|
|
509
708
|
else:
|
|
510
709
|
#self._ascending_alert_list.append(level)
|
|
@@ -551,20 +750,39 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
551
750
|
|
|
552
751
|
|
|
553
752
|
# Build total_counts array in expected format
|
|
753
|
+
# ALWAYS populate with all target categories to avoid empty arrays downstream
|
|
554
754
|
total_counts = []
|
|
555
755
|
if total > 0:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
756
|
+
total_counts.append({
|
|
757
|
+
"category": 'Fire/Smoke',
|
|
758
|
+
"count": 1
|
|
759
|
+
})
|
|
760
|
+
else:
|
|
761
|
+
# When no detections, send count=0 for each category to avoid empty array
|
|
762
|
+
total_counts.append({
|
|
763
|
+
"category": 'Fire',
|
|
764
|
+
"count": 0
|
|
765
|
+
})
|
|
766
|
+
total_counts.append({
|
|
767
|
+
"category": 'Smoke',
|
|
768
|
+
"count": 0
|
|
769
|
+
})
|
|
560
770
|
|
|
561
|
-
# Build current_counts array in expected format
|
|
771
|
+
# Build current_counts array in expected format
|
|
772
|
+
# ALWAYS populate with all target categories to avoid empty arrays downstream
|
|
562
773
|
current_counts = []
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
774
|
+
|
|
775
|
+
# Add Fire entry (count=1 if detected, count=0 if not)
|
|
776
|
+
current_counts.append({
|
|
777
|
+
"category": 'Fire',
|
|
778
|
+
"count": 1 if total_fire > 0 else 0
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
# Add Smoke entry (count=1 if detected, count=0 if not)
|
|
782
|
+
current_counts.append({
|
|
783
|
+
"category": 'Smoke',
|
|
784
|
+
"count": 1 if total_smoke > 0 else 0
|
|
785
|
+
})
|
|
568
786
|
|
|
569
787
|
human_lines = [f"CURRENT FRAME @ {current_timestamp}:"]
|
|
570
788
|
if total_fire > 0:
|
|
@@ -575,17 +793,17 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
575
793
|
human_lines.append(f"\t- No fire or smoke detected")
|
|
576
794
|
|
|
577
795
|
human_lines.append("")
|
|
578
|
-
human_lines.append(f"ALERTS SINCE @ {start_timestamp}:")
|
|
796
|
+
# human_lines.append(f"ALERTS SINCE @ {start_timestamp}:")
|
|
579
797
|
|
|
580
798
|
recent_fire_detected = any(entry.get("fire", 0) > 0 for entry in self._fire_smoke_recent_history)
|
|
581
799
|
recent_smoke_detected = any(entry.get("smoke", 0) > 0 for entry in self._fire_smoke_recent_history)
|
|
582
800
|
|
|
583
|
-
if recent_fire_detected:
|
|
584
|
-
|
|
585
|
-
if recent_smoke_detected:
|
|
586
|
-
|
|
587
|
-
if not recent_fire_detected and not recent_smoke_detected:
|
|
588
|
-
|
|
801
|
+
# if recent_fire_detected:
|
|
802
|
+
# human_lines.append(f"\t- Fire alert")
|
|
803
|
+
# if recent_smoke_detected:
|
|
804
|
+
# human_lines.append(f"\t- Smoke alert")
|
|
805
|
+
# if not recent_fire_detected and not recent_smoke_detected:
|
|
806
|
+
# human_lines.append(f"\t- No fire or smoke detected in recent frames")
|
|
589
807
|
|
|
590
808
|
human_text = "\n".join(human_lines)
|
|
591
809
|
|
|
@@ -635,8 +853,8 @@ class FireSmokeUseCase(BaseProcessor):
|
|
|
635
853
|
detections=detections, human_text=human_text, camera_info=camera_info, alerts=alerts, alert_settings=alert_settings,
|
|
636
854
|
reset_settings=reset_settings, start_time=high_precision_start_timestamp ,
|
|
637
855
|
reset_time=high_precision_reset_timestamp)
|
|
638
|
-
|
|
639
|
-
|
|
856
|
+
|
|
857
|
+
tracking_stat['target_categories'] = self.target_categories
|
|
640
858
|
tracking_stats.append(tracking_stat)
|
|
641
859
|
|
|
642
860
|
if len(self.id_hit_list)==1:
|