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,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
postprocessing utilities for caiman results.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_ops(ops_input) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
load ops from file or return dict as-is.
|
|
14
|
+
|
|
15
|
+
parameters
|
|
16
|
+
----------
|
|
17
|
+
ops_input : str, Path, dict, or None
|
|
18
|
+
path to ops.npy file, dictionary, or None.
|
|
19
|
+
|
|
20
|
+
returns
|
|
21
|
+
-------
|
|
22
|
+
dict
|
|
23
|
+
ops dictionary.
|
|
24
|
+
"""
|
|
25
|
+
if ops_input is None:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
if isinstance(ops_input, dict):
|
|
29
|
+
return ops_input.copy()
|
|
30
|
+
|
|
31
|
+
if isinstance(ops_input, (str, Path)):
|
|
32
|
+
ops_path = Path(ops_input)
|
|
33
|
+
|
|
34
|
+
# handle directory input
|
|
35
|
+
if ops_path.is_dir():
|
|
36
|
+
ops_path = ops_path / "ops.npy"
|
|
37
|
+
|
|
38
|
+
if ops_path.exists():
|
|
39
|
+
ops = np.load(ops_path, allow_pickle=True)
|
|
40
|
+
if isinstance(ops, np.ndarray):
|
|
41
|
+
ops = ops.item()
|
|
42
|
+
return dict(ops)
|
|
43
|
+
else:
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_planar_results(plane_dir) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
load all results from a plane directory.
|
|
52
|
+
|
|
53
|
+
parameters
|
|
54
|
+
----------
|
|
55
|
+
plane_dir : str or Path
|
|
56
|
+
path to plane output directory.
|
|
57
|
+
|
|
58
|
+
returns
|
|
59
|
+
-------
|
|
60
|
+
dict
|
|
61
|
+
dictionary containing ops, estimates, F, dff, etc.
|
|
62
|
+
"""
|
|
63
|
+
plane_dir = Path(plane_dir)
|
|
64
|
+
results = {}
|
|
65
|
+
|
|
66
|
+
# load ops
|
|
67
|
+
ops_file = plane_dir / "ops.npy"
|
|
68
|
+
if ops_file.exists():
|
|
69
|
+
results["ops"] = load_ops(ops_file)
|
|
70
|
+
|
|
71
|
+
# load estimates
|
|
72
|
+
estimates_file = plane_dir / "estimates.npy"
|
|
73
|
+
if estimates_file.exists():
|
|
74
|
+
results["estimates"] = np.load(estimates_file, allow_pickle=True).item()
|
|
75
|
+
|
|
76
|
+
# load fluorescence
|
|
77
|
+
F_file = plane_dir / "F.npy"
|
|
78
|
+
if F_file.exists():
|
|
79
|
+
results["F"] = np.load(F_file)
|
|
80
|
+
|
|
81
|
+
# load dff
|
|
82
|
+
dff_file = plane_dir / "dff.npy"
|
|
83
|
+
if dff_file.exists():
|
|
84
|
+
results["dff"] = np.load(dff_file)
|
|
85
|
+
|
|
86
|
+
# load spikes
|
|
87
|
+
spks_file = plane_dir / "spks.npy"
|
|
88
|
+
if spks_file.exists():
|
|
89
|
+
results["spks"] = np.load(spks_file)
|
|
90
|
+
|
|
91
|
+
# load motion correction results
|
|
92
|
+
shifts_file = plane_dir / "mcorr_shifts.npy"
|
|
93
|
+
if shifts_file.exists():
|
|
94
|
+
results["shifts"] = np.load(shifts_file, allow_pickle=True)
|
|
95
|
+
|
|
96
|
+
template_file = plane_dir / "mcorr_template.npy"
|
|
97
|
+
if template_file.exists():
|
|
98
|
+
results["template"] = np.load(template_file)
|
|
99
|
+
|
|
100
|
+
return results
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def dff_rolling_percentile(
|
|
104
|
+
F: np.ndarray,
|
|
105
|
+
window_size: int = None,
|
|
106
|
+
percentile: int = 20,
|
|
107
|
+
smooth_window: int = None,
|
|
108
|
+
fs: float = 30.0,
|
|
109
|
+
tau: float = 1.0,
|
|
110
|
+
) -> np.ndarray:
|
|
111
|
+
"""
|
|
112
|
+
compute dF/F using rolling percentile baseline.
|
|
113
|
+
|
|
114
|
+
parameters
|
|
115
|
+
----------
|
|
116
|
+
F : np.ndarray
|
|
117
|
+
fluorescence traces, shape (n_cells, n_frames).
|
|
118
|
+
window_size : int, optional
|
|
119
|
+
frames for rolling percentile. default: ~10*tau*fs.
|
|
120
|
+
percentile : int, default 20
|
|
121
|
+
percentile for baseline F0.
|
|
122
|
+
smooth_window : int, optional
|
|
123
|
+
smoothing window for dF/F.
|
|
124
|
+
fs : float, default 30.0
|
|
125
|
+
frame rate in Hz.
|
|
126
|
+
tau : float, default 1.0
|
|
127
|
+
decay time constant in seconds.
|
|
128
|
+
|
|
129
|
+
returns
|
|
130
|
+
-------
|
|
131
|
+
np.ndarray
|
|
132
|
+
dF/F traces, same shape as F.
|
|
133
|
+
"""
|
|
134
|
+
if F is None or F.size == 0:
|
|
135
|
+
return np.array([])
|
|
136
|
+
|
|
137
|
+
# ensure 2d
|
|
138
|
+
if F.ndim == 1:
|
|
139
|
+
F = F[np.newaxis, :]
|
|
140
|
+
|
|
141
|
+
n_cells, n_frames = F.shape
|
|
142
|
+
|
|
143
|
+
# auto-calculate window size
|
|
144
|
+
if window_size is None:
|
|
145
|
+
window_size = int(10 * tau * fs)
|
|
146
|
+
window_size = max(10, min(window_size, n_frames // 2))
|
|
147
|
+
|
|
148
|
+
# compute baseline using rolling percentile
|
|
149
|
+
from scipy.ndimage import percentile_filter
|
|
150
|
+
|
|
151
|
+
F0 = np.zeros_like(F)
|
|
152
|
+
for i in range(n_cells):
|
|
153
|
+
F0[i] = percentile_filter(F[i], percentile, size=window_size)
|
|
154
|
+
|
|
155
|
+
# avoid division by zero
|
|
156
|
+
F0 = np.maximum(F0, 1e-6)
|
|
157
|
+
|
|
158
|
+
# compute dF/F
|
|
159
|
+
dff = (F - F0) / F0
|
|
160
|
+
|
|
161
|
+
# optional smoothing
|
|
162
|
+
if smooth_window is not None and smooth_window > 1:
|
|
163
|
+
from scipy.ndimage import uniform_filter1d
|
|
164
|
+
dff = uniform_filter1d(dff, size=smooth_window, axis=1)
|
|
165
|
+
|
|
166
|
+
return dff
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def compute_roi_stats(plane_dir) -> dict:
|
|
170
|
+
"""
|
|
171
|
+
compute roi quality statistics.
|
|
172
|
+
|
|
173
|
+
parameters
|
|
174
|
+
----------
|
|
175
|
+
plane_dir : str or Path
|
|
176
|
+
path to plane output directory.
|
|
177
|
+
|
|
178
|
+
returns
|
|
179
|
+
-------
|
|
180
|
+
dict
|
|
181
|
+
dictionary of roi statistics.
|
|
182
|
+
"""
|
|
183
|
+
plane_dir = Path(plane_dir)
|
|
184
|
+
results = load_planar_results(plane_dir)
|
|
185
|
+
|
|
186
|
+
stats = {}
|
|
187
|
+
|
|
188
|
+
# get estimates
|
|
189
|
+
estimates = results.get("estimates", {})
|
|
190
|
+
A = estimates.get("A")
|
|
191
|
+
C = estimates.get("C")
|
|
192
|
+
SNR = estimates.get("SNR_comp")
|
|
193
|
+
r_values = estimates.get("r_values")
|
|
194
|
+
|
|
195
|
+
if A is not None:
|
|
196
|
+
from scipy import sparse
|
|
197
|
+
|
|
198
|
+
n_cells = A.shape[1]
|
|
199
|
+
stats["n_cells"] = n_cells
|
|
200
|
+
|
|
201
|
+
# compute cell sizes using sparse ops to avoid memory explosion
|
|
202
|
+
if sparse.issparse(A):
|
|
203
|
+
cell_sizes = np.array((A > 0).sum(axis=0)).ravel()
|
|
204
|
+
else:
|
|
205
|
+
cell_sizes = np.array([(A[:, i] > 0).sum() for i in range(n_cells)])
|
|
206
|
+
stats["cell_sizes"] = cell_sizes
|
|
207
|
+
stats["mean_cell_size"] = float(np.mean(cell_sizes))
|
|
208
|
+
stats["median_cell_size"] = float(np.median(cell_sizes))
|
|
209
|
+
|
|
210
|
+
if C is not None:
|
|
211
|
+
n_cells, n_frames = C.shape
|
|
212
|
+
stats["n_frames"] = n_frames
|
|
213
|
+
|
|
214
|
+
# compute signal statistics
|
|
215
|
+
stats["mean_signal"] = float(np.mean(C))
|
|
216
|
+
stats["max_signal"] = float(np.max(C))
|
|
217
|
+
|
|
218
|
+
if SNR is not None:
|
|
219
|
+
stats["snr"] = SNR
|
|
220
|
+
stats["mean_snr"] = float(np.mean(SNR))
|
|
221
|
+
stats["median_snr"] = float(np.median(SNR))
|
|
222
|
+
|
|
223
|
+
if r_values is not None:
|
|
224
|
+
stats["r_values"] = r_values
|
|
225
|
+
stats["mean_r_value"] = float(np.mean(r_values))
|
|
226
|
+
|
|
227
|
+
# save stats
|
|
228
|
+
np.save(plane_dir / "roi_stats.npy", stats)
|
|
229
|
+
|
|
230
|
+
return stats
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_accepted_cells(plane_dir) -> tuple:
|
|
234
|
+
"""
|
|
235
|
+
get indices of accepted and rejected cells.
|
|
236
|
+
|
|
237
|
+
parameters
|
|
238
|
+
----------
|
|
239
|
+
plane_dir : str or Path
|
|
240
|
+
path to plane output directory.
|
|
241
|
+
|
|
242
|
+
returns
|
|
243
|
+
-------
|
|
244
|
+
tuple
|
|
245
|
+
(accepted_indices, rejected_indices)
|
|
246
|
+
"""
|
|
247
|
+
plane_dir = Path(plane_dir)
|
|
248
|
+
estimates_file = plane_dir / "estimates.npy"
|
|
249
|
+
|
|
250
|
+
if not estimates_file.exists():
|
|
251
|
+
return np.array([]), np.array([])
|
|
252
|
+
|
|
253
|
+
estimates = np.load(estimates_file, allow_pickle=True).item()
|
|
254
|
+
|
|
255
|
+
accepted = estimates.get("idx_components")
|
|
256
|
+
rejected = estimates.get("idx_components_bad")
|
|
257
|
+
|
|
258
|
+
if accepted is None:
|
|
259
|
+
# if no evaluation done, accept all
|
|
260
|
+
A = estimates.get("A")
|
|
261
|
+
if A is not None:
|
|
262
|
+
accepted = np.arange(A.shape[1])
|
|
263
|
+
else:
|
|
264
|
+
accepted = np.array([])
|
|
265
|
+
|
|
266
|
+
if rejected is None:
|
|
267
|
+
rejected = np.array([])
|
|
268
|
+
|
|
269
|
+
return np.asarray(accepted), np.asarray(rejected)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_contours(plane_dir, threshold: float = 0.5) -> list:
|
|
273
|
+
"""
|
|
274
|
+
get cell contours for visualization.
|
|
275
|
+
|
|
276
|
+
parameters
|
|
277
|
+
----------
|
|
278
|
+
plane_dir : str or Path
|
|
279
|
+
path to plane output directory.
|
|
280
|
+
threshold : float, default 0.5
|
|
281
|
+
threshold for contour extraction (fraction of max).
|
|
282
|
+
|
|
283
|
+
returns
|
|
284
|
+
-------
|
|
285
|
+
list
|
|
286
|
+
list of contour coordinates for each cell.
|
|
287
|
+
"""
|
|
288
|
+
plane_dir = Path(plane_dir)
|
|
289
|
+
results = load_planar_results(plane_dir)
|
|
290
|
+
|
|
291
|
+
estimates = results.get("estimates", {})
|
|
292
|
+
ops = results.get("ops", {})
|
|
293
|
+
|
|
294
|
+
A = estimates.get("A")
|
|
295
|
+
if A is None:
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
from scipy import sparse
|
|
299
|
+
if sparse.issparse(A):
|
|
300
|
+
A = A.toarray()
|
|
301
|
+
|
|
302
|
+
Ly = ops.get("Ly", int(np.sqrt(A.shape[0])))
|
|
303
|
+
Lx = ops.get("Lx", int(np.sqrt(A.shape[0])))
|
|
304
|
+
|
|
305
|
+
contours = []
|
|
306
|
+
for i in range(A.shape[1]):
|
|
307
|
+
component = A[:, i].reshape((Ly, Lx), order="F")
|
|
308
|
+
|
|
309
|
+
# find contour
|
|
310
|
+
from skimage import measure
|
|
311
|
+
thresh = component.max() * threshold
|
|
312
|
+
contour_list = measure.find_contours(component, thresh)
|
|
313
|
+
|
|
314
|
+
if contour_list:
|
|
315
|
+
contours.append(contour_list[0])
|
|
316
|
+
else:
|
|
317
|
+
contours.append(np.array([]))
|
|
318
|
+
|
|
319
|
+
return contours
|