TwoPy 0.1.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.
- twopy/__init__.py +168 -0
- twopy/_segments.py +53 -0
- twopy/analysis/__init__.py +101 -0
- twopy/analysis/background_subtraction.py +672 -0
- twopy/analysis/dff.py +524 -0
- twopy/analysis/epoch_mapping.py +223 -0
- twopy/analysis/motion.py +66 -0
- twopy/analysis/persistence.py +975 -0
- twopy/analysis/response_processing/__init__.py +55 -0
- twopy/analysis/response_processing/apply.py +298 -0
- twopy/analysis/response_processing/options.py +192 -0
- twopy/analysis/response_processing/qc.py +270 -0
- twopy/analysis/response_processing/signals.py +200 -0
- twopy/analysis/responses.py +345 -0
- twopy/analysis/trials.py +341 -0
- twopy/analysis/workflow.py +457 -0
- twopy/api.py +78 -0
- twopy/config.py +200 -0
- twopy/conversion/__init__.py +40 -0
- twopy/conversion/alignment_crop.py +329 -0
- twopy/conversion/api.py +113 -0
- twopy/conversion/frame_ranges.py +35 -0
- twopy/conversion/hdf5_writing.py +362 -0
- twopy/conversion/matlab_values.py +123 -0
- twopy/conversion/source_loading.py +403 -0
- twopy/conversion/stimulus_code.py +248 -0
- twopy/conversion/types.py +295 -0
- twopy/converted.py +681 -0
- twopy/database/__init__.py +33 -0
- twopy/database/cache.py +166 -0
- twopy/database/catalog.py +90 -0
- twopy/database/connection.py +27 -0
- twopy/database/generic.py +254 -0
- twopy/database/modeled.py +419 -0
- twopy/database/sql.py +46 -0
- twopy/database/types.py +116 -0
- twopy/inspection.py +492 -0
- twopy/matlab.py +398 -0
- twopy/napari/__init__.py +27 -0
- twopy/napari/__main__.py +10 -0
- twopy/napari/constants.py +13 -0
- twopy/napari/controls.py +739 -0
- twopy/napari/display.py +181 -0
- twopy/napari/display_paths.py +197 -0
- twopy/napari/interactive.py +310 -0
- twopy/napari/launcher.py +175 -0
- twopy/napari/layout.py +147 -0
- twopy/napari/loading.py +207 -0
- twopy/napari/movie.py +71 -0
- twopy/napari/paths.py +181 -0
- twopy/napari/plotting/__init__.py +21 -0
- twopy/napari/plotting/data.py +266 -0
- twopy/napari/plotting/docks.py +1052 -0
- twopy/napari/plotting/export.py +803 -0
- twopy/napari/plotting/export_controls.py +209 -0
- twopy/napari/plotting/label_visibility.py +260 -0
- twopy/napari/plotting/options.py +269 -0
- twopy/napari/plotting/panels.py +119 -0
- twopy/napari/plotting/widgets.py +767 -0
- twopy/napari/protocols.py +90 -0
- twopy/napari/responses.py +91 -0
- twopy/napari/roi.py +238 -0
- twopy/napari/session.py +381 -0
- twopy/napari/state.py +106 -0
- twopy/napari/text.py +26 -0
- twopy/napari/types.py +39 -0
- twopy/napari/viewer.py +405 -0
- twopy/parity/__init__.py +30 -0
- twopy/parity/dff.py +399 -0
- twopy/parity/roi.py +145 -0
- twopy/parity/saved_analysis.py +174 -0
- twopy/photodiode.py +375 -0
- twopy/photodiode_classification.py +484 -0
- twopy/roi.py +462 -0
- twopy/session.py +167 -0
- twopy/spatial.py +154 -0
- twopy/stimulus.py +233 -0
- twopy/synchronization.py +235 -0
- twopy/typing_guards.py +255 -0
- twopy-0.1.0.dist-info/METADATA +311 -0
- twopy-0.1.0.dist-info/RECORD +84 -0
- twopy-0.1.0.dist-info/WHEEL +5 -0
- twopy-0.1.0.dist-info/entry_points.txt +2 -0
- twopy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Validate frame ranges used by conversion helpers.
|
|
2
|
+
|
|
3
|
+
Inputs: frame counts and optional start/stop frame numbers.
|
|
4
|
+
Outputs: explicit valid ``(start, stop)`` frame ranges.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_frame_range(
|
|
9
|
+
*,
|
|
10
|
+
frame_count: int,
|
|
11
|
+
start_frame: int | None,
|
|
12
|
+
stop_frame: int | None,
|
|
13
|
+
) -> tuple[int, int]:
|
|
14
|
+
"""Normalize and validate an optional movie frame range.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
frame_count: Total number of movie frames.
|
|
18
|
+
start_frame: Optional first frame.
|
|
19
|
+
stop_frame: Optional exclusive stop frame.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Validated ``(start, stop)`` frame range.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the requested range is empty or outside the movie.
|
|
26
|
+
"""
|
|
27
|
+
start = 0 if start_frame is None else start_frame
|
|
28
|
+
stop = frame_count if stop_frame is None else stop_frame
|
|
29
|
+
if start < 0 or stop > frame_count or start >= stop:
|
|
30
|
+
msg = (
|
|
31
|
+
"Invalid frame range for mean image: "
|
|
32
|
+
f"start={start}, stop={stop}, frame_count={frame_count}"
|
|
33
|
+
)
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
return start, stop
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Write converted twopy data into HDF5 files.
|
|
2
|
+
|
|
3
|
+
Inputs: loaded source conversion objects and computed summary images.
|
|
4
|
+
Outputs: HDF5 files owned by twopy, using gzip for large compressible arrays.
|
|
5
|
+
|
|
6
|
+
This module owns the on-disk converted layout. Source MATLAB and TIFF files are
|
|
7
|
+
read-only inputs handled before these writers run.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import h5py
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
from twopy.conversion.matlab_values import json_ready
|
|
18
|
+
from twopy.conversion.types import (
|
|
19
|
+
AlignedMovieSource,
|
|
20
|
+
AlignmentCropAudit,
|
|
21
|
+
PhotodiodeSignals,
|
|
22
|
+
SourceConversionInputs,
|
|
23
|
+
StimulusCodeMetadata,
|
|
24
|
+
StimulusData,
|
|
25
|
+
StimulusParameters,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ALIGNED_MOVIE_DATASET",
|
|
30
|
+
"CONVERTED_ALIGNED_MOVIE_FILENAME",
|
|
31
|
+
"CONVERTED_RECORDING_FILENAME",
|
|
32
|
+
"write_aligned_movie_file",
|
|
33
|
+
"write_recording_data_file",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
CONVERTED_RECORDING_FILENAME = "recording_data.h5"
|
|
37
|
+
CONVERTED_ALIGNED_MOVIE_FILENAME = "aligned_movie.h5"
|
|
38
|
+
ALIGNED_MOVIE_DATASET = "movie/aligned"
|
|
39
|
+
IMAGING_RES_PD_SAMPLE_DOMAIN = "one sample per aligned imaging frame"
|
|
40
|
+
HIGH_RES_PD_SAMPLE_DOMAIN = "high-rate photodiode signal for precise event detection"
|
|
41
|
+
SYNCHRONIZATION_MODEL = {
|
|
42
|
+
"imaging_clock": "imaging computer",
|
|
43
|
+
"stimulus_clock": "stimulus presentation computer",
|
|
44
|
+
"clock_relationship": (
|
|
45
|
+
"independent computers with different frame rates; align by photodiode, "
|
|
46
|
+
"not by nominal frame counts alone"
|
|
47
|
+
),
|
|
48
|
+
"sync_signal": "photodiode",
|
|
49
|
+
"event_encoding": (
|
|
50
|
+
"stimulus computer flashes the photodiode at start, trial transitions, "
|
|
51
|
+
"and end; flash pattern or duration identifies the event type"
|
|
52
|
+
),
|
|
53
|
+
"analysis_rule": (
|
|
54
|
+
"decode photodiode events before assigning imaging frames to stimulus "
|
|
55
|
+
"trials or computing responses"
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_recording_data_file(
|
|
61
|
+
*,
|
|
62
|
+
inputs: SourceConversionInputs,
|
|
63
|
+
output_path: Path,
|
|
64
|
+
mean_image: npt.NDArray[np.float64],
|
|
65
|
+
mean_start_frame: int,
|
|
66
|
+
mean_stop_frame: int,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Write non-movie converted recording data into the main HDF5 file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
inputs: Loaded source conversion inputs.
|
|
72
|
+
output_path: Destination ``recording_data.h5`` path.
|
|
73
|
+
mean_image: Precomputed quick-look image.
|
|
74
|
+
mean_start_frame: First frame included in the mean image.
|
|
75
|
+
mean_stop_frame: Exclusive stop frame for the mean image.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
None. The function writes ``recording_data.h5``.
|
|
79
|
+
"""
|
|
80
|
+
with h5py.File(output_path, "w") as h5_file:
|
|
81
|
+
h5_file.attrs["twopy_format"] = "converted-recording"
|
|
82
|
+
h5_file.attrs["source_session_dir"] = str(inputs.session_files.session_dir)
|
|
83
|
+
|
|
84
|
+
_write_movie_reference_group(
|
|
85
|
+
h5_file=h5_file,
|
|
86
|
+
movie=inputs.aligned_movie,
|
|
87
|
+
alignment_crop=inputs.alignment_crop,
|
|
88
|
+
mean_image=mean_image,
|
|
89
|
+
mean_start_frame=mean_start_frame,
|
|
90
|
+
mean_stop_frame=mean_stop_frame,
|
|
91
|
+
)
|
|
92
|
+
metadata_group = h5_file.create_group("metadata")
|
|
93
|
+
_write_attrs(metadata_group, inputs.acquisition.fields)
|
|
94
|
+
run_group = h5_file.create_group("run")
|
|
95
|
+
_write_attrs(run_group, inputs.run.fields)
|
|
96
|
+
_write_stimulus_group(
|
|
97
|
+
h5_file,
|
|
98
|
+
inputs.stimulus_data,
|
|
99
|
+
inputs.stimulus_parameters,
|
|
100
|
+
inputs.stimulus_code,
|
|
101
|
+
)
|
|
102
|
+
_write_photodiode_group(h5_file, inputs.photodiode)
|
|
103
|
+
_write_frame_count_audit_group(h5_file, inputs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def write_aligned_movie_file(
|
|
107
|
+
inputs: SourceConversionInputs,
|
|
108
|
+
movie_path: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Write the large aligned movie into its own twopy HDF5 file.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
inputs: Loaded source conversion inputs.
|
|
114
|
+
movie_path: Destination HDF5 path for aligned movie frames.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
None. The function writes the full aligned movie file.
|
|
118
|
+
"""
|
|
119
|
+
with h5py.File(movie_path, "w") as h5_file:
|
|
120
|
+
h5_file.attrs["twopy_format"] = "aligned-movie"
|
|
121
|
+
h5_file.attrs["source_session_dir"] = str(inputs.session_files.session_dir)
|
|
122
|
+
movie_group = h5_file.create_group("movie")
|
|
123
|
+
_copy_aligned_movie(inputs.aligned_movie, movie_group)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _write_movie_reference_group(
|
|
127
|
+
*,
|
|
128
|
+
h5_file: h5py.File,
|
|
129
|
+
movie: AlignedMovieSource,
|
|
130
|
+
alignment_crop: AlignmentCropAudit,
|
|
131
|
+
mean_image: npt.NDArray[np.float64],
|
|
132
|
+
mean_start_frame: int,
|
|
133
|
+
mean_stop_frame: int,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Write mean image data and a pointer to the separate aligned movie file.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
h5_file: Open ``recording_data.h5`` file.
|
|
139
|
+
movie: Source aligned movie metadata.
|
|
140
|
+
alignment_crop: Alignment-derived crop audit metadata.
|
|
141
|
+
mean_image: Precomputed quick-look image.
|
|
142
|
+
mean_start_frame: First frame included in the mean image.
|
|
143
|
+
mean_stop_frame: Exclusive stop frame for the mean image.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
None. The function writes the ``movie`` group.
|
|
147
|
+
"""
|
|
148
|
+
movie_group = h5_file.create_group("movie")
|
|
149
|
+
movie_group.attrs["aligned_movie_file"] = CONVERTED_ALIGNED_MOVIE_FILENAME
|
|
150
|
+
movie_group.attrs["aligned_movie_dataset"] = ALIGNED_MOVIE_DATASET
|
|
151
|
+
movie_group.attrs["aligned_movie_shape"] = movie.shape
|
|
152
|
+
movie_group.attrs["aligned_movie_dtype"] = movie.dtype
|
|
153
|
+
|
|
154
|
+
mean_dataset = movie_group.create_dataset(
|
|
155
|
+
"mean_image",
|
|
156
|
+
data=mean_image,
|
|
157
|
+
)
|
|
158
|
+
mean_dataset.attrs["start_frame"] = mean_start_frame
|
|
159
|
+
mean_dataset.attrs["stop_frame"] = mean_stop_frame
|
|
160
|
+
_write_alignment_crop_group(movie_group, alignment_crop)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _write_alignment_crop_group(
|
|
164
|
+
movie_group: h5py.Group,
|
|
165
|
+
alignment_crop: AlignmentCropAudit,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Write alignment-valid crop metadata under the ``movie`` group.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
movie_group: Open HDF5 ``movie`` group.
|
|
171
|
+
alignment_crop: ``AlignmentCropAudit`` object from source loading.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
None. The function writes crop bounds and audit attributes.
|
|
175
|
+
"""
|
|
176
|
+
crop = alignment_crop.crop
|
|
177
|
+
crop_group = movie_group.create_group("alignment_valid_crop")
|
|
178
|
+
crop_group.attrs["source"] = crop.source
|
|
179
|
+
crop_group.attrs["axis0_start"] = crop.axis0_start
|
|
180
|
+
crop_group.attrs["axis0_stop"] = crop.axis0_stop
|
|
181
|
+
crop_group.attrs["axis1_start"] = crop.axis1_start
|
|
182
|
+
crop_group.attrs["axis1_stop"] = crop.axis1_stop
|
|
183
|
+
crop_group.attrs["original_shape"] = crop.original_shape
|
|
184
|
+
crop_group.attrs["alignment_frame_start"] = alignment_crop.alignment_frame_start
|
|
185
|
+
crop_group.attrs["alignment_frame_stop"] = alignment_crop.alignment_frame_stop
|
|
186
|
+
crop_group.attrs["x_cutoff_pixels"] = alignment_crop.x_cutoff_pixels
|
|
187
|
+
crop_group.attrs["y_cutoff_pixels"] = alignment_crop.y_cutoff_pixels
|
|
188
|
+
crop_group.attrs["over_moved_frame_count"] = alignment_crop.over_moved_frame_count
|
|
189
|
+
crop_group.attrs["motion_threshold_pixels"] = alignment_crop.motion_threshold_pixels
|
|
190
|
+
crop_group.create_dataset(
|
|
191
|
+
"alignment_shift_pixels",
|
|
192
|
+
data=alignment_crop.alignment_shift_pixels,
|
|
193
|
+
compression="gzip",
|
|
194
|
+
)
|
|
195
|
+
crop_group.create_dataset(
|
|
196
|
+
"motion_artifact_mask",
|
|
197
|
+
data=alignment_crop.motion_artifact_mask,
|
|
198
|
+
compression="gzip",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _write_stimulus_group(
|
|
203
|
+
h5_file: h5py.File,
|
|
204
|
+
stimulus_data: StimulusData,
|
|
205
|
+
parameters: StimulusParameters,
|
|
206
|
+
code_metadata: StimulusCodeMetadata,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Write converted stimulus data and epoch parameters.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
h5_file: Open ``recording_data.h5`` file.
|
|
212
|
+
stimulus_data: Stimulus data loaded from ``stimdata.mat``.
|
|
213
|
+
parameters: Stimulus epoch parameters loaded from ``stimParams.mat``.
|
|
214
|
+
code_metadata: Stimulus-code metadata decoded from ``filebackup.zip``.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
None. The function writes the ``stimulus`` group.
|
|
218
|
+
"""
|
|
219
|
+
stimulus_group = h5_file.create_group("stimulus")
|
|
220
|
+
stimulus_group.attrs["clock_source"] = SYNCHRONIZATION_MODEL["stimulus_clock"]
|
|
221
|
+
stimulus_group.create_dataset(
|
|
222
|
+
"data",
|
|
223
|
+
data=stimulus_data.data,
|
|
224
|
+
compression="gzip",
|
|
225
|
+
)
|
|
226
|
+
stimulus_group.create_dataset(
|
|
227
|
+
"data_column_names",
|
|
228
|
+
data=np.asarray(stimulus_data.column_names, dtype=h5py.string_dtype("utf-8")),
|
|
229
|
+
)
|
|
230
|
+
stimulus_group.create_dataset(
|
|
231
|
+
"parameters_json",
|
|
232
|
+
data=json.dumps(json_ready(parameters.epochs)),
|
|
233
|
+
)
|
|
234
|
+
stimulus_group.create_dataset(
|
|
235
|
+
"function_lookup_json",
|
|
236
|
+
data=json.dumps(json_ready(code_metadata.function_lookup)),
|
|
237
|
+
)
|
|
238
|
+
stimulus_group.create_dataset(
|
|
239
|
+
"stimulus_specific_columns_json",
|
|
240
|
+
data=json.dumps(json_ready(code_metadata.stimulus_specific_columns)),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _write_photodiode_group(
|
|
245
|
+
h5_file: h5py.File,
|
|
246
|
+
photodiode: PhotodiodeSignals,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Write photodiode vectors plus synchronization metadata.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
h5_file: Open ``recording_data.h5`` file.
|
|
252
|
+
photodiode: Photodiode signals loaded from source MATLAB files.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
None. The function writes the ``photodiode`` group.
|
|
256
|
+
"""
|
|
257
|
+
photodiode_group = h5_file.create_group("photodiode")
|
|
258
|
+
_write_synchronization_metadata(photodiode_group)
|
|
259
|
+
|
|
260
|
+
imaging_res_pd = photodiode_group.create_dataset(
|
|
261
|
+
"imaging_res_pd",
|
|
262
|
+
data=photodiode.imaging_res_pd,
|
|
263
|
+
compression="gzip",
|
|
264
|
+
)
|
|
265
|
+
imaging_res_pd.attrs["sample_domain"] = IMAGING_RES_PD_SAMPLE_DOMAIN
|
|
266
|
+
|
|
267
|
+
high_res_pd = photodiode_group.create_dataset(
|
|
268
|
+
"high_res_pd",
|
|
269
|
+
data=photodiode.high_res_pd,
|
|
270
|
+
compression="gzip",
|
|
271
|
+
)
|
|
272
|
+
high_res_pd.attrs["sample_domain"] = HIGH_RES_PD_SAMPLE_DOMAIN
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _write_frame_count_audit_group(
|
|
276
|
+
h5_file: h5py.File,
|
|
277
|
+
inputs: SourceConversionInputs,
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Write source frame-count checks into the converted recording file.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
h5_file: Open ``recording_data.h5`` file.
|
|
283
|
+
inputs: Loaded source conversion inputs with frame-count audit values.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
None. The function writes the ``frame_counts`` group.
|
|
287
|
+
|
|
288
|
+
These values keep the observed ScanImage one-frame metadata offset visible
|
|
289
|
+
before response-analysis code starts assigning frames to trials.
|
|
290
|
+
"""
|
|
291
|
+
frame_counts = h5_file.create_group("frame_counts")
|
|
292
|
+
audit = inputs.frame_counts
|
|
293
|
+
frame_counts.attrs["aligned_movie_frames"] = audit.aligned_movie_frames
|
|
294
|
+
frame_counts.attrs["imaging_res_pd_samples"] = audit.imaging_res_pd_samples
|
|
295
|
+
frame_counts.attrs["acquisition_number_of_frames"] = (
|
|
296
|
+
audit.acquisition_number_of_frames
|
|
297
|
+
)
|
|
298
|
+
frame_counts.attrs["imaging_res_pd_minus_movie"] = audit.imaging_res_pd_minus_movie
|
|
299
|
+
frame_counts.attrs["acquisition_minus_movie"] = audit.acquisition_minus_movie
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _write_synchronization_metadata(group: h5py.Group) -> None:
|
|
303
|
+
"""Write the timing model that makes photodiode data interpretable.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
group: HDF5 group that receives synchronization attributes.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
None. The function writes human-readable HDF5 attributes.
|
|
310
|
+
|
|
311
|
+
These attributes are deliberately plain language. Later response-analysis
|
|
312
|
+
code should read them as the contract: stimulus rows and imaging frames are
|
|
313
|
+
on independent clocks until photodiode events align them.
|
|
314
|
+
"""
|
|
315
|
+
for key, value in SYNCHRONIZATION_MODEL.items():
|
|
316
|
+
group.attrs[key] = value
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _copy_aligned_movie(source: AlignedMovieSource, movie_group: h5py.Group) -> None:
|
|
320
|
+
"""Copy aligned movie frames into an HDF5 group in chunks.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
source: Lazy source movie metadata and path.
|
|
324
|
+
movie_group: Destination HDF5 group in the aligned movie file.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
None. The function writes ``movie/aligned``.
|
|
328
|
+
"""
|
|
329
|
+
chunk_frames = source.chunks[0] if source.chunks is not None else 64
|
|
330
|
+
with h5py.File(source.path, "r") as source_file:
|
|
331
|
+
source_dataset = source_file[source.dataset_name]
|
|
332
|
+
aligned = movie_group.create_dataset(
|
|
333
|
+
"aligned",
|
|
334
|
+
shape=source.shape,
|
|
335
|
+
dtype=source_dataset.dtype,
|
|
336
|
+
chunks=source.chunks,
|
|
337
|
+
compression="gzip",
|
|
338
|
+
)
|
|
339
|
+
aligned.attrs["source_path"] = str(source.path)
|
|
340
|
+
aligned.attrs["source_dataset"] = source.dataset_name
|
|
341
|
+
|
|
342
|
+
for start in range(0, source.shape[0], chunk_frames):
|
|
343
|
+
stop = min(start + chunk_frames, source.shape[0])
|
|
344
|
+
aligned[start:stop, :, :] = source_dataset[start:stop, :, :]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _write_attrs(group: h5py.Group, values: dict[str, object]) -> None:
|
|
348
|
+
"""Write scalar metadata values as HDF5 attributes.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
group: HDF5 group that receives attributes.
|
|
352
|
+
values: Metadata dictionary.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
None.
|
|
356
|
+
"""
|
|
357
|
+
for key, value in values.items():
|
|
358
|
+
ready = json_ready(value)
|
|
359
|
+
if isinstance(ready, str | int | float | bool):
|
|
360
|
+
group.attrs[key] = ready
|
|
361
|
+
else:
|
|
362
|
+
group.attrs[key] = json.dumps(ready)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Convert MATLAB-loaded values into plain Python values.
|
|
2
|
+
|
|
3
|
+
Inputs: objects returned by SciPy MATLAB loading.
|
|
4
|
+
Outputs: scalars, tuples, dictionaries, and JSON-ready values.
|
|
5
|
+
|
|
6
|
+
The conversion layer keeps MATLAB-specific object handling out of analysis code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Protocol, cast
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import scipy.io
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _MatlabStruct(Protocol):
|
|
17
|
+
"""Small protocol for SciPy MATLAB structs.
|
|
18
|
+
|
|
19
|
+
Inputs: SciPy ``mat_struct`` objects.
|
|
20
|
+
Outputs: field names used by conversion helpers.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_fieldnames: list[str] | tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_scipy_variable(path: Path, name: str) -> object:
|
|
27
|
+
"""Load one variable from an older MATLAB file through SciPy.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path: MATLAB file path.
|
|
31
|
+
name: Variable name to load.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Loaded MATLAB variable as a Python/NumPy object.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
KeyError: If the variable is absent.
|
|
38
|
+
"""
|
|
39
|
+
loaded = scipy.io.loadmat(path, squeeze_me=True, struct_as_record=False)
|
|
40
|
+
if name not in loaded:
|
|
41
|
+
msg = f"Missing variable {name!r} in {path}"
|
|
42
|
+
raise KeyError(msg)
|
|
43
|
+
return cast(object, loaded[name])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def nested_matlab_field(root: object, dotted_name: str) -> object:
|
|
47
|
+
"""Read a dotted field path from a MATLAB struct.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
root: MATLAB struct-like object.
|
|
51
|
+
dotted_name: Field path such as ``acq.frameRate``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Python value for the nested field.
|
|
55
|
+
"""
|
|
56
|
+
value = root
|
|
57
|
+
for part in dotted_name.split("."):
|
|
58
|
+
value = getattr(value, part)
|
|
59
|
+
return matlab_value_to_python(value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def matlab_struct_to_dict(value: object) -> dict[str, object]:
|
|
63
|
+
"""Convert a MATLAB struct-like object to a plain dictionary.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: Object returned by SciPy for a MATLAB struct.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary of field names to converted values.
|
|
70
|
+
"""
|
|
71
|
+
struct = cast(_MatlabStruct, value)
|
|
72
|
+
fields = tuple(struct._fieldnames)
|
|
73
|
+
return {field: matlab_value_to_python(getattr(struct, field)) for field in fields}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def matlab_value_to_python(value: object) -> object:
|
|
77
|
+
"""Convert small MATLAB values into plain Python-friendly values.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: MATLAB value loaded by SciPy.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Scalars as Python scalars, struct arrays as tuples, and numeric arrays as
|
|
84
|
+
NumPy arrays.
|
|
85
|
+
"""
|
|
86
|
+
if hasattr(value, "_fieldnames"):
|
|
87
|
+
return matlab_struct_to_dict(value)
|
|
88
|
+
|
|
89
|
+
if isinstance(value, np.ndarray):
|
|
90
|
+
if value.size == 0:
|
|
91
|
+
return ()
|
|
92
|
+
if value.dtype == object:
|
|
93
|
+
return tuple(matlab_value_to_python(item) for item in np.ravel(value))
|
|
94
|
+
if value.size == 1:
|
|
95
|
+
return matlab_value_to_python(np.ravel(value)[0])
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
if isinstance(value, np.generic):
|
|
99
|
+
return value.item()
|
|
100
|
+
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def json_ready(value: object) -> object:
|
|
105
|
+
"""Convert Python/NumPy values into JSON-compatible objects.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
value: Converted MATLAB value or Python scalar.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
JSON-compatible nested scalars, lists, and dictionaries.
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(value, dict):
|
|
114
|
+
return {str(key): json_ready(item) for key, item in value.items()}
|
|
115
|
+
if isinstance(value, tuple | list):
|
|
116
|
+
return [json_ready(item) for item in value]
|
|
117
|
+
if isinstance(value, np.ndarray):
|
|
118
|
+
return np.asarray(value, dtype=object).tolist()
|
|
119
|
+
if isinstance(value, np.generic):
|
|
120
|
+
return value.item()
|
|
121
|
+
if isinstance(value, Path):
|
|
122
|
+
return str(value)
|
|
123
|
+
return value
|