wasatch 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. wasatch/AbstractUSBDevice.py +29 -0
  2. wasatch/AndorDevice.py +910 -0
  3. wasatch/AreaScanImage.py +19 -0
  4. wasatch/AutoRaman.py +457 -0
  5. wasatch/AutoRamanRequest.py +96 -0
  6. wasatch/BLEDevice.py +1032 -0
  7. wasatch/BalanceAcquisition.py +136 -0
  8. wasatch/CSVLoader.py +188 -0
  9. wasatch/CommandSettings.py +95 -0
  10. wasatch/ControlObject.py +17 -0
  11. wasatch/DetectorROI.py +36 -0
  12. wasatch/DetectorRegions.py +158 -0
  13. wasatch/DeviceFinderBLE.py +107 -0
  14. wasatch/DeviceFinderTCP.py +20 -0
  15. wasatch/DeviceFinderUSB.py +236 -0
  16. wasatch/DeviceID.py +344 -0
  17. wasatch/EEPROM.py +1450 -0
  18. wasatch/EEPROM_multical.sav +48 -0
  19. wasatch/FPGAOptions.py +125 -0
  20. wasatch/FeatureIdentificationDevice.py +3961 -0
  21. wasatch/FirmwareRequirements.py +76 -0
  22. wasatch/HardwareInfo.py +36 -0
  23. wasatch/IDSCamera.py +916 -0
  24. wasatch/IDSDevice.py +329 -0
  25. wasatch/IMX385.py +179 -0
  26. wasatch/InterfaceDevice.py +40 -0
  27. wasatch/MockUSBDevice.py +353 -0
  28. wasatch/OceanDevice.py +215 -0
  29. wasatch/PollStatus.py +15 -0
  30. wasatch/ProcessedReading.py +352 -0
  31. wasatch/ROI.py +33 -0
  32. wasatch/Reading.py +80 -0
  33. wasatch/RealUSBDevice.py +64 -0
  34. wasatch/SPIDevice.py +628 -0
  35. wasatch/SpectrometerRequest.py +15 -0
  36. wasatch/SpectrometerResponse.py +38 -0
  37. wasatch/SpectrometerSettings.py +446 -0
  38. wasatch/SpectrometerState.py +255 -0
  39. wasatch/StatusMessage.py +14 -0
  40. wasatch/TCPDevice.py +409 -0
  41. wasatch/TakeOneRequest.py +43 -0
  42. wasatch/WasatchBus.py +87 -0
  43. wasatch/WasatchDevice.py +1060 -0
  44. wasatch/WasatchDeviceWrapper.py +452 -0
  45. wasatch/WrapperWorker.py +304 -0
  46. wasatch/__init__.py +4 -0
  47. wasatch/applog.py +177 -0
  48. wasatch/simulation_protocol.py +227 -0
  49. wasatch/utils.py +731 -0
  50. wasatch-2.3.2.dist-info/LICENSE +21 -0
  51. wasatch-2.3.2.dist-info/METADATA +347 -0
  52. wasatch-2.3.2.dist-info/RECORD +53 -0
  53. wasatch-2.3.2.dist-info/WHEEL +4 -0
wasatch/IDSCamera.py ADDED
@@ -0,0 +1,916 @@
1
+ import threading
2
+ import logging
3
+ import pathlib
4
+ import numpy as np
5
+ import time
6
+ import sys
7
+ import os
8
+
9
+ from datetime import datetime
10
+
11
+ from ids_peak import ids_peak as IDSPeak
12
+ from ids_peak import ids_peak_ipl_extension as EXT
13
+ from ids_peak_ipl import ids_peak_ipl as IPL
14
+
15
+ from wasatch.AreaScanImage import AreaScanImage
16
+ from wasatch import utils
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ class IDSCamera:
21
+ """
22
+ This class encapsulates access to the IDS Peak SDK. It is called and used by
23
+ IDSDevice. IDSDevice "is-an" InterfaceDevice, and is mimicking the same
24
+ upstream API as WasatchDevice, TCPDevice, AndorDevice etc. This class,
25
+ IDSCamera, is implementing the downstream IDSPeak SDK API.
26
+
27
+ We could have done this like AndorDevice and TCPDevice, and merged both
28
+ IDSDevice and IDSCamera into a single file or class (upstream and downsteam
29
+ APIs). However, I opted to follow the example of (WasatchDevice ->
30
+ FeatureInterfaceDevice) and (OceanDevice -> SeaBreezeWrapper), both of which
31
+ keep the two separate.
32
+
33
+ @par Vendor Documentation
34
+
35
+ @see https://www.ids-imaging.us/manuals/ids-peak/ids-peak-api-documentation/2.15.0/en/annotated.html
36
+ @see https://en.ids-imaging.com/manuals/ids-peak/ids-peak-ipl-documentation/2.15.0/en/annotated.html
37
+ ^^^--- different!
38
+ @see Program Files/IDS/ids_peak/generic_sdk/samples/source/python/start_stop_acquisition_software_trigger/main.py
39
+
40
+
41
+ @par Converting IDS --> Qt
42
+
43
+ Per cpp/multi_camera_live_qtwidgets/acquisitionworker.cpp, it looks like:
44
+
45
+ peak::ipl::PixelFormatName::BGRa8 --> QImage::Format_RGB32
46
+ peak::ipl::PixelFormatName::RGB8 --> QImage::Format_RGB888
47
+ peak::ipl::PixelFormatName::RGB10p32 --> QImage::Format_BGR30
48
+ peak::ipl::PixelFormatName::BayerRG8 --> QImage::Format_Grayscale8
49
+ peak::ipl::PixelFormatName::Mono8 --> QImage::Format_Grayscale8
50
+ otherwise --> QImage::Format_RGB32
51
+
52
+ @par NodeMap
53
+
54
+ As generated by dump_node("Root"):
55
+
56
+ Root (Category) =
57
+ TLParamsLockedCategory (Category) =
58
+ TLParamsLocked (Integer) = 0
59
+ AcquisitionControl (Category) =
60
+ --> AcquisitionMode (Enumeration) = Continuous (SingleFrame, MultiFrame, Continuous)
61
+ AcquisitionFrameCount (Integer) = 1
62
+ SensorShutterMode (Enumeration) = Rolling
63
+ --> AcquisitionAbort (Command)
64
+ AcquisitionFrameRateTarget (Float) = 20.0 Hz
65
+ SensorFrameTimeClocksTarget (Integer) = 1856250
66
+ AcquisitionFrameRateTargetEnable (Boolean) = False
67
+ --> AcquisitionStart (Command)
68
+ --> AcquisitionStop (Command)
69
+ TriggerSelector (Enumeration) = ReadOutStart
70
+ --> TriggerMode (Enumeration) = On (Off, On)
71
+ --> TriggerSource (Enumeration) = Software (Software, Line0)
72
+ TriggerActivation (Enumeration) = RisingEdge
73
+ --> TriggerSoftware (Command)
74
+ --> TriggerDelay (Float) = 0.0 us
75
+ TriggerDivider (Integer) = 1
76
+ SensorClockFrequency (Integer) = 37125000 Hz
77
+ ==> ExposureTime (Float) = 15006.38383838384 us
78
+ SensorExposureTimeClocks (Integer) = 557112
79
+ --> AcquisitionFrameRate (Float) = 54.910841063990915 Hz
80
+ SensorFrameTimeClocks (Integer) = 676096
81
+ ExposureMode (Enumeration) = Timed
82
+ TransferControl (Category) =
83
+ TransferControlMode (Enumeration) = Automatic
84
+ DeviceControl (Category) =
85
+ --> DeviceFirmwareVersion (String) = 3.30.21242
86
+ DeviceUserID (String) =
87
+ DeviceReset (Command)
88
+ DeviceBootStatus (Enumeration) = OK (OK, WatchdogReboot, RepeatedWatchdogError, WatchdogRebootAfterRepeatedWatchdogError, FirmwareCorruptionError, WatchdogRebootAfterFirmwareCorruptionError, FirmwareMissingError, WatchdogRebootAfterFirmwareMissingError, BootDataCorruptionError, WatchdogRebootAfterBootDataCorruptionError, CriticalErrorReboot, CriticalErrorWhileStarting, CriticalErrorWhileConfiguring, CriticalUSBErrorReboot, CriticalCoreErrorReboot)
89
+ DeviceBootStatusAdditionalInfo1 (Integer) = 0
90
+ DeviceBootStatusAdditionalInfo2 (Integer) = 0
91
+ ==> DeviceLinkSpeed (Integer) = 500_000_000 Bps (SuperSpeed USB 5Gbps per https://www.1stvision.com/cameras/IDS/IDS-manuals/en/device-link-speed.html)
92
+ DeviceSFNCVersionMajor (Integer) = 2
93
+ DeviceSFNCVersionMinor (Integer) = 7
94
+ DeviceSFNCVersionSubMinor (Integer) = 0
95
+ ResetToFactoryDefaults (Command)
96
+ BootOptions (Enumeration) = Runtime (Runtime, Service)
97
+ DeviceScanType (Enumeration) = Areascan
98
+ SensorOperationModeRaw (Enumeration) = Default
99
+ SensorOperationMode (Enumeration) = Default (LongExposure, Default, Linescan, LinescanSensor, HighSpeed)
100
+ --> DeviceVendorName (String) = IDS Imaging Development Systems GmbH
101
+ --> DeviceModelName (String) = U3-38CxXCP-M
102
+ DeviceFamilyName (String) = U3-XCP
103
+ --> DeviceManufacturerInfo (String) = U3-38C0XCP-M-GL Rev.1.2
104
+ ==> DeviceSerialNumber (String) = 4108809482
105
+ --> DeviceVersion (String) = 18.22.1.1.10000BC.1.352
106
+ DeviceRegistersStreamingStart (Command)
107
+ DeviceRegistersStreamingEnd (Command)
108
+ DeviceRegistersValid (Boolean) = True
109
+ --> DeviceLinkAcquisitionFrameRateLimit (Float) = 54.91084 Hz
110
+ DeviceLinkThroughputLimit (Integer) = 162_000_000 Bps
111
+ DeviceLinkCurrentThroughput (Integer) = 161_587_230 Bps
112
+ DeviceLinkThroughputLimitComponent (Enumeration) = Sensor (Link, Sensor)
113
+ DeviceClockSelector (Enumeration) = Sensor
114
+ DeviceClockFrequency (Float) = 37125000.0 Hz
115
+ -?> DeviceOpticalFilterType (Enumeration) = GL (Unknown, None, HQ, GL, DL)
116
+ TestControl (Category) =
117
+ TestPendingAck (Integer) = 0 ms
118
+ TriggerEventTest (Command)
119
+ TestEventGenerate (Command)
120
+ ManufacturerInfo (Category) =
121
+ LogicBoardType (Integer) = 24
122
+ LogicBoardFirehare (Integer) = 22
123
+ SensorBoardType (Integer) = 16777404
124
+ SensorBoardFirehare (Integer) = 1
125
+ ConnectorBoardType (Integer) = 1
126
+ ConnectorBoardFirehare (Integer) = 1
127
+ OpticControlPossible (Boolean) = False
128
+ FileAccessControl (Category) =
129
+ FileSelector (Enumeration) = UserData1 (UserData1, UserData2, CriticalEventLog)
130
+ -?> FileAccessBuffer (Register) = 0x80028e94 (len 480) (is this a 480-byte writable EEPROM? https://www.1stvision.com/cameras/IDS/IDS-manuals/en/file-access-buffer.html)
131
+ FileOperationSelector (Enumeration) = Open (Open, Delete)
132
+ FileOperationExecute (Command)
133
+ --> FileOpenMode (Enumeration) = Write (Read, Write)
134
+ FileOpenStatus (Enumeration) = Close (Close, OpenRead, OpenWrite)
135
+ FileOperationStatus (Enumeration) = Success (Success, Failure)
136
+ FileOperationResult (Integer) = 0
137
+ FileSize (Integer) = 0 B
138
+ SequencerControl (Category) =
139
+ SequencerMode (Enumeration) = Off (On, Off)
140
+ SequencerConfigurationMode (Enumeration) = Off (On, Off)
141
+ EventControl (Category) =
142
+ EventSelector (Enumeration) = EventDropped (Error, CriticalError, EventDropped)
143
+ EventReset (Command)
144
+ EventNotification (Enumeration) = On (Off, On, Once)
145
+ EventTestData (Category) =
146
+ EventTest (Integer) = 20479
147
+ EventErrorData (Category) =
148
+ EventError (Integer) = 36880
149
+ EventCriticalErrorData (Category) =
150
+ EventCriticalError (Integer) = 36881
151
+ EventEventDroppedData (Category) =
152
+ EventEventDropped (Integer) = 36882
153
+ UserSetControl (Category) =
154
+ UserSetSelector (Enumeration) = Default (Default, UserSet0, UserSet1)
155
+ UserSetLoad (Command)
156
+ UserSetSave (Command)
157
+ UserSetDefault (Enumeration) = Default (Default)
158
+ UserSetFeatureSelector (Enumeration) = Roi (Core, Roi)
159
+ UserSetFeatureEnable (Boolean) = True
160
+ ProtectionControl (Category) =
161
+ ProtectionPassword (String) =
162
+ ProtectionPasswordCheck (Command)
163
+ ProtectionNodeLockSelector (Enumeration) = DeviceManufacturerInfo (DeviceManufacturerInfo, DeviceUserID, UserData1, UserData2)
164
+ ProtectionNodeLockEnable (Boolean) = True
165
+ ProtectionReset (Command)
166
+ ProtectionSave (Command)
167
+ LEDControl (Category) =
168
+ LEDSelector (Enumeration) = LEDStatus (LEDStatus)
169
+ LEDTriggerSource (Enumeration) = CameraStatus (Off, CameraStatus, BlinkingSlow, BlinkingFast)
170
+ ==> DigitalIOControl (Category) =
171
+ LineSelector (Enumeration) = Line0 (Line0, Line1, Line2, Line3) (https://www.1stvision.com/cameras/IDS/IDS-manuals/en/line-selector.html)
172
+ LineMode (Enumeration) = Input (Input, Output)
173
+ LineFormat (Enumeration) = LVTTL
174
+ UserOutputSelector (Enumeration) = UserOutput0 (UserOutput0, UserOutput1, UserOutput2, UserOutput3) (https://www.1stvision.com/cameras/IDS/IDS-manuals/en/user-output-selector.html)
175
+ UserOutputValue (Boolean) = False
176
+ ImageFormatControl (Category) =
177
+ --> SensorWidth (Integer) = 1936
178
+ SensorPixelSize (Enumeration) = Bpp10
179
+ IsPixelSizeAvailable (Integer) = 2
180
+ --> SensorHeight (Integer) = 1096
181
+ WidthMax (Integer) = 1936
182
+ HeightMax (Integer) = 1096
183
+ ==> Width (Integer) = 1920
184
+ ==> Height (Integer) = 1080
185
+ OffsetX (Integer) = 8
186
+ OffsetY (Integer) = 8
187
+ BinningSelector (Enumeration) = Sensor
188
+ BinningHorizontal (Integer) = 1
189
+ -*? BinningVertical (Integer) = 1
190
+ BinningHVInvalidator (Integer) = 1
191
+ --> TestPattern (Enumeration) = Off (Off, Black, White, ColorBar, ColorBarVertical)
192
+ ==> PixelSize (Enumeration) = Bpp10 (Bpp1, Bpp2, Bpp4, Bpp8, Bpp10, Bpp12, Bpp14, Bpp16, Bpp24, Bpp30, Bpp32, Bpp36, Bpp48, Bpp64)
193
+ ==> PixelColorFilter (Enumeration) = None (None, BayerRG, BayerGB, BayerGR, BayerBG)
194
+ ==> PixelFilter (Enumeration) = None (None, BayerRG, BayerGB, BayerGR, BayerBG)
195
+ ComponentSelector (Enumeration) = Intensity
196
+ ComponentEnable (Boolean) = True
197
+ ComponentEnableAll (Integer) = 1
198
+ ==> PixelFormat (Enumeration) = Mono10g40IDS (Mono10g40IDS, Mono12g24IDS)
199
+ ==> SensorName (String) = Sony IMX662-AAMR-C
200
+ --> SensorPixelWidth (Float) = 2.9 um
201
+ --> SensorPixelHeight (Float) = 2.9 um
202
+ --> ReverseX (Boolean) = False
203
+ --> ReverseY (Boolean) = False
204
+ AnalogControl (Category) =
205
+ PixelDepthBitPerChannel (Integer) = 10
206
+ GainSelector (Enumeration) = AnalogAll
207
+ ==> Gain (Float) = 1.0
208
+ (range unclear -- see https://www.1stvision.com/cameras/IDS/IDS-manuals/en/gain.html)
209
+ (unit unclear; possibly just a floating-point multiplier, not dB, since default is 1.0?)
210
+ BlackLevelSelector (Enumeration) = Offset10Bit (All8Bit, All10Bit, All12Bit, All14Bit, All16Bit, Offset8Bit, Offset10Bit, Offset12Bit, Offset14Bit, Offset16Bit)
211
+ (https://www.1stvision.com/cameras/IDS/IDS-manuals/en/black-level-selector.html)
212
+ BlackLevel (Float) = 0.0 DN
213
+ BlackLevelInt (Integer) = 0
214
+ XX> BlackLevelAuto (Enumeration) = ContinuousWithOffset (unclear if this can be disabled -- see https://www.1stvision.com/cameras/IDS/IDS-manuals/en/black-level-auto.html)
215
+ TransportLayerControl (Category) =
216
+ PayloadSize (Integer) = 2_592_000
217
+
218
+ @par Backlog
219
+
220
+ The following need to be made much faster, via increased use of NumPy etc:
221
+
222
+ - vertical binning
223
+ - conversion to QPixmap
224
+
225
+ """
226
+
227
+ INITIALIZED = False
228
+
229
+ # @see site-packages/ids_peak_ipl/ids_peak_ipl.py
230
+ ALL_PIXEL_FORMAT_NAMES = [
231
+ "BGR10", "BGR10p32", "BGR12", "BGR8", "BGRa10", "BGRa12", "BGRa8",
232
+ "BayerBG10", "BayerBG10g40IDS", "BayerBG10p", "BayerBG12", "BayerBG12g24IDS", "BayerBG12p", "BayerBG8",
233
+ "BayerGB10", "BayerGB10g40IDS", "BayerGB10p", "BayerGB12", "BayerGB12g24IDS", "BayerGB12p", "BayerGB8",
234
+ "BayerGR10", "BayerGR10g40IDS", "BayerGR10p", "BayerGR12", "BayerGR12g24IDS", "BayerGR12p", "BayerGR8",
235
+ "BayerRG10", "BayerRG10g40IDS", "BayerRG10p", "BayerRG12", "BayerRG12g24IDS", "BayerRG12p", "BayerRG8",
236
+ "Confidence16", "Confidence8", "Coord3D_ABC32f", "Coord3D_C16", "Coord3D_C32f", "Coord3D_C8",
237
+ "Mono10", "Mono12", "Mono10p", "Mono12p", "Mono10g40IDS", "Mono12g24IDS", "Mono16", "Mono8",
238
+ "RGB10", "RGB10p32", "RGB12", "RGB8", "RGBa10", "RGBa12", "RGBa8",
239
+ "YUV420_8_YY_UV_SemiplanarIDS", "YUV420_8_YY_VU_SemiplanarIDS", "YUV422_8_UYVY", "Invalid"
240
+ ]
241
+
242
+ # generated via ImageConverter.SupportedOutputPixelFormatNames(PixelFormat(IPL.PixelFormatName_Mono10g40IDS))
243
+ # "Mono12g24IDS" omitted as unusable (can't be accessed as line data, can't be output to PNG)
244
+ SUPPORTED_CONVERSIONS = [
245
+ "Mono16", "Mono12", "Mono10", "Mono8",
246
+ "RGB12", "RGB10", "RGB8",
247
+ "BGR12", "BGR10", "BGR8",
248
+ "RGBa12", "RGBa10", "RGBa8",
249
+ "BGRa12", "BGRa10", "BGRa8"
250
+ ]
251
+
252
+ ############################################################################
253
+ # Lifecycle
254
+ ############################################################################
255
+
256
+ def __init__(self):
257
+
258
+ self.device = None
259
+ self.node_map = None
260
+ self.long_name = None
261
+ self.datastream = None
262
+
263
+ self.area_scan_enabled = False
264
+ self.take_one_request = None
265
+ self.taking_acquisition = False
266
+ self.shutdown_in_progress = False
267
+ self.last_asi_timestamp = None
268
+ self.last_area_scan_image = None
269
+ self.area_scan_image_timeout_sec = 1 # by default, generate new ASI at 1Hz
270
+ self.rotate_180 = True
271
+ self.shrink_area_scan = True
272
+ self.degrade_area_scan = True
273
+
274
+ self.buffers = []
275
+
276
+ # correspond to EEPROM
277
+ self.model_name = None
278
+ self.serial_number = None
279
+ self.width = 0
280
+ self.height = 0
281
+ self.start_line = 0
282
+ self.stop_line = 0
283
+ self.integration_time_ms = 15 # seems to be default?
284
+ self.last_integration_time_ms = 15
285
+
286
+ self.image_converter = None
287
+ self.image_transformer = None
288
+ # self.binning = None
289
+
290
+ # this is the default value, but IDSDevice may change it at runtime based
291
+ # on the ENLIGHTEN plugin
292
+ self.output_format_name = "Mono16"
293
+ self.output_format_obj = None
294
+
295
+ self.dir = os.path.join(utils.get_default_data_dir(), "idspeak")
296
+ pathlib.Path(self.dir).mkdir(exist_ok=True)
297
+
298
+ try:
299
+ if self.INITIALIZED:
300
+ log.debug("IDSPeak.Library already initialized")
301
+ else:
302
+ log.debug("initializing IDSPeak.Library")
303
+ IDSPeak.Library.Initialize()
304
+ self.INITIALIZED = True
305
+ self.device_manager = IDSPeak.DeviceManager.Instance()
306
+ self.init_pixel_format_names()
307
+ except:
308
+ log.critical("failed to instantiate IDSPeak.DeviceManager", exc_info=1)
309
+
310
+ def init_pixel_format_names(self):
311
+ """
312
+ If there's an in-built API method to convert from the name of a desired
313
+ format to a PixelFormat object or number, I didn't see one. (Okay, maybe
314
+ using Node.Entries() and SymbolicValue().)
315
+
316
+ @see IPL.PixelFormat class docs at https://en.ids-imaging.com/manuals/ids-peak/ids-peak-ipl-documentation/2.15.0/en/classpeak_1_1ipl_1_1_pixel_format.html
317
+ """
318
+ self.name_to_num = {}
319
+ log.debug("All PixelFormat types:")
320
+ for name in self.ALL_PIXEL_FORMAT_NAMES:
321
+ num = getattr(IPL, f"PixelFormatName_{name}") # int
322
+ fmt = IPL.PixelFormat(num) # num -> PixelFormat is easy
323
+ log.debug(f" {name:30} = 0x{num:08x} ({num})")
324
+ assert name == fmt.Name(), f"name {name} != Name() {fmt.Name()}"
325
+ assert num == fmt.PixelFormatName(), f"num {num} != PixelFormatName() {fmt.PixelFormatName()}" # poorly named
326
+ self.name_to_num[name] = num
327
+
328
+ def close(self):
329
+ log.debug("close: start")
330
+ self.stop()
331
+ try:
332
+ if self.datastream is not None:
333
+ log.debug("close: revoking buffers")
334
+ for buffer in self.datastream.AnnouncedBuffers():
335
+ self.datastream.RevokeBuffer(buffer)
336
+ except:
337
+ log.error(f"close: caught exception when clearing buffers", exc_info=1)
338
+ log.debug("close: done")
339
+
340
+ def __del__(self):
341
+ self.close()
342
+
343
+ def __repr__(self):
344
+ return self.long_name
345
+
346
+ def connect(self):
347
+ log.debug("connect: start")
348
+ self.device_manager.Update()
349
+ if self.device_manager.Devices().empty():
350
+ log.debug("connect: no devices found :-(")
351
+ return False
352
+
353
+ # List all available devices
354
+ log.debug("connect: found device(s):")
355
+ for device in self.device_manager.Devices():
356
+ model_name = device.ModelName()
357
+ parent_intf = device.ParentInterface().DisplayName()
358
+ parent_name = device.ParentInterface().ParentSystem().DisplayName()
359
+ parent_ver = device.ParentInterface().ParentSystem().Version()
360
+ long_name = f"{model_name} {parent_intf} ({parent_name} {parent_ver})"
361
+ log.debug(f" {long_name}")
362
+ if self.long_name is None:
363
+ self.long_name = long_name
364
+
365
+ # Open the first device in control mode
366
+ self.device = self.device_manager.Devices()[0].OpenDevice(IDSPeak.DeviceAccessType_Control)
367
+
368
+ # Get device's control nodes
369
+ self.node_map = self.device.RemoteDevice().NodeMaps()[0]
370
+
371
+ # Cache settings corresponding to Wasatch EEPROM
372
+ self.model_name = device.ModelName()
373
+ self.serial_number = self.node_map.FindNode("DeviceSerialNumber").Value()
374
+ self.width = self.node_map.FindNode("Width").Value()
375
+ self.height = self.node_map.FindNode("Height").Value()
376
+ self.last_integration_time_ms = self.node_map.FindNode("ExposureTime").Value() / 1000.0
377
+
378
+ self.start_line = 0
379
+ self.stop_line = self.height - 1
380
+
381
+ # Load the default settings
382
+ self.node_map.FindNode("UserSetSelector").SetCurrentEntry("Default")
383
+ self.node_map.FindNode("UserSetLoad").Execute()
384
+ self.node_map.FindNode("UserSetLoad").WaitUntilDone()
385
+
386
+ # initialize software trigger
387
+ # @see https://www.ids-imaging.us/manuals/ids-peak/ids-peak-user-manual/2.15.0/en/acquisition-control.html
388
+ log.debug("TriggerSelector set to ExposureStart")
389
+ self.node_map.FindNode("TriggerSelector").SetCurrentEntry("ReadOutStart") # "ExposureStart" not supported?
390
+ self.node_map.FindNode("TriggerMode").SetCurrentEntry("On")
391
+ self.node_map.FindNode("TriggerSource").SetCurrentEntry("Software")
392
+ self.node_map.FindNode("ExposureTime").SetValue(15_000) # value in µs
393
+
394
+ # nodeMapRemoteDevice.FindNode("SequencerMode") .SetCurrentEntry("Off")
395
+ # nodeMapRemoteDevice.FindNode("AcquisitionMode") .SetCurrentEntry("SingleFrame")
396
+ # nodeMapRemoteDevice.FindNode("ExposureMode") .SetCurrentEntry("Timed")
397
+ # nodeMapRemoteDevice.FindNode("ExposureAuto") .SetCurrentEntry("Off")
398
+ # nodeMapRemoteDevice.FindNode("TriggerSelector") .SetCurrentEntry("ExposureStart")
399
+ # nodeMapRemoteDevice.FindNode("ExposureTime") .SetValue(400_000) # value in µs
400
+ # nodeMapRemoteDevice.FindNode("AcquisitionStart").Execute()
401
+ # nodeMapRemoteDevice.FindNode("AcquisitionStart").WaitUntilDone()
402
+
403
+ self.dump_node(name="Root")
404
+
405
+ log.debug(f"connect: successfully connected to {self.long_name}")
406
+ return True
407
+
408
+ ############################################################################
409
+ # Acquisition Loop (Device)
410
+ ############################################################################
411
+
412
+ def set_rotate_180(self, flag):
413
+ # Setting this once in the camera firmware would be more efficient than
414
+ # calling self.image_transformer.MirrorUpDownLeftRightInPlace(converted)
415
+ # on every image received, but sadly the nodes seem not writable :-(
416
+ #
417
+ # self.node_map.FindNode("ReverseX").SetValue(flag)
418
+ # self.node_map.FindNode("ReverseY").SetValue(flag)
419
+ self.rotate_180 = flag
420
+
421
+ def set_start_line(self, n):
422
+ self.start_line = n
423
+
424
+ def set_stop_line(self, n):
425
+ self.stop_line = n
426
+
427
+ def set_integration_time_ms(self, ms):
428
+ # do we need to .stop and .start around this?
429
+ us = ms * 1000.0
430
+
431
+ node = self.node_map.FindNode("ExposureTime")
432
+ us = max(us, node.Minimum())
433
+ us = min(us, node.Maximum())
434
+
435
+ log.debug(f"set_integration_time_ms: integration time {us} µs")
436
+ self.node_map.FindNode("ExposureTime").SetValue(us)
437
+ self.integration_time_ms = ms
438
+ return ms
439
+
440
+ def set_gain_factor(self, factor):
441
+ node = self.node_map.FindNode("Gain")
442
+ factor = max(factor, node.Minimum())
443
+ factor = min(factor, node.Maximum())
444
+
445
+ log.debug(f"set_gain: factor {factor:.1f}")
446
+ self.node_map.FindNode("Gain").SetValue(factor)
447
+ return factor
448
+
449
+ def start(self):
450
+ log.debug("start: starting")
451
+ if self.device is None:
452
+ log.debug("start: no device")
453
+ return
454
+
455
+ if self.taking_acquisition:
456
+ log.debug("start: already running")
457
+ return
458
+
459
+ # self.datastream = None # kludge
460
+ if self.datastream is None:
461
+ log.debug("start: initializing datastream")
462
+ self.datastream = self.device.DataStreams()[0].OpenDataStream()
463
+ log.debug(f"start: datastream initialized")
464
+ self.reset()
465
+ else:
466
+ log.debug("start: datastream already initialized?")
467
+
468
+ log.debug("start: queueing buffers")
469
+ for buffer in self.buffers:
470
+ self.datastream.QueueBuffer(buffer)
471
+
472
+ try:
473
+ # Lock parameters that should not be accessed during acquisition
474
+ # MZ: what are these?
475
+ log.debug("start: locking parameters")
476
+ self.node_map.FindNode("TLParamsLocked").SetValue(1)
477
+
478
+ # determine initial camera output format (the "input format" to this class)
479
+ input_format_node = self.node_map.FindNode("PixelFormat")
480
+ input_format_entry = input_format_node.CurrentEntry()
481
+ input_format_num = input_format_entry.Value()
482
+ self.input_format_obj = IPL.PixelFormat(input_format_num)
483
+ input_format_name = self.input_format_obj.Name()
484
+
485
+ log.debug(f"start: width {self.width}, height {self.height}, outputting {input_format_name} 0x{input_format_num:08x} ({input_format_num})")
486
+
487
+ # Pre-allocate conversion buffers to speed up image conversion
488
+ # while the acquisition is running
489
+ if self.image_converter is None:
490
+ self.init_image_converter()
491
+
492
+ # use this opportunity to report all the different conversion
493
+ # options from the sensor's default format (the INPUT format to
494
+ # this class), which is likely "Mono10g40IDS" or "Mono12g24IDS"
495
+ log.debug(f"start: supported conversions from {input_format_name}:")
496
+ for num in self.image_converter.SupportedOutputPixelFormatNames(self.input_format_obj):
497
+ fmt = IPL.PixelFormat(num)
498
+ name = fmt.Name()
499
+ log.debug(f" {name:30} = 0x{num:08x} ({num})")
500
+
501
+ if self.image_transformer is None:
502
+ self.image_transformer = IPL.ImageTransformer()
503
+
504
+ # if self.binning is None:
505
+ # self.init_binning()
506
+
507
+ self.datastream.StartAcquisition()
508
+ self.node_map.FindNode("AcquisitionStart").Execute()
509
+ self.node_map.FindNode("AcquisitionStart").WaitUntilDone()
510
+ self.taking_acquisition = True
511
+
512
+ log.debug("start: started")
513
+ except:
514
+ log.error(f"Exception (start acquisition)", exc_info=1)
515
+
516
+ log.debug("start: done")
517
+
518
+ def init_image_converter(self, output_format_name=None):
519
+ self.image_converter = IPL.ImageConverter()
520
+
521
+ if output_format_name is None:
522
+ output_format_name = self.output_format_name
523
+ else:
524
+ self.output_format_name = output_format_name
525
+
526
+ # MZ: I'm not sure if this actually speeds ALL conversions, or just the FIRST one
527
+ output_format_num = self.name_to_num[self.output_format_name]
528
+ self.output_format_obj = IPL.PixelFormat(output_format_num)
529
+ self.image_converter.PreAllocateConversion(self.input_format_obj, self.output_format_obj, self.width, self.height)
530
+ log.debug("pre-allocated ImageConverter to output {self.output_format_name}")
531
+
532
+ def init_binning_NOT_USED(self):
533
+ """
534
+ It's unclear how much use IPL.Binning would be...the docs aren't entirely
535
+ clear on whether this could vertically bin rows (70, 790) of Mono16,
536
+ given several parameters take a uint8. According to the uEye manual, this
537
+ seems more intended for Bayer filter, bin2x2 etc.
538
+
539
+ @see https://www.ids-imaging.us//manuals/ids-peak/ids-peak-ipl-documentation/2.15.0/en/classpeak_1_1ipl_1_1_binning.html
540
+ @see https://www.1stvision.com/cameras/IDS/IDS-manuals/uEye_Manual/hw_binning.html
541
+ """
542
+ # self.binning = IPL.Binning()
543
+ # binning.SetMode(IPL.Binning.BinningMode_Sum)
544
+ pass
545
+
546
+ def stop(self):
547
+ """ Called during shutdown. """
548
+ if self.device is None:
549
+ return
550
+
551
+ if not self.taking_acquisition:
552
+ log.debug("stop: not running")
553
+ return
554
+
555
+ try:
556
+ log.debug("stop: stopping")
557
+ self.node_map.FindNode("AcquisitionStop").Execute()
558
+
559
+ self.datastream.StopAcquisition(IDSPeak.AcquisitionStopMode_Default)
560
+ # Discard all buffers from the acquisition engine
561
+ # They remain in the announced buffer pool
562
+ self.datastream.Flush(IDSPeak.DataStreamFlushMode_DiscardAll)
563
+ self.taking_acquisition = False
564
+
565
+ # Unlock parameters
566
+ self.node_map.FindNode("TLParamsLocked").SetValue(0)
567
+ except:
568
+ log.error(f"stop: caught exception", exc_info=1)
569
+ log.debug("stop: done")
570
+
571
+ def reset(self):
572
+ log.debug("resetting datastream")
573
+ if self.datastream is None:
574
+ log.error("no datastream to reset?!")
575
+ return
576
+
577
+ try:
578
+ # Check if buffers are already allocated
579
+ if self.datastream is not None:
580
+ # Remove buffers from the announced pool
581
+ for buffer in self.datastream.AnnouncedBuffers():
582
+ self.datastream.RevokeBuffer(buffer)
583
+ self.buffers = []
584
+
585
+ payload_size = self.node_map.FindNode("PayloadSize").Value()
586
+ buffer_amount = self.datastream.NumBuffersAnnouncedMinRequired()
587
+
588
+ for _ in range(buffer_amount):
589
+ buffer = self.datastream.AllocAndAnnounceBuffer(payload_size)
590
+ self.buffers.append(buffer)
591
+
592
+ log.debug("reset: allocated buffers")
593
+ except:
594
+ log.error(f"reset: caught exception", exc_info=1)
595
+
596
+ ############################################################################
597
+ # Acquisition Loop (Software)
598
+ ############################################################################
599
+
600
+ def acquisition_loop(self):
601
+ """
602
+ This method is only used by the main() demonstrator. This is essentially
603
+ done by WrapperWorker in ENLIGHTEN.
604
+ """
605
+ while not self.shutdown_in_progress:
606
+ try:
607
+ if self.take_one_request is not None:
608
+ self.send_trigger()
609
+ self.get_spectrum()
610
+ self.take_one_request = None
611
+ except:
612
+ log.error(f"acquisition_loop: caught exception", exc_info=1)
613
+ self.take_one_request = None
614
+
615
+ def send_trigger(self):
616
+ self.node_map.FindNode("TriggerSoftware").Execute()
617
+ self.node_map.FindNode("TriggerSoftware").WaitUntilDone()
618
+
619
+ def get_spectrum(self):
620
+ if self.datastream is None:
621
+ log.error("get_spectrum: no datastream?!")
622
+ return
623
+
624
+ timeout_ms = int(round(1000 + 2 * max(self.integration_time_ms, self.last_integration_time_ms)))
625
+ try:
626
+ buffer = self.datastream.WaitForFinishedBuffer(timeout_ms) # takes ms
627
+ except:
628
+ log.error(f"failed on datastream.WaitForFinishedBuffer({timeout_ms}ms)", exc_info=1)
629
+ return
630
+
631
+ # Get image from buffer (shallow copy)
632
+ image = EXT.BufferToImage(buffer)
633
+
634
+ if True:
635
+ # normal case, just vertically bin using the configured format
636
+ # (pass the buffer so it can be released after conversion)
637
+ spectrum = self.vertically_bin_image(image, buffer=buffer)
638
+ else:
639
+ # debug code to experimentally convert the same frame into every
640
+ # supported format for comparison (assumes we're saving PNG or CSV)
641
+
642
+ # this will take awhile, so make a deep-copy and release the buffer
643
+ clone = image.Clone()
644
+ self.datastream.QueueBuffer(buffer)
645
+
646
+ for format_name in self.SUPPORTED_CONVERSIONS:
647
+ try:
648
+ spectrum = self.vertically_bin_image(clone, format_name, save_csv=True, save_png=True)
649
+ except:
650
+ log.error(f"exception during conversion to {format_name}", exc_info=1)
651
+
652
+ self.last_integration_time_ms = self.integration_time_ms
653
+ return spectrum
654
+
655
+ def vertically_bin_image(self, image, format_name=None, buffer=None, save_csv=False, save_png=False):
656
+ """
657
+ @note Stores a handle to a new AreaScanImage if the old one has expired.
658
+ This handle isn't "returned" but can be read by IDSDevice.
659
+ @returns a single vertically-binned spectrum
660
+ """
661
+ asi = None
662
+
663
+ if format_name is None:
664
+ format_name = self.output_format_name
665
+ format_obj = self.output_format_obj
666
+ else:
667
+ format_num = self.name_to_num[format_name]
668
+ format_obj = IPL.PixelFormat(format_num)
669
+
670
+ converted = self.image_converter.Convert(image, format_obj)
671
+
672
+ if buffer is not None:
673
+ # we were sent a Buffer, so release it immediately after conversion
674
+ self.datastream.QueueBuffer(buffer)
675
+ buffer = None
676
+
677
+ if converted is None:
678
+ log.error("vertically_bin_image: failed converting image to {format_name}")
679
+ return
680
+
681
+ if self.rotate_180:
682
+ # would be nice if we could set this in the camera...
683
+ self.image_transformer.MirrorUpDownLeftRightInPlace(converted)
684
+
685
+ # this will hold the sum of all channels
686
+ spectrum = [0] * converted.Width()
687
+
688
+ # individual per-channel spectra for characterization
689
+ channel_count = format_obj.NumChannels()
690
+ # channel_spectra = [ [0] * converted.Width() for i in range(channel_count) ]
691
+
692
+ if self.output_format_name == "Mono16":
693
+ data = converted.get_numpy()
694
+ cropped = data[self.start_line:(self.stop_line + 1), :]
695
+ spectrum = np.sum(cropped, axis=0)
696
+
697
+ else:
698
+ # we'd have to validate numpy process against different formats
699
+
700
+ try:
701
+ # iterate over each line of the 2D image
702
+ binned_count = 0
703
+ for row in range(converted.Height()):
704
+ if self.start_line <= row <= self.stop_line:
705
+ pixel_row = IPL.PixelRow(converted, row)
706
+ binned_count += 1
707
+
708
+ # iterate over each channel (R, G, B, a, etc)
709
+ for channel_index, channel in enumerate(pixel_row.Channels()):
710
+ # iterate over each pixel in the line (for this channel)
711
+ values = channel.Values
712
+ for pixel, intensity in enumerate(values):
713
+ spectrum[pixel] += intensity
714
+ # channel_spectra[channel_index][pixel] += intensity
715
+ log.debug(f"binned {binned_count} lines from {self.start_line} to {self.stop_line}")
716
+ except:
717
+ log.error(f"vertically_bin_image: unable to vertically bin {format_name}", exc_info=1)
718
+ return
719
+
720
+ # save binned intensities (including per-channel breakdown) as CSV
721
+
722
+ if save_csv:
723
+ pathname_csv = os.path.join(self.dir, f"{format_name}.csv")
724
+ with open(pathname_csv, "w") as outfile:
725
+ outfile.write("pixel, intensity, " + ", ".join(['chan_'+str(i) for i in range(channel_count)]) + "\n")
726
+ for pixel, intensity in enumerate(spectrum):
727
+ outfile.write(f"{pixel}, {spectrum[pixel]}")
728
+ for i in range(channel_count):
729
+ outfile.write(f", {channel_spectra[i][pixel]}")
730
+ outfile.write("\n")
731
+ log.debug(f" saved {pathname_csv} ({channel_count} channels)")
732
+
733
+ # save converted image to PNG
734
+ if self.area_scan_enabled:
735
+ if self.last_asi_timestamp is None or (datetime.now() - self.last_asi_timestamp).total_seconds() >= self.area_scan_image_timeout_sec:
736
+ log.debug("time for a new AreaScanImage")
737
+ save_png = True
738
+ else:
739
+ log.debug("not yet time for a new AreaScanImage")
740
+
741
+ # only generate a fresh AreaScanImage if the old one has expired
742
+ if save_png:
743
+ try:
744
+ # Now that we've completed vertical binning using the full-
745
+ # size image, we COULD reduce it in size and quality for the
746
+ # visual area scan. Hyperspectral applications might prefer
747
+ # high-quality, so make any downgrade/shrinkage optional.
748
+
749
+ height_orig = converted.Height()
750
+ width_orig = converted.Width()
751
+ if self.shrink_area_scan:
752
+ factor = IPL.ScaleFactor()
753
+ factor.x = 0.5
754
+ factor.y = 0.5
755
+ converted = converted.Scale(factor)
756
+
757
+ # 20% quality -- I don't know if this is doing anything or not?
758
+ png_param = IPL.ImageWriterPNGParameter()
759
+ if self.degrade_area_scan:
760
+ png_param.Quality = 20
761
+
762
+ pathname_tmp = os.path.join(self.dir, f"{format_name}-tmp.png")
763
+ pathname_png = os.path.join(self.dir, f"{format_name}.png")
764
+ IPL.ImageWriter.WriteAsPNG(pathname_tmp, converted, png_param)
765
+ os.replace(pathname_tmp, pathname_png)
766
+ log.debug(f"saved {pathname_png}")
767
+
768
+ self.last_area_scan_image = AreaScanImage(
769
+ pathname_png = pathname_png,
770
+ width = converted.Width(),
771
+ height = converted.Height(),
772
+ width_orig = width_orig,
773
+ height_orig = height_orig,
774
+ format_name = format_name)
775
+ log.debug(f"stored {self.last_area_scan_image}")
776
+ self.last_asi_timestamp = datetime.now()
777
+ except:
778
+ log.error(f"vertically_bin_image: unable to save {format_name} as PNG", exc_info=1)
779
+
780
+ return spectrum
781
+
782
+ ############################################################################
783
+ # Utility
784
+ ############################################################################
785
+
786
+ def next_name(self, path, ext):
787
+ num = 0
788
+
789
+ def build_string():
790
+ return f"{path}_{num}{ext}"
791
+
792
+ while os.path.exists(build_string()):
793
+ num += 1
794
+ return build_string()
795
+
796
+ def dump_node(self, name=None, node=None, indent=" "):
797
+ if name is None and node is None:
798
+ log.error("must be called with node or name")
799
+ return
800
+
801
+ type_ = "UNKNOWN"
802
+ try:
803
+ if node is None:
804
+ node = self.node_map.FindNode(name)
805
+ if node is None:
806
+ log.error(f"'{name}' not found in node_map")
807
+ return
808
+
809
+ if name is None:
810
+ name = node.Name()
811
+
812
+ type_ = IDSPeak.NodeTypeEnumEntryToString(node.Type())
813
+ value = None
814
+
815
+ if not node.IsAvailable(): value = "NOT AVAILABLE"
816
+ elif not node.IsImplemented(): value = "NOT IMPLEMENTED"
817
+ elif node.IsDeprecated(): value = "DEPRECATED"
818
+ elif isinstance(node, IDSPeak.FloatNode): value = f"{node.Value()} {node.Unit()}"
819
+ elif isinstance(node, IDSPeak.StringNode): value = node.Value()
820
+ elif isinstance(node, IDSPeak.BooleanNode): value = node.Value()
821
+ elif isinstance(node, IDSPeak.IntegerNode): value = f"{node.Value()} {node.Unit()}"
822
+ elif isinstance(node, IDSPeak.RegisterNode): value = f"0x{node.Address():08x} (len {node.Length()})"
823
+ elif isinstance(node, IDSPeak.EnumerationEntryNode): value = node.SymbolicValue()
824
+ elif isinstance(node, IDSPeak.CommandNode): value = None
825
+ elif isinstance(node, IDSPeak.CategoryNode):
826
+ log.debug(f"{indent}{name} ({type_}) = ")
827
+ indent += " "
828
+ for subnode in node.SubNodes():
829
+ self.dump_node(node=subnode, indent=indent)
830
+ return
831
+ elif isinstance(node, IDSPeak.EnumerationNode):
832
+ value = node.CurrentEntry().SymbolicValue()
833
+ if len(node.AvailableEntries()) > 1:
834
+ value += " (" + ", ".join([ e.SymbolicValue() for e in node.AvailableEntries() ]) + ")"
835
+ else:
836
+ log.error(f"{indent}{name} is unknown type {type_}")
837
+
838
+ if not node.IsWriteable():
839
+ value = str(value) + " (READ-ONLY)"
840
+
841
+ if value is None:
842
+ log.debug(f"{indent}{name} ({type_})")
843
+ else:
844
+ log.debug(f"{indent}{name} ({type_}) = {value}")
845
+ except Exception as ex:
846
+ log.error(f"{indent}{type_} {name}: {ex}")
847
+ pass
848
+
849
+ if __name__ == '__main__':
850
+ """ invoke from Wasatch.PY folder as: python -u wasatch/IDSCamera.py """
851
+
852
+ from wasatch import applog
853
+ logger = applog.MainLogger("DEBUG")
854
+
855
+ log.debug("getting IDSPeak.DeviceManager instance")
856
+ camera = None
857
+ use_threading = False
858
+
859
+ try:
860
+ log.info("main: instantiating Camera")
861
+ camera = IDSCamera()
862
+
863
+ log.info("main: calling camera.connect")
864
+ if not camera.connect():
865
+ log.critical("No IDS camera found")
866
+ sys.exit(1)
867
+
868
+ log.info("main: starting camera acquisition")
869
+ camera.start()
870
+
871
+ if use_threading:
872
+ log.info("main: spawning acquisition loop")
873
+ thread = threading.Thread(target=camera.acquisition_loop, args=())
874
+ thread.start()
875
+
876
+ log.info("main: monitoring acquisitions")
877
+ try:
878
+ # break loop with ctrl-C
879
+ while True:
880
+
881
+ if use_threading:
882
+ log.info("main: dropping request into acquisition loop")
883
+ camera.take_one_request = object()
884
+ while camera.take_one_request is not None:
885
+ time.sleep(0.01)
886
+ pass
887
+ else:
888
+ log.info("main: taking spectrum")
889
+ camera.send_trigger()
890
+ camera.get_spectrum()
891
+
892
+ log.info("main: measurement completed, sleeping 5sec")
893
+ # time.sleep(5)
894
+ break
895
+
896
+ except:
897
+ log.error("main: exception during measuremnet loop", exc_info=1)
898
+ finally:
899
+ # make sure to always stop the acquisition_thread, otherwise
900
+ # we'd hang, e.g. on KeyboardInterrupt
901
+ log.info("main: shutting down (joining thread)")
902
+ camera.shutdown_in_progress= True
903
+ if use_threading:
904
+ thread.join()
905
+
906
+ except KeyboardInterrupt:
907
+ log.critical("User interrupt: Exiting...")
908
+ except:
909
+ log.error(f"Exception (main)", exc_info=1)
910
+ finally:
911
+ # Close camera and library after program ends
912
+ if camera is not None:
913
+ log.info("main: closing camera")
914
+ camera.close()
915
+ log.info("main: closing Library")
916
+ IDSPeak.Library.Close()