sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__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 (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,157 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Convenience I/O functions
4
+
5
+ This module provides convenient wrapper functions for input/output operations with
6
+ signals and images. These functions offer a simplified interface to the underlying
7
+ I/O system, making common tasks easier to perform.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os.path as osp
13
+ from typing import Generator, Sequence
14
+
15
+ import guidata.dataset as gds
16
+
17
+ from sigima.config import _
18
+ from sigima.io.common.basename import format_basenames
19
+ from sigima.io.image.base import ImageIORegistry
20
+ from sigima.io.signal.base import SignalIORegistry
21
+ from sigima.objects import ImageObj, SignalObj, TypeObj
22
+
23
+
24
+ class SaveToDirectoryParam(gds.DataSet):
25
+ """Save to directory parameters."""
26
+
27
+ def build_filenames(self, objs: list[TypeObj]) -> list[str]:
28
+ """Build filenames according to current parameters."""
29
+ filenames = format_basenames(objs, self.basename + self.extension)
30
+ used: set[str] = set() # Ensure all filenames are unique.
31
+ for i, filename in enumerate(filenames):
32
+ root, ext = osp.splitext(filename)
33
+ filepath = osp.join(self.directory, filename)
34
+ k = 1
35
+ while (filename in used) or (not self.overwrite and osp.exists(filepath)):
36
+ filename = f"{root}_{k}{ext}"
37
+ filepath = osp.join(self.directory, filename)
38
+ k += 1
39
+ used.add(filename)
40
+ filenames[i] = filename
41
+ return filenames
42
+
43
+ def generate_filepath_obj_pairs(
44
+ self, objs: list[TypeObj]
45
+ ) -> Generator[tuple[str, TypeObj], None, None]:
46
+ """Iterate over (filepath, object) pairs to be saved."""
47
+ for filename, obj in zip(self.build_filenames(objs), objs):
48
+ yield osp.join(self.directory, filename), obj
49
+
50
+ directory = gds.DirectoryItem(_("Directory"))
51
+ basename = gds.StringItem(
52
+ _("Basename pattern"),
53
+ default="{title}",
54
+ help=_("""Pattern accepts a Python format string.
55
+
56
+ Standard Python formatting fields may be used, including:
57
+ {title}, {index}, {count}, {xlabel}, {xunit}, {ylabel}, {yunit},
58
+ {metadata}, {metadata[key]}"""),
59
+ )
60
+ extension = gds.StringItem(
61
+ _("Extension"),
62
+ help=_("File extension with leading dot (e.g. .txt or .csv)"),
63
+ regexp=r"^\.\w+$",
64
+ )
65
+ overwrite = gds.BoolItem(
66
+ _("Overwrite"), default=False, help=_("Overwrite existing files")
67
+ )
68
+
69
+
70
+ def read_signals(filename: str) -> Sequence[SignalObj]:
71
+ """Read a list of signals from a file.
72
+
73
+ Args:
74
+ filename: File name.
75
+
76
+ Returns:
77
+ List of signals.
78
+ """
79
+ return SignalIORegistry.read(filename)
80
+
81
+
82
+ def read_signal(filename: str) -> SignalObj:
83
+ """Read a signal from a file.
84
+
85
+ Args:
86
+ filename: File name.
87
+
88
+ Returns:
89
+ Signal.
90
+ """
91
+ return read_signals(filename)[0]
92
+
93
+
94
+ def write_signal(filename: str, signal: SignalObj) -> None:
95
+ """Write a signal to a file.
96
+
97
+ Args:
98
+ filename: File name.
99
+ signal: Signal.
100
+ """
101
+ SignalIORegistry.write(filename, signal)
102
+
103
+
104
+ def write_signals(p: SaveToDirectoryParam, signals: list[SignalObj]) -> None:
105
+ """Write a list of signals to a file.
106
+
107
+ Args:
108
+ p: Save to directory parameters.
109
+ signals: List of signals.
110
+ """
111
+ for filepath, signal in p.generate_filepath_obj_pairs(signals):
112
+ SignalIORegistry.write(filepath, signal)
113
+
114
+
115
+ def read_images(filename: str) -> Sequence[ImageObj]:
116
+ """Read a list of images from a file.
117
+
118
+ Args:
119
+ filename: File name.
120
+
121
+ Returns:
122
+ List of images.
123
+ """
124
+ return ImageIORegistry.read(filename)
125
+
126
+
127
+ def read_image(filename: str) -> ImageObj:
128
+ """Read an image from a file.
129
+
130
+ Args:
131
+ filename: File name.
132
+
133
+ Returns:
134
+ Image.
135
+ """
136
+ return read_images(filename)[0]
137
+
138
+
139
+ def write_image(filename: str, image: ImageObj) -> None:
140
+ """Write an image to a file.
141
+
142
+ Args:
143
+ filename: File name.
144
+ image: Image.
145
+ """
146
+ ImageIORegistry.write(filename, image)
147
+
148
+
149
+ def write_images(p: SaveToDirectoryParam, images: list[ImageObj]) -> None:
150
+ """Write a list of images to files in a directory.
151
+
152
+ Args:
153
+ p: Save to directory parameters.
154
+ images: List of images.
155
+ """
156
+ for filepath, image in p.generate_filepath_obj_pairs(images):
157
+ ImageIORegistry.write(filepath, image)
sigima/io/enums.py ADDED
@@ -0,0 +1,17 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Common enum definitions for Sigima I/O support."""
4
+
5
+ # pylint: disable=invalid-name # Allows short reference names like x, y...
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+
12
+ class FileEncoding(str, Enum):
13
+ """File encodings."""
14
+
15
+ UTF8 = "utf-8"
16
+ UTF8_SIG = "utf-8-sig"
17
+ LATIN1 = "latin-1"
sigima/io/ftlab.py ADDED
@@ -0,0 +1,395 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """FT-Lab I/O common functions."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import struct
8
+ import typing
9
+ from enum import Enum
10
+
11
+ import numpy as np
12
+
13
+
14
+ def check_file_header(fileobj: typing.BinaryIO) -> None:
15
+ """Read and validate the file header.
16
+
17
+ Args:
18
+ fileobj: Opened file object in binary mode.
19
+
20
+ Raises:
21
+ ValueError: If the header is invalid or incomplete.
22
+ """
23
+ nb_bytes_to_read = 6
24
+ read_bytes = fileobj.read(nb_bytes_to_read)
25
+ if len(read_bytes) != nb_bytes_to_read:
26
+ raise ValueError(f"Header is incomplete (expected {nb_bytes_to_read} bytes).")
27
+ # Unpack the first six bytes to check the header.
28
+ # The first six bytes are expected to be in the format of three little-endian 16-bit
29
+ # signed integers.
30
+ i1, _, i3 = struct.unpack("<3h", read_bytes)
31
+ i1_possible_values = (-31609, -30844)
32
+ i3_expected_value = 8224
33
+ remanining_header_length = 250
34
+ if (i1 in i1_possible_values) and (i3 == i3_expected_value):
35
+ # Skip the rest of the header.
36
+ read_bytes = fileobj.read(remanining_header_length)
37
+ if len(read_bytes) != remanining_header_length:
38
+ raise ValueError(
39
+ f"Header is incomplete (expected {remanining_header_length} bytes)."
40
+ )
41
+ else:
42
+ raise ValueError("Unexpected values in header.")
43
+
44
+
45
+ def read_length_prefixed_string(
46
+ fileobj: typing.BinaryIO, encoding: str = "latin-1"
47
+ ) -> str:
48
+ """Read a length-prefixed string.
49
+
50
+ Args:
51
+ fileobj: Opened file object in binary mode.
52
+ encoding: Encoding to decode the string. Defaults to "latin-1".
53
+
54
+ Returns:
55
+ The decoded string.
56
+
57
+ Raises:
58
+ ValueError: If the string length is invalid or file is too short.
59
+ """
60
+ nb_bytes_to_read = 4
61
+ read_bytes = fileobj.read(nb_bytes_to_read)
62
+ if len(read_bytes) != nb_bytes_to_read:
63
+ raise ValueError("Failed to read string length.")
64
+ length_to_read = struct.unpack("<i", read_bytes)[0]
65
+ if length_to_read < 0:
66
+ raise ValueError("Negative string length.")
67
+ pad = length_to_read % 2 != 0
68
+ read_string = fileobj.read(length_to_read)
69
+ if len(read_string) != length_to_read:
70
+ raise ValueError("Failed to read data.")
71
+ if pad:
72
+ read_byte = fileobj.read(1) # Skip padding byte.
73
+ if len(read_byte) != 1:
74
+ raise ValueError("Failed to read padding byte.")
75
+ return read_string.decode(encoding, errors="replace")
76
+
77
+
78
+ class SignalType(Enum):
79
+ """Enum for FT-Lab signal types."""
80
+
81
+ REAL_WITH_GIVEN_X_RANGE = {1, 3, 4, 5, 27, 31, 32, 33, 34, 52, 61, 99}
82
+ REAL_WITH_GIVEN_X = {11, 21, 22, 23, 24, 26, 51, 71}
83
+ COMPLEX_WITH_GIVEN_X_RANGE = {2}
84
+ COMPLEX_WITH_GIVEN_X = {12}
85
+
86
+
87
+ class FTLabSignalFile:
88
+ """FT-Lab signal file."""
89
+
90
+ def __init__(self, file_path: str) -> None:
91
+ """Initialize an FTLabSignalFile object.
92
+
93
+ Args:
94
+ file_path: Path to the FT-Lab signal file (.sig).
95
+ """
96
+ self.file_path: str = file_path
97
+ self.x: np.ndarray
98
+ self.y: np.ndarray
99
+ self.xu: str
100
+ self.yu: str
101
+
102
+ def __repr__(self) -> str:
103
+ """Return a string representation of the object."""
104
+ return (
105
+ f"FTLabSignalFile("
106
+ f"file_path={self.file_path!r}, "
107
+ f"x_shape={None if self.x is None else self.x.shape}, "
108
+ f"y_shape={None if self.y is None else self.y.shape}, "
109
+ f"xu={self.xu!r}, "
110
+ f"yu={self.yu!r})"
111
+ )
112
+
113
+ def _check_header(self, fid: typing.BinaryIO) -> None:
114
+ """Check the file header.
115
+
116
+ Args:
117
+ fid: Opened file object in binary mode.
118
+ """
119
+ check_file_header(fid)
120
+
121
+ def _read_real_with_x_range(
122
+ self, fid: typing.BinaryIO, n: int, start: float, step: float
123
+ ) -> None:
124
+ """Read real data with a given x range.
125
+
126
+ Args:
127
+ fid: Opened file object in binary mode.
128
+ n: Number of data points to read.
129
+ start: Start of the x range.
130
+ step: Step size for the x range.
131
+ """
132
+ self.x = np.linspace(start, start + (n - 1) * step, n)
133
+ self.y = np.fromfile(fid, dtype="<d", count=n)
134
+ if self.y.size != n:
135
+ raise ValueError(f"Expected {n} values, got {self.y.size}")
136
+
137
+ def _read_real_with_x(self, fid: typing.BinaryIO, n: int) -> None:
138
+ """Read real data with given x values.
139
+
140
+ Args:
141
+ fid: Opened file object in binary mode.
142
+ n: Number of data points to read.
143
+ """
144
+ data = np.fromfile(fid, dtype="<d", count=2 * n)
145
+ if data.size != 2 * n:
146
+ raise ValueError(f"Expected {2 * n} values, got {data.size}")
147
+ self.x = data[::2]
148
+ self.y = data[1::2]
149
+
150
+ def _read_complex_with_x_range(
151
+ self, fid: typing.BinaryIO, n: int, start: float, step: float
152
+ ) -> None:
153
+ """Read complex data with a given x range.
154
+
155
+ Args:
156
+ fid: Opened file object in binary mode.
157
+ n: Number of data points to read.
158
+ start: Start of the x range.
159
+ step: Step size for the x range.
160
+ """
161
+ self.x = np.linspace(start, start + (n - 1) * step, n)
162
+ data = np.fromfile(fid, dtype="<d", count=2 * n)
163
+ if data.size != 2 * n:
164
+ raise ValueError(f"Expected {2 * n} values, got {data.size}")
165
+ self.y = data[::2] + 1j * data[1::2]
166
+
167
+ def _read_complex_with_x(self, fid: typing.BinaryIO, n: int) -> None:
168
+ """Read complex data with given x values.
169
+
170
+ Args:
171
+ fid: Opened file object in binary mode.
172
+ n: Number of data points to read.
173
+ """
174
+ data = np.fromfile(fid, dtype="<d", count=3 * n)
175
+ if data.size != 3 * n:
176
+ raise ValueError(f"Expected {3 * n} values, got {data.size}")
177
+ self.x = data[::3]
178
+ self.y = data[1::3] + 1j * data[2::3]
179
+
180
+ def read(self) -> np.ndarray:
181
+ """Read the FT-Lab signal file, populate data and metadata.
182
+
183
+ This method reads the signal data from the file, checking the header and
184
+ determining the signal type. It supports various signal formats.
185
+
186
+ Returns:
187
+ XY data.
188
+
189
+ Raises:
190
+ ValueError: If the file cannot be opened or the format is not recognized.
191
+ NotImplementedError: If the signal type is not supported.
192
+ """
193
+ try:
194
+ with open(self.file_path, "rb") as fid:
195
+ self._check_header(fid)
196
+
197
+ # Skip signal title
198
+ _ = read_length_prefixed_string(fid, encoding="latin-1")
199
+
200
+ # The following image header is expected to contain 20 double values.
201
+ nb_values = 20
202
+ header = np.fromfile(fid, dtype="<d", count=nb_values)
203
+ if header.size != nb_values:
204
+ raise ValueError("Incomplete signal header.")
205
+
206
+ # Check if the version is supported.
207
+ min_version = 5
208
+ if header[19] < min_version:
209
+ raise NotImplementedError(
210
+ f"Signal version {header[19]} is not supported."
211
+ )
212
+
213
+ # The first header value is the signal type.
214
+ stype = int(header[0])
215
+ # The second header value is the number of points.
216
+ n = int(header[1])
217
+
218
+ self.xu = read_length_prefixed_string(fid)
219
+ self.yu = read_length_prefixed_string(fid)
220
+
221
+ if stype in SignalType.REAL_WITH_GIVEN_X_RANGE.value:
222
+ start = header[4]
223
+ step = header[2]
224
+ self._read_real_with_x_range(fid, n, start, step)
225
+ elif stype in SignalType.REAL_WITH_GIVEN_X.value:
226
+ self._read_real_with_x(fid, n)
227
+ elif stype in SignalType.COMPLEX_WITH_GIVEN_X_RANGE.value:
228
+ start = header[4]
229
+ step = header[2]
230
+ self._read_complex_with_x_range(fid, n, start, step)
231
+ elif stype in SignalType.COMPLEX_WITH_GIVEN_X.value:
232
+ self._read_complex_with_x(fid, n)
233
+ else:
234
+ raise NotImplementedError(f"Unsupported signal type: {stype}")
235
+ return np.vstack((self.x, self.y))
236
+
237
+ except OSError as e:
238
+ raise ValueError(f"Error opening file: {e}") from e
239
+
240
+
241
+ def sigread_ftlabsig(filename: str):
242
+ """Read an FT-Lab signal file (.sig) and return the XY data.
243
+
244
+ Args:
245
+ filename: Path to FT-Lab signal file (.sig).
246
+
247
+ Returns:
248
+ XY data from the signal file.
249
+ """
250
+ sig = FTLabSignalFile(filename)
251
+ return sig.read()
252
+
253
+
254
+ class ImageType(Enum):
255
+ """Enum for FT-Lab image types."""
256
+
257
+ REAL = 101
258
+ COMPLEX = 102
259
+
260
+
261
+ class FTLabImageFile:
262
+ """Class of an FT-Lab image file (.ima)."""
263
+
264
+ def __init__(self, file_path: str) -> None:
265
+ """Initialize an FTLabImageFile object.
266
+
267
+ Args:
268
+ file_path: path to an FT-Lab image file (.ima).
269
+ """
270
+ self.file_path: str = file_path
271
+ self.image_type: ImageType
272
+ self.dtype: np.dtype
273
+ self.nb_columns: int
274
+ self.nb_lines: int
275
+ self.data: np.ndarray
276
+
277
+ def __repr__(self) -> str:
278
+ """Return a string representation of the object."""
279
+ return (
280
+ f"FTLabImageFile("
281
+ f"file_path={self.file_path!r}, "
282
+ f"image_type={getattr(self, 'image_type', None)}, "
283
+ f"dtype={getattr(self, 'dtype', None)}, "
284
+ f"nb_columns={getattr(self, 'nb_columns', None)}, "
285
+ f"nb_lines={getattr(self, 'nb_lines', None)})"
286
+ )
287
+
288
+ def _check_header(self, fid: typing.BinaryIO) -> None:
289
+ """Check the file header.
290
+
291
+ Args:
292
+ fid: Opened file object in binary mode.
293
+ """
294
+ check_file_header(fid)
295
+
296
+ def _read_image_data(self, fid):
297
+ """Read image data from the file.
298
+
299
+ Args:
300
+ fid: Opened file object in binary mode.
301
+ image_type: Type of the image (real or complex).
302
+ dtype: Data type of the image data.
303
+ nb_lines: Number of lines (rows) in the image.
304
+ nb_columns: Number of columns in the image.
305
+ """
306
+ size = self.nb_lines * self.nb_columns
307
+ if self.image_type == ImageType.REAL:
308
+ data = np.fromfile(fid, dtype=self.dtype, count=size)
309
+ if data.size != size:
310
+ raise ValueError("Unexpected end of file while reading image data.")
311
+ return data.reshape((self.nb_columns, self.nb_lines)).T
312
+ if self.image_type == ImageType.COMPLEX:
313
+ real = np.fromfile(fid, dtype=self.dtype, count=size)
314
+ imag = np.fromfile(fid, dtype=self.dtype, count=size)
315
+ if real.size != size or imag.size != size:
316
+ raise ValueError(
317
+ "Unexpected end of file while reading complex image data."
318
+ )
319
+ return (
320
+ real.reshape((self.nb_columns, self.nb_lines)).T
321
+ + 1j * imag.reshape((self.nb_columns, self.nb_lines)).T
322
+ )
323
+ raise NotImplementedError(f"Image type {self.image_type} is not supported.")
324
+
325
+ def read(self) -> np.ndarray:
326
+ """Read an image file and return its data.
327
+
328
+ Returns:
329
+ Image data.
330
+
331
+ Raises:
332
+ ValueError: If the file cannot be opened or the format is not recognized.
333
+ NotImplementedError: If the image type or version is not supported.
334
+ """
335
+ try:
336
+ with open(self.file_path, "rb") as fid:
337
+ self._check_header(fid)
338
+
339
+ # Skip image title.
340
+ _ = read_length_prefixed_string(fid, encoding="latin-1")
341
+
342
+ # The following image header is expected to contain 20 double values.
343
+ nb_values = 20
344
+ header = np.fromfile(fid, dtype="<d", count=nb_values)
345
+ if header.size != nb_values:
346
+ raise ValueError("Incomplete image header.")
347
+
348
+ # Check if the version is supported.
349
+ min_version = 7
350
+ if header[19] < min_version:
351
+ raise NotImplementedError(
352
+ f"Image version {header[19]} is not supported."
353
+ )
354
+
355
+ # The first header value is the image type.
356
+ try:
357
+ self.image_type = ImageType(int(header[0]))
358
+ except ValueError as exc:
359
+ raise NotImplementedError(
360
+ f"Image type {int(header[0])} is not supported."
361
+ ) from exc
362
+
363
+ # Data type.
364
+ data_type_map = {
365
+ 8: np.uint8,
366
+ 16: np.uint16,
367
+ 32: np.float32,
368
+ }
369
+ self.dtype = np.dtype(data_type_map.get(int(header[1])))
370
+
371
+ # Size parameters.
372
+ self.nb_columns = int(header[2])
373
+ self.nb_lines = int(header[3])
374
+
375
+ # Skip units.
376
+ for _ in range(3):
377
+ _ = read_length_prefixed_string(fid)
378
+
379
+ # Read data.
380
+ return self._read_image_data(fid)
381
+ except OSError as e:
382
+ raise ValueError(f"Error opening file: {e}") from e
383
+
384
+
385
+ def imread_ftlabima(filename: str) -> np.ndarray:
386
+ """Open an FT-Lab image file.
387
+
388
+ Args:
389
+ filename: path to FT-Lab image file.
390
+
391
+ Returns:
392
+ Image data.
393
+ """
394
+ ftlab_file = FTLabImageFile(filename)
395
+ return ftlab_file.read()
@@ -0,0 +1,9 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Image I/O features
5
+ """
6
+
7
+ # pylint: disable=unused-import
8
+ import sigima.io.image.formats # noqa: F401
9
+ from sigima.io.image.base import ImageIORegistry # noqa: F401