Merge branch 'colour-simualation-image' into 'v3'
Update simulation canvas to have colour so it works with background detect. See merge request openflexure/openflexure-microscope-server!328
This commit is contained in:
commit
f12ee9bd69
1 changed files with 63 additions and 20 deletions
|
|
@ -28,6 +28,12 @@ from ..stage import BaseStage
|
||||||
# higher related to a faster movement
|
# higher related to a faster movement
|
||||||
RATIO = 0.2
|
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):
|
class SimulatedCamera(BaseCamera):
|
||||||
"""A Thing that simulates a camera for testing."""
|
"""A Thing that simulates a camera for testing."""
|
||||||
|
|
@ -38,7 +44,7 @@ class SimulatedCamera(BaseCamera):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
shape: tuple[int, int, int] = (600, 800, 3),
|
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),
|
canvas_shape: tuple[int, int, int] = (3000, 4000, 3),
|
||||||
frame_interval: float = 0.1,
|
frame_interval: float = 0.1,
|
||||||
):
|
):
|
||||||
|
|
@ -64,39 +70,71 @@ class SimulatedCamera(BaseCamera):
|
||||||
|
|
||||||
def generate_sprites(self):
|
def generate_sprites(self):
|
||||||
"""Generate sprites to populate the image."""
|
"""Generate sprites to populate the image."""
|
||||||
|
sprite_sizes = [5, 7, 10, 21, 36, 40]
|
||||||
self.sprites = []
|
self.sprites = []
|
||||||
black = np.zeros(self.glyph_shape, dtype=np.uint8)
|
|
||||||
x = np.arange(black.shape[0])
|
channel_block = np.zeros(self.glyph_shape[0:2])
|
||||||
y = np.arange(black.shape[1])
|
x = np.arange(channel_block.shape[0])
|
||||||
rr = np.sqrt((x[:, None] - np.mean(x)) ** 2 + (y[None, :] - np.mean(y)) ** 2)
|
y = np.arange(channel_block.shape[1])
|
||||||
for i in [5, 7, 9, 11, 13, 15]:
|
# 2D grid of radii
|
||||||
sprite = black.copy()
|
r_coord = np.sqrt(
|
||||||
sprite[rr < i] = 255
|
(x[:, None] - np.mean(x)) ** 2 + (y[None, :] - np.mean(y)) ** 2
|
||||||
self.sprites.append(sprite)
|
)
|
||||||
|
|
||||||
|
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):
|
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
|
Blobs are characterised by X, Y, sprite
|
||||||
We also generate a KD tree to rapidly find blobs in an image
|
We also generate a KD tree to rapidly find blobs in an image
|
||||||
"""
|
"""
|
||||||
self.blobs = np.zeros((n_blobs, 3))
|
self.blobs = np.zeros((n_blobs, 3))
|
||||||
rng = np.random.default_rng()
|
|
||||||
w = np.max(self.glyph_shape)
|
w = np.max(self.glyph_shape)
|
||||||
self.blobs[:, 0] = rng.uniform(w / 2, self.canvas_shape[0] - w / 2, 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[:, 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[:, 2] = RNG.choice(len(self.sprites), n_blobs)
|
||||||
|
|
||||||
def generate_canvas(self):
|
def generate_canvas(self):
|
||||||
"""Generate a blank canvas."""
|
"""Generate a canvas.
|
||||||
self.canvas = np.zeros(self.canvas_shape, dtype=np.uint8)
|
|
||||||
self.canvas[...] = 255
|
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]
|
||||||
|
self.canvas[:, :, 2] *= BG_COLOR[2]
|
||||||
w, h, _ = self.glyph_shape
|
w, h, _ = self.glyph_shape
|
||||||
for x, y, sprite in self.blobs:
|
for x, y, sprite_size_index in self.blobs:
|
||||||
self.canvas[
|
self.canvas[
|
||||||
int(x) - w // 2 : int(x) - w // 2 + w,
|
int(x) - w // 2 : int(x) - w // 2 + w,
|
||||||
int(y) - h // 2 : int(y) - h // 2 + h,
|
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
|
||||||
|
|
||||||
def generate_image(self, pos: tuple[int, int, int]):
|
def generate_image(self, pos: tuple[int, int, int]):
|
||||||
"""Generate an image with blobs based on supplied coordinates."""
|
"""Generate an image with blobs based on supplied coordinates."""
|
||||||
|
|
@ -120,7 +158,12 @@ class SimulatedCamera(BaseCamera):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Image shape {image.shape} does not match intended shape {self.shape}"
|
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(
|
def attach_to_server(
|
||||||
self, server: lt.ThingServer, path: str, setting_storage_path: str
|
self, server: lt.ThingServer, path: str, setting_storage_path: str
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue