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,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)
|