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,366 @@
1
+ import scipy
2
+ from scipy import signal
3
+ from sklearn.decomposition import NMF
4
+ import cv2
5
+ import numpy as np
6
+ from scipy.ndimage import correlate
7
+ import sys
8
+
9
+
10
+ def _mean_psd(y, method="logmexp"):
11
+ """
12
+ Averaging the PSD
13
+
14
+ Args:
15
+ y: np.ndarray
16
+ PSD values
17
+
18
+ method: string
19
+ method of averaging the noise.
20
+ Choices:
21
+ 'mean': Mean
22
+ 'median': Median
23
+ 'logmexp': Exponential of the mean of the logarithm of PSD (default)
24
+
25
+ Returns:
26
+ mp: array
27
+ mean psd
28
+ """
29
+
30
+ if method == "mean":
31
+ mp = np.sqrt(np.mean(y / 2, axis=-1))
32
+ elif method == "median":
33
+ mp = np.sqrt(np.median(y / 2, axis=-1))
34
+ else:
35
+ mp = np.log((y + 1e-10) / 2)
36
+ mp = np.mean(mp, axis=-1)
37
+ mp = np.exp(mp)
38
+ mp = np.sqrt(mp)
39
+
40
+ return mp
41
+
42
+
43
+ def get_noise_fft(
44
+ Y, noise_range=None, noise_method="logmexp", max_num_samples_fft=3072
45
+ ):
46
+ """
47
+ Compute the noise level in the Fourier domain for a given signal.
48
+
49
+ Parameters
50
+ ----------
51
+ Y : ndarray
52
+ Input data array. The last dimension is treated as time.
53
+ noise_range : list of float, optional
54
+ Frequency range to estimate noise, by default [0.25, 0.5].
55
+ noise_method : str, optional
56
+ Method to compute the mean noise power spectral density (PSD), by default "logmexp".
57
+ max_num_samples_fft : int, optional
58
+ Maximum number of samples to use for FFT computation, by default 3072.
59
+
60
+ Returns
61
+ -------
62
+ tuple
63
+ - sn : float or ndarray
64
+ Estimated noise level.
65
+ - psdx : ndarray
66
+ Power spectral density of the input data.
67
+ """
68
+ if noise_range is None:
69
+ noise_range = [0.25, 0.5]
70
+ T = Y.shape[-1]
71
+ # Y=np.array(Y,dtype=np.float64)
72
+
73
+ if T > max_num_samples_fft:
74
+ Y = np.concatenate(
75
+ (
76
+ Y[..., 1: max_num_samples_fft // 3 + 1],
77
+ Y[
78
+ ...,
79
+ int(T // 2 - max_num_samples_fft / 3 / 2): int(
80
+ T // 2 + max_num_samples_fft / 3 / 2
81
+ ),
82
+ ],
83
+ Y[..., -max_num_samples_fft // 3:],
84
+ ),
85
+ axis=-1,
86
+ )
87
+ T = np.shape(Y)[-1]
88
+
89
+ # we create a map of what is the noise on the FFT space
90
+ ff = np.arange(0, 0.5 + 1.0 / T, 1.0 / T)
91
+ ind1 = ff > noise_range[0]
92
+ ind2 = ff <= noise_range[1]
93
+ ind = np.logical_and(ind1, ind2)
94
+ # we compute the mean of the noise spectral density s
95
+ if Y.ndim > 1:
96
+ xdft = np.fft.rfft(Y, axis=-1)
97
+ xdft = xdft[..., ind[: xdft.shape[-1]]]
98
+ psdx = 1.0 / T * abs(xdft) ** 2
99
+ psdx *= 2
100
+ sn = _mean_psd(psdx, method=noise_method)
101
+
102
+ else:
103
+ xdft = np.fliplr(np.fft.rfft(Y))
104
+ psdx = 1.0 / T * (xdft ** 2)
105
+ psdx[1:] *= 2
106
+ sn = _mean_psd(psdx[ind[: psdx.shape[0]]], method=noise_method)
107
+
108
+ return sn, psdx
109
+
110
+
111
+ def _imblur(Y, sig=5, siz=11, nDimBlur=None, kernel=None, opencv=True):
112
+ """
113
+ Spatial filtering with a Gaussian or user defined kernel
114
+
115
+ The parameters are specified in GreedyROI
116
+
117
+ Args:
118
+ Y: np.ndarray
119
+ d1 x d2 [x d3] x T movie, raw data.
120
+
121
+ sig: [optional] list,tuple
122
+ half size of neurons
123
+
124
+ siz: [optional] list,tuple
125
+ size of filter kernel (default 2*sig + 1).
126
+
127
+ nDimBlur: [optional]
128
+ if you want to specify the number of dimension
129
+
130
+ kernel: [optional]
131
+ if you want to specify a kernel
132
+
133
+ opencv: [optional]
134
+ if you want to process to the blur using OpenCV method
135
+
136
+ Returns:
137
+ the blurred image
138
+ """
139
+ # TODO: document (jerem)
140
+ if kernel is None:
141
+ if nDimBlur is None:
142
+ nDimBlur = Y.ndim - 1
143
+ else:
144
+ nDimBlur = np.min((Y.ndim, nDimBlur))
145
+
146
+ if np.isscalar(sig):
147
+ sig = sig * np.ones(nDimBlur)
148
+
149
+ if np.isscalar(siz):
150
+ siz = siz * np.ones(nDimBlur)
151
+
152
+ X = Y.copy()
153
+ if opencv and nDimBlur == 2:
154
+ if X.ndim > 2:
155
+ # if we are on a video we repeat for each frame
156
+ for frame in range(X.shape[-1]):
157
+ if sys.version_info >= (3, 0):
158
+ X[:, :, frame] = cv2.GaussianBlur(X[:, :, frame], tuple(
159
+ siz), sig[0], None, sig[1], cv2.BORDER_CONSTANT)
160
+ else:
161
+ X[:, :, frame] = cv2.GaussianBlur(X[:, :, frame], tuple(siz), sig[
162
+ 0], sig[1], cv2.BORDER_CONSTANT, 0)
163
+
164
+ else:
165
+ if sys.version_info >= (3, 0):
166
+ X = cv2.GaussianBlur(
167
+ X, tuple(siz), sig[0], None, sig[1], cv2.BORDER_CONSTANT)
168
+ else:
169
+ X = cv2.GaussianBlur(
170
+ X, tuple(siz), sig[0], sig[1], cv2.BORDER_CONSTANT, 0)
171
+ else:
172
+ for i in range(nDimBlur):
173
+ h = np.exp(-np.arange(-np.floor(siz[i] / 2),
174
+ np.floor(siz[i] / 2) + 1) ** 2 / (2 * sig[i] ** 2))
175
+ h /= np.sqrt(h.dot(h))
176
+ shape = [1] * len(Y.shape)
177
+ shape[i] = -1
178
+ X = correlate(X, h.reshape(shape), mode='constant')
179
+
180
+ else:
181
+ X = correlate(Y, kernel[..., np.newaxis], mode='constant')
182
+ # for t in range(np.shape(Y)[-1]):
183
+ # X[:,:,t] = correlate(Y[:,:,t],kernel,mode='constant', cval=0.0)
184
+
185
+ return X
186
+
187
+
188
+ def finetune(Y, cin, nIter=5):
189
+ """compute a initialized version of A and C
190
+
191
+ Args:
192
+ Y: D1*d2*T*K patches
193
+
194
+ c: array T*K
195
+ the initial calcium traces
196
+
197
+ nIter: int
198
+ True indicates that time is listed in the last axis of Y (matlab format)
199
+ and moves it in the front
200
+
201
+ Returns:
202
+ a: array (d1,D2) the computed A as l2(Y*C)/Y*C
203
+
204
+ c: array(T) C as the sum of As on x*y axis
205
+ """
206
+ debug_ = False
207
+ if debug_:
208
+ import os
209
+ f = open('_LOG_1_' + str(os.getpid()), 'w+')
210
+ f.write('Y:' + str(np.mean(Y)) + '\n')
211
+ f.write('cin:' + str(np.mean(cin)) + '\n')
212
+ f.close()
213
+
214
+ # we compute the multiplication of patches per traces ( non negatively )
215
+ for _ in range(nIter):
216
+ a = np.maximum(np.dot(Y, cin), 0)
217
+ a = a / np.sqrt(np.sum(a ** 2) + np.finfo(np.float32).eps) # compute the l2/a
218
+ # c as the variation of those patches
219
+ cin = np.sum(Y * a[..., np.newaxis], tuple(np.arange(Y.ndim - 1)))
220
+
221
+ return a, cin
222
+
223
+
224
+ def greedyROI(Y, nr=30, gSig=[5, 5], gSiz=[11, 11], nIter=5, kernel=None, nb=1,
225
+ rolling_sum=False, rolling_length=100, seed_method='auto'):
226
+ """
227
+ Greedy initialization of spatial and temporal components using spatial Gaussian filtering
228
+
229
+ Args:
230
+ Y: np.array
231
+ 3d or 4d array of fluorescence data with time appearing in the last axis.
232
+
233
+ nr: int
234
+ number of components to be found
235
+
236
+ gSig: scalar or list of integers
237
+ standard deviation of Gaussian kernel along each axis
238
+
239
+ gSiz: scalar or list of integers
240
+ size of spatial component
241
+
242
+ nIter: int
243
+ number of iterations when refining estimates
244
+
245
+ kernel: np.ndarray
246
+ User specified kernel to be used, if present, instead of Gaussian (default None)
247
+
248
+ nb: int
249
+ Number of background components
250
+
251
+ rolling_max: boolean
252
+ Detect new components based on a rolling sum of pixel activity (default: True)
253
+
254
+ rolling_length: int
255
+ Length of rolling window (default: 100)
256
+
257
+ seed_method: str {'auto', 'manual', 'semi'}
258
+ methods for choosing seed pixels
259
+ 'semi' detects nr components automatically and allows to add more manually
260
+ if running as notebook 'semi' and 'manual' require a backend that does not
261
+ inline figures, e.g. %matplotlib tk
262
+
263
+ Returns:
264
+ A: np.array
265
+ 2d array of size (# of pixels) x nr with the spatial components. Each column is
266
+ ordered columnwise (matlab format, order='F')
267
+
268
+ C: np.array
269
+ 2d array of size nr X T with the temporal components
270
+
271
+ center: np.array
272
+ 2d array of size nr x 2 [ or 3] with the components centroids
273
+
274
+ Author:
275
+ Eftychios A. Pnevmatikakis and Andrea Giovannucci based on a matlab implementation by Yuanjun Gao
276
+ Simons Foundation, 2015
277
+
278
+ See Also:
279
+ http://www.cell.com/neuron/pdf/S0896-6273(15)01084-3.pdf
280
+
281
+
282
+ """
283
+ d = np.shape(Y)
284
+ Y[np.isnan(Y)] = 0
285
+ med = np.median(Y, axis=-1)
286
+ Y = Y - med[..., np.newaxis]
287
+ gHalf = np.array(gSiz) // 2
288
+ gSiz = 2 * gHalf + 1
289
+ # we initialize every values to zero
290
+ if seed_method.lower() == 'manual':
291
+ nr = 0
292
+ A = np.zeros((np.prod(d[0:-1]), nr), dtype=np.float32)
293
+ C = np.zeros((nr, d[-1]), dtype=np.float32)
294
+ center = np.zeros((nr, Y.ndim - 1), dtype='uint16')
295
+
296
+ rho = _imblur(Y, sig=gSig, siz=gSiz, nDimBlur=Y.ndim - 1, kernel=kernel)
297
+
298
+ if rolling_sum:
299
+ rolling_filter = np.ones(
300
+ (rolling_length), dtype=np.float32) / rolling_length
301
+ rho_s = scipy.signal.lfilter(rolling_filter, 1., rho ** 2)
302
+ return rho_s
303
+ v = np.amax(rho_s, axis=-1)
304
+ else:
305
+ v = np.sum(rho ** 2, axis=-1)
306
+
307
+ if seed_method.lower() != 'manual':
308
+ for k in range(nr):
309
+ # we take the highest value of the blurred total image and we define it as
310
+ # the center of the neuron
311
+ ind = np.argmax(v)
312
+ ij = np.unravel_index(ind, d[0:-1])
313
+ for c, i in enumerate(ij):
314
+ center[k, c] = i
315
+
316
+ # we define a squared size around it
317
+ ijSig = [[np.maximum(ij[c] - gHalf[c], 0), np.minimum(ij[c] + gHalf[c] + 1, d[c])]
318
+ for c in range(len(ij))]
319
+ # we create an array of it (fl like) and compute the trace like the pixel ij through time
320
+ dataTemp = np.array(
321
+ Y[tuple([slice(*a) for a in ijSig])].copy(), dtype=np.float32)
322
+ traceTemp = np.array(np.squeeze(rho[ij]), dtype=np.float32)
323
+
324
+ coef, score = finetune(dataTemp, traceTemp, nIter=nIter)
325
+ C[k, :] = np.squeeze(score)
326
+ dataSig = coef[..., np.newaxis] * \
327
+ score.reshape([1] * (Y.ndim - 1) + [-1])
328
+ xySig = np.meshgrid(*[np.arange(s[0], s[1])
329
+ for s in ijSig], indexing='xy')
330
+ arr = np.array([np.reshape(s, (1, np.size(s)), order='F').squeeze()
331
+ for s in xySig], dtype=int)
332
+ indices = np.ravel_multi_index(arr, d[0:-1], order='F')
333
+
334
+ A[indices, k] = np.reshape(
335
+ coef, (1, np.size(coef)), order='C').squeeze()
336
+ Y[tuple([slice(*a) for a in ijSig])] -= dataSig.copy()
337
+ if k < nr - 1 or seed_method.lower() != 'auto':
338
+ Mod = [[np.maximum(ij[c] - 2 * gHalf[c], 0),
339
+ np.minimum(ij[c] + 2 * gHalf[c] + 1, d[c])] for c in range(len(ij))]
340
+ ModLen = [m[1] - m[0] for m in Mod]
341
+ Lag = [ijSig[c] - Mod[c][0] for c in range(len(ij))]
342
+ dataTemp = np.zeros(ModLen)
343
+ dataTemp[tuple([slice(*a) for a in Lag])] = coef
344
+ dataTemp = _imblur(dataTemp[..., np.newaxis],
345
+ sig=gSig, siz=gSiz, kernel=kernel)
346
+ temp = dataTemp * score.reshape([1] * (Y.ndim - 1) + [-1])
347
+ rho[tuple([slice(*a) for a in Mod])] -= temp.copy()
348
+ if rolling_sum:
349
+ rho_filt = scipy.signal.lfilter(
350
+ rolling_filter, 1., rho[tuple([slice(*a) for a in Mod])] ** 2)
351
+ v[tuple([slice(*a) for a in Mod])] = np.amax(rho_filt, axis=-1)
352
+ else:
353
+ v[tuple([slice(*a) for a in Mod])] = \
354
+ np.sum(rho[tuple([slice(*a) for a in Mod])] ** 2, axis=-1)
355
+ center = center.tolist()
356
+ else:
357
+ center = []
358
+
359
+ res = np.reshape(Y, (np.prod(d[0:-1]), d[-1]),
360
+ order='F') + med.flatten(order='F')[:, None]
361
+ # model = NMF(n_components=nb, init='random', random_state=0)
362
+ model = NMF(n_components=nb, init='nndsvdar')
363
+ b_in = model.fit_transform(np.maximum(res, 0)).astype(np.float32)
364
+ f_in = model.components_.astype(np.float32)
365
+
366
+ return A, C, np.array(center, dtype='uint16'), b_in, f_in
@@ -0,0 +1,17 @@
1
+ import numpy as np
2
+
3
+ def float2uint8(scan):
4
+ """ Converts an scan (or image) from floats to uint8 (preserving the range)."""
5
+ scan = (scan - scan.min()) / (scan.max() - scan.min())
6
+ scan = (scan * 255).astype(np.uint8, copy=False)
7
+ return scan
8
+
9
+
10
+ def smooth_data(data, window_size=5):
11
+ """Smooth the data using a moving average."""
12
+ return np.convolve(data, np.ones(window_size) / window_size, mode='valid')
13
+
14
+
15
+ def norm_minmax(images):
16
+ """ Normalize a NumPy array to the range [0, 1]. """
17
+ return (images - images.min()) / (images.max() - images.min())
@@ -0,0 +1,208 @@
1
+ import warnings
2
+ import numpy as np
3
+ from joblib import Parallel, delayed
4
+ from sklearn.base import BaseEstimator, TransformerMixin
5
+ from tqdm import tqdm
6
+
7
+
8
+ def vectorize(movie, pixel_indices: np.ndarray = None, order="C"):
9
+ """
10
+ Reshape an array: [time, df, cols] -> [n_pixels, time]
11
+
12
+ Parameters
13
+ ----------
14
+ movie: np.ndarray
15
+ movie of shape [time, df, cols]
16
+
17
+ pixel_indices: np.ndarray, default None
18
+ pixel indices to include in the vectorized output. 1D array of int that represents indices of a fully
19
+ vectorized movie
20
+
21
+ order: str, default "C"
22
+ "C" or "F" order
23
+
24
+ Returns
25
+ -------
26
+ np.ndarray
27
+ vectorized movie, shape [n_pixels, time]
28
+
29
+ """
30
+
31
+ Y = movie.transpose(1, 2, 0).reshape(np.prod(movie.shape[1:]), movie.shape[0], order=order)
32
+
33
+ if pixel_indices is not None:
34
+ return Y[pixel_indices]
35
+
36
+ return Y
37
+
38
+
39
+ def unvectorize(Y, shape: tuple[int, int], pixel_indices: np.ndarray = None, order="C"):
40
+ """
41
+ Reshape an array: [n_pixels, time] -> [time, df, cols]
42
+ or
43
+ [n_pixels,] -> [df, cols]
44
+
45
+
46
+ Parameters
47
+ ----------
48
+ Y: np.ndarray
49
+ vectorized movie, shape [n_pixels, time]
50
+
51
+ shape: tuple[int, int]
52
+ shape of one frame, [n_rows, n_cols]
53
+
54
+ pixel_indices: np.ndarray, default None
55
+ 1D array of indices that map pixel indices in Y to real pixel indices in a "full Y" with all pixels
56
+
57
+ order: str, default "C"
58
+ "C" or "F" order
59
+
60
+ Returns
61
+ -------
62
+ np.ndarray
63
+ movie of shape [time, df, cols] or a 2D image of shape [df, cols]
64
+
65
+ """
66
+
67
+ if pixel_indices is not None and Y.shape[0] < np.prod(shape):
68
+ if Y.ndim == 1:
69
+ Y_full_shape = (np.prod(shape),)
70
+ else:
71
+ Y_full_shape = (np.prod(shape), Y.shape[1])
72
+
73
+ Y_full = np.zeros(Y_full_shape, dtype=Y.dtype)
74
+ Y_full[:] = np.nan
75
+ Y_full[pixel_indices] = Y[:]
76
+ Y = Y_full
77
+
78
+ if Y.ndim == 1:
79
+ # return 2D image
80
+ return Y.reshape(shape, order=order)
81
+
82
+ # return movie
83
+ return Y.reshape(*shape, Y.shape[1], order=order).transpose(-1, 0, 1)
84
+
85
+
86
+ class Vectorizer(TransformerMixin, BaseEstimator):
87
+ """
88
+ Vectorize movies
89
+
90
+ Parameters
91
+ ----------
92
+ order: str, array order
93
+ "C" or "F"
94
+ """
95
+
96
+ def __init__(self, pixel_indices: np.ndarray = None, order: str = "C"):
97
+ self.pixel_indices = None
98
+ self.order = order
99
+
100
+ def fit(self, movie, y=None):
101
+ """
102
+ Does nothing, exists for API conformity
103
+ """
104
+
105
+ return self
106
+
107
+ def transform(self, movie: np.ndarray):
108
+ """
109
+ Vectorize the movie. Does nothing if the movie is already vectorized.
110
+
111
+ Parameters
112
+ ----------
113
+ movie : array-like, shape [time, df, cols]
114
+ input movie
115
+
116
+ Returns
117
+ -------
118
+ np.ndarray
119
+ vectorized movie, shape [n_pixels, time]
120
+
121
+ """
122
+
123
+ if movie.ndim == 2:
124
+ return movie
125
+
126
+ return vectorize(movie, pixel_indices=self.pixel_indices, order=self.order)
127
+
128
+ def _more_tags(self):
129
+ # This is a quick example to show the tags API:\
130
+ # https://scikit-learn.org/dev/developers/develop.html#estimator-tags
131
+ # Here, our transformer does not do any operation in `fit` and only validate
132
+ # the parameters. Thus, it is stateless.
133
+ return {"stateless": True}
134
+
135
+
136
+ class UnVectorizer(TransformerMixin, BaseEstimator):
137
+ """
138
+ Unvectorize movies
139
+
140
+ Parameters
141
+ ----------
142
+ shape: tuple, [n_rows, n_cols]
143
+ shape of a 2D frame of the movie
144
+
145
+ order: str, array order
146
+ "C" or "F"
147
+
148
+ """
149
+
150
+ def __init__(self, shape: tuple, pixel_indices: np.ndarray = None, order: str = "C"):
151
+ if len(shape) != 2:
152
+ raise ValueError
153
+ self.shape = shape
154
+
155
+ self.pixel_indices = pixel_indices
156
+ self.order = order
157
+
158
+ def fit(self, movie, y=None):
159
+ """
160
+ Does nothing, exists for API conformity
161
+ """
162
+
163
+ return self
164
+
165
+ def transform(self, Y: np.ndarray):
166
+ """
167
+ Unvectorize the movie. Does nothing if the movie is already unvectorized.
168
+
169
+ Parameters
170
+ ----------
171
+ Y: array-like, shape [n_pixels, time]
172
+ input movie
173
+
174
+ Returns
175
+ -------
176
+ np.ndarray
177
+ unvectorized movie, shape [time, df, cols]
178
+
179
+ """
180
+
181
+ if Y.ndim == 3:
182
+ return Y
183
+
184
+ return unvectorize(Y, shape=self.shape, pixel_indices=self.pixel_indices, order=self.order)
185
+
186
+ def _more_tags(self):
187
+ # This is a quick example to show the tags API:\
188
+ # https://scikit-learn.org/dev/developers/develop.html#estimator-tags
189
+ # Here, our transformer does not do any operation in `fit` and only validate
190
+ # the parameters. Thus, it is stateless.
191
+ return {"stateless": True}
192
+
193
+
194
+ def calculate_centers(A, dims):
195
+ def calculate_center_component(i):
196
+ with warnings.catch_warnings():
197
+ warnings.simplefilter("ignore", category=RuntimeWarning)
198
+ ixs = np.where(A[:, i].toarray() > 0.07)[0]
199
+ if ixs.size == 0:
200
+ return np.array([np.nan, np.nan]) # Handle empty slice explicitly
201
+ return np.array(np.unravel_index(ixs, dims)).mean(axis=1)[::-1]
202
+
203
+ print("Computing centers in parallel...")
204
+ centers = Parallel(n_jobs=-1)(
205
+ delayed(calculate_center_component)(i) for i in tqdm(range(A.shape[1]), desc="Calculating neuron center coordinates")
206
+ )
207
+
208
+ return np.array(centers)