mesc-io 0.1.0__tar.gz
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.
- mesc_io-0.1.0/LICENSE +21 -0
- mesc_io-0.1.0/PKG-INFO +125 -0
- mesc_io-0.1.0/README.md +87 -0
- mesc_io-0.1.0/pyproject.toml +52 -0
- mesc_io-0.1.0/setup.cfg +4 -0
- mesc_io-0.1.0/src/mesc_io/__init__.py +8 -0
- mesc_io-0.1.0/src/mesc_io/__main__.py +214 -0
- mesc_io-0.1.0/src/mesc_io/check.py +143 -0
- mesc_io-0.1.0/src/mesc_io/dff.py +238 -0
- mesc_io-0.1.0/src/mesc_io/errors.py +17 -0
- mesc_io-0.1.0/src/mesc_io/export.py +81 -0
- mesc_io-0.1.0/src/mesc_io/frames_io.py +78 -0
- mesc_io-0.1.0/src/mesc_io/reader.py +303 -0
- mesc_io-0.1.0/src/mesc_io/registration.py +246 -0
- mesc_io-0.1.0/src/mesc_io/values.py +62 -0
- mesc_io-0.1.0/src/mesc_io/viewer.html +1001 -0
- mesc_io-0.1.0/src/mesc_io/viewer.py +729 -0
- mesc_io-0.1.0/src/mesc_io/writeback.py +179 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/PKG-INFO +125 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/SOURCES.txt +36 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/dependency_links.txt +1 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/entry_points.txt +2 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/requires.txt +15 -0
- mesc_io-0.1.0/src/mesc_io.egg-info/top_level.txt +1 -0
- mesc_io-0.1.0/tests/test_check.py +42 -0
- mesc_io-0.1.0/tests/test_cli.py +60 -0
- mesc_io-0.1.0/tests/test_dff.py +177 -0
- mesc_io-0.1.0/tests/test_export.py +41 -0
- mesc_io-0.1.0/tests/test_multi_session.py +53 -0
- mesc_io-0.1.0/tests/test_reader.py +67 -0
- mesc_io-0.1.0/tests/test_registration.py +90 -0
- mesc_io-0.1.0/tests/test_roundtrip_cli.py +73 -0
- mesc_io-0.1.0/tests/test_streaming.py +42 -0
- mesc_io-0.1.0/tests/test_values.py +38 -0
- mesc_io-0.1.0/tests/test_viewer_geometry.py +65 -0
- mesc_io-0.1.0/tests/test_viewer_page.py +139 -0
- mesc_io-0.1.0/tests/test_viewer_server.py +344 -0
- mesc_io-0.1.0/tests/test_writeback.py +100 -0
mesc_io-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the mesc-io authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mesc_io-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mesc-io
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read Femtonics .mesc two-photon recordings, in the units the native reader shows
|
|
5
|
+
Author: Lynn Smith
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/LynnYuSmith/mesc-io
|
|
8
|
+
Project-URL: Repository, https://github.com/LynnYuSmith/mesc-io
|
|
9
|
+
Project-URL: Changelog, https://github.com/LynnYuSmith/mesc-io/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/LynnYuSmith/mesc-io/issues
|
|
11
|
+
Keywords: mesc,femtonics,two-photon,calcium-imaging,microscopy,hdf5
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: h5py>=3.6
|
|
27
|
+
Requires-Dist: numpy>=1.21
|
|
28
|
+
Provides-Extra: tiff
|
|
29
|
+
Requires-Dist: tifffile>=2021.11; extra == "tiff"
|
|
30
|
+
Provides-Extra: imagej
|
|
31
|
+
Requires-Dist: roifile>=2022.9; extra == "imagej"
|
|
32
|
+
Provides-Extra: register
|
|
33
|
+
Requires-Dist: suite2p<1.0,>=0.14; extra == "register"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
36
|
+
Requires-Dist: roifile>=2022.9; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# mesc-io
|
|
40
|
+
|
|
41
|
+
[](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml)
|
|
42
|
+
|
|
43
|
+
Read Femtonics `.mesc` two-photon recordings in Python — the frames in the units the native
|
|
44
|
+
reader shows, every unit's metadata, an export, a write-back, and a viewer that draws ROIs and
|
|
45
|
+
computes their dF/F.
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Not on PyPI yet. From the repository:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pip install "mesc-io @ git+https://github.com/LynnYuSmith/mesc-io"
|
|
55
|
+
pip install "mesc-io[tiff,imagej] @ git+https://github.com/LynnYuSmith/mesc-io" # + TIFF export, ImageJ ROI export
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Needs Python 3.9 or newer, numpy and h5py; nothing else for reading and viewing. Motion
|
|
59
|
+
correction (`mesc-io register`) is an extra, `[register]`, and pulls in Suite2p 0.14.
|
|
60
|
+
|
|
61
|
+
## Python
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from mesc_io import MescFile
|
|
65
|
+
|
|
66
|
+
with MescFile("recording.mesc") as f:
|
|
67
|
+
for u in f.units():
|
|
68
|
+
print(u.path, u.shape, u.frame_rate_hz, u.pixel_size_um)
|
|
69
|
+
|
|
70
|
+
frames = f.read("MUnit_0", channel=0) # reader units, float64
|
|
71
|
+
frames = f.read("MUnit_0", reader_units=False) # the stored integers
|
|
72
|
+
for block in f.iter_frames("MUnit_0", block=500): # bigger than memory
|
|
73
|
+
...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`read` refuses over 4 GB by default (`max_gb=None` to override). A unit name that matches in
|
|
77
|
+
more than one session is refused, not resolved to the first; `MSession_1/MUnit_0` always works.
|
|
78
|
+
|
|
79
|
+
## Command line
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
mesc-io info recording.mesc # units, rates, pixel sizes
|
|
83
|
+
mesc-io check recording.mesc # what the file disagrees with itself about
|
|
84
|
+
mesc-io export recording.mesc MUnit_0 u0.h5 # or u0.tif
|
|
85
|
+
mesc-io writeback recording.mesc out.mesc MUnit_0 u0.h5 # processed frames back in
|
|
86
|
+
mesc-io view recording.mesc # the recording in a browser
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`info` and `check` take `--json`.
|
|
90
|
+
|
|
91
|
+
### The viewer
|
|
92
|
+
|
|
93
|
+
`mesc-io view` opens the recording in a private browser window: units on the left with a
|
|
94
|
+
thumbnail each and the unit's own metadata (rate, pixel size, the stage position and the
|
|
95
|
+
position from the zero set on the rig); the image in
|
|
96
|
+
the middle — scrub it, average a window of N frames, zoom into a place by dragging a box;
|
|
97
|
+
ROIs on the right (spot, rectangle, polygon; drag to move, type a size); every ROI's time
|
|
98
|
+
course along the bottom, raw or dF/F by our pipeline's method (`mesc_io.dff`, numpy only,
|
|
99
|
+
held against the pipeline's functions in the tests), stacked or overlaid, zoomed by dragging a box. ROIs belong to the
|
|
100
|
+
unit they were drawn on and are saved as you go, beside the working directory, never beside
|
|
101
|
+
the raw file; so is the view itself, so reopening the file puts you back where you were.
|
|
102
|
+
Layout and decisions: [docs/viewer_design.md](docs/viewer_design.md).
|
|
103
|
+
|
|
104
|
+
To try it without a recording, make one:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
python tools/make_synthetic.py # writes synthetic_view.mesc: 3 units, 4 spots, 3 of them blinking
|
|
108
|
+
mesc-io view synthetic_view.mesc
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
pip install -e ".[tiff,dev]"
|
|
115
|
+
pytest
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The package lives under `src/`, so tests run against the installed copy. CI runs the suite on
|
|
119
|
+
Python 3.9, 3.12 and 3.13 on Linux, macOS and Windows, plus one job with Suite2p for the
|
|
120
|
+
registration tests; the browser checks (`tests/*_real_chrome.js`) run in a real Chrome and are
|
|
121
|
+
not part of pytest.
|
|
122
|
+
|
|
123
|
+
## Licence
|
|
124
|
+
|
|
125
|
+
MIT — see [LICENSE](LICENSE).
|
mesc_io-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# mesc-io
|
|
2
|
+
|
|
3
|
+
[](https://github.com/LynnYuSmith/mesc-io/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
Read Femtonics `.mesc` two-photon recordings in Python — the frames in the units the native
|
|
6
|
+
reader shows, every unit's metadata, an export, a write-back, and a viewer that draws ROIs and
|
|
7
|
+
computes their dF/F.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Not on PyPI yet. From the repository:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install "mesc-io @ git+https://github.com/LynnYuSmith/mesc-io"
|
|
17
|
+
pip install "mesc-io[tiff,imagej] @ git+https://github.com/LynnYuSmith/mesc-io" # + TIFF export, ImageJ ROI export
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Needs Python 3.9 or newer, numpy and h5py; nothing else for reading and viewing. Motion
|
|
21
|
+
correction (`mesc-io register`) is an extra, `[register]`, and pulls in Suite2p 0.14.
|
|
22
|
+
|
|
23
|
+
## Python
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from mesc_io import MescFile
|
|
27
|
+
|
|
28
|
+
with MescFile("recording.mesc") as f:
|
|
29
|
+
for u in f.units():
|
|
30
|
+
print(u.path, u.shape, u.frame_rate_hz, u.pixel_size_um)
|
|
31
|
+
|
|
32
|
+
frames = f.read("MUnit_0", channel=0) # reader units, float64
|
|
33
|
+
frames = f.read("MUnit_0", reader_units=False) # the stored integers
|
|
34
|
+
for block in f.iter_frames("MUnit_0", block=500): # bigger than memory
|
|
35
|
+
...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`read` refuses over 4 GB by default (`max_gb=None` to override). A unit name that matches in
|
|
39
|
+
more than one session is refused, not resolved to the first; `MSession_1/MUnit_0` always works.
|
|
40
|
+
|
|
41
|
+
## Command line
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
mesc-io info recording.mesc # units, rates, pixel sizes
|
|
45
|
+
mesc-io check recording.mesc # what the file disagrees with itself about
|
|
46
|
+
mesc-io export recording.mesc MUnit_0 u0.h5 # or u0.tif
|
|
47
|
+
mesc-io writeback recording.mesc out.mesc MUnit_0 u0.h5 # processed frames back in
|
|
48
|
+
mesc-io view recording.mesc # the recording in a browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`info` and `check` take `--json`.
|
|
52
|
+
|
|
53
|
+
### The viewer
|
|
54
|
+
|
|
55
|
+
`mesc-io view` opens the recording in a private browser window: units on the left with a
|
|
56
|
+
thumbnail each and the unit's own metadata (rate, pixel size, the stage position and the
|
|
57
|
+
position from the zero set on the rig); the image in
|
|
58
|
+
the middle — scrub it, average a window of N frames, zoom into a place by dragging a box;
|
|
59
|
+
ROIs on the right (spot, rectangle, polygon; drag to move, type a size); every ROI's time
|
|
60
|
+
course along the bottom, raw or dF/F by our pipeline's method (`mesc_io.dff`, numpy only,
|
|
61
|
+
held against the pipeline's functions in the tests), stacked or overlaid, zoomed by dragging a box. ROIs belong to the
|
|
62
|
+
unit they were drawn on and are saved as you go, beside the working directory, never beside
|
|
63
|
+
the raw file; so is the view itself, so reopening the file puts you back where you were.
|
|
64
|
+
Layout and decisions: [docs/viewer_design.md](docs/viewer_design.md).
|
|
65
|
+
|
|
66
|
+
To try it without a recording, make one:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
python tools/make_synthetic.py # writes synthetic_view.mesc: 3 units, 4 spots, 3 of them blinking
|
|
70
|
+
mesc-io view synthetic_view.mesc
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pip install -e ".[tiff,dev]"
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The package lives under `src/`, so tests run against the installed copy. CI runs the suite on
|
|
81
|
+
Python 3.9, 3.12 and 3.13 on Linux, macOS and Windows, plus one job with Suite2p for the
|
|
82
|
+
registration tests; the browser checks (`tests/*_real_chrome.js`) run in a real Chrome and are
|
|
83
|
+
not part of pytest.
|
|
84
|
+
|
|
85
|
+
## Licence
|
|
86
|
+
|
|
87
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mesc-io"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read Femtonics .mesc two-photon recordings, in the units the native reader shows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Lynn Smith" }]
|
|
14
|
+
keywords = ["mesc", "femtonics", "two-photon", "calcium-imaging", "microscopy", "hdf5"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["h5py>=3.6", "numpy>=1.21"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/LynnYuSmith/mesc-io"
|
|
32
|
+
Repository = "https://github.com/LynnYuSmith/mesc-io"
|
|
33
|
+
Changelog = "https://github.com/LynnYuSmith/mesc-io/blob/main/CHANGELOG.md"
|
|
34
|
+
Issues = "https://github.com/LynnYuSmith/mesc-io/issues"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
tiff = ["tifffile>=2021.11"]
|
|
38
|
+
imagej = ["roifile>=2022.9"]
|
|
39
|
+
register = ["suite2p>=0.14,<1.0"] # 1.x changed compute_reference/register_frames; validated on 0.14.x
|
|
40
|
+
dev = ["pytest>=7", "roifile>=2022.9"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
mesc-io = "mesc_io.__main__:main"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
# The viewer's page ships inside the package. Without this it is missing from the wheel and
|
|
49
|
+
# `mesc-io view` fails at the first request with a file-not-found — on someone else's machine,
|
|
50
|
+
# not on the one it was built on.
|
|
51
|
+
[tool.setuptools.package-data]
|
|
52
|
+
mesc_io = ["viewer.html"]
|
mesc_io-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Read Femtonics `.mesc` two-photon recordings, in the units the native reader shows."""
|
|
2
|
+
from .errors import MescIOError
|
|
3
|
+
from .reader import Channel, MescError, MescFile, Unit
|
|
4
|
+
from .values import ConversionError, from_reader_units, to_reader_units
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["MescFile", "Unit", "Channel", "MescError", "MescIOError",
|
|
8
|
+
"to_reader_units", "from_reader_units", "ConversionError", "__version__"]
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""`mesc-io` on the command line: look inside a .mesc, get frames out, put frames back."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .errors import MescIOError
|
|
10
|
+
from .reader import MescFile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _as_json(payload) -> int:
|
|
14
|
+
import json
|
|
15
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _info(args) -> int:
|
|
20
|
+
with MescFile(args.file) as f:
|
|
21
|
+
units = f.units()
|
|
22
|
+
if args.json:
|
|
23
|
+
return _as_json([{"unit": u.path, "frames": u.n_frames,
|
|
24
|
+
"height": u.height, "width": u.width, "dtype": u.dtype,
|
|
25
|
+
"frame_rate_hz": u.frame_rate_hz,
|
|
26
|
+
"duration_s": u.duration_s,
|
|
27
|
+
"pixel_size_um": u.pixel_size_um,
|
|
28
|
+
"comment": u.comment,
|
|
29
|
+
"channels": [{"name": c.name, "offset": c.offset,
|
|
30
|
+
"scale": c.scale} for c in u.channels]}
|
|
31
|
+
for u in units])
|
|
32
|
+
print(f"{Path(args.file).name}: {len(units)} unit(s)")
|
|
33
|
+
print(f"{'unit':<18}{'frames':>8}{'size':>12}{'rate':>11}{'pixel':>11} comment")
|
|
34
|
+
for u in units:
|
|
35
|
+
rate = f"{u.frame_rate_hz:.3f} Hz" if u.frame_rate_hz else "unknown"
|
|
36
|
+
px = f"{u.pixel_size_um:.4f} um" if u.pixel_size_um else "unknown"
|
|
37
|
+
print(f" {u.path:<16}{u.n_frames:>8}{f'{u.height}x{u.width}':>12}"
|
|
38
|
+
f"{rate:>11}{px:>11} {u.comment[:44]}")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check(args) -> int:
|
|
43
|
+
from .check import check
|
|
44
|
+
rep = check(args.file)
|
|
45
|
+
if args.json:
|
|
46
|
+
return _as_json({"file": rep.path, "units": rep.n_units, "ok": rep.ok,
|
|
47
|
+
"findings": [{"level": f.level, "code": f.code,
|
|
48
|
+
"message": f.message, "units": list(f.units)}
|
|
49
|
+
for f in rep.findings]})
|
|
50
|
+
print(rep)
|
|
51
|
+
if rep.ok:
|
|
52
|
+
print(" nothing that would mislead an analysis")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _export(args) -> int:
|
|
57
|
+
from . import export as _ex
|
|
58
|
+
with MescFile(args.file) as f:
|
|
59
|
+
out = Path(args.out)
|
|
60
|
+
writer = _ex.to_tiff if out.suffix.lower() in (".tif", ".tiff") else _ex.to_hdf5
|
|
61
|
+
written = writer(f, args.unit, out, channel=args.channel,
|
|
62
|
+
reader_units=not args.stored_units)
|
|
63
|
+
print(f" {written} ({'stored' if args.stored_units else 'reader'} units)")
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _writeback(args) -> int:
|
|
68
|
+
from .frames_io import load_frames
|
|
69
|
+
from .writeback import write_frames
|
|
70
|
+
|
|
71
|
+
frames, in_file = load_frames(args.frames, dataset=args.dataset)
|
|
72
|
+
if args.reader_units:
|
|
73
|
+
units = True
|
|
74
|
+
elif args.stored_units:
|
|
75
|
+
units = False
|
|
76
|
+
elif in_file is not None:
|
|
77
|
+
units = in_file
|
|
78
|
+
print(f" {Path(args.frames).name} says its values are in "
|
|
79
|
+
f"{'reader' if units else 'stored'} units")
|
|
80
|
+
else:
|
|
81
|
+
print(" the frames do not say which units they are in — pass --reader-units or "
|
|
82
|
+
"--stored-units. Getting this wrong writes a file that looks right and is not.",
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
report = write_frames(args.source, args.out, {args.unit: {f"Channel_{args.channel}": frames}},
|
|
87
|
+
reader_units=units, tag=None if args.no_tag else args.tag,
|
|
88
|
+
tolerance=args.tolerance)
|
|
89
|
+
for w in report["warnings"]:
|
|
90
|
+
print(f" note: {w}")
|
|
91
|
+
for what, delta in report["agreement"].items():
|
|
92
|
+
print(f" {what}: agrees with the frames it replaced to {delta:+.0f} counts")
|
|
93
|
+
print(f" {report['out']}")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _view(args) -> int:
|
|
98
|
+
from .viewer import serve
|
|
99
|
+
|
|
100
|
+
serve(args.source, port=args.port, open_browser=not args.no_browser,
|
|
101
|
+
rois_path=args.rois)
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _register(args) -> int:
|
|
106
|
+
from .registration import register_file
|
|
107
|
+
|
|
108
|
+
def _say(path, info):
|
|
109
|
+
print(f" {path}: {info['n_frames']} frames, {info['n_channels']} channel(s), "
|
|
110
|
+
f"{info['leading_flat_frames']} flat at the front, "
|
|
111
|
+
f"shift |y|<={abs(info['y_shift']).max()} |x|<={abs(info['x_shift']).max()} px")
|
|
112
|
+
|
|
113
|
+
rep = register_file(args.source, args.out, units=args.units or None, channel=args.channel,
|
|
114
|
+
reference_from=args.reference_from, nonrigid=args.nonrigid,
|
|
115
|
+
block_size=args.block_size, max_shift=args.max_shift,
|
|
116
|
+
max_shift_nr=args.max_shift_nr,
|
|
117
|
+
tag=None if args.no_tag else args.tag, progress=_say)
|
|
118
|
+
print(f" reference from {rep['reference_from']}"
|
|
119
|
+
f"{' (non-rigid)' if rep['nonrigid'] else ''}")
|
|
120
|
+
print(f" {rep['out']}")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv=None) -> int:
|
|
125
|
+
ap = argparse.ArgumentParser(prog="mesc-io", description=__doc__)
|
|
126
|
+
ap.add_argument("--version", action="version", version=f"mesc-io {__version__}")
|
|
127
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
128
|
+
|
|
129
|
+
p = sub.add_parser("info", help="list the units in a .mesc and what they say about themselves")
|
|
130
|
+
p.add_argument("file")
|
|
131
|
+
p.add_argument("--json", action="store_true", help="machine-readable output")
|
|
132
|
+
p.set_defaults(func=_info)
|
|
133
|
+
|
|
134
|
+
p = sub.add_parser("check", help="report what in a .mesc disagrees with itself")
|
|
135
|
+
p.add_argument("file")
|
|
136
|
+
p.add_argument("--json", action="store_true", help="machine-readable output")
|
|
137
|
+
p.set_defaults(func=_check)
|
|
138
|
+
|
|
139
|
+
p = sub.add_parser("export", help="write one unit to .h5 or .tif (by the output's suffix)")
|
|
140
|
+
p.add_argument("file")
|
|
141
|
+
p.add_argument("unit", help="MUnit_0, or MSession_0/MUnit_0")
|
|
142
|
+
p.add_argument("out")
|
|
143
|
+
p.add_argument("--channel", type=int, default=0)
|
|
144
|
+
p.add_argument("--stored-units", action="store_true",
|
|
145
|
+
help="write the file's own integers instead of the values the reader shows")
|
|
146
|
+
p.set_defaults(func=_export)
|
|
147
|
+
|
|
148
|
+
p = sub.add_parser("writeback",
|
|
149
|
+
help="put processed frames back into a copy of a .mesc")
|
|
150
|
+
p.add_argument("source", help="the original .mesc — it is copied, never modified")
|
|
151
|
+
p.add_argument("out", help="the copy to write")
|
|
152
|
+
p.add_argument("unit", help="MUnit_0, or MSession_0/MUnit_0")
|
|
153
|
+
p.add_argument("frames", help=".h5 or .tif holding the processed frames")
|
|
154
|
+
p.add_argument("--channel", type=int, default=0)
|
|
155
|
+
p.add_argument("--dataset", default=None,
|
|
156
|
+
help="which dataset in the .h5 (only needed if there is more than one)")
|
|
157
|
+
g = p.add_mutually_exclusive_group()
|
|
158
|
+
g.add_argument("--reader-units", action="store_true",
|
|
159
|
+
help="the frames hold the values the reader displays")
|
|
160
|
+
g.add_argument("--stored-units", action="store_true",
|
|
161
|
+
help="the frames hold the file's own integers")
|
|
162
|
+
p.add_argument("--tag", default="_MC",
|
|
163
|
+
help="appended to the written unit's comment (default: _MC)")
|
|
164
|
+
p.add_argument("--no-tag", action="store_true", help="do not mark the written unit")
|
|
165
|
+
p.add_argument("--tolerance", type=float, default=60.0,
|
|
166
|
+
help="how far, in stored counts, the new frames may differ from the ones "
|
|
167
|
+
"they replace before the write is refused (default: 60)")
|
|
168
|
+
p.set_defaults(func=_writeback)
|
|
169
|
+
|
|
170
|
+
p = sub.add_parser("view",
|
|
171
|
+
help="open the recording in a browser: the movie, the file's own "
|
|
172
|
+
"numbers, and traces of regions you draw")
|
|
173
|
+
p.add_argument("source")
|
|
174
|
+
p.add_argument("--port", type=int, default=8020)
|
|
175
|
+
p.add_argument("--no-browser", action="store_true",
|
|
176
|
+
help="do not open a browser; print the address and wait")
|
|
177
|
+
p.add_argument("--rois", default=None,
|
|
178
|
+
help="where the drawn regions are kept (default: beside the CWD, never "
|
|
179
|
+
"beside the raw file — raw data is not ours to add files to)")
|
|
180
|
+
p.set_defaults(func=_view)
|
|
181
|
+
|
|
182
|
+
p = sub.add_parser("register",
|
|
183
|
+
help="motion-correct units against one shared reference (needs Suite2p)")
|
|
184
|
+
p.add_argument("source")
|
|
185
|
+
p.add_argument("out")
|
|
186
|
+
p.add_argument("--units", nargs="*", default=None,
|
|
187
|
+
help="which units; all of them by default. Name them when the file holds "
|
|
188
|
+
"more than one field — one reference across two fields is meaningless")
|
|
189
|
+
p.add_argument("--channel", type=int, default=0,
|
|
190
|
+
help="the channel registration is computed on; the others take its shifts")
|
|
191
|
+
p.add_argument("--reference-from", default=None,
|
|
192
|
+
help="anchor unit (default: the first one), so the reference is the state "
|
|
193
|
+
"the session started in rather than an average over it")
|
|
194
|
+
p.add_argument("--nonrigid", action="store_true",
|
|
195
|
+
help="also correct a smooth position-dependent warp")
|
|
196
|
+
p.add_argument("--block-size", type=int, default=128)
|
|
197
|
+
p.add_argument("--max-shift", type=float, default=0.1,
|
|
198
|
+
help="rigid cap, as a fraction of the frame (default: 0.1)")
|
|
199
|
+
p.add_argument("--max-shift-nr", type=float, default=5.0,
|
|
200
|
+
help="non-rigid cap in px; keep it small (default: 5)")
|
|
201
|
+
p.add_argument("--tag", default="_MC")
|
|
202
|
+
p.add_argument("--no-tag", action="store_true")
|
|
203
|
+
p.set_defaults(func=_register)
|
|
204
|
+
|
|
205
|
+
args = ap.parse_args(argv)
|
|
206
|
+
try:
|
|
207
|
+
return args.func(args)
|
|
208
|
+
except (MescIOError, FileNotFoundError, KeyError, ImportError, MemoryError) as exc:
|
|
209
|
+
print(f" {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Look a `.mesc` over before you trust it.
|
|
2
|
+
|
|
3
|
+
A file can be perfectly readable and still not be what you assume. These are the
|
|
4
|
+
disagreements worth knowing about before an analysis is built on top of one — each is
|
|
5
|
+
something a file has actually done, not a hypothetical:
|
|
6
|
+
|
|
7
|
+
* **units recorded at different frame rates.** Normal in a session where the field or the
|
|
8
|
+
zoom changed, and ruinous if you take one rate for the whole file. A z-stack saved
|
|
9
|
+
alongside the recordings reports a frame period that is not a frame rate at all.
|
|
10
|
+
* **units at different pixel sizes.** The zoom was changed between recordings, so distances
|
|
11
|
+
in one unit are not distances in another.
|
|
12
|
+
* **conversion attributes missing.** Then the values you read are the stored integers and the
|
|
13
|
+
package has nothing to convert them with — it says so rather than assuming a common offset.
|
|
14
|
+
* **channels with different conversions across units**, which means a per-file constant is
|
|
15
|
+
the wrong model for that file.
|
|
16
|
+
* **empty or single-frame units**, which are usually a snapshot or an aborted recording and
|
|
17
|
+
will surprise anything that expects a movie.
|
|
18
|
+
|
|
19
|
+
`check` reports; it never repairs. What is a problem depends on what you are doing, so the
|
|
20
|
+
severities are advisory and the report names the units so you can look.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections import Counter, defaultdict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Dict, List
|
|
27
|
+
|
|
28
|
+
from .reader import MescFile
|
|
29
|
+
|
|
30
|
+
__all__ = ["check", "Report", "Finding"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Finding:
|
|
35
|
+
level: str # "note" | "warn"
|
|
36
|
+
code: str
|
|
37
|
+
message: str
|
|
38
|
+
units: tuple = ()
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
where = f" [{', '.join(self.units[:6])}{'…' if len(self.units) > 6 else ''}]" if self.units else ""
|
|
42
|
+
return f"{self.level:>4}: {self.message}{where}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Report:
|
|
47
|
+
path: str
|
|
48
|
+
n_units: int
|
|
49
|
+
findings: List[Finding] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def ok(self) -> bool:
|
|
53
|
+
return not any(f.level == "warn" for f in self.findings)
|
|
54
|
+
|
|
55
|
+
def __str__(self) -> str:
|
|
56
|
+
head = f"{self.path}: {self.n_units} unit(s), {len(self.findings)} finding(s)"
|
|
57
|
+
return "\n".join([head] + [str(f) for f in self.findings])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _group(values: Dict[str, object]) -> Dict[object, List[str]]:
|
|
61
|
+
out = defaultdict(list)
|
|
62
|
+
for unit, v in values.items():
|
|
63
|
+
out[v].append(unit)
|
|
64
|
+
return dict(out)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check(path) -> Report:
|
|
68
|
+
"""Read a `.mesc`'s metadata and report what disagrees with what."""
|
|
69
|
+
with MescFile(path) as f:
|
|
70
|
+
units = f.units()
|
|
71
|
+
rep = Report(path=str(f.path.name), n_units=len(units))
|
|
72
|
+
|
|
73
|
+
rates = {u.path: (round(u.frame_rate_hz, 3) if u.frame_rate_hz else None) for u in units}
|
|
74
|
+
missing = [u for u, r in rates.items() if r is None]
|
|
75
|
+
if missing:
|
|
76
|
+
rep.findings.append(Finding(
|
|
77
|
+
"warn", "no-frame-rate",
|
|
78
|
+
f"{len(missing)} unit(s) do not state a frame rate", tuple(missing)))
|
|
79
|
+
present = {u: r for u, r in rates.items() if r is not None}
|
|
80
|
+
by_rate = _group(present)
|
|
81
|
+
if len(by_rate) > 1:
|
|
82
|
+
common = Counter(present.values()).most_common(1)[0][0]
|
|
83
|
+
odd = tuple(u for u, r in present.items() if r != common)
|
|
84
|
+
rep.findings.append(Finding(
|
|
85
|
+
"warn", "mixed-frame-rates",
|
|
86
|
+
f"{len(by_rate)} different frame rates in one file "
|
|
87
|
+
f"({', '.join(f'{r:g} Hz x{len(v)}' for r, v in sorted(by_rate.items()))}) — "
|
|
88
|
+
f"use each unit's own rate, never one for the file", odd))
|
|
89
|
+
|
|
90
|
+
sizes = {u.path: u.pixel_size_um for u in units if u.pixel_size_um}
|
|
91
|
+
by_size = _group(sizes)
|
|
92
|
+
if len(by_size) > 1:
|
|
93
|
+
rep.findings.append(Finding(
|
|
94
|
+
"warn", "mixed-pixel-sizes",
|
|
95
|
+
f"{len(by_size)} different pixel sizes "
|
|
96
|
+
f"({', '.join(f'{s:.4f} um x{len(v)}' for s, v in sorted(by_size.items()))}) — "
|
|
97
|
+
"distances are not comparable across these units", ()))
|
|
98
|
+
if len(sizes) < len(units):
|
|
99
|
+
rep.findings.append(Finding(
|
|
100
|
+
"note", "no-pixel-size",
|
|
101
|
+
f"{len(units) - len(sizes)} unit(s) do not state a pixel size",
|
|
102
|
+
tuple(u.path for u in units if not u.pixel_size_um)))
|
|
103
|
+
|
|
104
|
+
shapes = _group({u.path: (u.height, u.width) for u in units})
|
|
105
|
+
if len(shapes) > 1:
|
|
106
|
+
rep.findings.append(Finding(
|
|
107
|
+
"note", "mixed-frame-shapes",
|
|
108
|
+
f"{len(shapes)} different frame shapes "
|
|
109
|
+
f"({', '.join(f'{h}x{w}' for h, w in sorted(shapes))})", ()))
|
|
110
|
+
|
|
111
|
+
thin = tuple(u.path for u in units if u.n_frames <= 1)
|
|
112
|
+
if thin:
|
|
113
|
+
rep.findings.append(Finding(
|
|
114
|
+
"note", "not-a-movie",
|
|
115
|
+
f"{len(thin)} unit(s) hold one frame or none — a snapshot or an aborted "
|
|
116
|
+
"recording", thin))
|
|
117
|
+
|
|
118
|
+
no_conv, per_channel = [], defaultdict(dict)
|
|
119
|
+
for u in units:
|
|
120
|
+
for c in u.channels:
|
|
121
|
+
per_channel[c.name][u.path] = (c.offset, c.scale)
|
|
122
|
+
if c.offset == 0.0 and c.scale == 1.0:
|
|
123
|
+
no_conv.append(f"{u.path}/{c.name}")
|
|
124
|
+
if no_conv:
|
|
125
|
+
rep.findings.append(Finding(
|
|
126
|
+
"warn", "no-conversion",
|
|
127
|
+
f"{len(no_conv)} channel(s) carry no conversion attributes — their values "
|
|
128
|
+
"cannot be put in reader units and come back as stored", tuple(no_conv)))
|
|
129
|
+
for name, per_unit in sorted(per_channel.items()):
|
|
130
|
+
distinct = set(per_unit.values())
|
|
131
|
+
if len(distinct) > 1:
|
|
132
|
+
rep.findings.append(Finding(
|
|
133
|
+
"warn", "mixed-conversion",
|
|
134
|
+
f"{name} does not use one conversion across the file "
|
|
135
|
+
f"({len(distinct)} different) — convert per unit, never per file", ()))
|
|
136
|
+
|
|
137
|
+
counts = Counter(len(u.channels) for u in units)
|
|
138
|
+
if len(counts) > 1:
|
|
139
|
+
rep.findings.append(Finding(
|
|
140
|
+
"note", "mixed-channel-counts",
|
|
141
|
+
f"units carry different channel counts "
|
|
142
|
+
f"({', '.join(f'{n} ch x{k}' for n, k in sorted(counts.items()))})", ()))
|
|
143
|
+
return rep
|