fix: stability fix
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 10s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m53s
Build Worker Base and Application Images / deploy-stack (push) Successful in 8s
All checks were successful
Build Worker Base and Application Images / check-base-changes (push) Successful in 10s
Build Worker Base and Application Images / build-base (push) Has been skipped
Build Worker Base and Application Images / build-docker (push) Successful in 2m53s
Build Worker Base and Application Images / deploy-stack (push) Successful in 8s
This commit is contained in:
parent
bfab574058
commit
0cf0bc8b91
3 changed files with 215 additions and 60 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"poll_interval_ms": 100,
|
||||
"max_streams": 20,
|
||||
"target_fps": 2,
|
||||
"target_fps": 4,
|
||||
"reconnect_interval_sec": 10,
|
||||
"max_retries": -1,
|
||||
"rtsp_buffer_size": 3,
|
||||
|
|
|
@ -31,40 +31,125 @@ class TrackedVehicle:
|
|||
last_position_history: List[Tuple[float, float]] = field(default_factory=list)
|
||||
avg_confidence: float = 0.0
|
||||
|
||||
def update_position(self, bbox: Tuple[int, int, int, int], confidence: float):
|
||||
# Hybrid validation fields
|
||||
track_id_changes: int = 0 # Number of times track ID changed for same position
|
||||
position_stability_score: float = 0.0 # Independent position-based stability
|
||||
continuous_stable_duration: float = 0.0 # Time continuously stable (ignoring track ID changes)
|
||||
last_track_id_change: Optional[float] = None # When track ID last changed
|
||||
original_track_id: int = None # First track ID seen at this position
|
||||
|
||||
def update_position(self, bbox: Tuple[int, int, int, int], confidence: float, new_track_id: Optional[int] = None):
|
||||
"""Update vehicle position and confidence."""
|
||||
self.bbox = bbox
|
||||
self.center = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2)
|
||||
self.last_seen = time.time()
|
||||
current_time = time.time()
|
||||
self.last_seen = current_time
|
||||
self.confidence = confidence
|
||||
self.total_frames += 1
|
||||
|
||||
# Track ID change detection
|
||||
if new_track_id is not None and new_track_id != self.track_id:
|
||||
self.track_id_changes += 1
|
||||
self.last_track_id_change = current_time
|
||||
logger.debug(f"Track ID changed from {self.track_id} to {new_track_id} for same vehicle")
|
||||
self.track_id = new_track_id
|
||||
|
||||
# Set original track ID if not set
|
||||
if self.original_track_id is None:
|
||||
self.original_track_id = self.track_id
|
||||
|
||||
# Update confidence average
|
||||
self.avg_confidence = ((self.avg_confidence * (self.total_frames - 1)) + confidence) / self.total_frames
|
||||
|
||||
# Maintain position history (last 10 positions)
|
||||
# Maintain position history (last 15 positions for better stability analysis)
|
||||
self.last_position_history.append(self.center)
|
||||
if len(self.last_position_history) > 10:
|
||||
if len(self.last_position_history) > 15:
|
||||
self.last_position_history.pop(0)
|
||||
|
||||
def calculate_stability(self) -> float:
|
||||
"""Calculate stability score based on position history."""
|
||||
if len(self.last_position_history) < 2:
|
||||
return 0.0
|
||||
# Update position-based stability
|
||||
self._update_position_stability()
|
||||
|
||||
def _update_position_stability(self):
|
||||
"""Update position-based stability score independent of track ID."""
|
||||
if len(self.last_position_history) < 5:
|
||||
self.position_stability_score = 0.0
|
||||
return
|
||||
|
||||
# Calculate movement variance
|
||||
positions = np.array(self.last_position_history)
|
||||
if len(positions) < 2:
|
||||
return 0.0
|
||||
|
||||
# Calculate standard deviation of positions
|
||||
# Calculate position variance (lower = more stable)
|
||||
std_x = np.std(positions[:, 0])
|
||||
std_y = np.std(positions[:, 1])
|
||||
|
||||
# Lower variance means more stable (inverse relationship)
|
||||
# Normalize to 0-1 range (assuming max reasonable std is 50 pixels)
|
||||
stability = max(0, 1 - (std_x + std_y) / 100)
|
||||
return stability
|
||||
# Calculate movement velocity
|
||||
if len(positions) >= 3:
|
||||
recent_movement = np.mean([
|
||||
np.sqrt((positions[i][0] - positions[i-1][0])**2 +
|
||||
(positions[i][1] - positions[i-1][1])**2)
|
||||
for i in range(-3, 0)
|
||||
])
|
||||
else:
|
||||
recent_movement = 0
|
||||
|
||||
# Position-based stability (0-1 where 1 = perfectly stable)
|
||||
max_reasonable_std = 150 # For HD resolution
|
||||
variance_score = max(0, 1 - (std_x + std_y) / max_reasonable_std)
|
||||
velocity_score = max(0, 1 - recent_movement / 20) # 20 pixels max reasonable movement
|
||||
|
||||
self.position_stability_score = (variance_score * 0.7 + velocity_score * 0.3)
|
||||
|
||||
# Update continuous stable duration
|
||||
if self.position_stability_score > 0.7:
|
||||
if self.continuous_stable_duration == 0:
|
||||
# Start tracking stable duration
|
||||
self.continuous_stable_duration = 0.1 # Small initial value
|
||||
else:
|
||||
# Continue tracking
|
||||
self.continuous_stable_duration = time.time() - self.first_seen
|
||||
else:
|
||||
# Reset if not stable
|
||||
self.continuous_stable_duration = 0.0
|
||||
|
||||
def calculate_stability(self) -> float:
|
||||
"""Calculate stability score based on position history."""
|
||||
return self.position_stability_score
|
||||
|
||||
def calculate_hybrid_stability(self) -> Tuple[float, str]:
|
||||
"""
|
||||
Calculate hybrid stability considering both track ID continuity and position stability.
|
||||
|
||||
Returns:
|
||||
Tuple of (stability_score, reasoning)
|
||||
"""
|
||||
if len(self.last_position_history) < 5:
|
||||
return 0.0, "Insufficient position history"
|
||||
|
||||
position_stable = self.position_stability_score > 0.7
|
||||
has_stable_duration = self.continuous_stable_duration > 2.0 # 2+ seconds stable
|
||||
recent_track_change = (self.last_track_id_change is not None and
|
||||
(time.time() - self.last_track_id_change) < 1.0)
|
||||
|
||||
# Base stability from position
|
||||
base_score = self.position_stability_score
|
||||
|
||||
# Penalties and bonuses
|
||||
if self.track_id_changes > 3:
|
||||
# Too many track ID changes - likely tracking issues
|
||||
base_score *= 0.8
|
||||
reason = f"Multiple track ID changes ({self.track_id_changes})"
|
||||
elif recent_track_change:
|
||||
# Recent track change - be cautious
|
||||
base_score *= 0.9
|
||||
reason = "Recent track ID change"
|
||||
else:
|
||||
reason = "Position-based stability"
|
||||
|
||||
# Bonus for long continuous stability regardless of track ID changes
|
||||
if has_stable_duration:
|
||||
base_score = min(1.0, base_score + 0.1)
|
||||
reason += f" + {self.continuous_stable_duration:.1f}s continuous"
|
||||
|
||||
return base_score, reason
|
||||
|
||||
def is_expired(self, timeout_seconds: float = 2.0) -> bool:
|
||||
"""Check if vehicle tracking has expired."""
|
||||
|
@ -90,14 +175,15 @@ class VehicleTracker:
|
|||
|
||||
# Tracking state
|
||||
self.tracked_vehicles: Dict[int, TrackedVehicle] = {}
|
||||
self.position_registry: Dict[str, TrackedVehicle] = {} # Position-based vehicle registry
|
||||
self.next_track_id = 1
|
||||
self.lock = Lock()
|
||||
|
||||
# Tracking parameters
|
||||
self.stability_threshold = 0.7
|
||||
self.min_stable_frames = 5
|
||||
self.position_tolerance = 50 # pixels
|
||||
self.timeout_seconds = 2.0
|
||||
self.stability_threshold = 0.65 # Lowered for gas station scenarios
|
||||
self.min_stable_frames = 8 # Increased for 4fps processing
|
||||
self.position_tolerance = 80 # pixels - increased for gas station scenarios
|
||||
self.timeout_seconds = 8.0 # Increased for gas station scenarios
|
||||
|
||||
logger.info(f"VehicleTracker initialized with trigger_classes={self.trigger_classes}, "
|
||||
f"min_confidence={self.min_confidence}")
|
||||
|
@ -127,6 +213,11 @@ class VehicleTracker:
|
|||
if vehicle.is_expired(self.timeout_seconds)
|
||||
]
|
||||
for track_id in expired_ids:
|
||||
vehicle = self.tracked_vehicles[track_id]
|
||||
# Remove from position registry too
|
||||
position_key = self._get_position_key(vehicle.center)
|
||||
if position_key in self.position_registry and self.position_registry[position_key] == vehicle:
|
||||
del self.position_registry[position_key]
|
||||
logger.debug(f"Removing expired track {track_id}")
|
||||
del self.tracked_vehicles[track_id]
|
||||
|
||||
|
@ -142,56 +233,115 @@ class VehicleTracker:
|
|||
if detection.class_name not in self.trigger_classes:
|
||||
continue
|
||||
|
||||
# Use track_id if available, otherwise generate one
|
||||
track_id = detection.track_id if detection.track_id is not None else self.next_track_id
|
||||
if detection.track_id is None:
|
||||
self.next_track_id += 1
|
||||
|
||||
# Get bounding box from Detection object
|
||||
# Get bounding box and center from Detection object
|
||||
x1, y1, x2, y2 = detection.bbox
|
||||
bbox = (int(x1), int(y1), int(x2), int(y2))
|
||||
|
||||
# Update or create tracked vehicle
|
||||
center = ((x1 + x2) / 2, (y1 + y2) / 2)
|
||||
confidence = detection.confidence
|
||||
if track_id in self.tracked_vehicles:
|
||||
# Update existing track
|
||||
vehicle = self.tracked_vehicles[track_id]
|
||||
vehicle.update_position(bbox, confidence)
|
||||
vehicle.display_id = display_id
|
||||
|
||||
# Check stability
|
||||
stability = vehicle.calculate_stability()
|
||||
if stability > self.stability_threshold:
|
||||
vehicle.stable_frames += 1
|
||||
if vehicle.stable_frames >= self.min_stable_frames:
|
||||
vehicle.is_stable = True
|
||||
# Hybrid approach: Try position-based association first, then track ID
|
||||
track_id = detection.track_id
|
||||
existing_vehicle = None
|
||||
position_key = self._get_position_key(center)
|
||||
|
||||
# 1. Check position registry first (same physical location)
|
||||
if position_key in self.position_registry:
|
||||
existing_vehicle = self.position_registry[position_key]
|
||||
if track_id is not None and track_id != existing_vehicle.track_id:
|
||||
# Track ID changed for same position - update vehicle
|
||||
existing_vehicle.update_position(bbox, confidence, track_id)
|
||||
logger.debug(f"Track ID changed {existing_vehicle.track_id}->{track_id} at same position")
|
||||
# Update tracking dict
|
||||
if existing_vehicle.track_id in self.tracked_vehicles:
|
||||
del self.tracked_vehicles[existing_vehicle.track_id]
|
||||
self.tracked_vehicles[track_id] = existing_vehicle
|
||||
else:
|
||||
vehicle.stable_frames = max(0, vehicle.stable_frames - 1)
|
||||
if vehicle.stable_frames < self.min_stable_frames:
|
||||
vehicle.is_stable = False
|
||||
# Same position, same/no track ID
|
||||
existing_vehicle.update_position(bbox, confidence)
|
||||
track_id = existing_vehicle.track_id
|
||||
|
||||
logger.debug(f"Updated track {track_id}: conf={confidence:.2f}, "
|
||||
f"stable={vehicle.is_stable}, stability={stability:.2f}")
|
||||
else:
|
||||
# Create new track
|
||||
vehicle = TrackedVehicle(
|
||||
# 2. If no position match, try track ID approach
|
||||
elif track_id is not None and track_id in self.tracked_vehicles:
|
||||
# Existing track ID, check if position moved significantly
|
||||
existing_vehicle = self.tracked_vehicles[track_id]
|
||||
old_position_key = self._get_position_key(existing_vehicle.center)
|
||||
|
||||
# If position moved significantly, update position registry
|
||||
if old_position_key != position_key:
|
||||
if old_position_key in self.position_registry:
|
||||
del self.position_registry[old_position_key]
|
||||
self.position_registry[position_key] = existing_vehicle
|
||||
|
||||
existing_vehicle.update_position(bbox, confidence)
|
||||
|
||||
# 3. Try closest track association (fallback)
|
||||
elif track_id is None:
|
||||
closest_track = self._find_closest_track(center)
|
||||
if closest_track:
|
||||
existing_vehicle = closest_track
|
||||
track_id = closest_track.track_id
|
||||
existing_vehicle.update_position(bbox, confidence)
|
||||
# Update position registry
|
||||
self.position_registry[position_key] = existing_vehicle
|
||||
logger.debug(f"Associated detection with existing track {track_id} based on proximity")
|
||||
|
||||
# 4. Create new vehicle if no associations found
|
||||
if existing_vehicle is None:
|
||||
track_id = track_id if track_id is not None else self.next_track_id
|
||||
if track_id == self.next_track_id:
|
||||
self.next_track_id += 1
|
||||
|
||||
existing_vehicle = TrackedVehicle(
|
||||
track_id=track_id,
|
||||
first_seen=current_time,
|
||||
last_seen=current_time,
|
||||
display_id=display_id,
|
||||
confidence=confidence,
|
||||
bbox=bbox,
|
||||
center=((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
total_frames=1
|
||||
center=center,
|
||||
total_frames=1,
|
||||
original_track_id=track_id
|
||||
)
|
||||
vehicle.last_position_history.append(vehicle.center)
|
||||
self.tracked_vehicles[track_id] = vehicle
|
||||
existing_vehicle.last_position_history.append(center)
|
||||
self.tracked_vehicles[track_id] = existing_vehicle
|
||||
self.position_registry[position_key] = existing_vehicle
|
||||
logger.info(f"New vehicle tracked: ID={track_id}, display={display_id}")
|
||||
|
||||
active_tracks.append(self.tracked_vehicles[track_id])
|
||||
# Check stability using hybrid approach
|
||||
stability_score, reason = existing_vehicle.calculate_hybrid_stability()
|
||||
if stability_score > self.stability_threshold:
|
||||
existing_vehicle.stable_frames += 1
|
||||
if existing_vehicle.stable_frames >= self.min_stable_frames:
|
||||
existing_vehicle.is_stable = True
|
||||
else:
|
||||
existing_vehicle.stable_frames = max(0, existing_vehicle.stable_frames - 1)
|
||||
if existing_vehicle.stable_frames < self.min_stable_frames:
|
||||
existing_vehicle.is_stable = False
|
||||
|
||||
logger.debug(f"Updated track {track_id}: conf={confidence:.2f}, "
|
||||
f"stable={existing_vehicle.is_stable}, hybrid_stability={stability_score:.2f} ({reason})")
|
||||
|
||||
active_tracks.append(existing_vehicle)
|
||||
|
||||
return active_tracks
|
||||
|
||||
def _get_position_key(self, center: Tuple[float, float]) -> str:
|
||||
"""
|
||||
Generate a position-based key for vehicle registry.
|
||||
Groups nearby positions into the same key for association.
|
||||
|
||||
Args:
|
||||
center: Center position (x, y)
|
||||
|
||||
Returns:
|
||||
Position key string
|
||||
"""
|
||||
# Grid-based quantization - 60 pixel grid for gas station scenarios
|
||||
grid_size = 60
|
||||
grid_x = int(center[0] // grid_size)
|
||||
grid_y = int(center[1] // grid_size)
|
||||
return f"{grid_x}_{grid_y}"
|
||||
|
||||
def _find_closest_track(self, center: Tuple[float, float]) -> Optional[TrackedVehicle]:
|
||||
"""
|
||||
Find the closest existing track to a given position.
|
||||
|
@ -206,7 +356,7 @@ class VehicleTracker:
|
|||
closest_track = None
|
||||
|
||||
for vehicle in self.tracked_vehicles.values():
|
||||
if vehicle.is_expired(0.5): # Shorter timeout for matching
|
||||
if vehicle.is_expired(1.0): # Allow slightly older tracks for matching
|
||||
continue
|
||||
|
||||
distance = np.sqrt(
|
||||
|
@ -287,6 +437,7 @@ class VehicleTracker:
|
|||
"""Reset all tracking state."""
|
||||
with self.lock:
|
||||
self.tracked_vehicles.clear()
|
||||
self.position_registry.clear()
|
||||
self.next_track_id = 1
|
||||
logger.info("Vehicle tracking state reset")
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ class StableCarValidator:
|
|||
|
||||
# Validation thresholds
|
||||
self.min_stable_duration = self.config.get('min_stable_duration', 3.0) # seconds
|
||||
self.min_stable_frames = self.config.get('min_stable_frames', 10)
|
||||
self.position_variance_threshold = self.config.get('position_variance_threshold', 25.0) # pixels
|
||||
self.min_stable_frames = self.config.get('min_stable_frames', 8)
|
||||
self.position_variance_threshold = self.config.get('position_variance_threshold', 40.0) # pixels - adjusted for HD
|
||||
self.min_confidence = self.config.get('min_confidence', 0.7)
|
||||
self.velocity_threshold = self.config.get('velocity_threshold', 5.0) # pixels/frame
|
||||
self.entering_zone_ratio = self.config.get('entering_zone_ratio', 0.3) # 30% of frame
|
||||
|
@ -188,9 +188,9 @@ class StableCarValidator:
|
|||
x_position = vehicle.center[0] / self.frame_width
|
||||
y_position = vehicle.center[1] / self.frame_height
|
||||
|
||||
# Check if vehicle is stable
|
||||
stability = vehicle.calculate_stability()
|
||||
if stability > 0.7 and velocity < self.velocity_threshold:
|
||||
# Check if vehicle is stable using hybrid approach
|
||||
stability_score, stability_reason = vehicle.calculate_hybrid_stability()
|
||||
if stability_score > 0.65 and velocity < self.velocity_threshold:
|
||||
# Check if it's been stable long enough
|
||||
duration = time.time() - vehicle.first_seen
|
||||
if duration > self.min_stable_duration and vehicle.stable_frames >= self.min_stable_frames:
|
||||
|
@ -294,11 +294,15 @@ class StableCarValidator:
|
|||
# All checks passed - vehicle is valid for processing
|
||||
self.last_processed_vehicles[vehicle.track_id] = time.time()
|
||||
|
||||
# Get hybrid stability info for detailed reasoning
|
||||
hybrid_stability, hybrid_reason = vehicle.calculate_hybrid_stability()
|
||||
processing_reason = f"Vehicle is stable and ready for processing (hybrid: {hybrid_reason})"
|
||||
|
||||
return ValidationResult(
|
||||
is_valid=True,
|
||||
state=VehicleState.STABLE,
|
||||
confidence=vehicle.avg_confidence,
|
||||
reason="Vehicle is stable and ready for processing",
|
||||
reason=processing_reason,
|
||||
should_process=True,
|
||||
track_id=vehicle.track_id
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue