Cooked I Am
This commit is contained in:
1 parent
7b2813be20
commit
d57a48eec6
20 files changed
+665
-276
No files matched your search
@@ -0,0 +1,13 @@
|
||||
"""Stream sources for the pipeline."""
|
||||
|
||||
from pipeline.sources.file import FileStreamSource
|
||||
from pipeline.sources.stream import ThreadedStreamSource
|
||||
|
||||
# Alias for backwards compatibility
|
||||
ThreadedRTSPSource = ThreadedStreamSource
|
||||
|
||||
__all__ = [
|
||||
"FileStreamSource",
|
||||
"ThreadedStreamSource",
|
||||
"ThreadedRTSPSource", # Alias
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
"""File-based video source."""
|
||||
|
||||
import glob
|
||||
import logging
|
||||
import time
|
||||
from typing import Iterator
|
||||
|
||||
import cv2
|
||||
|
||||
from pipeline.protocols import StreamSource
|
||||
from pipeline.types import Frame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileStreamSource(StreamSource):
|
||||
"""Reads frames from a video file."""
|
||||
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
self._fps = 30.0
|
||||
|
||||
# Probe for FPS if it's a single file
|
||||
if "*" not in self.path:
|
||||
try:
|
||||
cap = cv2.VideoCapture(self.path)
|
||||
if cap.isOpened():
|
||||
self._fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
logger.warning("Could not probe FPS from %s: %s", path, e)
|
||||
|
||||
@property
|
||||
def fps(self) -> float:
|
||||
return self._fps
|
||||
|
||||
def stream(self) -> Iterator[Frame]:
|
||||
# Handle glob patterns for image sequences
|
||||
if "*" in self.path:
|
||||
files = sorted(glob.glob(self.path))
|
||||
logger.info("Loading %d images from pattern: %s", len(files), self.path)
|
||||
for frame_id, file in enumerate(files):
|
||||
img = cv2.imread(file)
|
||||
if img is not None:
|
||||
yield Frame(timestamp=time.time(), data=img, frame_id=frame_id)
|
||||
return
|
||||
|
||||
logger.info("Opening video file: %s", self.path)
|
||||
cap = cv2.VideoCapture(self.path)
|
||||
frame_id = 0
|
||||
while cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
logger.info("End of video file reached.")
|
||||
break
|
||||
|
||||
yield Frame(timestamp=time.time(), data=frame, frame_id=frame_id)
|
||||
frame_id += 1
|
||||
cap.release()
|
||||
|
||||
def stop(self):
|
||||
"""No-op for file source (stream ends naturally)."""
|
||||
pass
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Threaded source for RTSP streams and webcams."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from queue import Empty, Queue
|
||||
from typing import Iterator
|
||||
|
||||
import cv2
|
||||
|
||||
from pipeline.protocols import StreamSource
|
||||
from pipeline.types import Frame
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThreadedStreamSource(StreamSource):
|
||||
"""
|
||||
Reads from RTSP stream or webcam in a background thread.
|
||||
|
||||
Prevents buffer buildup and allows main processing loop to run
|
||||
at its own pace without stalling the decoding.
|
||||
"""
|
||||
|
||||
def __init__(self, source: str | int, queue_size: int = 5):
|
||||
self.source = source
|
||||
self.queue: Queue[Frame] = Queue(maxsize=queue_size)
|
||||
self._fps = 30.0
|
||||
|
||||
# Probe FPS
|
||||
try:
|
||||
cap = cv2.VideoCapture(self.source)
|
||||
if cap.isOpened():
|
||||
self._fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
||||
cap.release()
|
||||
except cv2.error: # pylint: disable=catching-non-exception
|
||||
logger.warning("Could not probe FPS from source.")
|
||||
|
||||
self.stopped = False
|
||||
self.thread = threading.Thread(target=self._reader_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info("Initialized ThreadedStreamSource for %s", source)
|
||||
|
||||
@property
|
||||
def fps(self) -> float:
|
||||
return self._fps
|
||||
|
||||
def _reader_loop(self):
|
||||
"""Background thread that continuously reads frames."""
|
||||
logger.info("Reader thread started for %s", self.source)
|
||||
cap = cv2.VideoCapture(self.source)
|
||||
frame_id = 0
|
||||
|
||||
while not self.stopped:
|
||||
if not cap.isOpened():
|
||||
logger.warning("Stream not open, retrying...")
|
||||
time.sleep(1)
|
||||
cap = cv2.VideoCapture(self.source)
|
||||
continue
|
||||
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
logger.warning("Failed to read frame, retrying...")
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# Drop oldest if queue full (keep latest frames)
|
||||
if self.queue.full():
|
||||
try:
|
||||
self.queue.get_nowait()
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
self.queue.put(Frame(timestamp=time.time(), data=frame, frame_id=frame_id))
|
||||
frame_id += 1
|
||||
|
||||
cap.release()
|
||||
logger.info("Reader thread stopped.")
|
||||
|
||||
def stream(self) -> Iterator[Frame]:
|
||||
"""Yield frames from the queue."""
|
||||
while not self.stopped:
|
||||
try:
|
||||
yield self.queue.get(timeout=1)
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
def stop(self):
|
||||
"""Stop the reader thread and release resources."""
|
||||
self.stopped = True
|
||||
if self.thread.is_alive():
|
||||
self.thread.join(timeout=2.0)
|
||||
Reference in new issue
Block a user