what are these

This commit is contained in:
lda committed 2026-02-02 20:57:05 +07:00
1 parent 538276a5f0
commit 34bee8bf31
8 files changed
+288 -9

No files matched your search

+30 -1
View File
@@ -34,6 +34,8 @@ The pipeline operates in a "Fail-Fast" cascade with **early exit** optimization:
- **Active Mode (Max FPS)**: Switches to full speed when activity is detected. - **Active Mode (Max FPS)**: Switches to full speed when activity is detected.
- **Cooldown**: Stays active for 2 seconds after people leave frame. - **Cooldown**: Stays active for 2 seconds after people leave frame.
- **Non-Maximum Suppression (NMS)**: Cleans up overlapping boxes per-class. - **Non-Maximum Suppression (NMS)**: Cleans up overlapping boxes per-class.
- **Weighted Boxes Fusion (WBF)**: Alternative to NMS that merges overlapping boxes for better localization.
- **Inference Budget Caps**: Limits expensive model calls (e.g., max 3 person crops, max 5 verifications).
- **Smart Event Recording**: Captures 2s before + 3s after each smoking event as separate clips. - **Smart Event Recording**: Captures 2s before + 3s after each smoking event as separate clips.
- **Live Display**: Real-time visualization window with 'q' to quit. - **Live Display**: Real-time visualization window with 'q' to quit.
- **Metrics Tracking**: FPS, latency, detection counts with summary on exit. - **Metrics Tracking**: FPS, latency, detection counts with summary on exit.
@@ -179,7 +181,8 @@ gp/
│ │ └── api.py # APIPersonDetector, VLMVerifier (cloud stages) │ │ └── api.py # APIPersonDetector, VLMVerifier (cloud stages)
│ │ │ │
│ └── ops/ # Algorithms & utilities │ └── ops/ # Algorithms & utilities
│ ├── nms.py # Non-Maximum Suppression │ ├── nms.py # NMS + Weighted Boxes Fusion (WBF)
│ ├── selection.py # Budget-aware box selection (top-K, merge)
│ ├── geometry.py # Box expansion, zone filtering │ ├── geometry.py # Box expansion, zone filtering
│ └── drawing.py # Visualization helpers │ └── drawing.py # Visualization helpers
│ │
@@ -207,6 +210,12 @@ class PipelineConfig:
idle_interval: float = 0.5 # 2 FPS when idle idle_interval: float = 0.5 # 2 FPS when idle
active_interval: float = 0.0 # Max FPS when active active_interval: float = 0.0 # Max FPS when active
cooldown_frames: int = 60 # Stay active after detection cooldown_frames: int = 60 # Stay active after detection
# Inference budget caps
max_person_crops: int = 3 # Max cig model runs (0 = unlimited)
person_priority: str = "area" # "area", "confidence", "area_conf"
merge_people: bool = False # Merge overlapping people into regions
max_cig_verifications: int = 5 # Max verifier runs (0 = unlimited)
``` ```
Pass a custom config to the pipeline: Pass a custom config to the pipeline:
@@ -232,6 +241,26 @@ Key tuning parameters:
- `active_interval`: Set to 0.0 for max detection rate - `active_interval`: Set to 0.0 for max detection rate
- `cig_high_confidence`: Lower for faster alerts, higher for fewer false positives - `cig_high_confidence`: Lower for faster alerts, higher for fewer false positives
### Inference Budget
When using lightweight (nano) models that produce many false positives, cap expensive downstream calls:
```python
config = PipelineConfig(
max_person_crops=3, # Only process 3 people max per frame
person_priority="area", # Prioritize largest (closest to camera)
merge_people=True, # Merge overlapping people into regions
merge_people_iou=0.3, # IoU threshold for merging
max_cig_verifications=5, # Only verify top 5 cigarette candidates
)
```
| Priority | Strategy |
|----------|----------|
| `area` | Largest boxes first (closer to camera, more detail) |
| `confidence` | Highest scoring boxes first |
| `area_conf` | Balanced: area × confidence |
## 🔌 Pluggable Stage System ## 🔌 Pluggable Stage System
The `--staged` flag enables the modular stage pipeline, allowing you to mix local models with cloud APIs: The `--staged` flag enables the modular stage pipeline, allowing you to mix local models with cloud APIs:
+16
View File
@@ -2,6 +2,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from pipeline.ops.selection import SelectPriority
@dataclass @dataclass
class PipelineConfig: class PipelineConfig:
@@ -32,6 +34,20 @@ class PipelineConfig:
active_interval: float = 0.0 # Seconds between inference when active active_interval: float = 0.0 # Seconds between inference when active
cooldown_frames: int = 60 # Frames to stay active after last detection cooldown_frames: int = 60 # Frames to stay active after last detection
# === Inference Budget Caps ===
# Limits expensive model calls when there are too many detections
# Max person crops to run cig detection on (0 = unlimited)
max_person_crops: int = 3
# How to prioritize which people to process: "area", "confidence", "area_conf"
person_priority: SelectPriority = "area"
# Merge overlapping people into regions instead of processing separately
merge_people: bool = False
merge_people_iou: float = 0.3
# Max cigarette candidates to run verifier on (0 = unlimited)
max_cig_verifications: int = 5
# Default config instance # Default config instance
DEFAULT_CONFIG = PipelineConfig() DEFAULT_CONFIG = PipelineConfig()
+9
View File
@@ -18,6 +18,11 @@ from pipeline.ops.geometry import (
point_in_polygon, point_in_polygon,
) )
from pipeline.ops.nms import nms, nms_by_class, wbf, wbf_by_class from pipeline.ops.nms import nms, nms_by_class, wbf, wbf_by_class
from pipeline.ops.selection import (
merge_overlapping,
select_for_verification,
select_top_k,
)
__all__ = [ __all__ = [
# NMS / Box Fusion # NMS / Box Fusion
@@ -25,6 +30,10 @@ __all__ = [
"nms_by_class", "nms_by_class",
"wbf", "wbf",
"wbf_by_class", "wbf_by_class",
# Selection / Budget
"select_top_k",
"select_for_verification",
"merge_overlapping",
# Geometry # Geometry
"expand_box", "expand_box",
"point_in_box", "point_in_box",
+174
View File
@@ -0,0 +1,174 @@
"""Budget-aware box selection for capping expensive inference calls."""
from typing import List, Literal
from pipeline.types import BoundingBox, Detection
type SelectPriority = Literal["area", "confidence", "area_conf"]
def select_top_k(
detections: List[Detection],
k: int,
diversity_iou: float = 0.5,
priority: SelectPriority = "area",
) -> List[Detection]:
"""
Select top K detections with spatial diversity.
Picks the K "best" boxes while ensuring they don't overlap too much.
This caps expensive downstream inference (e.g., max 3 person crops → cig model).
Args:
detections: Candidate detections
k: Maximum number to select
diversity_iou: Reject candidates overlapping selected boxes above this IoU
priority: How to rank candidates:
- "area": Larger boxes first (closer to camera, more detail)
- "confidence": Higher confidence first
- "area_conf": area * confidence (balanced)
Returns:
Up to K diverse, high-priority detections
"""
if not detections or k <= 0:
return []
# Score each detection
def score(det: Detection) -> float:
box = det.box
# area = (box.x2 - box.x1) * (box.y2 - box.y1)
area = box.area
if priority == "area":
return area
elif priority == "confidence":
return det.score
else: # area_conf
return area * det.score
# Sort by priority (highest first)
ranked = sorted(detections, key=score, reverse=True)
selected = []
for det in ranked:
if len(selected) >= k:
break
# Check diversity: don't select if overlaps too much with already selected
dominated = False
for sel in selected:
if det.box.iou(sel.box) > diversity_iou:
dominated = True
break
if not dominated:
selected.append(det)
return selected
def merge_overlapping(
detections: List[Detection],
iou_threshold: float = 0.3,
) -> List[Detection]:
"""
Merge overlapping boxes into larger regions.
Instead of processing each overlapping person separately, merge them
into one bigger crop. Good when people are clustered together.
Args:
detections: List of detections (e.g., people)
iou_threshold: Merge boxes with IoU above this
Returns:
Merged detections (fewer, larger boxes)
"""
if not detections:
return []
# Sort by confidence
dets = sorted(detections, key=lambda d: d.score, reverse=True)
clusters: List[List[Detection]] = []
for det in dets:
merged = False
for cluster in clusters:
# Check if this det overlaps with any box in cluster
for member in cluster:
if det.box.iou(member.box) >= iou_threshold:
cluster.append(det)
merged = True
break
if merged:
break
if not merged:
clusters.append([det])
# Create merged boxes from clusters
result = []
for cluster in clusters:
merged_det = _merge_cluster(cluster)
result.append(merged_det)
return result
def _merge_cluster(cluster: List[Detection]) -> Detection:
"""Merge a cluster of detections into one encompassing box."""
if len(cluster) == 1:
return cluster[0]
# Bounding box that contains all
x1 = min(d.box.x1 for d in cluster)
y1 = min(d.box.y1 for d in cluster)
x2 = max(d.box.x2 for d in cluster)
y2 = max(d.box.y2 for d in cluster)
# Confidence: max of cluster (could also use average)
conf = max(d.score for d in cluster)
# Use label from highest confidence detection
best = max(cluster, key=lambda d: d.score)
return Detection(
box=BoundingBox(x1=x1, y1=y1, x2=x2, y2=y2, conf=conf),
score=conf,
label=best.label,
metadata={
"merged_count": len(cluster),
"original_boxes": [
(d.box.x1, d.box.y1, d.box.x2, d.box.y2) for d in cluster
],
},
)
def select_for_verification(
detections: List[Detection],
k: int,
min_confidence: float = 0.0,
) -> List[Detection]:
"""
Select top K cigarette candidates for expensive verification.
Simple confidence-based selection for the verifier stage.
Higher confidence cigs are more likely real → verify those first.
Args:
detections: Cigarette candidates
k: Max verifications to run
min_confidence: Skip candidates below this
Returns:
Top K candidates by confidence
"""
if not detections or k <= 0:
return []
filtered = [d for d in detections if d.score >= min_confidence]
ranked = sorted(filtered, key=lambda d: d.score, reverse=True)
return ranked[:k]
+1
View File
@@ -148,6 +148,7 @@ class APIPersonDetector(APIStageBase):
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
# haiku 4.5 is multimodal but i fear its going to ping 65 million images per second (we need cache control? who are we? why cant we upload video?) # haiku 4.5 is multimodal but i fear its going to ping 65 million images per second (we need cache control? who are we? why cant we upload video?)
class VLMVerifier(APIStageBase): class VLMVerifier(APIStageBase):
""" """
+56 -6
View File
@@ -5,7 +5,12 @@ import time
from typing import List, Optional from typing import List, Optional
from pipeline.config import DEFAULT_CONFIG, PipelineConfig from pipeline.config import DEFAULT_CONFIG, PipelineConfig
from pipeline.ops import nms_by_class from pipeline.ops import (
merge_overlapping,
nms_by_class,
select_for_verification,
select_top_k,
)
from pipeline.protocols import Model, Verifier from pipeline.protocols import Model, Verifier
from pipeline.stages.protocol import StageContext, StageResult from pipeline.stages.protocol import StageContext, StageResult
from pipeline.types import BoundingBox, Detection, Ticket from pipeline.types import BoundingBox, Detection, Ticket
@@ -66,10 +71,39 @@ class LocalCigaretteDetector:
cfg = self.cfg cfg = self.cfg
h_img, w_img = ctx.frame_data.shape[:2] h_img, w_img = ctx.frame_data.shape[:2]
# === Budget: Limit how many people we process ===
people_to_process = ctx.people
if cfg.merge_people:
# Merge overlapping people into regions
people_to_process = merge_overlapping(
people_to_process, cfg.merge_people_iou
)
if len(people_to_process) < len(ctx.people):
logger.debug(
"Merged %d people into %d regions",
len(ctx.people),
len(people_to_process),
)
if cfg.max_person_crops > 0 and len(people_to_process) > cfg.max_person_crops:
# Select top K diverse people by priority
people_to_process = select_top_k(
people_to_process,
k=cfg.max_person_crops,
priority=cfg.person_priority,
)
logger.debug(
"Budget: Processing %d/%d people (priority=%s)",
len(people_to_process),
len(ctx.people),
cfg.person_priority,
)
all_cigs: List[Detection] = [] all_cigs: List[Detection] = []
early_hit = None early_hit = None
for person in ctx.people: for person in people_to_process:
# Expand person box for crop # Expand person box for crop
crop_box = self._expand_box(person.box, cfg.person_margin, w_img, h_img) crop_box = self._expand_box(person.box, cfg.person_margin, w_img, h_img)
ex1, ey1, ex2, ey2 = crop_box ex1, ey1, ex2, ey2 = crop_box
@@ -195,12 +229,28 @@ class LocalVerifier:
cfg = self.cfg cfg = self.cfg
h_img, w_img = ctx.frame_data.shape[:2] h_img, w_img = ctx.frame_data.shape[:2]
# === Budget: Limit how many cigs we verify ===
cigs_to_verify = ctx.cigarettes
if (
cfg.max_cig_verifications > 0
and len(cigs_to_verify) > cfg.max_cig_verifications
):
# Select top K by confidence (most likely real → verify first)
cigs_to_verify = select_for_verification(
cigs_to_verify,
k=cfg.max_cig_verifications,
min_confidence=cfg.cig_min_confidence,
)
logger.debug(
"Budget: Verifying %d/%d cigarettes",
len(cigs_to_verify),
len(ctx.cigarettes),
)
confirmed: list[tuple[Detection, float]] = [] confirmed: list[tuple[Detection, float]] = []
for cig_det in ctx.cigarettes: for cig_det in cigs_to_verify:
offset = cig_det.metadata.get("crop_offset", (0, 0))
ex1, ey1 = offset
# Extract padded crop for classifier # Extract padded crop for classifier
crop = self._extract_cig_crop(ctx.frame_data, cig_det, w_img, h_img) crop = self._extract_cig_crop(ctx.frame_data, cig_det, w_img, h_img)
if crop.size == 0: if crop.size == 0:
+1 -1
View File
@@ -60,7 +60,7 @@ class StagePipeline:
stage.name, stage.name,
ctx.stop_reason, ctx.stop_reason,
) )
continue # break? continue # break?
try: try:
result = stage.process(ctx) result = stage.process(ctx)
+1 -1
View File
@@ -10,7 +10,7 @@ class BoundingBox:
y1: int y1: int
x2: int x2: int
y2: int y2: int
conf: float = 0.0 conf: float = 0.0 # legacy
@property @property
def area(self): def area(self):