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.
@@ -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