Formatted with Ruff

This commit is contained in:
Richard Bowman 2024-12-03 06:46:08 +00:00
parent 27aec769a2
commit 9fd8fc37f1
19 changed files with 680 additions and 392 deletions

View file

@ -5,6 +5,7 @@ camera together to perform an autofocus routine.
See repository root for licensing information.
"""
from __future__ import annotations
import io
import json
@ -28,22 +29,26 @@ from pydantic import RootModel
from . import BaseCamera, JPEGBlob
from ..stage import StageProtocol as Stage
class ArrayModel(RootModel):
"""A model for an array"""
root: NDArray
class SimulatedCamera(BaseCamera):
"""A Thing representing an OpenCV camera"""
_stage: Optional[Stage] = None
_server: Optional[ThingServer] = None
def __init__(
self,
shape: tuple[int, int, int] = (600, 800, 3),
glyph_shape: tuple[int, int, int] = (51, 51, 3),
canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
frame_interval: float = 0.1,
):
self,
shape: tuple[int, int, int] = (600, 800, 3),
glyph_shape: tuple[int, int, int] = (51, 51, 3),
canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
frame_interval: float = 0.1,
):
self.shape = shape
self.glyph_shape = glyph_shape
self.canvas_shape = canvas_shape
@ -53,14 +58,14 @@ class SimulatedCamera(BaseCamera):
self.generate_sprites()
self.generate_blobs()
self.generate_canvas()
def generate_sprites(self):
"""Generate sprites to populate the image"""
self.sprites = []
black = np.zeros(self.glyph_shape, dtype=np.uint8)
x = np.arange(black.shape[0])
y = np.arange(black.shape[1])
rr = np.sqrt((x[:, None] - np.mean(x))**2 + (y[None, :] - np.mean(y))**2)
rr = np.sqrt((x[:, None] - np.mean(x)) ** 2 + (y[None, :] - np.mean(y)) ** 2)
for i in [5, 7, 9, 11, 13, 15]:
sprite = black.copy()
sprite[rr < i] = 255
@ -68,15 +73,15 @@ class SimulatedCamera(BaseCamera):
def generate_blobs(self, N: int = 1000):
"""Generate coordinates of blobs
Blobs are characterised by X, Y, sprite
We also generate a KD tree to rapidly find blobs in an image
"""
self.blobs = np.zeros((N, 3))
rng = np.random.default_rng()
w = np.max(self.glyph_shape)
self.blobs[:, 0] = rng.uniform(w/2, self.canvas_shape[0]-w/2, N)
self.blobs[:, 1] = rng.uniform(w/2, self.canvas_shape[1]-w/2, N)
self.blobs[:, 0] = rng.uniform(w / 2, self.canvas_shape[0] - w / 2, N)
self.blobs[:, 1] = rng.uniform(w / 2, self.canvas_shape[1] - w / 2, N)
self.blobs[:, 2] = rng.choice(len(self.sprites), N)
def generate_canvas(self):
@ -86,31 +91,34 @@ class SimulatedCamera(BaseCamera):
w, h, _ = self.glyph_shape
for x, y, sprite in self.blobs:
self.canvas[
int(x) - w//2:int(x) - w//2 + w,
int(y) - h//2:int(y) - h//2 + h,
int(x) - w // 2 : int(x) - w // 2 + w,
int(y) - h // 2 : int(y) - h // 2 + h,
] -= self.sprites[int(sprite)]
def generate_image(self, pos: tuple[int, int]):
"""Generate an image with blobs based on supplied coordinates"""
cw, ch, _ = self.canvas_shape
w, h, _ = self.shape
tl = (int(pos[0]) - w//2 - cw//2, int(pos[1]) - h//2 - ch//2)
tl = (int(pos[0]) - w // 2 - cw // 2, int(pos[1]) - h // 2 - ch // 2)
image = self.canvas[
tuple(slice(tl[i],tl[i] + self.shape[i]) for i in range(2)) + (slice(None),)
tuple(slice(tl[i], tl[i] + self.shape[i]) for i in range(2))
+ (slice(None),)
]
if image.shape != self.shape:
raise ValueError(f"Image shape {image.shape} does not match intended shape {self.shape}")
raise ValueError(
f"Image shape {image.shape} does not match intended shape {self.shape}"
)
return image
def attach_to_server(self, server: ThingServer, path: str):
self._server = server
return super().attach_to_server(server, path)
def get_stage_position(self):
if not self._stage and self._server:
self._stage = self._server.things["/stage/"]
return self._stage.instantaneous_position
def generate_frame(self):
"""Generate a frame with blobs based on the stage coordinates"""
try:
@ -118,14 +126,14 @@ class SimulatedCamera(BaseCamera):
except Exception as e:
print(f"Failed to get stage position: {e}")
pos = {"x": 0, "y": 0}
return self.generate_image((pos["x"]/10, pos["y"]/10))
return self.generate_image((pos["x"] / 10, pos["y"] / 10))
def __enter__(self):
self._capture_enabled = True
self._capture_thread = Thread(target=self._capture_frames)
self._capture_thread.start()
return self
def __exit__(self, _exc_type, _exc_value, _traceback):
if self.stream_active:
self._capture_enabled = False
@ -137,6 +145,7 @@ class SimulatedCamera(BaseCamera):
if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive()
return False
mjpeg_stream = MJPEGStreamDescriptor()
lores_mjpeg_stream = MJPEGStreamDescriptor()
@ -148,7 +157,9 @@ 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()
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[
1
].tobytes()
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
except Exception as e:
logging.error(f"Failed to capture frame: {e}, retrying...")
@ -165,7 +176,7 @@ class SimulatedCamera(BaseCamera):
binary image formats will be added in due course.
"""
return self.generate_frame()
@thing_action
def capture_jpeg(
self,
@ -180,7 +191,9 @@ class SimulatedCamera(BaseCamera):
jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
exif_dict = {
"Exif": {
piexif.ExifIFD.UserComment: json.dumps(metadata_getter()).encode("utf-8")
piexif.ExifIFD.UserComment: json.dumps(metadata_getter()).encode(
"utf-8"
)
},
"GPS": {},
"Interop": {},