feat(edge): CSI camera source (picamera2) + CAMERA_BACKEND switch

Replace the PiCameraSource NotImplementedError stub with a working picamera2 CSI
source (SourcePort read()->(ok,frame), RGB888->BGR to match the pose adapters;
picamera2 lazy-imported as a system package, injected fake in tests). daemon.main()
gains CAMERA_BACKEND (csi|uvc) selecting PiCameraSource or UvcSource.
This commit is contained in:
유창욱 2026-07-12 22:36:05 +09:00
parent 2335faa568
commit 88ec0ba1d4
3 changed files with 67 additions and 6 deletions

View file

@ -1,10 +1,29 @@
"""CSI 카메라 소스 (실장비). 카메라당 별 프로세스로 picamera2 구동."""
"""CSI 카메라 소스 (실장비 Pi5, picamera2). Implements SourcePort.
picamera2 is a SYSTEM package on Raspberry Pi OS (apt: python3-picamera2 + libcamera),
NOT a pip wheel — the venv sees it via --system-site-packages (see deploy/README).
COLOR NOTE: configured for "RGB888", which picamera2 delivers as a 3-channel array in
**BGR** byte order (documented picamera2 quirk). The downstream pose adapters
(MoveNet/Hailo) expect BGR (they do cv2.cvtColor(BGR2RGB)), same as cv2.VideoCapture,
so read() returns the array as-is. Validate the channel order on device during bringup
(a BGR<->RGB swap yields plausible-but-wrong keypoints).
"""
class PiCameraSource:
def __init__(self, index: int = 0):
def __init__(self, index: int = 0, size=(640, 480), picam2=None):
self.index = index
raise NotImplementedError("실장비 전용: picamera2 배선 필요")
if picam2 is None: # pragma: no cover (device-only: needs picamera2 + CSI camera)
from picamera2 import Picamera2 # system apt package (python3-picamera2)
picam2 = Picamera2(camera_num=index)
picam2.configure(picam2.create_video_configuration(
main={"size": tuple(size), "format": "RGB888"}))
picam2.start()
self.picam2 = picam2
def read(self):
raise NotImplementedError
frame = self.picam2.capture_array()
if frame is None:
return False, None
return True, frame

View file

@ -40,7 +40,6 @@ def main() -> None: # pragma: no cover (integration entrypoint, exercised on de
from edge.alert import PostClient
from edge.queue import OfflineQueue
from edge.runtime import PipelineRuntime
from edge.adapters.uvc_source import UvcSource
cfg = load_config(dict(os.environ))
nonce = lambda: os.urandom(8).hex()
@ -49,7 +48,14 @@ def main() -> None: # pragma: no cover (integration entrypoint, exercised on de
client = CloudClient(base, cfg.device_id, cfg.secret, nonce, now)
post = PostClient(base + "/api/events", cfg.device_id, cfg.secret, nonce, now)
queue = OfflineQueue(os.environ.get("QUEUE_DB", "queue.db"), post)
source = UvcSource(int(os.environ.get("UVC_DEVICE", "0")))
# CAMERA_BACKEND selects the capture source: "csi" (Pi CSI cam via picamera2,
# uses CSI_DEVICE) or the default "uvc" (USB webcam via OpenCV, uses UVC_DEVICE).
if os.environ.get("CAMERA_BACKEND", "uvc").lower() == "csi":
from edge.adapters.picamera_source import PiCameraSource
source = PiCameraSource(int(os.environ.get("CSI_DEVICE", "0")))
else:
from edge.adapters.uvc_source import UvcSource
source = UvcSource(int(os.environ.get("UVC_DEVICE", "0")))
# POSE_BACKEND selects the accelerator path: "hailo" (Pi5 + Hailo-8, needs HAILO_HEF)
# or the default CPU "movenet" (needs MOVENET_ONNX).
if os.environ.get("POSE_BACKEND", "movenet").lower() == "hailo":

View file

@ -0,0 +1,36 @@
"""TDD for PiCameraSource (CSI camera via picamera2) — no hardware/picamera2 needed,
by injecting a fake Picamera2 object. Locks the SourcePort contract: read() -> (ok, frame)."""
import numpy as np
from edge.adapters.picamera_source import PiCameraSource
class _FakePicam2:
def __init__(self, frames):
self.frames = list(frames)
self.i = 0
self.started = False
def capture_array(self):
frame = self.frames[self.i % len(self.frames)]
self.i += 1
return frame
def test_read_returns_ok_and_frame():
frame = np.full((480, 640, 3), 128, np.uint8)
src = PiCameraSource(picam2=_FakePicam2([frame]))
ok, f = src.read()
assert ok is True
assert f.shape == (480, 640, 3)
def test_read_none_frame_returns_false():
class _Empty:
def capture_array(self):
return None
src = PiCameraSource(picam2=_Empty())
ok, f = src.read()
assert ok is False
assert f is None