21
This commit is contained in:
1 parent
92b1aa2407
commit
19f0b9f352
11 files changed
+220
-18
No files matched your search
@@ -1,2 +1,3 @@
|
|||||||
*.pt filter=lfs diff=lfs merge=lfs -text
|
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||||
*.jsonl filter=lfs diff=lfs merge=lfs -text
|
*.jsonl filter=lfs diff=lfs merge=lfs -text
|
||||||
Vendored
+2
-1
@@ -4,5 +4,6 @@
|
|||||||
"**/*.rpa": true,
|
"**/*.rpa": true,
|
||||||
"**/*.rpymc": true,
|
"**/*.rpymc": true,
|
||||||
"**/cache/": true
|
"**/cache/": true
|
||||||
}
|
},
|
||||||
|
"pylint.args": ["--generated-members", "cv2.*"]
|
||||||
}
|
}
|
||||||
Submodule Group_project_smoking_detection deleted from f5a695376c.
@@ -4,6 +4,7 @@ import sys
|
|||||||
|
|
||||||
from pipeline.core import SmokingDetectionPipeline
|
from pipeline.core import SmokingDetectionPipeline
|
||||||
from pipeline.models import YoloCigaretteDetector, YoloClassifier, YoloPersonDetector
|
from pipeline.models import YoloCigaretteDetector, YoloClassifier, YoloPersonDetector
|
||||||
|
from pipeline.outputs import VideoFileStore
|
||||||
from pipeline.sources import FileStreamSource, ThreadedRTSPSource
|
from pipeline.sources import FileStreamSource, ThreadedRTSPSource
|
||||||
from pipeline.types import Ticket
|
from pipeline.types import Ticket
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--cig-model",
|
"--cig-model",
|
||||||
type=str,
|
type=str,
|
||||||
default="yolov8n.pt",
|
default="models/cig-det-best.pt",
|
||||||
help="Path to Cigarette YOLO model",
|
help="Path to Cigarette YOLO model",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -44,6 +45,12 @@ def main():
|
|||||||
default="models/cig-cls-best.pt",
|
default="models/cig-cls-best.pt",
|
||||||
help="Path to Cigarette Classifier model",
|
help="Path to Cigarette Classifier model",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=str,
|
||||||
|
default="output.mp4",
|
||||||
|
help="Path to output video file (will also generate .jsonl)",
|
||||||
|
)
|
||||||
parser.add_argument("--debug", action="store_true", help="Increase verbosity")
|
parser.add_argument("--debug", action="store_true", help="Increase verbosity")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -63,12 +70,16 @@ def main():
|
|||||||
cigarette_model = YoloCigaretteDetector(args.cig_model)
|
cigarette_model = YoloCigaretteDetector(args.cig_model)
|
||||||
verifier = YoloClassifier(args.verify_model)
|
verifier = YoloClassifier(args.verify_model)
|
||||||
|
|
||||||
|
# Initialize Output Sink
|
||||||
|
video_sink = VideoFileStore(args.output)
|
||||||
|
|
||||||
pipeline = SmokingDetectionPipeline(
|
pipeline = SmokingDetectionPipeline(
|
||||||
person_model=person_model,
|
person_model=person_model,
|
||||||
cigarette_model=cigarette_model,
|
cigarette_model=cigarette_model,
|
||||||
verifier_model=verifier,
|
verifier_model=verifier,
|
||||||
vlm_model=None, # Optional
|
vlm_model=None, # Optional
|
||||||
store=ConsoleStore(),
|
store=ConsoleStore(),
|
||||||
|
sinks=[video_sink],
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.rtsp:
|
if args.rtsp:
|
||||||
@@ -82,9 +93,12 @@ def main():
|
|||||||
try:
|
try:
|
||||||
pipeline.process_stream(source)
|
pipeline.process_stream(source)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
if args.rtsp:
|
print("Stopped by user.")
|
||||||
|
finally:
|
||||||
|
if args.rtsp and hasattr(source, "stop"):
|
||||||
source.stop()
|
source.stop()
|
||||||
print("Stopped.")
|
if video_sink:
|
||||||
|
video_sink.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
+57
-1
@@ -22,12 +22,14 @@ class SmokingDetectionPipeline:
|
|||||||
verifier_model: Verifier,
|
verifier_model: Verifier,
|
||||||
vlm_model: Optional[Verifier] = None,
|
vlm_model: Optional[Verifier] = None,
|
||||||
store: Optional[AlertStore] = None,
|
store: Optional[AlertStore] = None,
|
||||||
|
sinks: Optional[list] = None,
|
||||||
):
|
):
|
||||||
self.person_model = person_model
|
self.person_model = person_model
|
||||||
self.cigarette_model = cigarette_model
|
self.cigarette_model = cigarette_model
|
||||||
self.verifier_model = verifier_model
|
self.verifier_model = verifier_model
|
||||||
self.vlm_model = vlm_model
|
self.vlm_model = vlm_model
|
||||||
self.store = store or ConsoleStore()
|
self.store = store or ConsoleStore()
|
||||||
|
self.sinks = sinks or []
|
||||||
|
|
||||||
# Buffer for temporal context (optional, for VLM or sequence logic)
|
# Buffer for temporal context (optional, for VLM or sequence logic)
|
||||||
self.frame_buffer = deque(maxlen=30)
|
self.frame_buffer = deque(maxlen=30)
|
||||||
@@ -45,14 +47,22 @@ class SmokingDetectionPipeline:
|
|||||||
logger.info(f"Processed {count} frames...")
|
logger.info(f"Processed {count} frames...")
|
||||||
self.frame_buffer.append(frame)
|
self.frame_buffer.append(frame)
|
||||||
self._process_frame(frame)
|
self._process_frame(frame)
|
||||||
|
|
||||||
|
# Update sinks (Visualizers, etc)
|
||||||
|
for sink in self.sinks:
|
||||||
|
sink.handle(frame)
|
||||||
|
|
||||||
logger.info("Stream processing finished.")
|
logger.info("Stream processing finished.")
|
||||||
|
|
||||||
def _process_frame(self, frame: Frame):
|
def _process_frame(self, frame: Frame):
|
||||||
# --- Step 1: Broad Phase (Person Detection) ---
|
# --- Step 1: Broad Phase (Person Detection) ---
|
||||||
# "Is there a human here?"
|
# "Is there a human here?"
|
||||||
# Optimization: Resize or skip frames if inference is slow
|
# optimization: Resize or skip frames if inference is slow
|
||||||
people = self.person_model.predict(frame.data)
|
people = self.person_model.predict(frame.data)
|
||||||
|
|
||||||
|
# Add to frame detections for visualization
|
||||||
|
frame.detections.extend(people)
|
||||||
|
|
||||||
if not people:
|
if not people:
|
||||||
return # FAST EXIT
|
return # FAST EXIT
|
||||||
|
|
||||||
@@ -78,6 +88,34 @@ class SmokingDetectionPipeline:
|
|||||||
if cig_candidates:
|
if cig_candidates:
|
||||||
candidates.append((person, cig_candidates))
|
candidates.append((person, cig_candidates))
|
||||||
|
|
||||||
|
# Check detections (translate to global coordinates)
|
||||||
|
for cig in cig_candidates:
|
||||||
|
# Clone or modify the cig detection to be global
|
||||||
|
# Assuming cig.box is relevant to person_crop
|
||||||
|
# Global X = person.box.x1 + local_x
|
||||||
|
# Global Y = person.box.y1 + local_y
|
||||||
|
|
||||||
|
# Update in place might affect other logic if it assumes local,
|
||||||
|
# but logic below (Expansion) seems to rely on cig.box being used on raw_person_crop
|
||||||
|
# which implies local coordinates.
|
||||||
|
# So we should create a COPY for visualization.
|
||||||
|
|
||||||
|
from pipeline.types import BoundingBox, Detection
|
||||||
|
|
||||||
|
global_cig = Detection(
|
||||||
|
box=BoundingBox(
|
||||||
|
x1=person.box.x1 + cig.box.x1,
|
||||||
|
y1=person.box.y1 + cig.box.y1,
|
||||||
|
x2=person.box.x1 + cig.box.x2,
|
||||||
|
y2=person.box.y1 + cig.box.y2,
|
||||||
|
conf=cig.box.conf,
|
||||||
|
),
|
||||||
|
score=cig.score,
|
||||||
|
label="candidate_cig",
|
||||||
|
metadata=cig.metadata,
|
||||||
|
)
|
||||||
|
frame.detections.append(global_cig)
|
||||||
|
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return # EXIT
|
return # EXIT
|
||||||
|
|
||||||
@@ -136,6 +174,24 @@ class SmokingDetectionPipeline:
|
|||||||
|
|
||||||
if score > 0.8: # High confidence threshold
|
if score > 0.8: # High confidence threshold
|
||||||
confirmed_hits.append((person, cig, score))
|
confirmed_hits.append((person, cig, score))
|
||||||
|
|
||||||
|
# Add verified detection to frame for visualization
|
||||||
|
from pipeline.types import BoundingBox, Detection
|
||||||
|
|
||||||
|
verified_det = Detection(
|
||||||
|
box=BoundingBox(
|
||||||
|
x1=person.box.x1 + cig.box.x1,
|
||||||
|
y1=person.box.y1 + cig.box.y1,
|
||||||
|
x2=person.box.x1 + cig.box.x2,
|
||||||
|
y2=person.box.y1 + cig.box.y2,
|
||||||
|
conf=score,
|
||||||
|
),
|
||||||
|
score=score,
|
||||||
|
label="verified_smoking",
|
||||||
|
metadata={"verifier_score": score},
|
||||||
|
)
|
||||||
|
frame.detections.append(verified_det)
|
||||||
|
|
||||||
elif score > 0.4 and self.vlm_model:
|
elif score > 0.4 and self.vlm_model:
|
||||||
# Ambiguous case: Send to VLM?
|
# Ambiguous case: Send to VLM?
|
||||||
vlm_score = self.vlm_model.verify(
|
vlm_score = self.vlm_model.verify(
|
||||||
|
|||||||
+14
-12
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
from ultralytics import YOLO
|
from ultralytics.models import YOLO
|
||||||
|
|
||||||
from pipeline.protocols import Model, Verifier
|
from pipeline.protocols import Model, Verifier
|
||||||
from pipeline.types import BoundingBox, Detection
|
from pipeline.types import BoundingBox, Detection
|
||||||
@@ -69,10 +69,6 @@ class YoloClassifier(Verifier):
|
|||||||
|
|
||||||
def verify(self, input_data: Any) -> float:
|
def verify(self, input_data: Any) -> float:
|
||||||
# input_data: numpy array (H, W, C) - Usually a crop
|
# input_data: numpy array (H, W, C) - Usually a crop
|
||||||
# Determine strict classification
|
|
||||||
# Assuming model returns a list of Results
|
|
||||||
# For classification, we look at probs
|
|
||||||
|
|
||||||
if input_data.size == 0:
|
if input_data.size == 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
@@ -86,10 +82,16 @@ class YoloClassifier(Verifier):
|
|||||||
if probs is None:
|
if probs is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# Assuming binary classification [negative, positive] or specific class index
|
# User specified Class 0 is "smoke"
|
||||||
# We need to know which class index corresponds to "smoking/cigarette"
|
# We return the probability of class 0 specifically.
|
||||||
# If it's binary trained: 0: Fire/Smoke/Cig, 1: No? Or vice versa.
|
# Ultralytics probs.data is a tensor of scores for each class.
|
||||||
# Let's assume class 0 is the target for now, or use probs.top1conf if we trust the label.
|
try:
|
||||||
|
return float(probs.data[0])
|
||||||
# Taking the highest confidence score
|
except (AttributeError, IndexError):
|
||||||
return float(probs.top1conf)
|
# Fallback if structure is unexpected, though data[0] is standard for classification
|
||||||
|
logger.warning(
|
||||||
|
"Could not retrieve Class 0 probability. Defaulting to top1conf if class 0."
|
||||||
|
)
|
||||||
|
if hasattr(probs, "top1") and probs.top1 == 0:
|
||||||
|
return float(probs.top1conf)
|
||||||
|
return 0.0
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from pipeline.types import Detection, Frame
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoFileStore:
|
||||||
|
"""
|
||||||
|
Writes processed frames to a video file with visualization overlays.
|
||||||
|
Also optionally writes metadata sidecar files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output_path: str, fps: int = 30, fourcc: str = "mp4v"):
|
||||||
|
self.output_path = output_path
|
||||||
|
self.metadata_path = str(Path(output_path).with_suffix(".jsonl"))
|
||||||
|
self.fps = fps
|
||||||
|
self.fourcc = fourcc
|
||||||
|
self.writer: Optional[cv2.VideoWriter] = None
|
||||||
|
self.frame_size = None
|
||||||
|
|
||||||
|
# Open metadata file
|
||||||
|
self.meta_file = open(self.metadata_path, "w")
|
||||||
|
logger.info(
|
||||||
|
f"Initialized VideoFileStore writing to {output_path} and {self.metadata_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, frame: Frame):
|
||||||
|
"""Process a frame: Draw boxes and write to video/log."""
|
||||||
|
vis_image = frame.data.copy()
|
||||||
|
|
||||||
|
# 1. Visualization
|
||||||
|
self._draw_detections(vis_image, frame.detections)
|
||||||
|
self._draw_overlay(vis_image, frame.frame_id)
|
||||||
|
|
||||||
|
# 2. Write Video
|
||||||
|
if self.writer is None:
|
||||||
|
h, w = vis_image.shape[:2]
|
||||||
|
self.frame_size = (w, h)
|
||||||
|
self.writer = cv2.VideoWriter(
|
||||||
|
self.output_path,
|
||||||
|
cv2.VideoWriter.fourcc(*self.fourcc),
|
||||||
|
self.fps,
|
||||||
|
self.frame_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.writer.write(vis_image)
|
||||||
|
|
||||||
|
# 3. Write Metadata
|
||||||
|
self._write_metadata(frame)
|
||||||
|
|
||||||
|
def _draw_detections(self, image: np.ndarray, detections: List[Detection]):
|
||||||
|
for det in detections:
|
||||||
|
x1, y1, x2, y2 = det.box.x1, det.box.y1, det.box.x2, det.box.y2
|
||||||
|
label = det.label
|
||||||
|
score = det.score
|
||||||
|
|
||||||
|
# Color coding
|
||||||
|
color = (255, 255, 255) # White default
|
||||||
|
if "person" in label:
|
||||||
|
color = (255, 0, 0) # Blue
|
||||||
|
elif "candidate" in label:
|
||||||
|
color = (0, 255, 255) # Yellow
|
||||||
|
elif "verified" in label:
|
||||||
|
color = (0, 0, 255) # Red (Confirmed Smoke)
|
||||||
|
|
||||||
|
# Draw Box
|
||||||
|
cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
|
||||||
|
|
||||||
|
# Draw Label
|
||||||
|
text = f"{label} {score:.2f}"
|
||||||
|
(tw, th), _ = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
||||||
|
cv2.rectangle(image, (x1, y1 - 20), (x1 + tw, y1), color, -1)
|
||||||
|
cv2.putText(
|
||||||
|
image, text, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
def _draw_overlay(self, image: np.ndarray, frame_id: int):
|
||||||
|
cv2.putText(
|
||||||
|
image,
|
||||||
|
f"Frame: {frame_id}",
|
||||||
|
(10, 30),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
1.0,
|
||||||
|
(0, 255, 0),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_metadata(self, frame: Frame):
|
||||||
|
meta = {
|
||||||
|
"frame_id": frame.frame_id,
|
||||||
|
"timestamp": frame.timestamp,
|
||||||
|
"detections": [
|
||||||
|
{
|
||||||
|
"label": d.label,
|
||||||
|
"score": d.score,
|
||||||
|
"box": [d.box.x1, d.box.y1, d.box.x2, d.box.y2],
|
||||||
|
"meta": d.metadata,
|
||||||
|
}
|
||||||
|
for d in frame.detections
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self.meta_file.write(json.dumps(meta) + "\n")
|
||||||
|
self.meta_file.flush()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.writer:
|
||||||
|
self.writer.release()
|
||||||
|
if self.meta_file:
|
||||||
|
self.meta_file.close()
|
||||||
|
logger.info("VideoFileStore closed.")
|
||||||
Reference in new issue
Block a user