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
twopy/inspection.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Inspect all recognized files in a two-photon recording session.
|
|
2
|
+
|
|
3
|
+
Inputs: a session folder that follows the twopy data layout.
|
|
4
|
+
Outputs: typed summaries for MATLAB, TIFF, text, CSV, ZIP, and other files.
|
|
5
|
+
|
|
6
|
+
The goal is fast orientation: users and tests can see what the recording
|
|
7
|
+
contains without loading the full movie or committing to an analysis method.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import csv
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal, cast
|
|
14
|
+
from zipfile import ZipFile
|
|
15
|
+
|
|
16
|
+
import tifffile
|
|
17
|
+
|
|
18
|
+
from twopy.matlab import MatlabFileSummary, inspect_mat_file
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"FileInspection",
|
|
22
|
+
"SessionInspection",
|
|
23
|
+
"TIFF_METADATA_CSV_COLUMNS",
|
|
24
|
+
"TiffSummary",
|
|
25
|
+
"inspect_recording_files",
|
|
26
|
+
"write_tiff_metadata_csv",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
FileKind = Literal["matlab", "tiff", "text", "csv", "zip", "other"]
|
|
30
|
+
|
|
31
|
+
TIFF_METADATA_CSV_COLUMNS = (
|
|
32
|
+
"relative_path",
|
|
33
|
+
"size_bytes",
|
|
34
|
+
"series_count",
|
|
35
|
+
"series_shapes",
|
|
36
|
+
"series_dtypes",
|
|
37
|
+
"page_count",
|
|
38
|
+
"first_page_shape",
|
|
39
|
+
"first_page_dtype",
|
|
40
|
+
"image_width",
|
|
41
|
+
"image_length",
|
|
42
|
+
"bits_per_sample",
|
|
43
|
+
"samples_per_pixel",
|
|
44
|
+
"x_resolution",
|
|
45
|
+
"y_resolution",
|
|
46
|
+
"resolution_unit",
|
|
47
|
+
"image_description_chars",
|
|
48
|
+
"scanimage_config_name",
|
|
49
|
+
"scanimage_software_version",
|
|
50
|
+
"scanimage_acq_lines_per_frame",
|
|
51
|
+
"scanimage_acq_pixels_per_line",
|
|
52
|
+
"scanimage_acq_number_of_frames",
|
|
53
|
+
"scanimage_acq_number_of_channels_save",
|
|
54
|
+
"scanimage_acq_frame_rate",
|
|
55
|
+
"scanimage_acq_zoom_factor",
|
|
56
|
+
"scanimage_acq_pixel_time",
|
|
57
|
+
"scanimage_acq_ms_per_line",
|
|
58
|
+
"scanimage_acq_z_step_size",
|
|
59
|
+
"scanimage_acq_scan_angle_multiplier_fast",
|
|
60
|
+
"scanimage_acq_scan_angle_multiplier_slow",
|
|
61
|
+
"scanimage_acq_scan_rotation",
|
|
62
|
+
"scanimage_acq_scan_shift_fast",
|
|
63
|
+
"scanimage_acq_scan_shift_slow",
|
|
64
|
+
"scanimage_acq_x_step",
|
|
65
|
+
"scanimage_acq_y_step",
|
|
66
|
+
"scanimage_motor_abs_x_position",
|
|
67
|
+
"scanimage_motor_abs_y_position",
|
|
68
|
+
"scanimage_motor_abs_z_position",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_SCANIMAGE_CSV_KEY_BY_COLUMN = {
|
|
72
|
+
"scanimage_config_name": "configName",
|
|
73
|
+
"scanimage_software_version": "software.version",
|
|
74
|
+
"scanimage_acq_lines_per_frame": "acq.linesPerFrame",
|
|
75
|
+
"scanimage_acq_pixels_per_line": "acq.pixelsPerLine",
|
|
76
|
+
"scanimage_acq_number_of_frames": "acq.numberOfFrames",
|
|
77
|
+
"scanimage_acq_number_of_channels_save": "acq.numberOfChannelsSave",
|
|
78
|
+
"scanimage_acq_frame_rate": "acq.frameRate",
|
|
79
|
+
"scanimage_acq_zoom_factor": "acq.zoomFactor",
|
|
80
|
+
"scanimage_acq_pixel_time": "acq.pixelTime",
|
|
81
|
+
"scanimage_acq_ms_per_line": "acq.msPerLine",
|
|
82
|
+
"scanimage_acq_z_step_size": "acq.zStepSize",
|
|
83
|
+
"scanimage_acq_scan_angle_multiplier_fast": "acq.scanAngleMultiplierFast",
|
|
84
|
+
"scanimage_acq_scan_angle_multiplier_slow": "acq.scanAngleMultiplierSlow",
|
|
85
|
+
"scanimage_acq_scan_rotation": "acq.scanRotation",
|
|
86
|
+
"scanimage_acq_scan_shift_fast": "acq.scanShiftFast",
|
|
87
|
+
"scanimage_acq_scan_shift_slow": "acq.scanShiftSlow",
|
|
88
|
+
"scanimage_acq_x_step": "acq.xstep",
|
|
89
|
+
"scanimage_acq_y_step": "acq.ystep",
|
|
90
|
+
"scanimage_motor_abs_x_position": "motor.absXPosition",
|
|
91
|
+
"scanimage_motor_abs_y_position": "motor.absYPosition",
|
|
92
|
+
"scanimage_motor_abs_z_position": "motor.absZPosition",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class TiffSummary:
|
|
98
|
+
"""Small metadata summary for one TIFF movie.
|
|
99
|
+
|
|
100
|
+
Inputs: TIFF file path.
|
|
101
|
+
Outputs: TIFF layout, tags, and parsed ScanImage metadata.
|
|
102
|
+
|
|
103
|
+
This avoids loading image pixels while still proving the movie can be opened
|
|
104
|
+
and inspected by Python. ``scanimage_state`` is a flat dictionary keyed by
|
|
105
|
+
paths such as ``acq.zoomFactor`` for easy script access.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
series_count: int
|
|
109
|
+
series_shapes: tuple[tuple[int, ...], ...]
|
|
110
|
+
series_dtypes: tuple[str, ...]
|
|
111
|
+
page_count: int
|
|
112
|
+
first_page_shape: tuple[int, ...]
|
|
113
|
+
first_page_dtype: str
|
|
114
|
+
tags: dict[str, str]
|
|
115
|
+
scanimage_state: dict[str, str]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class FileInspection:
|
|
120
|
+
"""Inspection result for one file inside a recording.
|
|
121
|
+
|
|
122
|
+
Inputs: one file path under the session directory.
|
|
123
|
+
Outputs: file kind, size, and type-specific summary fields.
|
|
124
|
+
|
|
125
|
+
Optional fields stay ``None`` unless they apply to that file kind. This keeps
|
|
126
|
+
the object simple and avoids a hierarchy before the data model is stable.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
relative_path: Path
|
|
130
|
+
kind: FileKind
|
|
131
|
+
size_bytes: int
|
|
132
|
+
matlab: MatlabFileSummary | None
|
|
133
|
+
tiff: TiffSummary | None
|
|
134
|
+
text_preview: tuple[str, ...] | None
|
|
135
|
+
zip_entries: tuple[str, ...] | None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class SessionInspection:
|
|
140
|
+
"""Inspection result for all recognized files in a recording.
|
|
141
|
+
|
|
142
|
+
Inputs: session directory.
|
|
143
|
+
Outputs: sorted file inspection entries.
|
|
144
|
+
|
|
145
|
+
This is the Python object that the GUI can use to show what twopy found.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
session_dir: Path
|
|
149
|
+
files: tuple[FileInspection, ...]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def inspect_recording_files(
|
|
153
|
+
session_dir: Path,
|
|
154
|
+
*,
|
|
155
|
+
tiff_metadata_csv: Path | None = None,
|
|
156
|
+
) -> SessionInspection:
|
|
157
|
+
"""Inspect all files below one recording session directory.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
session_dir: Recording directory to inspect recursively.
|
|
161
|
+
tiff_metadata_csv: Optional CSV path that receives one row per TIFF
|
|
162
|
+
movie with selected TIFF tags and ScanImage metadata.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
A sorted session inspection with one entry per file.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
FileNotFoundError: If ``session_dir`` is missing.
|
|
169
|
+
|
|
170
|
+
The function inspects metadata only: MATLAB variable summaries, TIFF series
|
|
171
|
+
metadata, short text previews, and ZIP entry names.
|
|
172
|
+
"""
|
|
173
|
+
root = session_dir.expanduser()
|
|
174
|
+
if not root.is_dir():
|
|
175
|
+
msg = f"Missing recording directory: {root}"
|
|
176
|
+
raise FileNotFoundError(msg)
|
|
177
|
+
|
|
178
|
+
inspections = tuple(
|
|
179
|
+
_inspect_file(root, path)
|
|
180
|
+
for path in sorted(
|
|
181
|
+
candidate for candidate in root.rglob("*") if candidate.is_file()
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
inspection = SessionInspection(session_dir=root, files=inspections)
|
|
186
|
+
if tiff_metadata_csv is not None:
|
|
187
|
+
write_tiff_metadata_csv(inspection, tiff_metadata_csv)
|
|
188
|
+
return inspection
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def write_tiff_metadata_csv(inspection: SessionInspection, path: Path) -> None:
|
|
192
|
+
"""Write selected TIFF and ScanImage metadata to a CSV file.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
inspection: Session inspection returned by ``inspect_recording_files``.
|
|
196
|
+
path: CSV file path to write.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
None. The function writes a human-readable metadata table to disk.
|
|
200
|
+
|
|
201
|
+
The CSV keeps the high-value fields stable while the full parsed ScanImage
|
|
202
|
+
dictionary remains available on each ``TiffSummary`` Python object.
|
|
203
|
+
"""
|
|
204
|
+
output_path = path.expanduser()
|
|
205
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
|
|
207
|
+
with output_path.open("w", encoding="utf-8", newline="") as csv_file:
|
|
208
|
+
writer = csv.DictWriter(csv_file, fieldnames=TIFF_METADATA_CSV_COLUMNS)
|
|
209
|
+
writer.writeheader()
|
|
210
|
+
for file in inspection.files:
|
|
211
|
+
if file.tiff is not None:
|
|
212
|
+
writer.writerow(_tiff_csv_row(file))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _inspect_file(root: Path, path: Path) -> FileInspection:
|
|
216
|
+
"""Inspect one file using the simplest reader for its suffix.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
root: Session root used to compute relative paths.
|
|
220
|
+
path: File to inspect.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A file inspection with metadata for recognized file types.
|
|
224
|
+
|
|
225
|
+
The suffix switch is intentionally boring and explicit. It is easier to
|
|
226
|
+
audit than a registry while the project has only a handful of file types.
|
|
227
|
+
"""
|
|
228
|
+
suffix = path.suffix.lower()
|
|
229
|
+
size_bytes = path.stat().st_size
|
|
230
|
+
|
|
231
|
+
if suffix == ".mat":
|
|
232
|
+
return FileInspection(
|
|
233
|
+
relative_path=path.relative_to(root),
|
|
234
|
+
kind="matlab",
|
|
235
|
+
size_bytes=size_bytes,
|
|
236
|
+
matlab=inspect_mat_file(path),
|
|
237
|
+
tiff=None,
|
|
238
|
+
text_preview=None,
|
|
239
|
+
zip_entries=None,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if suffix in {".tif", ".tiff"}:
|
|
243
|
+
return FileInspection(
|
|
244
|
+
relative_path=path.relative_to(root),
|
|
245
|
+
kind="tiff",
|
|
246
|
+
size_bytes=size_bytes,
|
|
247
|
+
matlab=None,
|
|
248
|
+
tiff=_inspect_tiff(path),
|
|
249
|
+
text_preview=None,
|
|
250
|
+
zip_entries=None,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if suffix == ".zip":
|
|
254
|
+
return FileInspection(
|
|
255
|
+
relative_path=path.relative_to(root),
|
|
256
|
+
kind="zip",
|
|
257
|
+
size_bytes=size_bytes,
|
|
258
|
+
matlab=None,
|
|
259
|
+
tiff=None,
|
|
260
|
+
text_preview=None,
|
|
261
|
+
zip_entries=_inspect_zip(path),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if suffix in {".txt", ".csv", ".batch"}:
|
|
265
|
+
return FileInspection(
|
|
266
|
+
relative_path=path.relative_to(root),
|
|
267
|
+
kind="csv" if suffix == ".csv" else "text",
|
|
268
|
+
size_bytes=size_bytes,
|
|
269
|
+
matlab=None,
|
|
270
|
+
tiff=None,
|
|
271
|
+
text_preview=_text_preview(path),
|
|
272
|
+
zip_entries=None,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return FileInspection(
|
|
276
|
+
relative_path=path.relative_to(root),
|
|
277
|
+
kind="other",
|
|
278
|
+
size_bytes=size_bytes,
|
|
279
|
+
matlab=None,
|
|
280
|
+
tiff=None,
|
|
281
|
+
text_preview=None,
|
|
282
|
+
zip_entries=None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _inspect_tiff(path: Path) -> TiffSummary:
|
|
287
|
+
"""Inspect TIFF series metadata without reading the full movie.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
path: TIFF movie path.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Series count, shape, and dtype metadata.
|
|
294
|
+
|
|
295
|
+
tifffile exposes series metadata lazily, which is enough for an initial GUI
|
|
296
|
+
summary and avoids loading hundreds of megabytes into memory.
|
|
297
|
+
"""
|
|
298
|
+
with tifffile.TiffFile(path) as tif:
|
|
299
|
+
shapes = tuple(
|
|
300
|
+
tuple(int(length) for length in series.shape) for series in tif.series
|
|
301
|
+
)
|
|
302
|
+
dtypes = tuple(str(series.dtype) for series in tif.series)
|
|
303
|
+
first_page = cast(tifffile.TiffPage, tif.pages[0])
|
|
304
|
+
tags = _selected_tiff_tags(first_page)
|
|
305
|
+
return TiffSummary(
|
|
306
|
+
series_count=len(tif.series),
|
|
307
|
+
series_shapes=shapes,
|
|
308
|
+
series_dtypes=dtypes,
|
|
309
|
+
page_count=len(tif.pages),
|
|
310
|
+
first_page_shape=tuple(int(length) for length in first_page.shape),
|
|
311
|
+
first_page_dtype=str(first_page.dtype),
|
|
312
|
+
tags=tags,
|
|
313
|
+
scanimage_state=_parse_scanimage_description(
|
|
314
|
+
tags.get("ImageDescription", ""),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _selected_tiff_tags(page: tifffile.TiffPage) -> dict[str, str]:
|
|
320
|
+
"""Read the TIFF tags that are useful for imaging metadata.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
page: First TIFF page from the movie.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Mapping from TIFF tag name to string value.
|
|
327
|
+
|
|
328
|
+
ScanImage writes the same metadata onto each page in the observed files, so
|
|
329
|
+
the first page is enough and avoids iterating thousands of frames.
|
|
330
|
+
"""
|
|
331
|
+
tag_names = (
|
|
332
|
+
"ImageWidth",
|
|
333
|
+
"ImageLength",
|
|
334
|
+
"BitsPerSample",
|
|
335
|
+
"SamplesPerPixel",
|
|
336
|
+
"XResolution",
|
|
337
|
+
"YResolution",
|
|
338
|
+
"ResolutionUnit",
|
|
339
|
+
"ImageDescription",
|
|
340
|
+
"Software",
|
|
341
|
+
"DateTime",
|
|
342
|
+
)
|
|
343
|
+
tags: dict[str, str] = {}
|
|
344
|
+
for name in tag_names:
|
|
345
|
+
tag = page.tags.get(name)
|
|
346
|
+
if tag is not None:
|
|
347
|
+
tags[name] = _metadata_value_to_text(cast(object, tag.value))
|
|
348
|
+
return tags
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _parse_scanimage_description(description: str) -> dict[str, str]:
|
|
352
|
+
"""Parse ScanImage ``state.*`` lines from a TIFF ImageDescription.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
description: Raw TIFF ``ImageDescription`` tag value.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Flat metadata dictionary without the leading ``state.`` prefix.
|
|
359
|
+
|
|
360
|
+
ScanImage stores one assignment per line. Keeping values as strings avoids
|
|
361
|
+
guessing units or numeric intent too early while still making fields easy to
|
|
362
|
+
search and write to CSV.
|
|
363
|
+
"""
|
|
364
|
+
state: dict[str, str] = {}
|
|
365
|
+
for raw_line in description.splitlines():
|
|
366
|
+
line = raw_line.strip()
|
|
367
|
+
if not line.startswith("state.") or "=" not in line:
|
|
368
|
+
continue
|
|
369
|
+
key, value = line.removeprefix("state.").split("=", 1)
|
|
370
|
+
state[key] = _clean_scanimage_value(value)
|
|
371
|
+
return state
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _clean_scanimage_value(value: str) -> str:
|
|
375
|
+
"""Normalize one ScanImage assignment value for Python and CSV use.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
value: Raw value text from one ``state.*`` line.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Value with surrounding whitespace and simple MATLAB string quotes removed.
|
|
382
|
+
"""
|
|
383
|
+
stripped = value.strip()
|
|
384
|
+
if len(stripped) >= 2 and stripped[0] == "'" and stripped[-1] == "'":
|
|
385
|
+
return stripped[1:-1]
|
|
386
|
+
return stripped
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _metadata_value_to_text(value: object) -> str:
|
|
390
|
+
"""Convert TIFF tag values into stable text.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
value: TIFF tag value returned by tifffile.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Compact string representation for Python summaries and CSV output.
|
|
397
|
+
"""
|
|
398
|
+
if isinstance(value, bytes):
|
|
399
|
+
return value.decode("utf-8", errors="replace")
|
|
400
|
+
if isinstance(value, tuple):
|
|
401
|
+
return " ".join(str(part) for part in value)
|
|
402
|
+
return str(value)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _tiff_csv_row(file: FileInspection) -> dict[str, str]:
|
|
406
|
+
"""Convert one TIFF file inspection into a CSV row.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
file: File inspection with a non-``None`` TIFF summary.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Dictionary keyed by ``TIFF_METADATA_CSV_COLUMNS``.
|
|
413
|
+
"""
|
|
414
|
+
if file.tiff is None:
|
|
415
|
+
msg = "Expected TIFF summary when writing TIFF metadata CSV"
|
|
416
|
+
raise ValueError(msg)
|
|
417
|
+
|
|
418
|
+
tiff = file.tiff
|
|
419
|
+
row = {
|
|
420
|
+
"relative_path": str(file.relative_path),
|
|
421
|
+
"size_bytes": str(file.size_bytes),
|
|
422
|
+
"series_count": str(tiff.series_count),
|
|
423
|
+
"series_shapes": ";".join(
|
|
424
|
+
_shape_to_text(shape) for shape in tiff.series_shapes
|
|
425
|
+
),
|
|
426
|
+
"series_dtypes": ";".join(tiff.series_dtypes),
|
|
427
|
+
"page_count": str(tiff.page_count),
|
|
428
|
+
"first_page_shape": _shape_to_text(tiff.first_page_shape),
|
|
429
|
+
"first_page_dtype": tiff.first_page_dtype,
|
|
430
|
+
"image_width": tiff.tags.get("ImageWidth", ""),
|
|
431
|
+
"image_length": tiff.tags.get("ImageLength", ""),
|
|
432
|
+
"bits_per_sample": tiff.tags.get("BitsPerSample", ""),
|
|
433
|
+
"samples_per_pixel": tiff.tags.get("SamplesPerPixel", ""),
|
|
434
|
+
"x_resolution": tiff.tags.get("XResolution", ""),
|
|
435
|
+
"y_resolution": tiff.tags.get("YResolution", ""),
|
|
436
|
+
"resolution_unit": tiff.tags.get("ResolutionUnit", ""),
|
|
437
|
+
"image_description_chars": str(len(tiff.tags.get("ImageDescription", ""))),
|
|
438
|
+
}
|
|
439
|
+
for column, key in _SCANIMAGE_CSV_KEY_BY_COLUMN.items():
|
|
440
|
+
row[column] = tiff.scanimage_state.get(key, "")
|
|
441
|
+
return row
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _shape_to_text(shape: tuple[int, ...]) -> str:
|
|
445
|
+
"""Format an array shape for one CSV cell.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
shape: Tuple of array dimensions.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Shape dimensions joined by ``x``.
|
|
452
|
+
"""
|
|
453
|
+
return "x".join(str(length) for length in shape)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _text_preview(path: Path, line_count: int = 5) -> tuple[str, ...]:
|
|
457
|
+
"""Read the first few lines of a text-like file.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
path: Text file path.
|
|
461
|
+
line_count: Maximum number of lines to return.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Preview lines without trailing newline characters.
|
|
465
|
+
|
|
466
|
+
A short preview is enough to recognize metadata files without treating them
|
|
467
|
+
as a stable parsed format too early.
|
|
468
|
+
"""
|
|
469
|
+
lines: list[str] = []
|
|
470
|
+
with path.open("r", encoding="utf-8", errors="replace") as text_file:
|
|
471
|
+
for _ in range(line_count):
|
|
472
|
+
line = text_file.readline()
|
|
473
|
+
if line == "":
|
|
474
|
+
break
|
|
475
|
+
lines.append(line.rstrip("\n"))
|
|
476
|
+
return tuple(lines)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _inspect_zip(path: Path) -> tuple[str, ...]:
|
|
480
|
+
"""List files contained inside a ZIP archive.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
path: ZIP archive path.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Sorted entry names stored in the archive.
|
|
487
|
+
|
|
488
|
+
The archive is not extracted. Listing entries is enough for inspection and
|
|
489
|
+
avoids writing data back into the source recording folder.
|
|
490
|
+
"""
|
|
491
|
+
with ZipFile(path) as archive:
|
|
492
|
+
return tuple(sorted(archive.namelist()))
|