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.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
sigima/io/convenience.py
ADDED
|
@@ -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
|