Small tweaks to simulation camera to get scanning working.

This commit is contained in:
Julian Stirling 2025-08-04 16:23:37 +01:00
parent b75af87b84
commit 8ce06991f9
3 changed files with 45 additions and 13 deletions

View file

@ -295,12 +295,11 @@ class BaseCamera(lt.Thing):
) )
return portal.call(stream.next_frame_size) return portal.call(stream.next_frame_size)
@lt.thing_action
def capture_image( def capture_image(
self, self,
stream_name: Literal["main", "lores", "raw"], stream_name: Literal["main", "lores", "raw"],
wait: Optional[float], wait: Optional[float],
) -> None: ) -> Image:
"""Capture a PIL image from stream stream_name with timeout wait.""" """Capture a PIL image from stream stream_name with timeout wait."""
raise NotImplementedError( raise NotImplementedError(
"CameraThings must define their own capture_image method" "CameraThings must define their own capture_image method"

View file

@ -28,6 +28,7 @@ from threading import RLock
from pydantic import BaseModel, BeforeValidator from pydantic import BaseModel, BeforeValidator
import piexif import piexif
import numpy as np import numpy as np
from PIL import Image
from picamera2 import Picamera2 from picamera2 import Picamera2
from picamera2.encoders import MJPEGEncoder from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import Output from picamera2.outputs import Output
@ -484,12 +485,11 @@ class StreamingPiCamera2(BaseCamera):
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
cam.capture_metadata() cam.capture_metadata()
@lt.thing_action
def capture_image( def capture_image(
self, self,
stream_name: Literal["main", "lores", "raw"] = "main", stream_name: Literal["main", "lores", "raw"] = "main",
wait: Optional[float] = 0.9, wait: Optional[float] = 0.9,
) -> None: ) -> Image:
"""Acquire one image from the camera. """Acquire one image from the camera.
Return it as a PIL Image Return it as a PIL Image

View file

@ -16,6 +16,7 @@ import time
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image
import piexif import piexif
from scipy.ndimage import gaussian_filter from scipy.ndimage import gaussian_filter
@ -51,7 +52,7 @@ class SimulatedCamera(BaseCamera):
def __init__( def __init__(
self, self,
shape: tuple[int, int, int] = (600, 800, 3), shape: tuple[int, int, int] = (616, 820, 3),
glyph_shape: tuple[int, int, int] = (91, 91, 3), glyph_shape: tuple[int, int, int] = (91, 91, 3),
canvas_shape: tuple[int, int, int] = (3000, 4000, 3), canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
frame_interval: float = 0.1, frame_interval: float = 0.1,
@ -208,9 +209,7 @@ class SimulatedCamera(BaseCamera):
def __enter__(self): def __enter__(self):
"""Start the capture thread when the Thing context manager is opened.""" """Start the capture thread when the Thing context manager is opened."""
self._capture_enabled = True self.start_streaming()
self._capture_thread = Thread(target=self._capture_frames)
self._capture_thread.start()
return self return self
def __exit__(self, _exc_type, _exc_value, _traceback): def __exit__(self, _exc_type, _exc_value, _traceback):
@ -219,6 +218,24 @@ class SimulatedCamera(BaseCamera):
self._capture_enabled = False self._capture_enabled = False
self._capture_thread.join() self._capture_thread.join()
@lt.thing_action
def start_streaming(
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 1
) -> None:
"""Start the live stream.
This should be used to adjust the resolution the simulation doesn't yet do
this.
"""
logging.warning(
f"Simulation camera doesn't respect {main_resolution=} or {buffer_count=} "
"arguments."
)
if not self.stream_active:
self._capture_enabled = True
self._capture_thread = Thread(target=self._capture_frames)
self._capture_thread.start()
@lt.thing_property @lt.thing_property
def stream_active(self) -> bool: def stream_active(self) -> bool:
"""Whether the MJPEG stream is active.""" """Whether the MJPEG stream is active."""
@ -236,9 +253,11 @@ class SimulatedCamera(BaseCamera):
frame = self.generate_frame() frame = self.generate_frame()
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
self.mjpeg_stream.add_frame(jpeg, portal) self.mjpeg_stream.add_frame(jpeg, portal)
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[ # Downsample for lores
1 ds_frame = cv2.resize(
].tobytes() frame, (320, 240), interpolation=cv2.INTER_NEAREST
)
jpeg_lores = cv2.imencode(".jpg", ds_frame)[1].tobytes()
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal) self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
except Exception as e: except Exception as e:
logging.error(f"Failed to capture frame: {e}, retrying...") logging.error(f"Failed to capture frame: {e}, retrying...")
@ -261,7 +280,7 @@ class SimulatedCamera(BaseCamera):
It's likely to be highly inefficient - raw and/or uncompressed captures using It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. binary image formats will be added in due course.
""" """
logging.warning(f"Simulation camera doesn't respect {resolution} setting") logging.warning(f"Simulation camera doesn't respect {resolution=} setting")
return self.generate_frame() return self.generate_frame()
@lt.thing_action @lt.thing_action
@ -274,7 +293,7 @@ class SimulatedCamera(BaseCamera):
This function will produce a JPEG image. This function will produce a JPEG image.
""" """
logging.warning(f"Simulation camera doesn't respect {resolution} setting") logging.warning(f"Simulation camera doesn't respect {resolution=} setting")
frame = self.capture_array() frame = self.capture_array()
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
exif_dict = { exif_dict = {
@ -292,6 +311,20 @@ class SimulatedCamera(BaseCamera):
piexif.insert(piexif.dump(exif_dict), jpeg, output) piexif.insert(piexif.dump(exif_dict), jpeg, output)
return JPEGBlob.from_bytes(output.getvalue()) return JPEGBlob.from_bytes(output.getvalue())
def capture_image(
self,
stream_name: Literal["main", "lores", "raw"],
wait: Optional[float] = None,
) -> Image:
"""Capture to a PIL image. This is not exposed as a ThingAction.
It is used for capture to memory.
"""
logging.warning(
f"Simulation camera doesn't respect {stream_name=} or {wait=} arguments."
)
return Image.fromarray(self.generate_frame())
@lt.thing_action @lt.thing_action
def remove_sample(self): def remove_sample(self):
"""Show the simulated background with no sample.""" """Show the simulated background with no sample."""