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.
- wasatch/AbstractUSBDevice.py +29 -0
- wasatch/AndorDevice.py +910 -0
- wasatch/AreaScanImage.py +19 -0
- wasatch/AutoRaman.py +457 -0
- wasatch/AutoRamanRequest.py +96 -0
- wasatch/BLEDevice.py +1032 -0
- wasatch/BalanceAcquisition.py +136 -0
- wasatch/CSVLoader.py +188 -0
- wasatch/CommandSettings.py +95 -0
- wasatch/ControlObject.py +17 -0
- wasatch/DetectorROI.py +36 -0
- wasatch/DetectorRegions.py +158 -0
- wasatch/DeviceFinderBLE.py +107 -0
- wasatch/DeviceFinderTCP.py +20 -0
- wasatch/DeviceFinderUSB.py +236 -0
- wasatch/DeviceID.py +344 -0
- wasatch/EEPROM.py +1450 -0
- wasatch/EEPROM_multical.sav +48 -0
- wasatch/FPGAOptions.py +125 -0
- wasatch/FeatureIdentificationDevice.py +3961 -0
- wasatch/FirmwareRequirements.py +76 -0
- wasatch/HardwareInfo.py +36 -0
- wasatch/IDSCamera.py +916 -0
- wasatch/IDSDevice.py +329 -0
- wasatch/IMX385.py +179 -0
- wasatch/InterfaceDevice.py +40 -0
- wasatch/MockUSBDevice.py +353 -0
- wasatch/OceanDevice.py +215 -0
- wasatch/PollStatus.py +15 -0
- wasatch/ProcessedReading.py +352 -0
- wasatch/ROI.py +33 -0
- wasatch/Reading.py +80 -0
- wasatch/RealUSBDevice.py +64 -0
- wasatch/SPIDevice.py +628 -0
- wasatch/SpectrometerRequest.py +15 -0
- wasatch/SpectrometerResponse.py +38 -0
- wasatch/SpectrometerSettings.py +446 -0
- wasatch/SpectrometerState.py +255 -0
- wasatch/StatusMessage.py +14 -0
- wasatch/TCPDevice.py +409 -0
- wasatch/TakeOneRequest.py +43 -0
- wasatch/WasatchBus.py +87 -0
- wasatch/WasatchDevice.py +1060 -0
- wasatch/WasatchDeviceWrapper.py +452 -0
- wasatch/WrapperWorker.py +304 -0
- wasatch/__init__.py +4 -0
- wasatch/applog.py +177 -0
- wasatch/simulation_protocol.py +227 -0
- wasatch/utils.py +731 -0
- wasatch-2.3.2.dist-info/LICENSE +21 -0
- wasatch-2.3.2.dist-info/METADATA +347 -0
- wasatch-2.3.2.dist-info/RECORD +53 -0
- 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()
|