Merge branch 'simulation-scanning' into 'v3'
Scanning in simulation Closes #395 See merge request openflexure/openflexure-microscope-server!344
This commit is contained in:
commit
0921a73a81
5 changed files with 51 additions and 14 deletions
|
|
@ -74,7 +74,7 @@ def test_client_connection() -> None:
|
|||
cam_client = lt.ThingClient.from_url("http://localhost:5000/camera/")
|
||||
img = Image.open(cam_client.grab_jpeg().open())
|
||||
print(f"Successfully grabbed image of size {img.size}")
|
||||
assert img.size == (800, 600)
|
||||
assert img.size == (820, 616)
|
||||
print("Successfully grabbed image from camera mjpeg stream")
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -295,12 +295,11 @@ class BaseCamera(lt.Thing):
|
|||
)
|
||||
return portal.call(stream.next_frame_size)
|
||||
|
||||
@lt.thing_action
|
||||
def capture_image(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw"],
|
||||
wait: Optional[float],
|
||||
) -> None:
|
||||
) -> Image:
|
||||
"""Capture a PIL image from stream stream_name with timeout wait."""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own capture_image method"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from threading import RLock
|
|||
from pydantic import BaseModel, BeforeValidator
|
||||
import piexif
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from picamera2 import Picamera2
|
||||
from picamera2.encoders import MJPEGEncoder
|
||||
from picamera2.outputs import Output
|
||||
|
|
@ -484,12 +485,11 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self._streaming_picamera() as cam:
|
||||
cam.capture_metadata()
|
||||
|
||||
@lt.thing_action
|
||||
def capture_image(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw"] = "main",
|
||||
wait: Optional[float] = 0.9,
|
||||
) -> None:
|
||||
) -> Image:
|
||||
"""Acquire one image from the camera.
|
||||
|
||||
Return it as a PIL Image
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import time
|
|||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import piexif
|
||||
from scipy.ndimage import gaussian_filter
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
def __init__(
|
||||
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),
|
||||
canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
|
||||
frame_interval: float = 0.1,
|
||||
|
|
@ -208,9 +209,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
def __enter__(self):
|
||||
"""Start the capture thread when the Thing context manager is opened."""
|
||||
self._capture_enabled = True
|
||||
self._capture_thread = Thread(target=self._capture_frames)
|
||||
self._capture_thread.start()
|
||||
self.start_streaming()
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
|
|
@ -219,6 +218,29 @@ class SimulatedCamera(BaseCamera):
|
|||
self._capture_enabled = False
|
||||
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.
|
||||
|
||||
The start_streaming method is used a camera ``Thing`` to begin streaming
|
||||
images or to adjust the stream resolution if streaming is already active.
|
||||
|
||||
The simulation camera does not currently support the resolution argument.
|
||||
It will always issue a warning that the resolution is not respected.
|
||||
If called while already streaming, the warning will be emitted and no other
|
||||
action will be taken.
|
||||
"""
|
||||
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
|
||||
def stream_active(self) -> bool:
|
||||
"""Whether the MJPEG stream is active."""
|
||||
|
|
@ -236,9 +258,11 @@ class SimulatedCamera(BaseCamera):
|
|||
frame = self.generate_frame()
|
||||
jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
|
||||
self.mjpeg_stream.add_frame(jpeg, portal)
|
||||
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[
|
||||
1
|
||||
].tobytes()
|
||||
# Downsample for lores
|
||||
ds_frame = cv2.resize(
|
||||
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)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to capture frame: {e}, retrying...")
|
||||
|
|
@ -261,7 +285,7 @@ class SimulatedCamera(BaseCamera):
|
|||
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
||||
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()
|
||||
|
||||
@lt.thing_action
|
||||
|
|
@ -274,7 +298,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
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()
|
||||
jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
|
||||
exif_dict = {
|
||||
|
|
@ -292,6 +316,20 @@ class SimulatedCamera(BaseCamera):
|
|||
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
||||
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
|
||||
def remove_sample(self):
|
||||
"""Show the simulated background with no sample."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue