lvpyioTools 2.4.5__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.
File without changes
@@ -0,0 +1,105 @@
1
+ from enum import StrEnum
2
+
3
+ class FrameAttribute(StrEnum):
4
+ """
5
+ Enumeration of the different attributes of an ImageFrame object.
6
+ """
7
+
8
+ ACQ_ATTRIBUTES_TRANSFORMED = "Acq.AttributesTransformed"
9
+ ACQ_CAMERA_ID = "Acq.Camera.ID"
10
+ """The ID of the camera used for the acquisition."""
11
+ ACQ_CAMERA_INDEX = "Acq.Camera.Index"
12
+ """Index of the camera used for the acquisition. This is an array."""
13
+ ACQ_CAMERA_LABEL = "Acq.Camera.Label"
14
+ """Label of the camera (i.e. Camera 1, Camera 2)."""
15
+ ACQ_TIME = "Acq.Time"
16
+ """Time of the acquisition of this frame. This is an array."""
17
+ ACQ_TIME_TO_TRIGGER = "Acq.TimeToTrigger"
18
+ """Time to trigger of the acquisition of this frame. This is an array."""
19
+ ACQ_TIME_SERIES = "AcqTimeSeries"
20
+ CCD_EXPOSURE_TIME = "CCDExposureTime"
21
+ """Exposure time of the camera. This is a string value with the unit."""
22
+ CAM_PIXEL_SIZE = "CamPixelSize"
23
+ """Pixel size of the camera. This is a string value with the unit."""
24
+ CAMERA_MAX_INTENSITY = "CameraMaxIntensity"
25
+ """Maximum intensity of the pixel values. This is a string value."""
26
+ CAMERA_MAX_NX = "CameraMaxNx"
27
+ """Maximum number of pixels in the x-direction. This is a string value."""
28
+ CAMERA_MAX_NY = "CameraMaxNy"
29
+ """Maximum number of pixels in the y-direction. This is a string value."""
30
+ CAMERA_NAME = "CameraName"
31
+ """Name of the camera."""
32
+ FRAME_PROCESSING = "FrameProcessing"
33
+ RGB_FRAME = "RGBFrame"
34
+ """Whenever this is a RGB frame or not. This is a string value of 0 or 1."""
35
+
36
+ class BufferAttribute(StrEnum):
37
+ """
38
+ Enumeration of the different attributes of a Buffer object.
39
+ """
40
+ ACQ_INPUT_SPEED_SELECT = "Acq.Input.SpeedSelect"
41
+ ACQ_INPUT_START_TRIGGER = "Acq.Input.StartTrigger"
42
+ ACQ_STATUS_RECORD_POST = "Acq.Status.RecordPost"
43
+ ACQ_TIME_SERIES = "AcqTimeSeries"
44
+ CUSTOM_IMAGE_TAG_COUNT = "CustomImageTag_Count"
45
+ DEV_DATA_ALIAS0 = "DevDataAlias0"
46
+ DEV_DATA_ALIAS1 = "DevDataAlias1"
47
+ DEV_DATA_ALIAS2 = "DevDataAlias2"
48
+ DEV_DATA_ALIAS3 = "DevDataAlias3"
49
+ DEV_DATA_ALIAS4 = "DevDataAlias4"
50
+ DEV_DATA_CHANNEL0 = "DevDataChannel0"
51
+ DEV_DATA_CHANNEL1 = "DevDataChannel1"
52
+ DEV_DATA_CHANNEL2 = "DevDataChannel2"
53
+ DEV_DATA_CHANNEL3 = "DevDataChannel3"
54
+ DEV_DATA_CHANNEL4 = "DevDataChannel4"
55
+ DEV_DATA_CLASS0 = "DevDataClass0"
56
+ DEV_DATA_CLASS1 = "DevDataClass1"
57
+ DEV_DATA_CLASS2 = "DevDataClass2"
58
+ DEV_DATA_CLASS3 = "DevDataClass3"
59
+ DEV_DATA_CLASS4 = "DevDataClass4"
60
+ DEV_DATA_NAME0 = "DevDataName0"
61
+ DEV_DATA_NAME1 = "DevDataName1"
62
+ DEV_DATA_NAME2 = "DevDataName2"
63
+ DEV_DATA_NAME3 = "DevDataName3"
64
+ DEV_DATA_NAME4 = "DevDataName4"
65
+ DEV_DATA_SCALE0 = "DevDataScale0"
66
+ DEV_DATA_SCALE1 = "DevDataScale1"
67
+ DEV_DATA_SCALE2 = "DevDataScale2"
68
+ DEV_DATA_SCALE3 = "DevDataScale3"
69
+ DEV_DATA_SCALE4 = "DevDataScale4"
70
+ DEV_DATA_SCALE_I0 = "DevDataScaleI0"
71
+ DEV_DATA_SCALE_I1 = "DevDataScaleI1"
72
+ DEV_DATA_SCALE_I2 = "DevDataScaleI2"
73
+ DEV_DATA_SCALE_I3 = "DevDataScaleI3"
74
+ DEV_DATA_SCALE_I4 = "DevDataScaleI4"
75
+ DEV_DATA_X0 = "DevDataX0"
76
+ DEV_DATA_X1 = "DevDataX1"
77
+ DEV_DATA_X2 = "DevDataX2"
78
+ DEV_DATA_X3 = "DevDataX3"
79
+ DEV_DATA_X4 = "DevDataX4"
80
+ DEV_DATA_SOURCES = "DevDataSources"
81
+ DEV_DATA_TRACE0 = "DevDataTrace0"
82
+ DEV_DATA_TRACE1 = "DevDataTrace1"
83
+ DEV_DATA_TRACE2 = "DevDataTrace2"
84
+ DEV_DATA_TRACE3 = "DevDataTrace3"
85
+ DEV_DATA_TRACE4 = "DevDataTrace4"
86
+ EX_PATH_VALUE_F = "ExPathValue_f"
87
+ LOAD_FILE = "LoadFile"
88
+ """Source file of the buffer attributes and data. For `.im7` files this is the original `.im7` file for example."""
89
+ LOAD_SET = "LoadSet"
90
+ """Original set file from which this buffer was loaded. The extension (.set) is omitted."""
91
+ LOAD_SET_INDEX = "LoadSetIndex"
92
+ LOAD_SET_TYPE = "LoadSetType"
93
+ TIMESTAMP = "Timestamp"
94
+ """Timestamp in ISO format.
95
+
96
+ See: https://en.wikipedia.org/wiki/ISO_8601
97
+ """
98
+ DAVIS_VERSION = "_DaVisVersion"
99
+ """DaVis version used to create the buffer."""
100
+ DATE = "_Date"
101
+ """Can be empty."""
102
+ HEADER_PACK_TYPE = "_Header_PackType"
103
+ TIME = "_Time"
104
+ """Can be empty. See Timestamp instead."""
105
+
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+ import xml.etree.ElementTree as ET
4
+
5
+ from lvpyio.types.scale import Scale, Scales
6
+
7
+ def _get_for_axis(scales, id: Literal['LinearScaleX', 'LinearScaleY', 'LinearScaleZ', 'LinearScaleI']):
8
+ linearScale = scales.find(id)
9
+ if linearScale is None:
10
+ raise ValueError(f"Could not find {id} in the calibration file.")
11
+
12
+ factor = linearScale.get("FactorMmPerPixel")
13
+ offset = linearScale.get("OffsetMm")
14
+ unit = linearScale.get("Unit")
15
+ description = linearScale.get("Description")
16
+ if factor is None or offset is None or unit is None:
17
+ raise ValueError(f"Could not find FactorMmPerPixel, OffsetMm, or Unit in the calibration file for {id}.")
18
+
19
+ return Scale(slope=float(factor), offset=float(offset), unit=unit, description=description)
20
+
21
+
22
+ def get_calibration(calibration_file: Path) -> Scales:
23
+ """
24
+ Get the calibration settings from the given calibration file which should be a XML file. Values are rounded to fit the calibration application of DaVis.
25
+
26
+ Args:
27
+ calibration_file (Path): Calibration file in XML format.
28
+
29
+ Returns:
30
+ Scales: A `Scales` object containing the calibration settings for x, y, z, and i axes.
31
+ """
32
+ # Read and parse the XML file
33
+ tree = ET.parse(calibration_file)
34
+ root = tree.getroot()
35
+ # Search for "Scales" in the XML file
36
+ scales = root.find(".//Scales")
37
+
38
+ x = _get_for_axis(scales, id='LinearScaleX')
39
+ y = _get_for_axis(scales, id='LinearScaleY')
40
+ z = _get_for_axis(scales, id='LinearScaleZ')
41
+ i = _get_for_axis(scales, id='LinearScaleI')
42
+
43
+ return Scales(x, y, z, i)
lvpyioTools/frame.py ADDED
@@ -0,0 +1,132 @@
1
+ """
2
+ Helper class for working lvpyio frames retrieved from a set.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+ from enum import StrEnum
7
+
8
+ from lvpyio.types.frame import ImageFrame
9
+ from lvpyio.types.scale import Scales
10
+
11
+ from .attribute import FrameAttribute
12
+ if TYPE_CHECKING:
13
+ from .set import LVSet
14
+
15
+ class LVFrame():
16
+ """
17
+ Wrapper class for working with lvpyio frames retrieved from a set. This class provides a convenient interface to access frame attributes, image data, and scale information.
18
+ """
19
+ def __init__(self, frame: ImageFrame, set: 'LVSet'):
20
+ self.frame = frame
21
+ self.set = set
22
+
23
+ def replace_frame(self, new_frame: ImageFrame):
24
+ """
25
+ Update the current frame with a new frame. Notably used for performance reasons to not create a new LVFrame object for each frame in a set.
26
+
27
+ Args:
28
+ new_frame (ImageFrame): The new frame to update with.
29
+ """
30
+ self.frame = new_frame
31
+
32
+ def __len__(self):
33
+ return len(self.frame.images)
34
+
35
+ def __repr__(self):
36
+ return f"<LVFrame: {len(self.frame.images)} images, shape={self.frame.shape}>"
37
+
38
+ def shape(self):
39
+ """
40
+ Get the images and masks shapes.
41
+
42
+ Returns:
43
+ tuple: A tuple containing the shapes of the images and masks.
44
+ """
45
+ return self.frame.shape
46
+
47
+ def get(self, image_number: int = 0):
48
+ """
49
+ Read the image data from a specific frame and image number.
50
+
51
+ Args:
52
+ image_number (int, optional): The index of the image to retrieve. Defaults to 0.
53
+
54
+ Raises:
55
+ IndexError: If the image_number is out of range.
56
+
57
+ Returns:
58
+ numpy.ndarray: The image data as a NumPy array.
59
+ """
60
+ if image_number < 0 or image_number >= len(self.frame.images):
61
+ raise IndexError(f"Image number {image_number} is out of range. Available images: 0 to {len(self.frame.images)-1}.")
62
+ return self.frame.images[image_number]
63
+
64
+ def show(self, image_number: int = 0,
65
+ vmin: float | None = None, vmax: float | None = None,
66
+ cmap: str = 'gray',
67
+ _show: bool = True):
68
+ """
69
+ Display the image data from a specific frame and image number using the default image viewer.
70
+
71
+ Args:
72
+ image_number (int, optional): The index of the image to display. Defaults to 0.
73
+ """
74
+ import matplotlib.pyplot as plt
75
+ image_data = self.get(image_number)
76
+ fig, ax = plt.subplots()
77
+ ax.imshow(image_data, cmap=cmap, vmin=vmin, vmax=vmax)
78
+ ax.axis('off')
79
+ plt.tight_layout()
80
+ if _show:
81
+ plt.show()
82
+ return fig, ax
83
+
84
+ def scale(self) -> Scales:
85
+ """
86
+ Read the scale information from the frame, that is a class containing the following attributes:
87
+ - `x`: The scale in the x-direction (in meters per pixel).
88
+ - `y`: The scale in the y-direction (in meters per pixel).
89
+ - `z`: The scale in the z-direction (in meters per pixel).
90
+ - `i`: The scale in the intensity direction (generally with a slope of 1).
91
+
92
+ Each attributes are `Scale` objects, which contain the following attributes:
93
+ - `slope`: The slope of the scale
94
+ - `offset`: The offset of the scale
95
+ - `unit`: The unit of the scale (mm, counts, etc.)
96
+ - `description`: Mostly just empty
97
+
98
+ Returns:
99
+ Scales: A `Scales` object containing the scale information for the frame.
100
+ """
101
+ return self.frame.scales
102
+
103
+ def get_attribute(self, attribute: FrameAttribute):
104
+ """
105
+ Get a specific attribute from the frame.
106
+
107
+ Args:
108
+ attribute (FrameAttribute): The attribute to retrieve.
109
+
110
+ Returns:
111
+ Any: The value of the requested attribute, or `None` if the attribute is not found in the frame.
112
+ """
113
+ return self.frame.attributes.get(attribute.value, None)
114
+
115
+ def get_max_intensity(self):
116
+ """
117
+ Get the maximum intensity of the pixel values from the frame.
118
+
119
+ Raises:
120
+ ValueError: If the CAMERA_MAX_INTENSITY attribute is not found or is invalid.
121
+
122
+ Returns:
123
+ float: The maximum intensity value of the pixel values from the frame.
124
+ """
125
+ max_intensity = self.get_attribute(FrameAttribute.CAMERA_MAX_INTENSITY)
126
+ if max_intensity is not None:
127
+ try:
128
+ return float(max_intensity)
129
+ except ValueError:
130
+ raise ValueError(f"Invalid CAMERA_MAX_INTENSITY value: {max_intensity}")
131
+ else:
132
+ raise ValueError("CAMERA_MAX_INTENSITY attribute not found in frame.")
lvpyioTools/set.py ADDED
@@ -0,0 +1,397 @@
1
+ """
2
+ Helper class for working with lvpyio sets.
3
+ """
4
+ from pathlib import Path
5
+ from typing import Any
6
+ import warnings
7
+
8
+ from PIL import Image
9
+
10
+ import lvpyio as lv
11
+ # from lvpyio.types
12
+ from lvpyio.types.frame import ImageFrame
13
+ from lvpyio.types.buffer import Buffer
14
+ from lvpyio.types.scale import Scales
15
+ from lvpyio.io.set import Set
16
+
17
+ from . import setParser, calibration
18
+ from .frame import LVFrame
19
+
20
+
21
+
22
+
23
+ class LVSet(): # numpydoc ignore=SA01
24
+ """
25
+ Helper class for working with DaVis sets.
26
+
27
+ Set files are simple text files that contain some generic information that were saved on creation. They don't give any information about the actual data, where it is stored, or how to read it but the folder placed in the same directory as the set file contains all the data.
28
+
29
+ This provides a simple interface to easily manipulate and read the set files.
30
+
31
+ A set of images is handled this way in DaVis:
32
+ 1. buffer (set[buffer_frame])
33
+ 2. frame (set[buffer_frame][frame_number])
34
+ 3. image (set[buffer_frame][frame_number].images[image_number])
35
+
36
+ Parameters
37
+ ----------
38
+ file (Path): The path to the .set file.
39
+
40
+ Examples
41
+ --------
42
+
43
+ ```python
44
+ from lvpyioTools.sets import LVSet
45
+ from pathlib import Path
46
+
47
+ set_file = Path("example/example.set")
48
+ with LVSet(set_file) as set:
49
+ set.show()
50
+ print(f"Number of frames in the set: {len(set)}")
51
+ ```
52
+ """
53
+ set: Set | None = None
54
+ frames: tuple[LVFrame, ...] | None = None
55
+
56
+ def __init__(self, file: Path):
57
+ # verify provided file
58
+ if not file.exists():
59
+ raise FileNotFoundError(f"File {file} does not exist.")
60
+ if not file.is_file():
61
+ raise ValueError(f"Provided path {file} is not a file.")
62
+ if not file.suffix in [".set", ".exp"]:
63
+ raise ValueError(f"Provided file {file} is not a .set or .exp file.")
64
+
65
+ self.file = file
66
+ self.properties = self.get_properties()
67
+
68
+ def __repr__(self):
69
+ if self.is_experiment():
70
+ return f"<LVSet: {self.file.name}, experiment set, properties={len(self.properties)}>"
71
+ if self.is_open():
72
+ return f"<LVSet: {self.file.name}, {len(self)} frames, properties={len(self.properties)}>"
73
+ return f"<LVSet: {self.file.name}, closed, properties={len(self.properties)}>"
74
+
75
+
76
+ ## LOADER / SAVER
77
+
78
+ def __enter__(self):
79
+ """
80
+ Context manager entry point.
81
+
82
+ Returns:
83
+ LVSet: The current instance of LVSet.
84
+ """
85
+ self.open()
86
+ return self
87
+
88
+ def __exit__(self, *args):
89
+ """
90
+ Safeguard to make sure the set gets closed.
91
+ """
92
+ self.close()
93
+
94
+ def __del__(self):
95
+ """
96
+ Safeguard to make sure the set gets closed.
97
+ """
98
+ self.close()
99
+
100
+ def is_open(self) -> bool:
101
+ """
102
+ Check if the set is currently open.
103
+
104
+ Returns:
105
+ bool: True if the set is open, False otherwise.
106
+ """
107
+ return self.set is not None
108
+
109
+ def open(self):
110
+ """
111
+ Load the set with lvpyio.
112
+ """
113
+ # safeguard to ensure we properly close the set
114
+ self.close()
115
+ if self.is_experiment():
116
+ raise ValueError(f"Cannot open an experiment set (`.exp`) directly.")
117
+ self.set = lv.read_set(self.file)
118
+
119
+ def close(self):
120
+ """
121
+ Close the currently opened set. If no set is open, this method does nothing.
122
+ """
123
+ if self.set is None:
124
+ return
125
+ self.set.close()
126
+ self.set = None
127
+
128
+
129
+ ## PARENTS / CHILDREN
130
+
131
+ def is_experiment(self) -> bool:
132
+ """
133
+ Check if the set is an experiment set (`.exp`).
134
+ """
135
+ return self.file.suffix == ".exp"
136
+
137
+ def get_parent(self) -> 'LVSet | None':
138
+ """
139
+ Retrieve parent set holding this current set if exists. If the current set is an experiment set, it has no parent and this method will return None.
140
+
141
+ Returns:
142
+ LVSet | None: The parent set if it exists, otherwise None.
143
+ """
144
+ isParent = self.is_experiment()
145
+ if isParent:
146
+ return None
147
+
148
+ # get parent theorical path
149
+ set_dir = self.file.parent
150
+ parent_dir = set_dir.parent
151
+
152
+ # find .set or .exp file if exists
153
+ for suffix in [".set", ".exp"]:
154
+ # try to access the set file
155
+ parent_set_file = parent_dir / (set_dir.name + suffix)
156
+ if parent_set_file.exists():
157
+ return LVSet(parent_set_file)
158
+
159
+ return None
160
+
161
+ def get_experiment(self, max_iteration: int = 100) -> 'LVSet | None':
162
+ """
163
+ Retrieve the experiment set holding this current set if exists.
164
+
165
+ Args:
166
+ max_iteration (int, optional): Maximum number of iterations to search for the experiment set. Defaults to 100.
167
+
168
+ Returns:
169
+ LVSet | None: The experiment set if it exists, otherwise None.
170
+ """
171
+ current_set = self
172
+ iteration = 0
173
+ while current_set is not None:
174
+ if current_set.is_experiment():
175
+ return current_set
176
+ current_set = current_set.get_parent()
177
+
178
+ # stop after too many iterations to avoid infinite loops
179
+ iteration += 1
180
+ if iteration > max_iteration:
181
+ warnings.warn(f"Reached maximum iteration ({max_iteration}) while searching for experiment set. Stopping search.")
182
+ break
183
+ return None
184
+
185
+ def get_children(self) -> list['LVSet']:
186
+ """
187
+ Retrieve all child sets of the current set.
188
+
189
+ Returns:
190
+ list[LVSet]: A list of child sets.
191
+ """
192
+ children = []
193
+ set_dir = self.file.parent
194
+ for child_dir in set_dir.iterdir():
195
+ if child_dir.is_dir():
196
+ for suffix in [".set", ".exp"]:
197
+ child_set_file = child_dir / (child_dir.name + suffix)
198
+ if child_set_file.exists():
199
+ children.append(LVSet(child_set_file))
200
+ return children
201
+
202
+ def get_calibration(self) -> 'Scales | None':
203
+ """
204
+ Retrieve the calibration settings from the experiment set if it exists.
205
+
206
+ Returns:
207
+ Scales | None: The calibration settings if they exist, otherwise None.
208
+ """
209
+ experiment = self.get_experiment()
210
+ if experiment is None:
211
+ warnings.warn(f"No experiment set found for {self.file}. Cannot retrieve calibration.")
212
+ return None
213
+
214
+ # get calibration file
215
+ calibration_file = experiment.file.with_suffix("") / "Properties" / "Calibration" / "Calibration.xml"
216
+ if not calibration_file.exists():
217
+ warnings.warn(f"Calibration file {calibration_file} does not exist. Cannot retrieve calibration.")
218
+ return None
219
+
220
+ return calibration.get_calibration(calibration_file)
221
+
222
+
223
+ ## GENERIC INFORMATION ABOUT THE SET
224
+
225
+ def __len__(self):
226
+ """
227
+ Return the number of frames in the set.
228
+
229
+ Returns:
230
+ int: The number of frames in the set.
231
+
232
+ Raises:
233
+ RuntimeError: Set is not open.
234
+ """
235
+ if self.set is None:
236
+ raise RuntimeError("Set is not open. Please call 'open()' before accessing the length.")
237
+ return len(self.set)
238
+
239
+ def read(self) -> str:
240
+ """
241
+ Read the set file and display its content.
242
+ """
243
+ with open(self.file, 'r') as f:
244
+ return f.read().strip()
245
+
246
+ def get_properties(self) -> dict[setParser.SetProperty, Any]:
247
+ """
248
+ Read the set file and return its properties as a dictionary.
249
+
250
+ Returns:
251
+ dict[SetProperty, Any]: A dictionary containing the set properties and their values.
252
+ """
253
+ return setParser.read(self.file)
254
+
255
+
256
+ ## READERS
257
+
258
+ def get_buffer(self, buffer_frame: int) -> Buffer:
259
+ """
260
+ Get a specific buffer from the set.
261
+
262
+ Args:
263
+ buffer_frame (int): The index of the buffer frame to retrieve.
264
+
265
+ Raises:
266
+ RuntimeError: Set is not open.
267
+ IndexError: Buffer frame index is out of range.
268
+
269
+ Returns:
270
+ Buffer: The requested buffer object.
271
+ """
272
+ # verify set is open
273
+ if self.set is None:
274
+ raise RuntimeError("Set is not open. Please call `open()` before accessing buffers.")
275
+
276
+ size = len(self)
277
+ if buffer_frame < 0 or buffer_frame >= size:
278
+ raise IndexError(f"Buffer frame index {buffer_frame} is out of range. Valid range is 0 to {size - 1}.")
279
+ return self.set[buffer_frame]
280
+
281
+ def get_frames(self, buffer_frame: int) -> tuple[LVFrame, ...]:
282
+ buffer = self.get_buffer(buffer_frame)
283
+
284
+ # init LVFrame instances for each frame in the buffer
285
+ if self.frames is None:
286
+ frames_count = len(buffer)
287
+ self.frames = tuple(LVFrame(buffer[i], self) for i in range(frames_count))
288
+
289
+ # if already exists, then simply replace the frames in the existing LVFrame instances
290
+ else:
291
+ for i in range(len(buffer)):
292
+ self.frames[i].replace_frame(buffer[i])
293
+
294
+ return self.frames
295
+
296
+ def get_frame(self, buffer_frame: int, frame_number: int = 0) -> LVFrame:
297
+ """
298
+ Get a specific frame from a buffer in the set.
299
+
300
+ Args:
301
+ buffer_frame (int): The index of the buffer frame to retrieve the frame from.
302
+ frame_number (int, optional): The index of the frame within the buffer. Defaults to 0.
303
+
304
+ Returns:
305
+ LVFrame: The requested frame object.
306
+ """
307
+ frames = self.get_frames(buffer_frame)
308
+ if frame_number < 0 or frame_number >= len(frames):
309
+ raise IndexError(f"Frame number {frame_number} is out of range. Valid range is 0 to {len(frames) - 1}.")
310
+ return frames[frame_number]
311
+
312
+ def get_image(self, buffer_frame: int,
313
+ frame_number: int = 0,
314
+ image_number: int = 0):
315
+ """
316
+ Get a specific image from a frame in a buffer in the set.
317
+
318
+ Args:
319
+ buffer_frame (int): The index of the buffer frame to retrieve the image from.
320
+ frame_number (int, optional): The index of the frame within the buffer. Defaults to 0.
321
+ image_number (int, optional): The index of the image within the frame. Defaults to 0.
322
+
323
+ Returns:
324
+ Image: The requested image object.
325
+ """
326
+ frame = self.get_frame(buffer_frame, frame_number)
327
+ return frame.get(image_number)
328
+
329
+
330
+ ## EXPORTS
331
+
332
+ def export(self, output_dir: Path, extension: str = ".tif"):
333
+ if not self.is_open():
334
+ raise RuntimeError("Set is not open. Please call `open()` before exporting.")
335
+ output_dir.mkdir(parents=True, exist_ok=True)
336
+ for buffer_frame in range(len(self)):
337
+ image = self.get_image(buffer_frame)
338
+ output_file = output_dir / f"buffer_{buffer_frame:05d}{extension}"
339
+ img = Image.fromarray(image)
340
+ img.save(output_file)
341
+
342
+
343
+ ## TEST SCRIPTS
344
+
345
+ if __name__ == "__main__":
346
+ from pprint import pprint
347
+ # Example usage
348
+ set_file = Path("example/example.set")
349
+ print(set_file)
350
+ with LVSet(set_file) as set:
351
+ pprint(set.get_properties())
352
+ print(f"Number of frames in the set: {len(set)}")
353
+
354
+ print()
355
+
356
+ outside_set = Path("/media/scadet03/CADET_MAIN/Manips/2025-10/data.2025-10.piv/DaVis/Upstream/jonc_2/f=0.7, S0=0.05, d=0.06, N=5.0/1/1.set")
357
+ print(outside_set)
358
+ with LVSet(outside_set) as set:
359
+ pprint(set.get_properties())
360
+ print(f"Number of frames in the set: {len(set)}")
361
+
362
+ frame = set.get_frame(0)
363
+ print(frame)
364
+
365
+ print()
366
+
367
+ outside_set = Path("/media/scadet03/CADET_MAIN/Manips/2025-10/data.2025-10.piv/temporary_calibration_ref_data/jonc_1/f=0.8, S0=0.05, d=0.06, N=1.0/Scale.set")
368
+ print(outside_set)
369
+ with LVSet(outside_set) as set:
370
+ pprint(set.get_properties())
371
+ print(f"Number of frames in the set: {len(set)}")
372
+
373
+ buffer = set.get_buffer(0)
374
+ frame = set.get_frame(0)
375
+ print(frame)
376
+
377
+ print()
378
+
379
+ outside_set = Path("/media/scadet03/CADET_MAIN/Manips/2025-10/data.2025-10.piv/DaVis/Upstream/jonc_1/f=0.7, S0=0.05, d=0.06, N=1.0/1/1.set")
380
+ print(outside_set)
381
+ with LVSet(outside_set) as set:
382
+ print(set)
383
+ print(f"Number of frames in the set: {len(set)}")
384
+
385
+ buffer = set.get_buffer(0)
386
+ frame = set.get_frame(0)
387
+ print(frame)
388
+
389
+ parent = set.get_parent()
390
+ print(parent.file if parent is not None else "No parent set found.")
391
+
392
+ experiment = set.get_experiment()
393
+ print(experiment)
394
+ print(experiment.file if experiment is not None else "No experiment set found.")
395
+
396
+ calib = set.get_calibration()
397
+ print(calib)
@@ -0,0 +1,104 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ import warnings
4
+
5
+ from .setProperties import SetProperty, property_types
6
+
7
+
8
+ _PROPERTY_PATTERN = r"(?P<property>\w+) = \"?(?P<value>[\S\s]*?)\"?;"
9
+
10
+
11
+ def read_property(key: str, value: str):
12
+ """
13
+ Read a property from the set file and convert it to the appropriate type and property key.
14
+
15
+ Args:
16
+ key (str): The property key from the set file.
17
+ value (str): The property value from the set file.
18
+
19
+ Returns:
20
+ tuple[SetProperty | None, Any]: A tuple containing the property key as a `SetProperty` enum member (or `None` if unknown) and the converted property value (or `None` if conversion failed).
21
+ """
22
+ try:
23
+ prop = SetProperty(key)
24
+ except ValueError:
25
+ warnings.warn(f"Unknown property '{key}' found in the set file.")
26
+ return None, None
27
+
28
+ # convert to type
29
+ prop_type = property_types.get(prop, None)
30
+ if prop_type is None:
31
+ warnings.warn(f"Unknown property '{key}' found in the set file.")
32
+ else:
33
+ try:
34
+ value = prop_type(value) # convert to the appropriate type
35
+ except Exception as e:
36
+ warnings.warn(f"Error converting property '{key}' with value '{value}': {e}")
37
+ return None, None
38
+
39
+ return prop, value
40
+
41
+ def read(file: Path) -> dict[SetProperty, Any]:
42
+ """
43
+ Read the contents of a file.
44
+
45
+ Args:
46
+ file (Path): The path to the file to read.
47
+
48
+ Returns:
49
+ dict[SetProperty, Any]: A dictionary containing the set properties and their values.
50
+ """
51
+ with open(file, 'r') as f:
52
+ content = f.read().strip()
53
+ lines = content.splitlines()
54
+
55
+ # identify line with "#GROUP Sets"
56
+ group_line_index = None
57
+ for i, line in enumerate(lines):
58
+ if line.strip() == "#GROUP Sets":
59
+ group_line_index = i
60
+ break
61
+
62
+ if group_line_index is None:
63
+ raise ValueError("The file does not contain a '#GROUP Sets' line.")
64
+
65
+ # extract lines after "#GROUP Sets"
66
+ set_lines = lines[group_line_index + 1:]
67
+
68
+ import re
69
+ set_dict = {}
70
+ for line in set_lines:
71
+ line = line.strip()
72
+ if not line or line.startswith("#"):
73
+ continue # skip empty lines and comments
74
+ match = re.match(_PROPERTY_PATTERN, line)
75
+ if match:
76
+ key = match.group('property')
77
+ value = match.group('value')
78
+
79
+ prop, value = read_property(key, value)
80
+ if prop is None:
81
+ continue
82
+
83
+ set_dict[prop] = value
84
+ return set_dict
85
+
86
+
87
+
88
+ if __name__ == "__main__":
89
+ from pprint import pprint
90
+ examples = Path("example")
91
+
92
+ for set_file in examples.rglob("*.set"):
93
+ print(set_file)
94
+ set_dict = read(set_file)
95
+ pprint(set_dict)
96
+
97
+ if SetProperty.SetTime in set_dict:
98
+ print(f"SetTime: {set_dict[SetProperty.SetTime].isoformat()}")
99
+
100
+
101
+ outside_set = Path("/media/scadet03/CADET_MAIN/Manips/2025-10/data.2025-10.piv/temporary_calibration_ref_data/jonc_1/f=0.8, S0=0.05, d=0.06, N=1.0/Scale.set")
102
+ print(outside_set)
103
+ set_dict = read(outside_set)
104
+ pprint(set_dict)
@@ -0,0 +1,113 @@
1
+ from enum import Enum, StrEnum
2
+ from datetime import datetime
3
+ import locale, warnings
4
+
5
+ def _read_date(value: str) -> datetime | None:
6
+ """
7
+ Read the .set file date format and convert into datetime object.
8
+ The date uses english format, e.g. "Fri Sep 26 11:25:26 2025",
9
+ so conversion to a specific locale is needed to ensure correct parsing regardless of the system locale.
10
+
11
+ See:
12
+ https://stackoverflow.com/questions/38303217/datetime-strptime-unexpected-behavior-locale-issue
13
+
14
+ Args:
15
+ value (str): The date string from the .set file.
16
+
17
+ Returns:
18
+ datetime | None: The corresponding datetime object, or `None` if parsing failed.
19
+ """
20
+ try:
21
+ # use 'C' locale to ensure parsing works regardless of system locale
22
+ old_locale = locale.setlocale(locale.LC_TIME)
23
+ try:
24
+ locale.setlocale(locale.LC_TIME, 'C')
25
+ return datetime.strptime(value, "%a %b %d %H:%M:%S %Y")
26
+ finally:
27
+ # restore original locale
28
+ try:
29
+ locale.setlocale(locale.LC_TIME, old_locale)
30
+ except locale.Error:
31
+ # restoration fails, just proceed
32
+ pass
33
+ except ValueError:
34
+ warnings.warn(f"Could not parse date from value '{value}'")
35
+ return None
36
+
37
+
38
+
39
+ class SetType(Enum):
40
+ """
41
+ Classifications of the different values of the `SetType` property.
42
+ """
43
+ IMAGE = 256
44
+ """`.im7` images"""
45
+ VECTORS = 512
46
+ """`.vc7` files"""
47
+ CINE = 4352
48
+ """`.cine` file"""
49
+ PROPERTIES = 8192
50
+ """`Properties` folder"""
51
+ FOLDER = 16384
52
+ """Simple folder"""
53
+ CALIBRATION = 131072
54
+ """Calibration set"""
55
+
56
+ def _read_set_type(value: str) -> SetType | None:
57
+ """
58
+ Convert a string value to a `SetType` enum member.
59
+
60
+ Args:
61
+ value (str): The string representation of the set type, which is expected to be an integer in string format.
62
+
63
+ Returns:
64
+ SetType: The corresponding `SetType` enum member.
65
+ """
66
+ # convert to int
67
+ try:
68
+ value_int = int(value)
69
+ except ValueError:
70
+ warnings.warn(f"Could not convert value '{value}' to int for SetType")
71
+ return None
72
+
73
+ # try to convert to SetType
74
+ try:
75
+ return SetType(value_int)
76
+ except ValueError:
77
+ warnings.warn(f"Unknown SetType value '{value_int}'")
78
+ return None
79
+
80
+ class SetProperty(StrEnum):
81
+ """
82
+ Definitions of the different properties that can be found in a set file.
83
+ """
84
+ SetType = "SetType"
85
+ SetGroups = "SetGroups"
86
+ SetTime = "SetTime"
87
+ SetComments = "SetComments"
88
+ SetStart = "SetStart"
89
+ SetInc = "SetInc"
90
+ SetSourceSet = "SetSourceSet"
91
+ SetViewCallback = "SetViewCallback"
92
+ SetLoadCallback = "SetLoadCallback"
93
+ bpInfoString = "bpInfoString"
94
+ SetIdentifier = "SetIdentifier"
95
+
96
+
97
+ property_types = {
98
+ SetProperty.SetType: _read_set_type,
99
+ SetProperty.SetGroups: str,
100
+ SetProperty.SetTime: _read_date,
101
+ SetProperty.SetComments: str,
102
+ SetProperty.SetStart: int,
103
+ SetProperty.SetInc: int,
104
+ SetProperty.SetSourceSet: str,
105
+ SetProperty.SetViewCallback: str,
106
+ SetProperty.SetLoadCallback: str,
107
+ SetProperty.bpInfoString: str,
108
+ SetProperty.SetIdentifier: str,
109
+ }
110
+ """Associate each SetProperty to a function or type for convertion to a specific type."""
111
+
112
+ if __name__ == "__main__":
113
+ print(SetType(256))
lvpyioTools/viewer.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ Show a specific image from a DaVis set file.
3
+ """
4
+ from pathlib import Path
5
+
6
+ from .set import LVSet
7
+
8
+ def main():
9
+ import argparse
10
+
11
+ parser = argparse.ArgumentParser(description="LVPyIO Tools Viewer")
12
+ parser.add_argument("set", type=str, help="Path to the DaVis set file")
13
+ parser.add_argument("--buffer", type=int, default=0, help="Buffer frame number to display (default: 0)")
14
+ parser.add_argument("--frame", type=int, default=0, help="Frame number to display (default: 0)")
15
+ parser.add_argument("--image", type=int, default=0, help="Image number to display (default: 0)")
16
+ parser.add_argument("--vmin", type=float, default=None, help="Minimum value for image display (default: None)")
17
+ parser.add_argument("--vmax", type=float, default=None, help="Maximum value for image display (default: None)")
18
+ parser.add_argument("--cmap", type=str, default='gray', help="Colormap for image display (default: 'gray')")
19
+ args = parser.parse_args()
20
+
21
+ with LVSet(Path(args.set)) as lv_set:
22
+ try:
23
+ frame = lv_set.get_frame(args.buffer, args.frame)
24
+ frame.show(image_number=args.image,
25
+ vmin=args.vmin, vmax=args.vmax,
26
+ cmap=args.cmap)
27
+ except IndexError as e:
28
+ print(f"Error: {e}")
29
+ except Exception as e:
30
+ print(f"An unexpected error occurred: {e}")
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: lvpyioTools
3
+ Version: 2.4.5
4
+ Summary: A set of tools to handle LabView files and data via the lvpyio library.
5
+ Author-email: SimKDT <simon.cadet79+github@proton.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SimKDT/lvpyio-tools
8
+ Project-URL: Issues, https://github.com/SimKDT/lvpyio-tools/issues
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Requires-Python: >=3.12.3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: Pillow
16
+ Requires-Dist: lvpyio>=1.4.0
17
+ Dynamic: license-file
18
+
19
+ # lvpyio-tools
20
+
21
+ ![License](https://img.shields.io/github/license/SimKDT/lvpyio-tools?label=License)
22
+ ![Downloads](https://img.shields.io/github/downloads/SimKDT/lvpyio-tools/total?label=Downloads)
23
+ ![Code Size](https://img.shields.io/github/languages/code-size/SimKDT/lvpyio-tools?label=Code%20Size)
24
+ ![PyPi Version](https://img.shields.io/pypi/v/lvpyio-tools)
25
+
26
+ A collection of tools to manipulate lvpyio objects such as Sets.
27
+
28
+ ## Installation
29
+
30
+ <details open>
31
+ <summary>PyPi</summary>
32
+
33
+ ```bash
34
+ pip install lvpyio-tools
35
+ ```
36
+ </details>
37
+
38
+ <details>
39
+ <summary>Git</summary>
40
+
41
+ ```bash
42
+ pip install git+https://github.com/SimKDT/lvpyio-tools.git
43
+ ```
44
+ </details>
45
+
46
+ <details>
47
+ <summary>Local</summary>
48
+
49
+ ```bash
50
+ pip install .
51
+ ```
52
+ </details>
53
+
54
+ ## Usage
55
+
56
+ ```python
57
+ from pathlib import Path
58
+ from lvpyioTools.set import LVSet
59
+
60
+ set_file = Path("path/to/your/set.set")
61
+ with LVSet(set_file) as lv_set:
62
+ print(lv_set)
63
+ print(f"Number of frames: {len(lv_set)}")
64
+ print(f"Properties: {lv_set.properties}")
65
+ ```
66
+
67
+ ## Doc (build)
68
+ ```bash
69
+ make html
70
+ ```
71
+
72
+ Serving the documentation locally:
73
+ ```bash
74
+ make serve
75
+ ```
76
+
77
+ ## License
78
+ See [LICENSE](LICENSE) for details.
@@ -0,0 +1,14 @@
1
+ lvpyioTools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ lvpyioTools/attribute.py,sha256=R8n-yJqGqVSJnCG4XadYZNfh4z_lq1aNQp4bm6oGzOc,4202
3
+ lvpyioTools/calibration.py,sha256=rhSEuzR2_73upX2IIKKTDpifUTmx6DeHcopVkl-_QNo,1639
4
+ lvpyioTools/frame.py,sha256=m408OXZmZ1PX5WiNT7UkNKY_KDSVGxCOvM28yk-vqDs,4685
5
+ lvpyioTools/set.py,sha256=yoJGul3k07fJWyzF6jT5sRSj1I24KqqbiERWqGRX4MQ,12921
6
+ lvpyioTools/setParser.py,sha256=saQDAoHePYUt6TbuWtwmYReK2CoT9iMYo1-gU3MV1hA,3140
7
+ lvpyioTools/setProperties.py,sha256=FOLo9VHzzx4dGHzQ-agKJlB8WqOeEBa3AGB8wL5iQaw,3372
8
+ lvpyioTools/viewer.py,sha256=gXaljfftPAX8SHmwSHVoCpdB2VvbGHgPv2SR0dNT-nc,1384
9
+ lvpyiotools-2.4.5.dist-info/licenses/LICENSE,sha256=dXpnFFNJDdreUeL3TbqHT1ndbbeDjzwmQLslFgl6h84,1054
10
+ lvpyiotools-2.4.5.dist-info/METADATA,sha256=GSwRfEzATFJcSt-WOWiCospmlqU3Lxzayo_0tYIxnbw,1736
11
+ lvpyiotools-2.4.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ lvpyiotools-2.4.5.dist-info/entry_points.txt,sha256=42n7kQJZeSkd2m7gvdhBdEnO-3-vqsNMS0J_kEW-spk,55
13
+ lvpyiotools-2.4.5.dist-info/top_level.txt,sha256=OCJnQiLKSAfJbC1jdexsEZPnzZ-W9QTOS2LXHtyUG_8,12
14
+ lvpyiotools-2.4.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lvpyioShow = lvpyioTools.viewer:main
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Simon Cadet
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ lvpyioTools