lbm_caiman_python 0.2.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.
- lbm_caiman_python/__init__.py +63 -0
- lbm_caiman_python/__main__.py +302 -0
- lbm_caiman_python/_version.py +8 -0
- lbm_caiman_python/batch.py +188 -0
- lbm_caiman_python/collation.py +125 -0
- lbm_caiman_python/default_ops.py +92 -0
- lbm_caiman_python/gui/__init__.py +3 -0
- lbm_caiman_python/gui/_store_model.py +170 -0
- lbm_caiman_python/gui/rungui.py +13 -0
- lbm_caiman_python/gui/widgets.py +114 -0
- lbm_caiman_python/helpers.py +262 -0
- lbm_caiman_python/postprocessing.py +319 -0
- lbm_caiman_python/run_lcp.py +1059 -0
- lbm_caiman_python/stdout.py +3 -0
- lbm_caiman_python/summary.py +569 -0
- lbm_caiman_python/util/__init__.py +87 -0
- lbm_caiman_python/util/exceptions.py +6 -0
- lbm_caiman_python/util/quality.py +366 -0
- lbm_caiman_python/util/signal.py +17 -0
- lbm_caiman_python/util/transform.py +208 -0
- lbm_caiman_python/visualize.py +522 -0
- lbm_caiman_python-0.2.0.dist-info/METADATA +161 -0
- lbm_caiman_python-0.2.0.dist-info/RECORD +27 -0
- lbm_caiman_python-0.2.0.dist-info/WHEEL +5 -0
- lbm_caiman_python-0.2.0.dist-info/entry_points.txt +2 -0
- lbm_caiman_python-0.2.0.dist-info/licenses/LICENSE.md +38 -0
- lbm_caiman_python-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from . import _version
|
|
2
|
+
|
|
3
|
+
# core pipeline api
|
|
4
|
+
from .default_ops import default_ops, mcorr_ops, cnmf_ops
|
|
5
|
+
from .run_lcp import (
|
|
6
|
+
pipeline,
|
|
7
|
+
run_volume,
|
|
8
|
+
run_plane,
|
|
9
|
+
add_processing_step,
|
|
10
|
+
generate_plane_dirname,
|
|
11
|
+
)
|
|
12
|
+
from .postprocessing import (
|
|
13
|
+
load_ops,
|
|
14
|
+
load_planar_results,
|
|
15
|
+
dff_rolling_percentile,
|
|
16
|
+
compute_roi_stats,
|
|
17
|
+
get_accepted_cells,
|
|
18
|
+
get_contours,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# existing utilities
|
|
22
|
+
from .collation import combine_z_planes
|
|
23
|
+
from .util.transform import vectorize, unvectorize, calculate_centers
|
|
24
|
+
from .util.quality import get_noise_fft, greedyROI
|
|
25
|
+
from .util.signal import smooth_data, norm_minmax
|
|
26
|
+
from .helpers import (
|
|
27
|
+
generate_patch_view,
|
|
28
|
+
get_single_patch_coords,
|
|
29
|
+
extract_center_square,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = _version.get_versions()['version']
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# core pipeline
|
|
36
|
+
"pipeline",
|
|
37
|
+
"run_volume",
|
|
38
|
+
"run_plane",
|
|
39
|
+
"default_ops",
|
|
40
|
+
"mcorr_ops",
|
|
41
|
+
"cnmf_ops",
|
|
42
|
+
"add_processing_step",
|
|
43
|
+
"generate_plane_dirname",
|
|
44
|
+
|
|
45
|
+
# postprocessing
|
|
46
|
+
"load_ops",
|
|
47
|
+
"load_planar_results",
|
|
48
|
+
"dff_rolling_percentile",
|
|
49
|
+
"compute_roi_stats",
|
|
50
|
+
"get_accepted_cells",
|
|
51
|
+
"get_contours",
|
|
52
|
+
|
|
53
|
+
# existing utilities
|
|
54
|
+
"combine_z_planes",
|
|
55
|
+
"generate_patch_view",
|
|
56
|
+
"get_noise_fft",
|
|
57
|
+
"calculate_centers",
|
|
58
|
+
"greedyROI",
|
|
59
|
+
"get_single_patch_coords",
|
|
60
|
+
"extract_center_square",
|
|
61
|
+
"smooth_data",
|
|
62
|
+
"norm_minmax",
|
|
63
|
+
]
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli entry point for lbm-caiman-python.
|
|
3
|
+
|
|
4
|
+
usage:
|
|
5
|
+
lcp <input> <output> [options]
|
|
6
|
+
python -m lbm_caiman_python <input> <output> [options]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from functools import partial
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
print = partial(print, flush=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_args(parser: argparse.ArgumentParser):
|
|
18
|
+
"""add command-line arguments to the parser."""
|
|
19
|
+
from lbm_caiman_python.default_ops import default_ops
|
|
20
|
+
|
|
21
|
+
defaults = default_ops()
|
|
22
|
+
|
|
23
|
+
# positional arguments
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"input",
|
|
26
|
+
type=str,
|
|
27
|
+
nargs="?",
|
|
28
|
+
default=None,
|
|
29
|
+
help="Input file or directory",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"output",
|
|
33
|
+
type=str,
|
|
34
|
+
nargs="?",
|
|
35
|
+
default=None,
|
|
36
|
+
help="Output directory",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# processing flags
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--planes",
|
|
42
|
+
type=int,
|
|
43
|
+
nargs="+",
|
|
44
|
+
default=None,
|
|
45
|
+
help="Planes to process (1-based)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--no-mcorr",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Skip motion correction",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--no-cnmf",
|
|
54
|
+
action="store_true",
|
|
55
|
+
help="Skip CNMF",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--force-mcorr",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Force re-run motion correction",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--force-cnmf",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Force re-run CNMF",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--num-frames",
|
|
69
|
+
type=int,
|
|
70
|
+
default=None,
|
|
71
|
+
help="Limit number of frames to process",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# motion correction parameters
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--max-shifts",
|
|
77
|
+
type=int,
|
|
78
|
+
nargs=2,
|
|
79
|
+
default=None,
|
|
80
|
+
help=f"Maximum rigid shifts (default: {defaults['max_shifts']})",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--strides",
|
|
84
|
+
type=int,
|
|
85
|
+
nargs=2,
|
|
86
|
+
default=None,
|
|
87
|
+
help=f"Patch strides for piecewise-rigid (default: {defaults['strides']})",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--overlaps",
|
|
91
|
+
type=int,
|
|
92
|
+
nargs=2,
|
|
93
|
+
default=None,
|
|
94
|
+
help=f"Patch overlaps (default: {defaults['overlaps']})",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--gSig-filt",
|
|
98
|
+
type=int,
|
|
99
|
+
nargs=2,
|
|
100
|
+
default=None,
|
|
101
|
+
help=f"Gaussian filter size (default: {defaults['gSig_filt']})",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--no-pw-rigid",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="Disable piecewise-rigid correction (use rigid only)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# cnmf parameters
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--K",
|
|
112
|
+
type=int,
|
|
113
|
+
default=None,
|
|
114
|
+
help=f"Expected number of neurons (default: {defaults['K']})",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--gSig",
|
|
118
|
+
type=int,
|
|
119
|
+
nargs=2,
|
|
120
|
+
default=None,
|
|
121
|
+
help=f"Expected neuron half-width (default: {defaults['gSig']})",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--min-SNR",
|
|
125
|
+
type=float,
|
|
126
|
+
default=None,
|
|
127
|
+
help=f"Minimum SNR threshold (default: {defaults['min_SNR']})",
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--rval-thr",
|
|
131
|
+
type=float,
|
|
132
|
+
default=None,
|
|
133
|
+
help=f"Correlation threshold (default: {defaults['rval_thr']})",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--merge-thresh",
|
|
137
|
+
type=float,
|
|
138
|
+
default=None,
|
|
139
|
+
help=f"Merging threshold (default: {defaults['merge_thresh']})",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# general parameters
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--fr",
|
|
145
|
+
type=float,
|
|
146
|
+
default=None,
|
|
147
|
+
help=f"Frame rate in Hz (default: {defaults['fr']})",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--n-processes",
|
|
151
|
+
type=int,
|
|
152
|
+
default=None,
|
|
153
|
+
help="Number of parallel processes (default: auto)",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# utility flags
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--version",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Show version information",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--debug",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Enable debug mode",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return parser
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_ops_from_args(args) -> dict:
|
|
172
|
+
"""build ops dictionary from command-line arguments."""
|
|
173
|
+
from lbm_caiman_python.default_ops import default_ops
|
|
174
|
+
|
|
175
|
+
ops = default_ops()
|
|
176
|
+
|
|
177
|
+
# map cli args to ops keys
|
|
178
|
+
arg_to_ops = {
|
|
179
|
+
"max_shifts": "max_shifts",
|
|
180
|
+
"strides": "strides",
|
|
181
|
+
"overlaps": "overlaps",
|
|
182
|
+
"gSig_filt": "gSig_filt",
|
|
183
|
+
"K": "K",
|
|
184
|
+
"gSig": "gSig",
|
|
185
|
+
"min_SNR": "min_SNR",
|
|
186
|
+
"rval_thr": "rval_thr",
|
|
187
|
+
"merge_thresh": "merge_thresh",
|
|
188
|
+
"fr": "fr",
|
|
189
|
+
"n_processes": "n_processes",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for arg_name, ops_key in arg_to_ops.items():
|
|
193
|
+
value = getattr(args, arg_name.replace("-", "_"), None)
|
|
194
|
+
if value is not None:
|
|
195
|
+
if isinstance(value, list) and len(value) == 2:
|
|
196
|
+
ops[ops_key] = tuple(value)
|
|
197
|
+
else:
|
|
198
|
+
ops[ops_key] = value
|
|
199
|
+
|
|
200
|
+
# handle boolean flags
|
|
201
|
+
if args.no_mcorr:
|
|
202
|
+
ops["do_motion_correction"] = False
|
|
203
|
+
if args.no_cnmf:
|
|
204
|
+
ops["do_cnmf"] = False
|
|
205
|
+
if args.no_pw_rigid:
|
|
206
|
+
ops["pw_rigid"] = False
|
|
207
|
+
|
|
208
|
+
return ops
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main():
|
|
212
|
+
"""main cli entry point."""
|
|
213
|
+
print("\n")
|
|
214
|
+
print("--- LBM-CaImAn Pipeline ---")
|
|
215
|
+
print("\n")
|
|
216
|
+
|
|
217
|
+
parser = argparse.ArgumentParser(
|
|
218
|
+
description="LBM-CaImAn processing pipeline",
|
|
219
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
220
|
+
epilog="""
|
|
221
|
+
examples:
|
|
222
|
+
lcp data.tiff output/
|
|
223
|
+
lcp data/ results/ --planes 1 2 3
|
|
224
|
+
lcp movie.tif results/ --K 100 --gSig 5 5
|
|
225
|
+
lcp data.tiff output/ --no-mcorr # skip motion correction
|
|
226
|
+
""",
|
|
227
|
+
)
|
|
228
|
+
parser = add_args(parser)
|
|
229
|
+
args = parser.parse_args()
|
|
230
|
+
|
|
231
|
+
# handle version
|
|
232
|
+
if args.version:
|
|
233
|
+
import lbm_caiman_python
|
|
234
|
+
print(f"lbm_caiman_python v{lbm_caiman_python.__version__}")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# check required arguments
|
|
238
|
+
if args.input is None:
|
|
239
|
+
parser.print_help()
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# setup logging
|
|
243
|
+
if args.debug:
|
|
244
|
+
import logging
|
|
245
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
246
|
+
print("Debug mode enabled.")
|
|
247
|
+
|
|
248
|
+
# validate input
|
|
249
|
+
input_path = Path(args.input).expanduser().resolve()
|
|
250
|
+
if not input_path.exists():
|
|
251
|
+
print(f"Error: Input path does not exist: {input_path}")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
# setup output
|
|
255
|
+
if args.output is None:
|
|
256
|
+
output_path = input_path.parent / (input_path.stem + "_caiman_results")
|
|
257
|
+
else:
|
|
258
|
+
output_path = Path(args.output).expanduser().resolve()
|
|
259
|
+
|
|
260
|
+
print(f"Input: {input_path}")
|
|
261
|
+
print(f"Output: {output_path}")
|
|
262
|
+
|
|
263
|
+
# build ops from arguments
|
|
264
|
+
ops = build_ops_from_args(args)
|
|
265
|
+
|
|
266
|
+
# run pipeline
|
|
267
|
+
from lbm_caiman_python.run_lcp import pipeline
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
results = pipeline(
|
|
271
|
+
input_data=input_path,
|
|
272
|
+
save_path=str(output_path),
|
|
273
|
+
ops=ops,
|
|
274
|
+
planes=args.planes,
|
|
275
|
+
force_mcorr=args.force_mcorr,
|
|
276
|
+
force_cnmf=args.force_cnmf,
|
|
277
|
+
num_timepoints=args.num_frames,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
print("\n")
|
|
281
|
+
print("--- Processing Complete ---")
|
|
282
|
+
print(f"Results saved to: {output_path}")
|
|
283
|
+
print(f"Processed {len(results)} plane(s)")
|
|
284
|
+
|
|
285
|
+
# print summary
|
|
286
|
+
for ops_path in results:
|
|
287
|
+
from lbm_caiman_python.postprocessing import load_ops
|
|
288
|
+
ops_result = load_ops(ops_path)
|
|
289
|
+
plane = ops_result.get("plane", "?")
|
|
290
|
+
n_cells = ops_result.get("n_cells", 0)
|
|
291
|
+
print(f" Plane {plane}: {n_cells} cells")
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
print(f"\nError: {e}")
|
|
295
|
+
if args.debug:
|
|
296
|
+
import traceback
|
|
297
|
+
traceback.print_exc()
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import re as regex
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
import pathlib
|
|
6
|
+
import lbm_mc as mc
|
|
7
|
+
|
|
8
|
+
COMPUTE_BACKEND_SUBPROCESS = "subprocess" #: subprocess backend
|
|
9
|
+
COMPUTE_BACKEND_SLURM = "slurm" #: SLURM backend
|
|
10
|
+
COMPUTE_BACKEND_LOCAL = "local"
|
|
11
|
+
|
|
12
|
+
COMPUTE_BACKENDS = [
|
|
13
|
+
COMPUTE_BACKEND_SUBPROCESS,
|
|
14
|
+
COMPUTE_BACKEND_SLURM,
|
|
15
|
+
COMPUTE_BACKEND_LOCAL,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
DATAFRAME_COLUMNS = [
|
|
19
|
+
"algo",
|
|
20
|
+
"item_name",
|
|
21
|
+
"input_movie_path",
|
|
22
|
+
"params",
|
|
23
|
+
"outputs",
|
|
24
|
+
"added_time",
|
|
25
|
+
"ran_time",
|
|
26
|
+
"algo_duration",
|
|
27
|
+
"comments",
|
|
28
|
+
"uuid",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def _set_posix_windows():
|
|
34
|
+
posix_backup = pathlib.PosixPath
|
|
35
|
+
try:
|
|
36
|
+
pathlib.PosixPath = pathlib.WindowsPath
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
pathlib.PosixPath = posix_backup
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def _set_windows_posix():
|
|
44
|
+
"""
|
|
45
|
+
Set the Path class to WindowsPath on a POSIX system.
|
|
46
|
+
"""
|
|
47
|
+
windows_backup = pathlib.WindowsPath
|
|
48
|
+
try:
|
|
49
|
+
pathlib.WindowsPath = pathlib.PosixPath
|
|
50
|
+
yield
|
|
51
|
+
finally:
|
|
52
|
+
pathlib.WindowsPath = windows_backup
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_batch(batch_path: str | Path):
|
|
56
|
+
"""
|
|
57
|
+
Load a batch after transfering it from a Windows to a POSIX system or vice versa.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
batch_path : str, Path
|
|
62
|
+
The path to the batch file.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
pandas.DataFrame
|
|
67
|
+
The loaded batch.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
with _set_windows_posix():
|
|
71
|
+
return mc.load_batch(batch_path)
|
|
72
|
+
except Exception:
|
|
73
|
+
with _set_posix_windows():
|
|
74
|
+
return mc.load_batch(batch_path)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clean_batch(df):
|
|
78
|
+
"""
|
|
79
|
+
Clean a batch of DataFrame entries by removing unsuccessful df from storage.
|
|
80
|
+
|
|
81
|
+
This function iterates over the df of the given DataFrame, identifies
|
|
82
|
+
df where the 'outputs' column is either `None` or a dictionary containing
|
|
83
|
+
a 'success' key with a `False` value. For each such row, the corresponding
|
|
84
|
+
item is removed using the `df.caiman.remove_item()` method, and the removal
|
|
85
|
+
is saved to disk.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
df : pandas.DataFrame
|
|
90
|
+
The DataFrame to be cleaned. It must have a 'uuid' column for identification
|
|
91
|
+
and an 'outputs' column containing a dictionary with a 'success' key.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
pandas.DataFrame
|
|
96
|
+
The DataFrame reloaded from disk after unsuccessful items have been removed.
|
|
97
|
+
|
|
98
|
+
Notes
|
|
99
|
+
-----
|
|
100
|
+
- If 'outputs' is None or does not contain 'success' as a key with a value of
|
|
101
|
+
`False`, the row will be removed.
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
>>> import pandas as pd
|
|
106
|
+
>>> df = pd.DataFrame({
|
|
107
|
+
... 'uuid': ['123', '456', '789'],
|
|
108
|
+
... 'outputs': [{'success': True}, {'success': False}, None]
|
|
109
|
+
... })
|
|
110
|
+
>>> cleaned_df = clean_batch(df)
|
|
111
|
+
Removing unsuccessful batch row 1.
|
|
112
|
+
Row 1 deleted.
|
|
113
|
+
Removing unsuccessful batch row 2.
|
|
114
|
+
Row 2 deleted.
|
|
115
|
+
"""
|
|
116
|
+
for index, row in df.iterrows():
|
|
117
|
+
# Check if 'outputs' is a dictionary and has 'success' key with value False
|
|
118
|
+
if isinstance(row["outputs"], dict) and row["outputs"].get("success") is False or row["outputs"] is None:
|
|
119
|
+
uuid = row["uuid"]
|
|
120
|
+
print(f"Removing unsuccessful batch row {row.index}.")
|
|
121
|
+
df.caiman.remove_item(uuid, remove_data=True, safe_removal=False)
|
|
122
|
+
print(f"Row {row.index} deleted.")
|
|
123
|
+
df.caiman.save_to_disk()
|
|
124
|
+
return df.caiman.reload_from_disk()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def delete_batch_rows(df, rows_delete, remove_data=False, safe_removal=True):
|
|
128
|
+
rows_delete = [rows_delete] if isinstance(rows_delete, int) else rows_delete
|
|
129
|
+
uuids_delete = [row.uuid for i, row in df.iterrows() if i in rows_delete]
|
|
130
|
+
for uuid in uuids_delete:
|
|
131
|
+
df.caiman.remove_item(uuid, remove_data=remove_data, safe_removal=safe_removal)
|
|
132
|
+
df.caiman.save_to_disk()
|
|
133
|
+
return df
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_path(path: Union[str, Path]):
|
|
137
|
+
if not regex.match("^[A-Za-z0-9@\/\\\:._-]*$", str(path)):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"Paths must only contain alphanumeric characters, "
|
|
140
|
+
"hyphens ( - ), underscores ( _ ) or periods ( . )"
|
|
141
|
+
)
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def drop_duplicates(df):
|
|
146
|
+
"""
|
|
147
|
+
Remove duplicate items from a batch DataFrame.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
df : pandas.DataFrame
|
|
152
|
+
The batch DataFrame to remove duplicates from.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
None
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
import hashlib
|
|
160
|
+
df["hash"] = df.apply(lambda row: hashlib.sha256(row.mcorr.get_output().tobytes()).hexdigest(), axis=1)
|
|
161
|
+
uuids_to_remove = []
|
|
162
|
+
for _, group in df.groupby("hash"):
|
|
163
|
+
if len(group) > 1:
|
|
164
|
+
for idx in group.index[1:]:
|
|
165
|
+
uuid = df.loc[idx, "uuid"]
|
|
166
|
+
uuids_to_remove.append(uuid)
|
|
167
|
+
if not uuids_to_remove:
|
|
168
|
+
print("No duplicates found.")
|
|
169
|
+
return
|
|
170
|
+
for uuid in uuids_to_remove:
|
|
171
|
+
print(f"Removing duplicate item {uuid}.")
|
|
172
|
+
df.caiman.remove_item(uuid, remove_data=True, safe_removal=False)
|
|
173
|
+
df.drop(columns="hash", inplace=True)
|
|
174
|
+
df.caiman.save_to_disk()
|
|
175
|
+
return df
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_batch_from_path(batch_path):
|
|
179
|
+
"""
|
|
180
|
+
Load or create a batch at the given batch_path.
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
df = mc.load_batch(batch_path)
|
|
184
|
+
print(f"Batch found at {batch_path}")
|
|
185
|
+
except (IsADirectoryError, FileNotFoundError):
|
|
186
|
+
print(f"Creating batch at {batch_path}")
|
|
187
|
+
df = mc.create_batch(batch_path)
|
|
188
|
+
return df
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.sparse import hstack
|
|
4
|
+
import copy
|
|
5
|
+
import scipy.signal
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def combine_z_planes(results: dict):
|
|
10
|
+
"""
|
|
11
|
+
Combines all z-planes in the results dictionary into a single estimates object.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
results (dict): Dictionary with estimates for each z-plane.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
estimates.Estimates: Combined estimates for all z-planes.
|
|
20
|
+
"""
|
|
21
|
+
from caiman.source_extraction.cnmf import estimates
|
|
22
|
+
keys = sorted(results.keys())
|
|
23
|
+
e_list = [results[k].estimates for k in keys]
|
|
24
|
+
|
|
25
|
+
# Initialize lists to collect components
|
|
26
|
+
A_list = []
|
|
27
|
+
b_list = []
|
|
28
|
+
C_list = []
|
|
29
|
+
f_list = []
|
|
30
|
+
R_list = []
|
|
31
|
+
|
|
32
|
+
for e in e_list:
|
|
33
|
+
A_list.append(e.A)
|
|
34
|
+
b_list.append(e.b)
|
|
35
|
+
C_list.append(e.C)
|
|
36
|
+
f_list.append(e.f)
|
|
37
|
+
R_list.append(e.R)
|
|
38
|
+
|
|
39
|
+
# Combine the components
|
|
40
|
+
A_new = hstack(A_list).tocsr()
|
|
41
|
+
b_new = np.concatenate(b_list, axis=0)
|
|
42
|
+
C_new = np.concatenate(C_list, axis=0)
|
|
43
|
+
f_new = np.concatenate(f_list, axis=0)
|
|
44
|
+
R_new = np.concatenate(R_list, axis=0)
|
|
45
|
+
|
|
46
|
+
# Assuming all z-planes have the same spatial dimensions
|
|
47
|
+
dims_new = e_list[0].dims # e.g., (height, width)
|
|
48
|
+
|
|
49
|
+
# Create new estimates object
|
|
50
|
+
e_new = estimates.Estimates(
|
|
51
|
+
A=A_new,
|
|
52
|
+
C=C_new,
|
|
53
|
+
b=b_new,
|
|
54
|
+
f=f_new,
|
|
55
|
+
R=R_new,
|
|
56
|
+
dims=dims_new
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return e_new
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def calculate_interplane_shifts(volume, n_planes, params, json_logger=None):
|
|
63
|
+
"""
|
|
64
|
+
"""
|
|
65
|
+
interplane_shifts = np.zeros((n_planes - 1, 2), dtype=int)
|
|
66
|
+
accumulated_shifts = np.zeros((n_planes - 1, 2), dtype=int)
|
|
67
|
+
|
|
68
|
+
for i_plane in range(n_planes - 1):
|
|
69
|
+
im1_copy = copy.deepcopy(volume[0, :, :, i_plane])
|
|
70
|
+
im2_copy = copy.deepcopy(volume[0, :, :, i_plane + 1])
|
|
71
|
+
|
|
72
|
+
# Removing NaNs
|
|
73
|
+
nonan_mask = np.stack(
|
|
74
|
+
(~np.isnan(im1_copy), ~np.isnan(im2_copy)), axis=0
|
|
75
|
+
)
|
|
76
|
+
nonan_mask = np.all(nonan_mask, axis=0)
|
|
77
|
+
coord_nonan_pixels = np.where(nonan_mask)
|
|
78
|
+
min_x, max_x = np.min(coord_nonan_pixels[0]), np.max(coord_nonan_pixels[0])
|
|
79
|
+
min_y, max_y = np.min(coord_nonan_pixels[1]), np.max(coord_nonan_pixels[1])
|
|
80
|
+
|
|
81
|
+
im1_nonan = im1_copy[min_x:max_x + 1, min_y:max_y + 1]
|
|
82
|
+
im2_nonan = im2_copy[min_x:max_x + 1, min_y:max_y + 1]
|
|
83
|
+
|
|
84
|
+
# Normalize intensities
|
|
85
|
+
im1_nonan -= np.min(im1_nonan)
|
|
86
|
+
im2_nonan -= np.min(im2_nonan)
|
|
87
|
+
|
|
88
|
+
# Cross-correlation
|
|
89
|
+
cross_corr_img = scipy.signal.fftconvolve(
|
|
90
|
+
im1_nonan, im2_nonan[::-1, ::-1], mode="same"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Find correlation peak
|
|
94
|
+
corr_img_peak_x, corr_img_peak_y = np.unravel_index(
|
|
95
|
+
np.argmax(cross_corr_img), cross_corr_img.shape
|
|
96
|
+
)
|
|
97
|
+
self_corr_peak_x, self_corr_peak_y = [
|
|
98
|
+
dim / 2 for dim in cross_corr_img.shape
|
|
99
|
+
]
|
|
100
|
+
interplane_shift = [
|
|
101
|
+
corr_img_peak_x - self_corr_peak_x,
|
|
102
|
+
corr_img_peak_y - self_corr_peak_y,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
interplane_shifts[i_plane] = copy.deepcopy(interplane_shift)
|
|
106
|
+
accumulated_shifts[i_plane] = np.sum(
|
|
107
|
+
interplane_shifts, axis=0, dtype=int
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Normalize shifts to start at zero
|
|
111
|
+
min_accumulated_shift = np.min(accumulated_shifts, axis=0)
|
|
112
|
+
for xy in range(2):
|
|
113
|
+
accumulated_shifts[:, xy] -= min_accumulated_shift[xy]
|
|
114
|
+
|
|
115
|
+
# Optional JSON logging
|
|
116
|
+
if params.get("json_logging") and json_logger:
|
|
117
|
+
json_logger.info(
|
|
118
|
+
json.dumps({"accumulated_shifts": accumulated_shifts.tolist()})
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return accumulated_shifts
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
pass
|