BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
__init__.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 David Miguel Susano Pinto <carandraug@gmail.com>
4## Copyright (C) 2020 Mick Phillips <mick.phillips@gmail.com>
5##
6## This file is part of Microscope.
7##
8## Microscope is free software: you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation, either version 3 of the License, or
11## (at your option) any later version.
12##
13## Microscope is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
20
21"""Simulated devices for use during development.
22
23This module provides a series of test devices, which mimic real
24hardware behaviour. They implement the different ABC.
25
26"""
27
28import logging
29import random
30import time
31import typing
32
33import numpy as np
34from PIL import Image, ImageDraw, ImageFont
35
36import microscope
37import microscope._utils
38import microscope.abc
39
40
41_logger = logging.getLogger(__name__)
42
43
44def _theta_generator():
45 """A generator that yields values between 0 and 2*pi"""
46 TWOPI = 2 * np.pi
47 th = 0
48 while True:
49 yield th
50 th = (th + 0.01 * TWOPI) % TWOPI
51
52
54 """Generates test images, with methods for configuration via a Setting."""
55
56 def __init__(self):
57 self._methods = (
58 self.noise,
59 self.gradient,
60 self.sawtooth,
61 self.one_gaussian,
62 self.black,
63 self.white,
64 )
65 self._method_index = 0
66 self._datatypes = (np.uint8, np.uint16, float)
67 self._datatype_index = 0
68 self._theta = _theta_generator()
69 self.numbering = True
70 # Font for rendering counter in images.
71 self._font = ImageFont.load_default()
72
73 def enable_numbering(self, enab):
74 self.numbering = enab
75
76 def get_data_types(self):
77 return (t.__name__ for t in self._datatypes)
78
79 def data_type(self):
80 return self._datatype_index
81
82 def set_data_type(self, index):
83 self._datatype_index = index
84
85 def get_methods(self):
86 """Return the names of available image generation methods"""
87 return (m.__name__ for m in self._methods)
88
89 def method(self):
90 """Return the index of the current image generation method."""
91 return self._method_index
92
93 def set_method(self, index):
94 """Set the image generation method."""
95 self._method_index = index
96
97 def get_image(self, width, height, dark=0, light=255, index=None):
98 """Return an image using the currently selected method."""
99 m = self._methods[self._method_index]
100 d = self._datatypes[self._datatype_index]
101 # return Image.fromarray(m(width, height, dark, light).astype(d), 'L')
102 data = m(width, height, dark, light).astype(d)
103 if self.numbering and index is not None:
104 text = "%d" % index
105 size = tuple(d + 2 for d in self._font.getsize(text))
106 img = Image.new("L", size)
107 ctx = ImageDraw.Draw(img)
108 ctx.text((1, 1), text, fill=light)
109 data[0 : size[1], 0 : size[0]] = np.asarray(img)
110 return data
111
112 def black(self, w, h, dark, light):
113 """Ignores dark and light - returns zeros"""
114 return np.zeros((h, w))
115
116 def white(self, w, h, dark, light):
117 """Ignores dark and light - returns max value for current data type."""
118 d = self._datatypes[self._datatype_index]
119 if issubclass(d, np.integer):
120 value = np.iinfo(d).max
121 else:
122 value = 1.0
123 return value * np.ones((h, w)).astype(d)
124
125 def gradient(self, w, h, dark, light):
126 """A single gradient across the whole image from top left to bottom right."""
127 xx, yy = np.meshgrid(range(w), range(h))
128 return dark + light * (xx + yy) / (xx.max() + yy.max())
129
130 def noise(self, w, h, dark, light):
131 """Random noise."""
132 return np.random.randint(dark, light, size=(h, w))
133
134 def one_gaussian(self, w, h, dark, light):
135 "A single gaussian"
136 sigma = 0.01 * max(w, h)
137 x0 = np.random.randint(w)
138 y0 = np.random.randint(h)
139 xx, yy = np.meshgrid(range(w), range(h))
140 return dark + light * np.exp(
141 -((xx - x0) ** 2 + (yy - y0) ** 2) / (2 * sigma**2)
142 )
143
144 def sawtooth(self, w, h, dark, light):
145 """A sawtooth gradient that rotates about 0,0."""
146 th = next(self._theta)
147 xx, yy = np.meshgrid(range(w), range(h))
148 wrap = 0.1 * max(xx.max(), yy.max())
149 return dark + light * ((np.sin(th) * xx + np.cos(th) * yy) % wrap) / (
150 wrap
151 )
152
153
156):
157 def __init__(self, **kwargs):
158 super().__init__(**kwargs)
159 # Binning and ROI
160 self._roi = microscope.ROI(0, 0, 512, 512)
161 self._binning = microscope.Binning(1, 1)
162 # Function used to generate test image
164 self.add_setting(
165 "image pattern",
166 "enum",
167 self._image_generator.method,
168 self._image_generator.set_method,
169 self._image_generator.get_methods,
170 )
171 self.add_setting(
172 "image data type",
173 "enum",
174 self._image_generator.data_type,
175 self._image_generator.set_data_type,
176 self._image_generator.get_data_types,
177 )
178 self.add_setting(
179 "display image number",
180 "bool",
181 lambda: self._image_generator.numbering,
182 self._image_generator.enable_numbering,
183 None,
184 )
185 # Software buffers and parameters for data conversion.
186 self._a_setting = 0
187 self.add_setting(
188 "a_setting",
189 "int",
190 lambda: self._a_setting,
191 lambda val: setattr(self, "_a_setting", val),
192 lambda: (1, 100),
193 )
194 self._error_percent = 0
195 self.add_setting(
196 "_error_percent",
197 "int",
198 lambda: self._error_percent,
200 lambda: (0, 100),
201 )
202 self._gain = 0
203 self.add_setting(
204 "gain",
205 "int",
206 lambda: self._gain,
207 self._set_gain,
208 lambda: (0, 8192),
209 )
210 self._acquiring_acquiring = False
211 self._exposure_time = 0.1
212 self._triggered = 0
213 # Count number of images sent since last enable.
214 self._sent = 0
215
216 def _set_error_percent(self, value):
217 self._error_percent = value
218 self._a_setting = value // 10
219
220 def _set_gain(self, value):
221 self._gain = value
222
223 def _purge_buffers(self):
224 """Purge buffers on both camera and PC."""
225 _logger.info("Purging buffers.")
226
227 def _create_buffers(self):
228 """Create buffers and store values needed to remove padding later."""
229 self._purge_buffers()
230 _logger.info("Creating buffers.")
231
232 def _fetch_data(self):
233 if self._acquiring_acquiring and self._triggered > 0:
234 if random.randint(0, 100) < self._error_percent:
235 _logger.info("Raising exception")
237 "Exception raised in SimulatedCamera._fetch_data"
238 )
239 _logger.info("Sending image")
240 time.sleep(self._exposure_time)
241 self._triggered -= 1
242 # Create an image
243 dark = int(32 * np.random.rand())
244 light = int(255 - 128 * np.random.rand())
245 width = self._roi.width // self._binning.h
246 height = self._roi.height // self._binning.v
247 image = self._image_generator.get_image(
248 width, height, dark, light, index=self._sent
249 )
250 self._sent += 1
251 return image
252
253 def abort(self):
254 _logger.info("Disabling acquisition; %d images sent.", self._sent)
255 if self._acquiring_acquiring:
256 self._acquiring_acquiring = False
257
258 def _do_disable(self):
259 self.abortabort()
260
261 def _do_enable(self):
262 _logger.info("Preparing for acquisition.")
263 if self._acquiring_acquiring:
264 self.abortabort()
265 self._create_buffers()
266 self._acquiring_acquiring = True
267 self._sent = 0
268 _logger.info("Acquisition enabled.")
269 return True
270
271 def set_exposure_time(self, value):
272 self._exposure_time = value
273
275 return self._exposure_time
276
277 def get_cycle_time(self):
278 return self._exposure_time
279
280 def _get_sensor_shape(self):
281 return (512, 512)
282
283 def soft_trigger(self):
284 # deprecated, use self.trigger()
285 self.trigger()
286
287 def _do_trigger(self) -> None:
288 _logger.info(
289 "Trigger received; self._acquiring is %s.", self._acquiring_acquiring
290 )
291 if self._acquiring_acquiring:
292 self._triggered += 1
293
294 def _get_binning(self):
295 return self._binning
296
297 @microscope.abc.keep_acquiring
298 def _set_binning(self, binning):
299 self._binning = binning
300
301 def _get_roi(self):
302 return self._roi
303
304 @microscope.abc.keep_acquiring
305 def _set_roi(self, roi):
306 self._roi = roi
307
308 def _do_shutdown(self) -> None:
309 pass
310
311
312class SimulatedController(microscope.abc.Controller):
313 def __init__(
314 self, devices: typing.Mapping[str, microscope.abc.Device]
315 ) -> None:
316 self._devices = devices.copy()
317
318 @property
319 def devices(self) -> typing.Mapping[str, microscope.abc.Device]:
320 return self._devices
321
322
324 def __init__(self, **kwargs):
325 super().__init__(**kwargs)
326 self._position = 0
327
328 def _do_get_position(self):
329 return self._position
330
331 def _do_set_position(self, position):
332 _logger.info("Setting position to %s", position)
333 self._position = position
334
335 def _do_shutdown(self) -> None:
336 pass
337
338
339class SimulatedLightSource(
342):
343 def __init__(self, **kwargs):
344 super().__init__(**kwargs)
345 self._power = 0.0
346 self._emission = False
347
348 def get_status(self):
349 return [str(x) for x in (self._emission, self._power, self._set_point)]
350
351 def _do_enable(self):
352 self._emission = True
353 return self._emission
354
355 def _do_shutdown(self) -> None:
356 pass
357
358 def _do_disable(self):
359 self._emission = False
360 return self._emission
361
362 def get_is_on(self):
363 return self._emission
364
365 def _do_set_power(self, power: float) -> None:
366 _logger.info("Power set to %s.", power)
367 self._power = power
368
369 def _do_get_power(self) -> float:
370 if self._emission:
371 return self._power
372 else:
373 return 0.0
374
375
379):
380 def __init__(self, n_actuators, **kwargs):
381 super().__init__(**kwargs)
382 self._n_actuators = n_actuators
383
384 def _do_shutdown(self) -> None:
385 pass
386
387 @property
388 def n_actuators(self) -> int:
389 return self._n_actuators
390
391 def _do_apply_pattern(self, pattern):
392 self._current_pattern = pattern
393
395 """Method for debug purposes only.
396
397 This method is not part of the DeformableMirror ABC, it only
398 exists on this test device to help during development.
399 """
400 return self._current_pattern
401
402
404 def __init__(self, limits: microscope.AxisLimits) -> None:
405 super().__init__()
406 self._limits = limits
407 # Start axis in the middle of its range.
408 self._position = self._limits.lower + (
409 (self._limits.upper - self._limits.lower) / 2.0
410 )
411
412 @property
413 def position(self) -> float:
414 return self._position
415
416 @property
417 def limits(self) -> microscope.AxisLimits:
418 return self._limits
419
420 def move_by(self, delta: float) -> None:
421 self.move_tomove_to(self._position + delta)
422
423 def move_to(self, pos: float) -> None:
424 if pos < self._limits.lower:
425 self._position = self._limits.lower
426 elif pos > self._limits.upper:
427 self._position = self._limits.upper
428 else:
429 self._position = pos
430
431
433 """A test stage with any number of axis.
434
435 Args:
436 limits: map of test axis to be created and their limits.
437
438 .. code-block:: python
439
440 # Test XY motorized stage of square shape:
441 xy_stage = SimulatedStage({
442 'X' : AxisLimits(0, 5000),
443 'Y' : AxisLimits(0, 5000),
444 })
445
446 # XYZ stage, on rectangular shape and negative coordinates:
447 xyz_stage = SimulatedStage({
448 'X' : AxisLimits(-5000, 5000),
449 'Y' : AxisLimits(-10000, 12000),
450 'Z' : AxisLimits(0, 1000),
451 })
452
453 """
454
455 def __init__(
456 self, limits: typing.Mapping[str, microscope.AxisLimits], **kwargs
457 ) -> None:
458 super().__init__(**kwargs)
459 self._axes = {
460 name: SimulatedStageAxis(lim) for name, lim in limits.items()
461 }
462
463 def _do_shutdown(self) -> None:
464 pass
465
466 def may_move_on_enable(self) -> bool:
467 return False
468
469 @property
470 def axes(self) -> typing.Mapping[str, microscope.abc.StageAxis]:
471 return self._axes
472
473 def move_by(self, delta: typing.Mapping[str, float]) -> None:
474 for name, rpos in delta.items():
475 self.axesaxes[name].move_by(rpos)
476
477 def move_to(self, position: typing.Mapping[str, float]) -> None:
478 for name, pos in position.items():
479 self.axesaxes[name].move_to(pos)
None abort(self)
Definition: abc.py:588
None add_setting(self, name, dtype, get_func, set_func, values, typing.Optional[typing.Callable[[], bool]] readonly=None)
Definition: abc.py:407
None move_to(self, float pos)
Definition: abc.py:1370
typing.Mapping[str, StageAxis] axes(self)
Definition: abc.py:1455
def gradient(self, w, h, dark, light)
Definition: __init__.py:125
def white(self, w, h, dark, light)
Definition: __init__.py:116
def one_gaussian(self, w, h, dark, light)
Definition: __init__.py:134
def black(self, w, h, dark, light)
Definition: __init__.py:112
def sawtooth(self, w, h, dark, light)
Definition: __init__.py:144
def get_image(self, width, height, dark=0, light=255, index=None)
Definition: __init__.py:97
def noise(self, w, h, dark, light)
Definition: __init__.py:130
None move_by(self, float delta)
Definition: __init__.py:420
microscope.AxisLimits limits(self)
Definition: __init__.py:417
None move_by(self, typing.Mapping[str, float] delta)
Definition: __init__.py:473
None move_to(self, typing.Mapping[str, float] position)
Definition: __init__.py:477
typing.Mapping[str, microscope.abc.StageAxis] axes(self)
Definition: __init__.py:470