Skip to content

Commit f5e7603

Browse files
Merge pull request #11 from Attolab/add-pixel-length
Several general improvements
2 parents 13622e9 + 689c38f commit f5e7603

File tree

6 files changed

+98
-49
lines changed

6 files changed

+98
-49
lines changed

README.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ pymodaq_plugins_basler
1414
.. image:: https://github.com/BenediktBurger/pymodaq_plugins_basler/actions/workflows/Test.yml/badge.svg
1515
:target: https://github.com/BenediktBurger/pymodaq_plugins_basler/actions/workflows/Test.yml
1616

17-
Set of PyMoDAQ plugins for cameras by Basler.
17+
Set of PyMoDAQ plugins for cameras by Basler, using the pypylon library. It handles basic camera functionalities (gain, exposure, ROI).
18+
The data is emitted together with spatial axes corresponding either to pixels or to real-world units (um). The pixel size of different camera model is hardcoded in the hardware/basler.py file.
19+
If the camera model is not specified, the pixel size is set to 1 um and can be changed manually by the user in the interface.
20+
21+
The plugin was tested using an acA640-120gm camera. It is compatible with PyMoDAQ 4.4.7.
1822

1923
Authors
2024
=======
2125

2226
* Benedikt Burger
27+
* Romain Geneaux
2328

2429

2530
Instruments
@@ -54,5 +59,4 @@ Installation instructions
5459
=========================
5560

5661
* You need the manufacturer's driver `Pylon <https://www.baslerweb.com/pylon>`_ for the cameras.
57-
* This package uses the work of a `genergic pylablib camera driver <https://github.com/rgeneaux/pymodaq_plugins_test_pylablib>`_, which is not yet available.
58-
Relevant code is included in this package, so no additional installation needed.
62+

plugin_info.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license = 'MIT'
1111
[plugin-install]
1212
#packages required for your plugin:
1313
packages-required = [
14-
'pymodaq>=4.1.0',
14+
'pymodaq>=4.4.7',
1515
'numpy', # for Basler camera
1616
'pypylon',
1717
]

src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Basler.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@
44
from pymodaq.utils.daq_utils import ThreadCommand
55
from pymodaq.control_modules.viewer_utility_classes import main
66

7-
try:
8-
from pymodaq_plugins_pylablib_camera.daq_viewer_plugins.plugins_2D.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera
9-
# available here: https://github.com/rgeneaux/pymodaq_plugins_test_pylablib
10-
except ModuleNotFoundError:
11-
# Fall back to the internal version
12-
from pymodaq_plugins_basler.daq_viewer_plugins.plugins_2D.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera
13-
7+
from pymodaq_plugins_basler.hardware.daq_2Dviewer_GenericPylablibCamera import DAQ_2DViewer_GenericPylablibCamera
148
from pymodaq_plugins_basler.hardware.basler import DartCamera
159

1610

@@ -20,19 +14,14 @@ class DAQ_2DViewer_Basler(DAQ_2DViewer_GenericPylablibCamera):
2014
controller: DartCamera
2115
live_mode_available = True
2216

23-
# Generate a **list** of available cameras.
24-
# Two cases:
25-
# 1) Some pylablib classes have a .list_cameras method, which returns a list of available cameras, so we can just use that
26-
# 2) Other classes have a .get_cameras_number(), which returns the number of connected cameras
27-
# in this case we can define the list as self.camera_list = [*range(number_of_cameras)]
28-
2917
# For Basler, this returns a list of friendly names
3018
camera_list = [cam.GetFriendlyName() for cam in DartCamera.list_cameras()]
3119

32-
# Update the params (nothing to change here)
20+
# Update the params
3321
params = DAQ_2DViewer_GenericPylablibCamera.params + [
3422
{'title': 'Automatic exposure:', 'name': 'auto_exposure', 'type': 'bool', 'value': False},
35-
{'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0, 'limits': [0, 18]},
23+
{'title': 'Gain (dB)', 'name': 'gain', 'type': 'float', 'value': 0},#, 'limits': [0, 18]},
24+
{'title': 'Pixel size (um)', 'name': 'pixel_length', 'type': 'float', 'value': 1, 'default' : 1, 'visible': False},
3625
]
3726
params[next((i for i, item in enumerate(params) if item["name"] == "camera_list"), None)]['limits'] = camera_list # type: ignore
3827

@@ -70,11 +59,23 @@ def ini_detector(self, controller=None):
7059
self.ini_detector_init(old_controller=controller,
7160
new_controller=self.init_controller())
7261

62+
# Check if pixel length is known
63+
if self.controller.pixel_length is None:
64+
model = self.controller.camera.GetDeviceInfo().GetModelName()
65+
self.emit_status(ThreadCommand('Update_Status', [(f"No pixel length known for camera model '{model}', defaulting to user-chosen one"), 'log']))
66+
self.settings.child('pixel_length').show()
67+
68+
# Check gain mode
69+
if self.controller.raw_gain:
70+
self.settings.child('gain').setOpts(type="int")
71+
self.settings.child('gain').setOpts(title="Gain (raw)")
72+
self.settings.child('gain').setValue(self.controller.gain)
73+
7374
# Get camera name
7475
self.settings.child('camera_info').setValue(self.controller.get_device_info()[1])
7576

7677
# Set exposure time
77-
self.controller.set_exposure(self.settings.child('timing_opts', 'exposure_time').value() / 1000)
78+
self.controller.exposure = self.settings.child('timing_opts', 'exposure_time').value() / 1000
7879

7980
# FPS visibility
8081
self.settings.child('timing_opts', 'fps').setOpts(visible=self.settings.child('timing_opts', 'fps_on').value())
@@ -106,8 +107,12 @@ def commit_settings(self, param: Parameter) -> None:
106107
if param.name() == "auto_exposure":
107108
self.controller.camera.ExposureAuto.SetValue(
108109
"Continuous" if self.settings['auto_exposure'] else "Off")
110+
elif param.name() == "exposure_time":
111+
self.controller.exposure = param.value()/1000
109112
elif param.name() == "gain":
110-
self.controller.camera.Gain.SetValue(param.value())
113+
self.controller.gain = param.value()
114+
elif param.name() == "pixel_length":
115+
self.controller.pixel_length = param.value()
111116
else:
112117
super().commit_settings(param=param)
113118

@@ -134,4 +139,4 @@ def callback(self, array) -> None:
134139

135140

136141
if __name__ == '__main__':
137-
main(__file__)
142+
main(__file__, init=False)

src/pymodaq_plugins_basler/hardware/basler.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"daA1280-54um": 3.75,
1818
"daA2500-14um": 2.2,
1919
"daA3840-45um": 2,
20+
"acA640-120gm": 5.6,
21+
"acA645-100gm": 5.6,
2022
}
2123

2224

@@ -37,6 +39,10 @@ def __init__(self, name: str, callback: Optional[Callable] = None, **kwargs):
3739
# create camera object
3840
self.tlFactory = pylon.TlFactory.GetInstance()
3941
self.camera = pylon.InstantCamera()
42+
self._exposure = None
43+
self._gain = None
44+
self.raw_gain = False
45+
4046
# register configuration event handler
4147
self.configurationEventHandler = ConfigurationHandler()
4248
self.camera.RegisterConfiguration(
@@ -61,6 +67,29 @@ def open(self, name: str) -> None:
6167
self.camera.Attach(device)
6268
self.camera.Open()
6369
self.attributes["PixelWidth"] = self.pixel_length
70+
self.check_attribute_names()
71+
72+
def check_attribute_names(self):
73+
possible_exposures = ["ExposureTime", "ExposureTimeAbs"]
74+
for exp in possible_exposures:
75+
try:
76+
if hasattr(self.camera, exp):
77+
self._exposure = getattr(self.camera, exp)
78+
break
79+
except pylon.LogicalErrorException:
80+
pass
81+
82+
possible_gains = ["Gain", "GainRaw"]
83+
for gain in possible_gains:
84+
try:
85+
if hasattr(self.camera, gain):
86+
self._gain = getattr(self.camera, gain)
87+
88+
if gain == "GainRaw":
89+
self.raw_gain = True
90+
break
91+
except pylon.LogicalErrorException:
92+
pass
6493

6594
def set_callback(
6695
self, callback: Callable[[NDArray], None], replace_all: bool = True
@@ -103,13 +132,25 @@ def get_device_info(self) -> List[Any]:
103132
None,
104133
]
105134

106-
def get_exposure(self) -> float:
135+
@property
136+
def exposure(self) -> float:
107137
"""Get the exposure time in s."""
108-
return self.camera.ExposureTime.GetValue() / 1e6
138+
return self._exposure.GetValue() / 1e6
109139

110-
def set_exposure(self, value: float) -> None:
140+
@exposure.setter
141+
def exposure(self, value: float) -> None:
111142
"""Set the exposure time in s."""
112-
self.camera.ExposureTime.SetValue(value * 1e6)
143+
self._exposure.SetValue(value * 1e6)
144+
145+
@property
146+
def gain(self) -> Union[float, int]:
147+
"""Get the gain"""
148+
return self._gain.GetValue()
149+
150+
@gain.setter
151+
def gain(self, value: Union[float, int]) -> None:
152+
"""Set the gain"""
153+
self._gain.SetValue(value)
113154

114155
def get_roi(self) -> Tuple[float, float, float, float, int, int]:
115156
"""Return x0, width, y0, height, xbin, ybin."""
@@ -165,7 +206,7 @@ def wait_for_frame(
165206
raise NotImplementedError("Not implemented")
166207

167208
def get_all_attributes(self):
168-
self.attributes
209+
return self.attributes
169210

170211
def get_attribute_value(self, name, error_on_missing=True):
171212
"""Get the camera attribute with the given name"""
@@ -216,7 +257,10 @@ def start_grabbing(self, max_frame_rate: int = 1000) -> None:
216257
217258
Whenever a grab succeeded, the callback defined in :meth:`set_callback` is called.
218259
"""
219-
self.camera.AcquisitionFrameRate.SetValue(max_frame_rate)
260+
try:
261+
self.camera.AcquisitionFrameRate.SetValue(max_frame_rate)
262+
except pylon.LogicalErrorException:
263+
pass
220264
self.camera.StartGrabbing(
221265
pylon.GrabStrategy_LatestImageOnly, pylon.GrabLoop_ProvidedByInstantCamera
222266
)
@@ -227,17 +271,21 @@ def stop_grabbing(self) -> None:
227271
@property
228272
def pixel_length(self) -> float:
229273
"""Get the pixel length of the camera in µm.
230-
231-
:raises: KeyError if the pixel length of the specific model is not known
274+
275+
Returns None if the pixel length of the specific model is not known
232276
"""
233277
if self._pixel_length is None:
234278
model = self.camera.GetDeviceInfo().GetModelName()
235279
try:
236280
self._pixel_length = pixel_lengths[model]
237281
except KeyError:
238-
raise KeyError(f"No pixel length known for camera model '{model}'.")
282+
self._pixel_length = None
239283
return self._pixel_length
240284

285+
@pixel_length.setter
286+
def pixel_length(self, value):
287+
self._pixel_length = value
288+
241289

242290
class ConfigurationHandler(pylon.ConfigurationEventHandler):
243291
"""Handle the configuration events."""

src/pymodaq_plugins_basler/daq_viewer_plugins/plugins_2D/daq_2Dviewer_GenericPylablibCamera.py renamed to src/pymodaq_plugins_basler/hardware/daq_2Dviewer_GenericPylablibCamera.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from time import perf_counter
77
import numpy as np
88

9-
109
class DAQ_2DViewer_GenericPylablibCamera(DAQ_Viewer_base):
1110
"""
1211
IMPORTANT: THIS IS A GENERIC CLASS THAT DOES NOT WORK ON ITS OWN!
@@ -27,21 +26,20 @@ class DAQ_2DViewer_GenericPylablibCamera(DAQ_Viewer_base):
2726
{'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1},
2827
{'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True, 'default': 1},
2928
{'title': 'Timing', 'name': 'timing_opts', 'type': 'group', 'children':
30-
[{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1, 'default': 1},
29+
[{'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 100, 'default': 100},
3130
{'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True, 'default': True},
3231
{'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True, 'default': 0.0}]
3332
}
3433
]
3534
callback_signal = QtCore.Signal()
36-
roi_pos_size = QtCore.QRectF(0, 0, 10, 10)
35+
roi_info = None
3736
axes = []
3837

3938
def init_controller(self):
4039
raise NotImplementedError('This is a generic camera plugin for which .init_controller() has not been defined.')
4140

4241
def ini_attributes(self):
4342
self.controller: None
44-
self.pixel_width = None # pixel size in microns
4543
self.x_axis = None
4644
self.y_axis = None
4745
self.last_tick = 0.0 # time counter used to compute FPS
@@ -70,10 +68,8 @@ def commit_settings(self, param: Parameter):
7068
# We handle ROI and binning separately for clarity
7169
(old_x, _, old_y, _, xbin, ybin) = self.controller.get_roi() # Get current binning
7270

73-
x0 = self.roi_pos_size.x()
74-
y0 = self.roi_pos_size.y()
75-
width = self.roi_pos_size.width()
76-
height = self.roi_pos_size.height()
71+
y0, x0 = self.roi_info.origin.coordinates
72+
height, width = self.roi_info.size.coordinates
7773

7874
# Values need to be rescaled by binning factor and shifted by current x0,y0 to be correct.
7975
new_x = (old_x + x0) * xbin
@@ -107,8 +103,8 @@ def commit_settings(self, param: Parameter):
107103
self.update_rois(new_roi)
108104
param.setValue(False)
109105

110-
def ROISelect(self, roi_pos_size):
111-
self.roi_pos_size = roi_pos_size
106+
def roi_select(self, roi_info, ind_viewer):
107+
self.roi_info = roi_info
112108

113109
def ini_detector(self, controller=None):
114110
"""Detector communication initialization
@@ -159,10 +155,6 @@ def ini_detector(self, controller=None):
159155
self.callback_thread.callback = callback
160156
self.callback_thread.start()
161157

162-
# Check if pixel width is available
163-
if 'PixelWidth' in self.controller.get_all_attributes():
164-
self.pixel_width = self.controller.get_attribute_value('PixelWidth')
165-
166158
self._prepare_view()
167159

168160
info = "Initialized camera"
@@ -184,12 +176,12 @@ def _prepare_view(self):
184176
self.settings.child('vdet').setValue(height)
185177
mock_data = np.zeros((width, height))
186178

187-
if self.pixel_width: # if pixel_width is defined
188-
scaling = self.pixel_width
179+
if self.controller.pixel_length: # if pixel_width is defined
180+
scaling = self.controller.pixel_length
189181
unit = 'um'
190182
else:
191183
scaling = 1
192-
unit = 'Pxls'
184+
unit = 'pixels'
193185

194186
self.x_axis = Axis(offset = vstart * scaling, scaling=scaling * xbin, size=width // xbin, label="X", units=unit, index=0)
195187

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.1
1+
1.0.0

0 commit comments

Comments
 (0)