Skip to content

Commit 96b7e10

Browse files
iandobbiecarandraug
authored andcommitted
StageAwareCamera: pad image with zeros when near image/stage limits (#231)
StageAwareCamera would error when stage position would cause it to return pixels values outside the image border. This commit handles that case by returning an image padded with zeros.
1 parent b27c799 commit 96b7e10

File tree

2 files changed

+75
-7
lines changed

2 files changed

+75
-7
lines changed

microscope/simulators/stage_aware_camera.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,48 @@ def _fetch_data(self) -> typing.Optional[np.ndarray]:
103103
self._triggered -= 1
104104
_logger.info("Creating image")
105105

106-
# Use stage position to compute bounding box.
107-
width = self._roi.width // self._binning.h
108-
height = self._roi.height // self._binning.v
109-
x = int((self._stage.position["x"] / self._pixel_size) - (width / 2))
110-
y = int((self._stage.position["y"] / self._pixel_size) - (height / 2))
111-
112106
# Use filter wheel position to select the image channel.
113107
channel = self._filterwheel.position
114108

115-
subsection = self._image[y : y + height, x : x + width, channel]
109+
width = self._roi.width // self._binning.h
110+
height = self._roi.height // self._binning.v
111+
112+
# Use stage position to compute bounding box.
113+
xstart = int(
114+
(self._stage.position["x"] / self._pixel_size) - (width / 2)
115+
)
116+
ystart = int(
117+
(self._stage.position["y"] / self._pixel_size) - (height / 2)
118+
)
119+
xend = xstart + width
120+
yend = ystart + height
121+
122+
# Need to check that the bounding box in entirely within the
123+
# source image (see #231).
124+
if (
125+
xstart < 0
126+
or ystart < 0
127+
or xend > self._image.shape[1]
128+
or yend > self._image.shape[0]
129+
):
130+
# If part of image is out of bounds, pad with zeros, ...
131+
subsection = np.zeros((height, width), dtype=self._image.dtype)
132+
# work out the relevant parts of input image ...
133+
img_x0 = max(0, xstart)
134+
img_x1 = min(xend, self._image.shape[1])
135+
img_y0 = max(0, ystart)
136+
img_y1 = min(yend, self._image.shape[0])
137+
# and work out where to place it in output image.
138+
sub_x0 = max(-xstart, 0)
139+
sub_y0 = max(-ystart, 0)
140+
sub_x1 = sub_x0 + (img_x1 - img_x0)
141+
sub_y1 = sub_y0 + (img_y1 - img_y0)
142+
143+
subsection[sub_y0:sub_y1, sub_x0:sub_x1] = self._image[
144+
img_y0:img_y1, img_x0:img_x1, channel
145+
]
146+
else:
147+
subsection = self._image[ystart:yend, xstart:xend, channel]
116148

117149
# Gaussian filter on abs Z position to simulate being out of
118150
# focus (Z position zero is in focus).

microscope/testsuite/test_devices.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import numpy
4343

44+
import microscope
4445
import microscope.testsuite.devices as dummies
4546
import microscope.testsuite.mock_devices as mocks
4647
from microscope import simulators
@@ -441,6 +442,41 @@ def test_non_square_patterns_shape(self):
441442
self.assertEqual(image.shape, (height, width))
442443

443444

445+
class TestStageAwareCamera(unittest.TestCase, CameraTests):
446+
def setUp(self):
447+
image = numpy.full((3000, 1500, 1), 42, dtype=numpy.uint8)
448+
self.sensor_shape = (128, 128)
449+
self.stage = simulators.SimulatedStage(
450+
{
451+
"x": microscope.AxisLimits(0, image.shape[1]),
452+
"y": microscope.AxisLimits(0, image.shape[0]),
453+
"z": microscope.AxisLimits(-50, 50),
454+
}
455+
)
456+
self.stage.enable()
457+
self.filterwheel = simulators.SimulatedFilterWheel(positions=1)
458+
self.device = StageAwareCamera(
459+
image, self.stage, self.filterwheel, sensor_shape=self.sensor_shape
460+
)
461+
self.buffer = Queue()
462+
self.device.set_client(self.buffer)
463+
self.device.enable()
464+
465+
def test_image_limits(self):
466+
limits = self.stage.limits
467+
for x, y in [
468+
(limits["x"].lower, limits["y"].lower),
469+
(limits["x"].lower, limits["y"].upper),
470+
(limits["x"].upper, limits["y"].lower),
471+
(limits["x"].upper, limits["y"].upper),
472+
]:
473+
with self.subTest("limit x=%d and y=%d" % (x, y)):
474+
self.stage.move_to({"x": x, "y": y})
475+
self.device.trigger()
476+
img = self.buffer.get()
477+
self.assertEqual(img.shape, self.sensor_shape)
478+
479+
444480
class TestDummyController(unittest.TestCase, ControllerTests):
445481
def setUp(self):
446482
self.laser = simulators.SimulatedLightSource()

0 commit comments

Comments
 (0)