intial commit

This commit is contained in:
zakariasaputra committed 2026-07-17 14:58:29 +07:00
commit a54a070ca9
49 files changed
+2960

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
"""Detect backward cart motion using sparse optical flow on the background."""
from __future__ import annotations
import cv2
import numpy as np
from chicken_counter.config import MotionConfig, RoiConfig
from chicken_counter.types import MotionState, TrackObservation
class BackwardMotionDetector:
def __init__(self, config: MotionConfig, roi: RoiConfig) -> None:
self.config = config
self.roi = roi
self.previous_gray: np.ndarray | None = None
self.state = MotionState()
self._roi_bounds = self._compute_roi_bounds()
def _compute_roi_bounds(self) -> tuple[int, int, int, int]:
x_values = [point[0] for point in self.roi.points]
y_values = [point[1] for point in self.roi.points]
return min(x_values), min(y_values), max(x_values), max(y_values)
def update(
self,
frame: np.ndarray,
tracks: list[TrackObservation],
frame_index: int,
) -> MotionState:
if not self.config.enabled:
return self.state
stride = max(1, self.config.stride_frames)
if frame_index % stride != 0:
return self.state
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
x_min, y_min, x_max, y_max = self._roi_bounds
gray = gray[y_min:y_max, x_min:x_max]
scale = self.config.flow_scale
if scale < 1.0:
target_width = max(1, int(gray.shape[1] * scale))
target_height = max(1, int(gray.shape[0] * scale))
gray = cv2.resize(gray, (target_width, target_height), interpolation=cv2.INTER_AREA)
else:
scale = 1.0
mask = np.full(gray.shape, 255, dtype=np.uint8)
for track in tracks:
x1, y1, x2, y2 = track.bbox_xyxy
r = self.config.block_radius
local_x1 = int((max(0, x1 - r) - x_min) * scale)
local_y1 = int((max(0, y1 - r) - y_min) * scale)
local_x2 = int((min(x_max, x2 + r) - x_min) * scale)
local_y2 = int((min(y_max, y2 + r) - y_min) * scale)
if local_x2 <= local_x1 or local_y2 <= local_y1:
continue
cv2.rectangle(mask, (local_x1, local_y1), (local_x2, local_y2), 0, -1)
points = cv2.goodFeaturesToTrack(
gray,
maxCorners=self.config.max_corners,
qualityLevel=self.config.quality_level,
minDistance=self.config.min_distance,
mask=mask,
)
if self.previous_gray is None or points is None or len(points) < self.config.min_features:
self.previous_gray = gray
return self.state
next_points, status, _ = cv2.calcOpticalFlowPyrLK(self.previous_gray, gray, points, None)
self.previous_gray = gray
if next_points is None or status is None:
return self.state
valid_prev = points[status.flatten() == 1]
valid_next = next_points[status.flatten() == 1]
if len(valid_prev) < self.config.min_features:
return self.state
flow = valid_next - valid_prev
axis_values = flow[:, 0, 1] if self.config.axis == "vertical" else flow[:, 0, 0]
median_axis_speed = float(np.median(axis_values)) * self.config.forward_sign
alpha = self.config.ema_alpha
self.state.smoothed_speed = alpha * median_axis_speed + (1.0 - alpha) * self.state.smoothed_speed
if self.state.smoothed_speed <= self.config.reverse_enter_threshold:
self.state.consecutive_reverse_frames += 1
elif self.state.smoothed_speed > self.config.reverse_exit_threshold:
self.state.consecutive_reverse_frames = 0
self.state.backward_active = False
if self.state.consecutive_reverse_frames >= self.config.debounce_frames:
self.state.backward_active = True
return self.state