From 570122515848a0fbc14a6a888859cf2b03bea38b Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 18 Jul 2025 21:21:25 +0100 Subject: [PATCH 1/2] Update simulation canvas to have colour so it works with background detect. --- .../things/camera/simulation.py | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 73321082..0770d947 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -28,6 +28,9 @@ from ..stage import BaseStage # higher related to a faster movement RATIO = 0.2 +# Some colour variation, for bg detect. +BG_COLOR = [220, 215, 217] + class SimulatedCamera(BaseCamera): """A Thing that simulates a camera for testing.""" @@ -38,7 +41,7 @@ class SimulatedCamera(BaseCamera): def __init__( self, shape: tuple[int, int, int] = (600, 800, 3), - glyph_shape: tuple[int, int, int] = (51, 51, 3), + glyph_shape: tuple[int, int, int] = (91, 91, 3), canvas_shape: tuple[int, int, int] = (3000, 4000, 3), frame_interval: float = 0.1, ): @@ -64,18 +67,42 @@ class SimulatedCamera(BaseCamera): def generate_sprites(self): """Generate sprites to populate the image.""" + sprite_sizes = [5, 7, 10, 21, 36, 40] 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) - for i in [5, 7, 9, 11, 13, 15]: - sprite = black.copy() - sprite[rr < i] = 255 - self.sprites.append(sprite) + + channel_block = np.zeros(self.glyph_shape[0:2]) + x = np.arange(channel_block.shape[0]) + y = np.arange(channel_block.shape[1]) + # 2D grid of radii + r_coord = np.sqrt( + (x[:, None] - np.mean(x)) ** 2 + (y[None, :] - np.mean(y)) ** 2 + ) + + for sprite_size in sprite_sizes: + # Mask of where this sprite is + sprite_mask = r_coord < sprite_size + # Calculate a sharp edged circle with value varying from 0 in centre to 1 + # at the edge + sprite_px = r_coord[sprite_mask] + sprite_px -= np.min(sprite_px) + sprite_px /= np.max(sprite_px) + # Create each channel. Note these will be subtracted from the white value. + sprite_r = channel_block.copy() + sprite_r[sprite_mask] = 70 * sprite_px + sprite_g = channel_block.copy() + sprite_g[sprite_mask] = 200 * sprite_px + sprite_b = channel_block.copy() + sprite_b[sprite_mask] = 70 * sprite_px + # Stack into a negative image of the sprite + sprite = np.stack([sprite_r, sprite_g, sprite_b], axis=2) + # Convert to uint8 and append to the list + self.sprites.append(sprite.astype(np.uint8)) def generate_blobs(self, n_blobs: int = 1000): - """Generate coordinates of blobs. + """Generate coordinates of blobs and their sizes. + + A 1000x3 array is returned. Each row represents (x,y) coordinate + of the sprite and the index representing the size of the sprite. Blobs are characterised by X, Y, sprite We also generate a KD tree to rapidly find blobs in an image @@ -88,15 +115,20 @@ class SimulatedCamera(BaseCamera): self.blobs[:, 2] = rng.choice(len(self.sprites), n_blobs) def generate_canvas(self): - """Generate a blank canvas.""" - self.canvas = np.zeros(self.canvas_shape, dtype=np.uint8) - self.canvas[...] = 255 + """Generate a canvas.""" + self.canvas = np.ones(self.canvas_shape, dtype=np.int16) + self.canvas[:, :, 0] *= BG_COLOR[0] + self.canvas[:, :, 1] *= BG_COLOR[1] + self.canvas[:, :, 2] *= BG_COLOR[2] w, h, _ = self.glyph_shape - for x, y, sprite in self.blobs: + for x, y, sprite_size_index in self.blobs: self.canvas[ int(x) - w // 2 : int(x) - w // 2 + w, int(y) - h // 2 : int(y) - h // 2 + h, - ] -= self.sprites[int(sprite)] + ] -= self.sprites[int(sprite_size_index)] + self.canvas[self.canvas < 0] = 0 + self.canvas[self.canvas > 255] = 255 + self.canvas = self.canvas.astype(np.uint8) def generate_image(self, pos: tuple[int, int, int]): """Generate an image with blobs based on supplied coordinates.""" From 31e3d15890cb2091ccbb87e37d4205f6247a92f8 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Thu, 24 Jul 2025 16:17:42 +0100 Subject: [PATCH 2/2] Add random noise to simulation image --- .../things/camera/simulation.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 0770d947..1bb3715c 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -31,6 +31,9 @@ RATIO = 0.2 # Some colour variation, for bg detect. BG_COLOR = [220, 215, 217] +# Random Number Generator +RNG = np.random.default_rng() + class SimulatedCamera(BaseCamera): """A Thing that simulates a camera for testing.""" @@ -108,14 +111,18 @@ class SimulatedCamera(BaseCamera): We also generate a KD tree to rapidly find blobs in an image """ self.blobs = np.zeros((n_blobs, 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_blobs) - self.blobs[:, 1] = rng.uniform(w / 2, self.canvas_shape[1] - w / 2, n_blobs) - self.blobs[:, 2] = rng.choice(len(self.sprites), n_blobs) + self.blobs[:, 0] = RNG.uniform(w / 2, self.canvas_shape[0] - w / 2, n_blobs) + self.blobs[:, 1] = RNG.uniform(w / 2, self.canvas_shape[1] - w / 2, n_blobs) + self.blobs[:, 2] = RNG.choice(len(self.sprites), n_blobs) def generate_canvas(self): - """Generate a canvas.""" + """Generate a canvas. + + Canvas is int16 so that random noise can be added to simulation image before + changing to unit8 to stop wrapping. + """ self.canvas = np.ones(self.canvas_shape, dtype=np.int16) self.canvas[:, :, 0] *= BG_COLOR[0] self.canvas[:, :, 1] *= BG_COLOR[1] @@ -128,7 +135,6 @@ class SimulatedCamera(BaseCamera): ] -= self.sprites[int(sprite_size_index)] self.canvas[self.canvas < 0] = 0 self.canvas[self.canvas > 255] = 255 - self.canvas = self.canvas.astype(np.uint8) def generate_image(self, pos: tuple[int, int, int]): """Generate an image with blobs based on supplied coordinates.""" @@ -152,7 +158,12 @@ class SimulatedCamera(BaseCamera): raise ValueError( f"Image shape {image.shape} does not match intended shape {self.shape}" ) - return image + + # Add noise and convert to uint8 + image += RNG.normal(scale=2, size=self.shape).astype("int16") + image[image < 0] = 0 + image[image > 255] = 255 + return image.astype("uint8") def attach_to_server( self, server: lt.ThingServer, path: str, setting_storage_path: str