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/")
|
cam_client = lt.ThingClient.from_url("http://localhost:5000/camera/")
|
||||||
img = Image.open(cam_client.grab_jpeg().open())
|
img = Image.open(cam_client.grab_jpeg().open())
|
||||||
print(f"Successfully grabbed image of size {img.size}")
|
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")
|
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)
|
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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,29 @@ 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.
|
||||||
|
|
||||||
|
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
|
@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 +258,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 +285,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 +298,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 +316,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."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue