optimouse 0.1.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.
- modules/__init__.py +21 -0
- modules/bruker_concat_tif.py +650 -0
- modules/compute_zcorr.py +2040 -0
- modules/extraction.py +559 -0
- modules/motion_correction.py +982 -0
- modules/utils/plot_zones.py +125 -0
- modules/utils/save_z_motion_args.py +93 -0
- optimouse-0.1.0.dist-info/METADATA +321 -0
- optimouse-0.1.0.dist-info/RECORD +24 -0
- optimouse-0.1.0.dist-info/WHEEL +5 -0
- optimouse-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- optimouse-0.1.0.dist-info/top_level.txt +2 -0
- pipeline/__init__.py +0 -0
- pipeline/configs/config_C57_O1M2_10022023_run1run2.json +107 -0
- pipeline/configs/config_C57_O1M2_10022023_run1run2run3run4.json +114 -0
- pipeline/configs/config_template_cnmf.json +123 -0
- pipeline/configs/config_template_suite2p.json +102 -0
- pipeline/configs/config_test.json +114 -0
- pipeline/pipeline_cnmf.py +263 -0
- pipeline/pipeline_mcorr.py +299 -0
- pipeline/roi_zcorr.py +65 -0
- pipeline/utils/__init__.py +0 -0
- pipeline/utils/config_loader.py +92 -0
- pipeline/utils/pipeline_utils.py +537 -0
modules/compute_zcorr.py
ADDED
|
@@ -0,0 +1,2040 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script contains functions to compute the movement in depth (z-correlation) and subtract the z-motion from the movie, either on a per-pixel or per-neuron basis.
|
|
3
|
+
|
|
4
|
+
All other functions are original.
|
|
5
|
+
Authors: Vincent Prevosto, Ting Lou, Manuel Levy
|
|
6
|
+
Date: 2024-02-21
|
|
7
|
+
License: CC-BY-SA 4.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Add project root to path
|
|
17
|
+
PROJECT_ROOT = Path(__file__).parent.parent
|
|
18
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
20
|
+
|
|
21
|
+
# Set a consistent seed
|
|
22
|
+
random_seed = 42
|
|
23
|
+
import numpy as np
|
|
24
|
+
np.random.seed(random_seed)
|
|
25
|
+
import random
|
|
26
|
+
random.seed(random_seed)
|
|
27
|
+
|
|
28
|
+
import time
|
|
29
|
+
import os
|
|
30
|
+
import glob
|
|
31
|
+
import warnings
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import json
|
|
35
|
+
import pandas as pd
|
|
36
|
+
import gc
|
|
37
|
+
|
|
38
|
+
from caiman.mmapping import load_memmap
|
|
39
|
+
|
|
40
|
+
from PIL import Image
|
|
41
|
+
from matplotlib.colors import ListedColormap
|
|
42
|
+
from matplotlib import cm, patches
|
|
43
|
+
import matplotlib.pyplot as plt
|
|
44
|
+
from tifffile import TiffFile, TiffWriter, imread
|
|
45
|
+
|
|
46
|
+
from modules import bruker_concat_tif as ct
|
|
47
|
+
from pipeline.utils.pipeline_utils import log_and_print
|
|
48
|
+
|
|
49
|
+
from scipy.ndimage import gaussian_filter, map_coordinates, shift, affine_transform, label, find_objects
|
|
50
|
+
from scipy.stats import mode, linregress
|
|
51
|
+
from scipy.io import savemat
|
|
52
|
+
|
|
53
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
|
54
|
+
from tqdm import tqdm
|
|
55
|
+
from joblib import Parallel, delayed
|
|
56
|
+
import multiprocessing
|
|
57
|
+
|
|
58
|
+
from sklearn.linear_model import HuberRegressor
|
|
59
|
+
from skimage.transform import warp, AffineTransform
|
|
60
|
+
from skimage.registration import phase_cross_correlation
|
|
61
|
+
|
|
62
|
+
import cv2
|
|
63
|
+
import argparse
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_zshift_geometry(zshift_params, imaging_params=None):
|
|
67
|
+
"""Return frame dimensions and micron scaling from z-shift params or imaging metadata."""
|
|
68
|
+
Nx = zshift_params.get('Nx')
|
|
69
|
+
Ny = zshift_params.get('Ny')
|
|
70
|
+
micron_per_pixel = zshift_params.get('micron_per_pixel')
|
|
71
|
+
|
|
72
|
+
if imaging_params:
|
|
73
|
+
if Nx is None:
|
|
74
|
+
Nx = imaging_params.get('Npixel_x', Nx)
|
|
75
|
+
if Ny is None:
|
|
76
|
+
Ny = imaging_params.get('Npixel_y', Ny)
|
|
77
|
+
if micron_per_pixel is None:
|
|
78
|
+
micron_per_pixel = imaging_params.get('microns_per_pixel', micron_per_pixel)
|
|
79
|
+
|
|
80
|
+
if Nx is None or Ny is None:
|
|
81
|
+
raise KeyError(
|
|
82
|
+
"Missing frame dimensions for z-stack shift. Provide 'Npixel_x' and 'Npixel_y' "
|
|
83
|
+
"under imaging or keep them in params_mcorr.z_motion_correction.zstack_shift."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if micron_per_pixel is None:
|
|
87
|
+
raise KeyError(
|
|
88
|
+
"Missing micron_per_pixel for z-stack shift. Provide 'microns_per_pixel' under imaging "
|
|
89
|
+
"or keep 'micron_per_pixel' in the zstack_shift block."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return int(Nx), int(Ny), float(micron_per_pixel)
|
|
93
|
+
|
|
94
|
+
# Type checking imports (not loaded at runtime)
|
|
95
|
+
if TYPE_CHECKING:
|
|
96
|
+
from suite2p.registration import rigid
|
|
97
|
+
|
|
98
|
+
def self_align_zstack(Zstack, method=cv2.MOTION_TRANSLATION):
|
|
99
|
+
# Initialize storage for the aligned images
|
|
100
|
+
aligned_images = np.zeros_like(Zstack)
|
|
101
|
+
|
|
102
|
+
# Initialize storage for the shifts
|
|
103
|
+
shifts = np.zeros((Zstack.shape[0], 2), dtype=np.float32) # Store x, y shifts for each frame
|
|
104
|
+
|
|
105
|
+
# Choose the reference frame (e.g., middle frame of the stack)
|
|
106
|
+
reference_image = Zstack[Zstack.shape[0] // 2, :, :]
|
|
107
|
+
|
|
108
|
+
# Specify the number of iterations and the termination threshold
|
|
109
|
+
number_of_iterations = 5000
|
|
110
|
+
termination_eps = 1e-10
|
|
111
|
+
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, number_of_iterations, termination_eps)
|
|
112
|
+
|
|
113
|
+
# Run the registration
|
|
114
|
+
for iz in range(Zstack.shape[0]):
|
|
115
|
+
# Reset the warp matrix for each frame
|
|
116
|
+
if method == cv2.MOTION_HOMOGRAPHY:
|
|
117
|
+
warp_matrix = np.eye(3, 3, dtype=np.float32)
|
|
118
|
+
else:
|
|
119
|
+
warp_matrix = np.eye(2, 3, dtype=np.float32)
|
|
120
|
+
|
|
121
|
+
# Perform alignment using the ECC algorithm
|
|
122
|
+
(cc, warp_matrix) = cv2.findTransformECC(reference_image, Zstack[iz, :, :], warp_matrix, method, criteria)
|
|
123
|
+
|
|
124
|
+
# Apply the transformation to align the images
|
|
125
|
+
if method == cv2.MOTION_HOMOGRAPHY:
|
|
126
|
+
aligned_images[iz, :, :] = cv2.warpPerspective(Zstack[iz, :, :], warp_matrix, (Zstack.shape[2], Zstack.shape[1]), flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP)
|
|
127
|
+
else:
|
|
128
|
+
aligned_images[iz, :, :] = cv2.warpAffine(Zstack[iz, :, :], warp_matrix, (Zstack.shape[2], Zstack.shape[1]), flags=cv2.INTER_LINEAR + cv2.WARP_INVERSE_MAP)
|
|
129
|
+
|
|
130
|
+
# Extract and store the shifts
|
|
131
|
+
if method != cv2.MOTION_HOMOGRAPHY: # Homography involves complex transformations
|
|
132
|
+
dx, dy = warp_matrix[0, 2], warp_matrix[1, 2]
|
|
133
|
+
shifts[iz] = [dx, dy]
|
|
134
|
+
|
|
135
|
+
# Return both the aligned images and the shifts
|
|
136
|
+
return aligned_images, shifts
|
|
137
|
+
|
|
138
|
+
def shift_zstack(zshift_params, zstack_in, z_shifted_file, scale_range=False, imaging_params=None):
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
Shift images in zstack by dx and dy to compensate for drift due to objective angle.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
- zshift_params: dictionary containing the parameters for z-shift.
|
|
145
|
+
e.g. zshift_params = {
|
|
146
|
+
"file_name": "zstack_shift.tif",
|
|
147
|
+
"Ch": 2,
|
|
148
|
+
"alpha": 25,
|
|
149
|
+
"beta": 6.5,
|
|
150
|
+
"step": 1,
|
|
151
|
+
"micron_per_pixel": 1.46,
|
|
152
|
+
"Nz": 41
|
|
153
|
+
}
|
|
154
|
+
Frame dimensions are taken from ``imaging_params`` when ``Nx``/``Ny``
|
|
155
|
+
are omitted.
|
|
156
|
+
- zstack_in: Path to the folder containing the z-stack images.
|
|
157
|
+
e.g. zstack_in='D:/Analysis_2P/Data/C57_O1M2/10022023/ZSeries-10022023-1300-004'
|
|
158
|
+
- z_shifted_file: Name of the output shifted z-stack file.
|
|
159
|
+
- scale_range: Boolean indicating whether to scale the output to uint16 range.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
- zstack_out: Path to the folder containing the shifted z-stack images.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
# Extract parameters
|
|
166
|
+
Ch = zshift_params['Ch']
|
|
167
|
+
alpha = zshift_params['alpha']
|
|
168
|
+
beta = zshift_params['beta']
|
|
169
|
+
step = zshift_params['step']
|
|
170
|
+
Nx, Ny, micron_per_pixel = _resolve_zshift_geometry(zshift_params, imaging_params)
|
|
171
|
+
Nz = zshift_params['Nz']
|
|
172
|
+
|
|
173
|
+
# Compute shifts in microns
|
|
174
|
+
dx = np.tan(np.radians(alpha)) * step
|
|
175
|
+
dy = np.tan(np.radians(beta)) * step
|
|
176
|
+
|
|
177
|
+
# Initialize empty array for shifted images
|
|
178
|
+
shifted_Zstack = np.zeros((Nz, Ny, Nx))
|
|
179
|
+
|
|
180
|
+
# Load and shift images
|
|
181
|
+
# List files with {zstack_in.name}_Cycle00001_Ch{Ch}_{iz + 1:06d}.ome.tif pattern in zstack_in folder
|
|
182
|
+
tif_list = glob.glob(f"{zstack_in}/{zstack_in.name}_Cycle00001_Ch{Ch}_*.ome.tif")
|
|
183
|
+
if len(tif_list) == 1 and ct.is_multi_page_tiff(tif_list[0]):
|
|
184
|
+
# Multi-page tiff format
|
|
185
|
+
with TiffFile(tif_list[0]) as tif:
|
|
186
|
+
for iz, page in enumerate(tif.pages):
|
|
187
|
+
V = page.asarray()
|
|
188
|
+
dxi = dx * (iz - Nz // 2)
|
|
189
|
+
dyi = dy * (iz - Nz // 2)
|
|
190
|
+
|
|
191
|
+
# Create a grid for the shifted image
|
|
192
|
+
X, Y = np.meshgrid(np.arange(Nx) * micron_per_pixel - dxi, np.arange(Ny) * micron_per_pixel - dyi)
|
|
193
|
+
|
|
194
|
+
# Interpolate the image on the new grid
|
|
195
|
+
coordinates = np.array([Y.ravel() / micron_per_pixel, X.ravel() / micron_per_pixel])
|
|
196
|
+
shifted_Zstack[iz] = map_coordinates(V, coordinates, order=1, mode='nearest').reshape(Ny, Nx)
|
|
197
|
+
else:
|
|
198
|
+
for iz in range(Nz):
|
|
199
|
+
filename_temp = f"{zstack_in}/{zstack_in.name}_Cycle00001_Ch{Ch}_{iz + 1:06d}.ome.tif"
|
|
200
|
+
V = imread(filename_temp)
|
|
201
|
+
# For some images, the header contains information about the whole 41 image stack.
|
|
202
|
+
# If so, imread will erroneously return dimensions as (41, Ny, Nx) instead of (Ny, Nx)
|
|
203
|
+
if V.ndim == 3:
|
|
204
|
+
V = V[0]
|
|
205
|
+
|
|
206
|
+
dxi = dx * (iz - Nz // 2)
|
|
207
|
+
dyi = dy * (iz - Nz // 2)
|
|
208
|
+
|
|
209
|
+
# Create a grid for the shifted image
|
|
210
|
+
X, Y = np.meshgrid(np.arange(Nx) * micron_per_pixel - dxi, np.arange(Ny) * micron_per_pixel - dyi)
|
|
211
|
+
|
|
212
|
+
# Interpolate the image on the new grid
|
|
213
|
+
coordinates = np.array([Y.ravel() / micron_per_pixel, X.ravel() / micron_per_pixel])
|
|
214
|
+
shifted_Zstack[iz] = map_coordinates(V, coordinates, order=1, mode='nearest').reshape(Ny, Nx)
|
|
215
|
+
|
|
216
|
+
if scale_range:
|
|
217
|
+
# Normalize to uint16 range, from uint12 range (even though in float64 format at that point)
|
|
218
|
+
shifted_Zstack = (shifted_Zstack * ((2**16 - 1) / (2**12 - 1))).astype(np.float32)
|
|
219
|
+
else:
|
|
220
|
+
shifted_Zstack = shifted_Zstack.astype(np.float32)
|
|
221
|
+
|
|
222
|
+
# print(f"Aligned zstack shape: {shifted_Zstack.shape}, data type: {shifted_Zstack.dtype}, min: {shifted_Zstack.min()}, max: {shifted_Zstack.max()}")
|
|
223
|
+
|
|
224
|
+
# Refine alignment by registering the z-stack to iself
|
|
225
|
+
registered_Zstack, shifts = self_align_zstack(shifted_Zstack)
|
|
226
|
+
|
|
227
|
+
# Clip and convert to uint16
|
|
228
|
+
registered_Zstack = np.clip(registered_Zstack, 0, 2**16-1).astype(np.uint16)
|
|
229
|
+
|
|
230
|
+
# print(f"Aligned zstack shape: {registered_Zstack.shape}, data type: {registered_Zstack.dtype}, min: {registered_Zstack.min()}, max: {registered_Zstack.max()}")
|
|
231
|
+
|
|
232
|
+
# # Concatenate the zstack and the aligned zstack horizontally
|
|
233
|
+
# zstack_concat = np.concatenate((np.moveaxis(shifted_Zstack.astype(np.uint16), 0, -1), np.moveaxis(registered_Zstack.astype(np.uint16), 0, -1)), axis=1)
|
|
234
|
+
# # Save the concatenated zstacks as tiff
|
|
235
|
+
# tiff_save_path = zstack_in / 'zstack_aligned.tif'
|
|
236
|
+
# with TiffWriter(tiff_save_path, bigtiff=True) as tif:
|
|
237
|
+
# for iz in range(zstack_concat.shape[2]):
|
|
238
|
+
# tif.write(zstack_concat[:, :, iz].astype(np.uint16), photometric='minisblack')
|
|
239
|
+
|
|
240
|
+
zstack_out = zstack_in / z_shifted_file
|
|
241
|
+
with TiffWriter(zstack_out, bigtiff=True) as writer:
|
|
242
|
+
for iz in range(Nz):
|
|
243
|
+
writer.write(registered_Zstack[iz])
|
|
244
|
+
|
|
245
|
+
print("Shifted and self-registered z-stack saved.")
|
|
246
|
+
|
|
247
|
+
return zstack_out
|
|
248
|
+
|
|
249
|
+
def default_zcorr_params():
|
|
250
|
+
return {
|
|
251
|
+
"smooth_sigma": 1.15,
|
|
252
|
+
"pre_smooth": 0,
|
|
253
|
+
"spatial_hp_reg": 42,
|
|
254
|
+
"spatial_taper": 40,
|
|
255
|
+
"maxregshift": 0.1,
|
|
256
|
+
"smooth_sigma_time": 0,
|
|
257
|
+
"nonrigid": False
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def compute_zcorr_for_frame(nfr, Treg, refAndMasks, ops):
|
|
261
|
+
"""
|
|
262
|
+
Compute the z-correlation for a single frame, using phase correlation between the current frame and the cross-correlation reference image.
|
|
263
|
+
|
|
264
|
+
Parameters:
|
|
265
|
+
- nfr: int, the frame number
|
|
266
|
+
- Treg: 3D numpy array, the registered movie
|
|
267
|
+
- refAndMasks: list of tuples, each tuple containing the maskMul, maskOffset, and cfRefImg
|
|
268
|
+
- ops: dictionary, the parameters for z-correlation-
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
- yshift: float, the y-shift
|
|
272
|
+
- xshift: float, the x-shift
|
|
273
|
+
- zcorr: float, the z-correlation
|
|
274
|
+
"""
|
|
275
|
+
data = Treg[nfr:nfr + 1] # Get the current frame
|
|
276
|
+
yshift = np.zeros((len(refAndMasks), 1), np.int32)
|
|
277
|
+
xshift = np.zeros((len(refAndMasks), 1), np.int32)
|
|
278
|
+
zcorr = np.zeros((len(refAndMasks), 1), np.float32)
|
|
279
|
+
|
|
280
|
+
for z, ref in enumerate(refAndMasks):
|
|
281
|
+
maskMul, maskOffset, cfRefImg = ref
|
|
282
|
+
cfRefImg = cfRefImg.squeeze()
|
|
283
|
+
|
|
284
|
+
# _, _, zcorr[z] = rigid.phasecorr(
|
|
285
|
+
yshift[z], xshift[z], zcorr[z] = rigid.phasecorr(
|
|
286
|
+
data=rigid.apply_masks(data=data, maskMul=maskMul, maskOffset=maskOffset),
|
|
287
|
+
cfRefImg=cfRefImg,
|
|
288
|
+
maxregshift=ops["maxregshift"],
|
|
289
|
+
smooth_sigma_time=ops["smooth_sigma_time"],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# return zcorr
|
|
293
|
+
return yshift, xshift, zcorr
|
|
294
|
+
|
|
295
|
+
def compute_zcorrel_for_frame(frame, Z_stack, compute_shifts=False):
|
|
296
|
+
"""
|
|
297
|
+
Compute correlation between the current frame and each frame in the Z-stack
|
|
298
|
+
"""
|
|
299
|
+
# Initialize arrays
|
|
300
|
+
zcorr = np.zeros(Z_stack.shape[0], np.float32)
|
|
301
|
+
if compute_shifts:
|
|
302
|
+
yshift = np.zeros(Z_stack.shape[0], np.float32)
|
|
303
|
+
xshift = np.zeros(Z_stack.shape[0], np.float32)
|
|
304
|
+
|
|
305
|
+
for z_idx in range(Z_stack.shape[0]):
|
|
306
|
+
if compute_shifts:
|
|
307
|
+
shift_values, _, _ = phase_cross_correlation(frame,
|
|
308
|
+
Z_stack[z_idx],
|
|
309
|
+
upsample_factor=100)
|
|
310
|
+
yshift[z_idx] = shift_values[0]
|
|
311
|
+
xshift[z_idx] = shift_values[1]
|
|
312
|
+
|
|
313
|
+
# Apply the computed shifts to align the z-stack frame with the current frame
|
|
314
|
+
shifted_z_frame = shift(Z_stack[z_idx], shift=(shift_values[0], shift_values[1]), mode='constant', cval=0.0)
|
|
315
|
+
|
|
316
|
+
# Compute correlation coefficient after alignment
|
|
317
|
+
zcorr[z_idx] = np.corrcoef(frame.ravel(), shifted_z_frame.ravel())[0, 1]
|
|
318
|
+
|
|
319
|
+
else:
|
|
320
|
+
# Compute correlation coefficient without computing shifts
|
|
321
|
+
zcorr[z_idx] = np.corrcoef(frame.ravel(), Z_stack[z_idx].ravel())[0, 1]
|
|
322
|
+
|
|
323
|
+
if compute_shifts:
|
|
324
|
+
return zcorr, yshift, xshift
|
|
325
|
+
else:
|
|
326
|
+
return zcorr
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def plot_z_position(zpos, export_path):
|
|
330
|
+
"""
|
|
331
|
+
Plot the Z-position over time and its histogram.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
zpos: Array of best z-positions per frame
|
|
335
|
+
export_path: Path to save the plot
|
|
336
|
+
"""
|
|
337
|
+
import matplotlib.pyplot as plt
|
|
338
|
+
try:
|
|
339
|
+
# Check if zpos is valid (not all NaNs)
|
|
340
|
+
if zpos is None or not np.any(np.isfinite(zpos)):
|
|
341
|
+
print("Z-position data is empty or all NaNs. Skipping plot.")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10))
|
|
345
|
+
|
|
346
|
+
# Plot 1: Z-position over time
|
|
347
|
+
ax1.plot(zpos, label='zpos')
|
|
348
|
+
ax1.set_xlabel('T-series Frame')
|
|
349
|
+
ax1.set_ylabel('Z-stack Frame')
|
|
350
|
+
ax1.set_title('Z Position over Frames')
|
|
351
|
+
ax1.grid(True, alpha=0.3)
|
|
352
|
+
|
|
353
|
+
# Plot 2: Histogram of Z-positions
|
|
354
|
+
# Handle scaling/centering text if needed, similar to the Matlab "mean +/- sd"
|
|
355
|
+
z_mean = np.nanmean(zpos)
|
|
356
|
+
z_std = np.nanstd(zpos)
|
|
357
|
+
|
|
358
|
+
ax2.hist(zpos, bins=30, color='gray', edgecolor='black', alpha=0.7)
|
|
359
|
+
ax2.set_xlabel('Z-stack Frame')
|
|
360
|
+
ax2.set_ylabel('Frequency')
|
|
361
|
+
ax2.set_title(f'Z-position Distribution: {z_mean:.2f} +/- {z_std:.2f}')
|
|
362
|
+
ax2.grid(True, alpha=0.3)
|
|
363
|
+
|
|
364
|
+
plt.tight_layout()
|
|
365
|
+
|
|
366
|
+
plot_export_path = export_path / 'plots'
|
|
367
|
+
plot_export_path.mkdir(parents=True, exist_ok=True)
|
|
368
|
+
|
|
369
|
+
plt.savefig(plot_export_path / "z_drift.png", dpi=150)
|
|
370
|
+
plt.close(fig)
|
|
371
|
+
print(f"Saved Z-position drift plot to {plot_export_path / 'z_drift.png'}")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
print(f"Error plotting z-position: {e}")
|
|
374
|
+
|
|
375
|
+
def compute_zcorrel(zstack_file, movie_mmap_path, smooth_sigma=3, return_shifts=False, export_path=None):
|
|
376
|
+
"""
|
|
377
|
+
Computes correlation and x/y shifts in z for each frame in the movie, using the anatomical z-stack as reference.
|
|
378
|
+
Computes z-position for each frame by finding the z-stack frame with the highest correlation.
|
|
379
|
+
"""
|
|
380
|
+
#####################################
|
|
381
|
+
## 1. Load and prepare the z-stack ##
|
|
382
|
+
#####################################
|
|
383
|
+
|
|
384
|
+
# Load the z-stack and stack frames into a 3D array
|
|
385
|
+
with TiffFile(zstack_file) as zstack_tiff_file:
|
|
386
|
+
pages = [page.asarray() for page in zstack_tiff_file.pages]
|
|
387
|
+
Z_stack = np.stack(pages, axis=0).astype(np.float32)
|
|
388
|
+
|
|
389
|
+
# Flip Z_stack to match the orientation of the movie
|
|
390
|
+
Z_stack = np.flip(Z_stack, axis=1)
|
|
391
|
+
|
|
392
|
+
# Convert to float (not strictly necessary, but better be consistent with the movie data type)
|
|
393
|
+
Z_stack = Z_stack.astype(np.float32)
|
|
394
|
+
|
|
395
|
+
###################################
|
|
396
|
+
## 2. Load and prepare the movie ##
|
|
397
|
+
###################################
|
|
398
|
+
|
|
399
|
+
# Load the movie
|
|
400
|
+
movie_16bit, dims, T = load_memmap(movie_mmap_path)
|
|
401
|
+
T_series = np.reshape(movie_16bit.T, [T] + list(dims), order='F')
|
|
402
|
+
|
|
403
|
+
# Get the number of frames and the size of the movie
|
|
404
|
+
nFrames, Ny, Nx = T_series.shape
|
|
405
|
+
|
|
406
|
+
# Check that Z-stack matches the size of the movie
|
|
407
|
+
if Z_stack.shape[1] != Ny or Z_stack.shape[2] != Nx:
|
|
408
|
+
print("Z-stack dimensions do not match the movie dimensions.")
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
########################################################
|
|
412
|
+
## 3. Register the z-stack to the movie’s average FOV ##
|
|
413
|
+
########################################################
|
|
414
|
+
|
|
415
|
+
# Zstack_0 is the frame in the z-stack with the best correlation with the mean of the T-series
|
|
416
|
+
# To find it, we first realign each frame of the zstack to the mean of the T-series
|
|
417
|
+
|
|
418
|
+
# Compute a mean image of the movie
|
|
419
|
+
fov_image = np.mean(T_series, axis=0)
|
|
420
|
+
|
|
421
|
+
# Find the best z-plane vs. the FOV
|
|
422
|
+
|
|
423
|
+
# For each plane z, compute the rigid shift needed to align Z_stack[z] with fov_image using phase_cross_correlation.
|
|
424
|
+
# Apply the shift, then compute the correlation coefficient.
|
|
425
|
+
# Select the plane with the highest correlation as the “best” reference plane Zstack_0.
|
|
426
|
+
|
|
427
|
+
correlations = np.zeros(Z_stack.shape[0], np.float32)
|
|
428
|
+
for z_idx in range(Z_stack.shape[0]):
|
|
429
|
+
# Perform z-stack image registration
|
|
430
|
+
shift, _ , _ = phase_cross_correlation(Z_stack[z_idx], fov_image, upsample_factor=100)
|
|
431
|
+
print(f"Shift for z-stack frame {z_idx}: {shift}")
|
|
432
|
+
# Create the transformation matrix (for translation only)
|
|
433
|
+
tform = AffineTransform(translation=(shift[1], shift[0]))
|
|
434
|
+
|
|
435
|
+
# Translate z-stack frames to minimize shift with mean of registered tseries frames (FOV)
|
|
436
|
+
shifted_z_frame = warp(np.float32(Z_stack[z_idx, :, :]), tform)
|
|
437
|
+
smoothed_shifted_z_frame = gaussian_filter(shifted_z_frame, sigma=1)
|
|
438
|
+
|
|
439
|
+
# compute correlation coefficient after alignment
|
|
440
|
+
correlations[z_idx] = np.corrcoef(fov_image.ravel(), smoothed_shifted_z_frame.ravel())[0, 1]
|
|
441
|
+
print(f"Correlation for z-stack frame {z_idx}: {correlations[z_idx]}")
|
|
442
|
+
|
|
443
|
+
best_z_index = np.argmax(correlations)
|
|
444
|
+
Zstack_0 = Z_stack[best_z_index]
|
|
445
|
+
|
|
446
|
+
# Print the index of the frame in the z-stack
|
|
447
|
+
print(f'The frame in the z-stack with the best correlation is frame {best_z_index}')
|
|
448
|
+
|
|
449
|
+
# Apply the best-plane alignment to the whole stack
|
|
450
|
+
# We perform image registration again, but just between the best frame (reference) in the z-stack and the FOV
|
|
451
|
+
shift, _ , _ = phase_cross_correlation(Zstack_0, fov_image, upsample_factor=100)
|
|
452
|
+
|
|
453
|
+
# Create the transformation matrix (for translation only)
|
|
454
|
+
tform = AffineTransform(translation=(shift[1], shift[0]))
|
|
455
|
+
|
|
456
|
+
# Translate z-stack frames to minimize shift with mean of registered tseries frames (FOV)
|
|
457
|
+
Zstack_reg = np.zeros_like(Z_stack)
|
|
458
|
+
for iz in range(Z_stack.shape[0]):
|
|
459
|
+
Zstack_reg[iz, :, :] = warp(np.float32(Z_stack[iz, :, :]), tform)
|
|
460
|
+
|
|
461
|
+
###################################################
|
|
462
|
+
## 4. Compute z-correlation for each movie frame ##
|
|
463
|
+
###################################################
|
|
464
|
+
|
|
465
|
+
# For each frame in the movie:
|
|
466
|
+
|
|
467
|
+
# Compare it to each plane z in the registered z-stack.
|
|
468
|
+
# If return_shifts=True, use phase_cross_correlation to find sub-pixel x–y shifts and align that z-plane.
|
|
469
|
+
# Compute the correlation coefficient of the aligned plane vs. the frame.
|
|
470
|
+
# Store the correlation in zcorr[z, frame].
|
|
471
|
+
|
|
472
|
+
# Initialize arrays
|
|
473
|
+
zcorr = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
474
|
+
if return_shifts:
|
|
475
|
+
yshift = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
476
|
+
xshift = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
477
|
+
|
|
478
|
+
del Z_stack, Zstack_0, shift
|
|
479
|
+
gc.collect()
|
|
480
|
+
|
|
481
|
+
t0 = time.time()
|
|
482
|
+
|
|
483
|
+
smoothed_f_func = gaussian_filter(T_series, sigma=[0, smooth_sigma, smooth_sigma])
|
|
484
|
+
smoothed_z_stack = gaussian_filter(Zstack_reg, sigma=[0, smooth_sigma, smooth_sigma])
|
|
485
|
+
|
|
486
|
+
# Use Parallel
|
|
487
|
+
results = Parallel(n_jobs=-1)(
|
|
488
|
+
delayed(compute_zcorrel_for_frame)(frame, smoothed_z_stack, compute_shifts=return_shifts)
|
|
489
|
+
for frame in tqdm(smoothed_f_func, desc="Computing z-correlations")
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if return_shifts:
|
|
493
|
+
for frameNum, result in enumerate(results):
|
|
494
|
+
zcorr[:, frameNum], yshift[:, frameNum], xshift[:, frameNum]= result
|
|
495
|
+
else:
|
|
496
|
+
for frameNum, result in enumerate(results):
|
|
497
|
+
zcorr[:, frameNum] = result
|
|
498
|
+
|
|
499
|
+
# Kill all LokyProcess workers (Windows only)
|
|
500
|
+
if os.name == 'nt':
|
|
501
|
+
for p in multiprocessing.active_children():
|
|
502
|
+
if 'LokyProcess' in p.name:
|
|
503
|
+
p.terminate()
|
|
504
|
+
|
|
505
|
+
print (f"Correlations for {nFrames} frames computed in {time.time() - t0:.2f} sec.")
|
|
506
|
+
|
|
507
|
+
# Get the optimal z position for each frame
|
|
508
|
+
zpos = np.argmax(gaussian_filter(zcorr, sigma=3, axes=0), axis=0)
|
|
509
|
+
zpos = zpos.astype(np.uint16)
|
|
510
|
+
|
|
511
|
+
###################################
|
|
512
|
+
## 5. Return correlation results ##
|
|
513
|
+
###################################
|
|
514
|
+
|
|
515
|
+
# The function returns a dictionary containing:
|
|
516
|
+
|
|
517
|
+
# zcorr: A 2D array [z, frame] of correlation coefficients.
|
|
518
|
+
# zpos: The chosen best z-plane per frame.
|
|
519
|
+
# (Optional) yshift and xshift arrays if return_shifts=True.
|
|
520
|
+
|
|
521
|
+
# Save z-correlation variables to a file
|
|
522
|
+
if return_shifts:
|
|
523
|
+
z_correlation = {
|
|
524
|
+
'yshift': yshift.astype(np.float32),
|
|
525
|
+
'xshift': xshift.astype(np.float32),
|
|
526
|
+
'zcorr': zcorr.astype(np.float32),
|
|
527
|
+
'zpos': zpos.astype(np.uint16)
|
|
528
|
+
}
|
|
529
|
+
else:
|
|
530
|
+
z_correlation = {
|
|
531
|
+
'zcorr': zcorr.astype(np.float32),
|
|
532
|
+
'zpos': zpos.astype(np.uint16)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if export_path is None:
|
|
536
|
+
export_path = movie_mmap_path.parents[1]
|
|
537
|
+
export_path = Path(export_path)
|
|
538
|
+
np.savez(export_path / "z_correlation.npz", **z_correlation)
|
|
539
|
+
|
|
540
|
+
del zcorr, zpos, smoothed_f_func, smoothed_z_stack, T_series, movie_16bit
|
|
541
|
+
gc.collect()
|
|
542
|
+
|
|
543
|
+
# Plot results
|
|
544
|
+
try:
|
|
545
|
+
plot_z_position(
|
|
546
|
+
z_correlation['zpos'],
|
|
547
|
+
export_path
|
|
548
|
+
)
|
|
549
|
+
except Exception as e:
|
|
550
|
+
print(f"Failed to plot z-position summary: {e}")
|
|
551
|
+
|
|
552
|
+
return z_correlation
|
|
553
|
+
|
|
554
|
+
def _get_suite2p_rigid():
|
|
555
|
+
"""Lazy import of Suite2p registration functions"""
|
|
556
|
+
try:
|
|
557
|
+
from suite2p.registration import rigid
|
|
558
|
+
return rigid
|
|
559
|
+
except ImportError:
|
|
560
|
+
raise ImportError(
|
|
561
|
+
"Suite2p required for this function. "
|
|
562
|
+
"Install with: pip install suite2p"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def compute_zcorrel_suite2p(zstack_file, movie_mmap_path, z_corr_params=None, smoothing=False, smooth_sigma=1, return_shifts=False):
|
|
566
|
+
"""
|
|
567
|
+
Computes correlation and x/y shifts in z for each frame in the movie, using the anatomical z-stack as reference.
|
|
568
|
+
Computes z-position for each frame by finding the z-stack frame with the highest correlation.
|
|
569
|
+
|
|
570
|
+
The function is adapted from Suite2P's function `compute_zpos`:
|
|
571
|
+
https://github.com/MouseLand/suite2p/blob/193e7f1f656bfbd1c100eb51411737c80f54ac3c/suite2p/registration/zalign.py#L125
|
|
572
|
+
Copyright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Marius Pachitariu.
|
|
573
|
+
"""
|
|
574
|
+
rigid = _get_suite2p_rigid()
|
|
575
|
+
|
|
576
|
+
# Load the z-stack and stack frames into a 3D array
|
|
577
|
+
zstack_tiff_file = TiffFile(zstack_file)
|
|
578
|
+
Z_stack = []
|
|
579
|
+
for page in zstack_tiff_file.pages:
|
|
580
|
+
Z_stack.append(page.asarray())
|
|
581
|
+
Z_stack = np.stack(Z_stack, axis=0)
|
|
582
|
+
zstack_tiff_file.close()
|
|
583
|
+
|
|
584
|
+
# Flip Z_stack to match the orientation of the movie
|
|
585
|
+
Z_stack = np.flip(Z_stack, axis=1)
|
|
586
|
+
|
|
587
|
+
# Load the movie
|
|
588
|
+
movie_16bit, dims, T = load_memmap(movie_mmap_path)
|
|
589
|
+
T_series = np.reshape(movie_16bit.T, [T] + list(dims), order='F')
|
|
590
|
+
|
|
591
|
+
# Get the size of the movie
|
|
592
|
+
Ny, Nx = T_series.shape[1], T_series.shape[2]
|
|
593
|
+
|
|
594
|
+
# Check that Z-stack matches the size of the movie
|
|
595
|
+
if Z_stack.shape[1] != Ny or Z_stack.shape[2] != Nx:
|
|
596
|
+
print("Z-stack dimensions do not match the movie dimensions.")
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
# Get the number of frames in the movie
|
|
600
|
+
nFrames, _, _ = T_series.shape
|
|
601
|
+
|
|
602
|
+
# Initialize arrays
|
|
603
|
+
# if return_shifts:
|
|
604
|
+
yshift = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
605
|
+
xshift = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
606
|
+
zcorr = np.zeros((Z_stack.shape[0], nFrames), np.float32)
|
|
607
|
+
|
|
608
|
+
t0 = time.time()
|
|
609
|
+
|
|
610
|
+
# Set z_corr_params to default if not provided
|
|
611
|
+
if z_corr_params is None:
|
|
612
|
+
ops = default_zcorr_params()
|
|
613
|
+
else:
|
|
614
|
+
ops = z_corr_params
|
|
615
|
+
ops["nonrigid"] = False
|
|
616
|
+
|
|
617
|
+
refAndMasks = []
|
|
618
|
+
for Z in Z_stack:
|
|
619
|
+
maskMul, maskOffset = rigid.compute_masks(
|
|
620
|
+
refImg=Z,
|
|
621
|
+
maskSlope=3 * ops["smooth_sigma"],
|
|
622
|
+
)
|
|
623
|
+
# Compute the cross-correlation reference image
|
|
624
|
+
cfRefImag = rigid.phasecorr_reference(refImg=Z, smooth_sigma=ops["smooth_sigma"])
|
|
625
|
+
# Add a new axis to cfRefImag
|
|
626
|
+
cfRefImag = cfRefImag[np.newaxis, :, :]
|
|
627
|
+
# Append the maskMul, maskOffset, and cfRefImag to refAndMasks
|
|
628
|
+
refAndMasks.append((maskMul, maskOffset, cfRefImag))
|
|
629
|
+
|
|
630
|
+
# # Compute zcorr for each frame
|
|
631
|
+
# with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
632
|
+
# # Submit the compute_zcorr_for_frame function to the executor
|
|
633
|
+
# futures = [executor.submit(compute_zcorr_for_frame, nfr, Treg, refAndMasks, ops) for nfr in range(nFrames)]
|
|
634
|
+
# # As the threads complete, store the results in the yshift, xshift, and zcorr arrays
|
|
635
|
+
# for i, future in enumerate(tqdm(concurrent.futures.as_completed(futures), total=nFrames, desc="Processing frames")):
|
|
636
|
+
# yshift_res, xshift_res, zcorr_res = future.result()
|
|
637
|
+
# yshift[:, i], xshift[:, i], zcorr[:, i] = (
|
|
638
|
+
# np.asarray(yshift_res, dtype=np.int32).squeeze(),
|
|
639
|
+
# np.asarray(xshift_res, dtype=np.int32).squeeze(),
|
|
640
|
+
# np.asarray(zcorr_res, dtype=np.float32).squeeze()
|
|
641
|
+
# )
|
|
642
|
+
# print (f"{nFrames} frames processed in {time.time() - t0:.2f} sec.")
|
|
643
|
+
|
|
644
|
+
# Use joblib.Parallel
|
|
645
|
+
results = Parallel(n_jobs=-1)(delayed(compute_zcorr_for_frame)(nfr, T_series, refAndMasks, ops) for nfr in tqdm(range(nFrames), desc="Processing frames"))
|
|
646
|
+
|
|
647
|
+
# As the threads complete, store the results in the yshift, xshift, and zcorr arrays
|
|
648
|
+
for i, result in enumerate(results):
|
|
649
|
+
yshift_res, xshift_res, zcorr_res = result
|
|
650
|
+
yshift[:, i], xshift[:, i], zcorr[:, i] = (
|
|
651
|
+
np.asarray(yshift_res, dtype=np.int32).squeeze(),
|
|
652
|
+
np.asarray(xshift_res, dtype=np.int32).squeeze(),
|
|
653
|
+
np.asarray(zcorr_res, dtype=np.float32).squeeze()
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Kill all LokyProcess workers (Windows only)
|
|
657
|
+
if os.name == 'nt':
|
|
658
|
+
for p in multiprocessing.active_children():
|
|
659
|
+
if 'LokyProcess' in p.name:
|
|
660
|
+
p.terminate()
|
|
661
|
+
|
|
662
|
+
print (f"{nFrames} frames processed in {time.time() - t0:.2f} sec.")
|
|
663
|
+
|
|
664
|
+
# # Without threading
|
|
665
|
+
# for frameNum in range(nFrames):
|
|
666
|
+
# yshift_res, xshift_res, zcorr_res = compute_zcorr_for_frame(frameNum, T_series, refAndMasks, ops)
|
|
667
|
+
# zcorr[:, frameNum] = zcorr_res.squeeze()
|
|
668
|
+
# yshift[:, frameNum] = yshift_res.squeeze()
|
|
669
|
+
# xshift[:, frameNum] = xshift_res.squeeze()
|
|
670
|
+
# if frameNum % 10 == 0:
|
|
671
|
+
# print(f"{frameNum}/{nFrames} frames, {time.time() - t0:.2f} sec.")
|
|
672
|
+
|
|
673
|
+
# print(f"{frameNum}/{nFrames} frames, {time.time() - t0:.2f} sec.")
|
|
674
|
+
|
|
675
|
+
# Get the optimal z position for each frame
|
|
676
|
+
zpos = np.argmax(gaussian_filter(zcorr, sigma=3, axes=0), axis=0)
|
|
677
|
+
zpos = zpos.astype(np.uint16)
|
|
678
|
+
|
|
679
|
+
# import matplotlib.pyplot as plt
|
|
680
|
+
# plt.figure()
|
|
681
|
+
# plt.plot(zpos)
|
|
682
|
+
# plt.xlabel('Frames')
|
|
683
|
+
# plt.ylabel('Z Position')
|
|
684
|
+
# plt.title('Z Position over Frames')
|
|
685
|
+
# plt.grid(True)
|
|
686
|
+
# plt.savefig(movie_mmap_path.parents[1] / "z_drift_s3.png")
|
|
687
|
+
|
|
688
|
+
# Save z-correlation variables to a file in the mesmerize folder, preserving data types and accuracy
|
|
689
|
+
if return_shifts:
|
|
690
|
+
z_correlation = {
|
|
691
|
+
'yshift': yshift.astype(np.int32),
|
|
692
|
+
'xshift': xshift.astype(np.int32),
|
|
693
|
+
'zcorr': zcorr.astype(np.float32),
|
|
694
|
+
'zpos': zpos.astype(np.uint16)
|
|
695
|
+
}
|
|
696
|
+
warnings.warn("compute_zcorrel_suite2p returns x/y shifts as integers. Use compute_zcorrel for float x/y shifts.")
|
|
697
|
+
else:
|
|
698
|
+
z_correlation = {
|
|
699
|
+
'zcorr': zcorr.astype(np.float32),
|
|
700
|
+
'zpos': zpos.astype(np.uint16)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
np.savez(movie_mmap_path.parents[1] / "z_correlation.npz", **z_correlation)
|
|
704
|
+
|
|
705
|
+
return z_correlation
|
|
706
|
+
|
|
707
|
+
def save_mmap_movie(movie, movie_path):
|
|
708
|
+
"""
|
|
709
|
+
Save an array as a memmaped numpy array.
|
|
710
|
+
Original array must have dimensions T, y, x
|
|
711
|
+
"""
|
|
712
|
+
# Save the movie as a memmaped numpy array
|
|
713
|
+
movie_path = Path(movie_path)
|
|
714
|
+
# Transpose the array to (y, x, T)
|
|
715
|
+
transposed_array = movie.transpose(1, 2, 0)
|
|
716
|
+
# Flatten the transposed array in 'F' order (to align with the loading code)
|
|
717
|
+
flattened_array = transposed_array.flatten(order='F')
|
|
718
|
+
# Create a new memmap file with write access
|
|
719
|
+
movie_ = np.memmap(movie_path, dtype='float32', mode='w+', shape=flattened_array.shape)
|
|
720
|
+
movie_[:] = flattened_array[:]
|
|
721
|
+
# Flush changes to disk and close the memmap
|
|
722
|
+
del movie_
|
|
723
|
+
del flattened_array
|
|
724
|
+
gc.collect()
|
|
725
|
+
|
|
726
|
+
# Add the path to the memmap file to the memmap_paths.json file
|
|
727
|
+
memmap_paths = {'zcorr_movie_path': str(movie_path)}
|
|
728
|
+
with open(movie_path.parent / 'memmap_paths.json', 'w') as f:
|
|
729
|
+
json.dump(memmap_paths, f)
|
|
730
|
+
|
|
731
|
+
return movie_path
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def patch_regress(frame_data, shift_patches=False):
|
|
735
|
+
"""
|
|
736
|
+
Calculate the linear least-squares regression between the F_func movie and the z-stack
|
|
737
|
+
for each patch at several depths around zpos.
|
|
738
|
+
|
|
739
|
+
Inputs:
|
|
740
|
+
- frame_data: tuple containing the frame number, the frame data, the z-stack,
|
|
741
|
+
the dimensions of the movie, the patch size,
|
|
742
|
+
the step size, the current zpos, and the valid indices.
|
|
743
|
+
|
|
744
|
+
Outputs:
|
|
745
|
+
- results: list of dictionaries containing the r_squared, slope,
|
|
746
|
+
intercept, frame number, patch number, patch x/y limits,
|
|
747
|
+
patch z index, and patch z position.
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
# np.seterr(over='raise')
|
|
751
|
+
|
|
752
|
+
# fram_start_time = time.time()
|
|
753
|
+
|
|
754
|
+
if shift_patches:
|
|
755
|
+
print("Shifting patches option is not tested yet.")
|
|
756
|
+
|
|
757
|
+
frameNum, frame_F_func, Zstack, Nx, Ny, patch_size, step_size, current_zpos, valid_indices = frame_data
|
|
758
|
+
|
|
759
|
+
patch_regress_results = []
|
|
760
|
+
|
|
761
|
+
# Clip Z_frame_shifted to uint16 range
|
|
762
|
+
Z_frame_clipped = np.clip(Zstack, 0, 2**16-1)
|
|
763
|
+
|
|
764
|
+
# Initialize patch number
|
|
765
|
+
patch_num = -1
|
|
766
|
+
|
|
767
|
+
# Loop over patches, moving vertically (along columns) first, then horizontally (along rows)
|
|
768
|
+
for i in range(0, Nx, step_size[1]):
|
|
769
|
+
for j in range(0, Ny, step_size[0]):
|
|
770
|
+
# Allow for a last partial patch on the right and bottom borders, but not more
|
|
771
|
+
if i + patch_size[1] > Nx and patch_size_x < patch_size[1] and patch_size_y < patch_size[0]:
|
|
772
|
+
continue
|
|
773
|
+
if j + patch_size[0] > Ny and patch_size_y < patch_size[0]:
|
|
774
|
+
continue
|
|
775
|
+
# Adjust the patch size for patches on the bottom and right borders
|
|
776
|
+
patch_size_x = patch_size[1] if i + patch_size[1] <= Nx else Nx - i
|
|
777
|
+
patch_size_y = patch_size[0] if j + patch_size[0] <= Ny else Ny - j
|
|
778
|
+
|
|
779
|
+
# Get the patch from the frame data
|
|
780
|
+
T_patch = frame_F_func[j:j+patch_size_y, i:i+patch_size_x, ].ravel()
|
|
781
|
+
# Increment patch number
|
|
782
|
+
patch_num += 1
|
|
783
|
+
|
|
784
|
+
# print(f"Frame {frameNum}, Patch {patch_num}, Patch Size: {patch_size_y}x{patch_size_x}")
|
|
785
|
+
|
|
786
|
+
if shift_patches:
|
|
787
|
+
# Define the maximum shift in pixels (both directions)
|
|
788
|
+
max_shift = 3
|
|
789
|
+
best_corr = -np.inf
|
|
790
|
+
best_shift = (0, 0)
|
|
791
|
+
|
|
792
|
+
# Initialize results list
|
|
793
|
+
patch_results = []
|
|
794
|
+
# Loop over valid depth indices
|
|
795
|
+
for z_idx in valid_indices:
|
|
796
|
+
Z_patch = Z_frame_clipped[j:j+patch_size_y, i:i+patch_size_x, z_idx].ravel()
|
|
797
|
+
|
|
798
|
+
if T_patch.size == Z_patch.size:
|
|
799
|
+
# If shift_patches is True, find the best shift for T_patch
|
|
800
|
+
if shift_patches:
|
|
801
|
+
# Brute-force search for the best (dy, dx) shift that maximizes correlation
|
|
802
|
+
for dy in range(-max_shift, max_shift + 1):
|
|
803
|
+
for dx in range(-max_shift, max_shift + 1):
|
|
804
|
+
shifted_img = np.roll(T_patch, shift=(dy, dx), axis=(0, 1))
|
|
805
|
+
# Compute correlation coefficient between shifted T_patch and Z_patch
|
|
806
|
+
corr = np.corrcoef(shifted_img.ravel(), Z_patch.ravel())[0, 1]
|
|
807
|
+
if corr > best_corr:
|
|
808
|
+
best_corr = corr
|
|
809
|
+
best_shift = (dy, dx)
|
|
810
|
+
|
|
811
|
+
# Apply the best shift to T_patch_img and flatten it for regression
|
|
812
|
+
T_patch = np.roll(T_patch, shift=best_shift, axis=(0, 1)).ravel()
|
|
813
|
+
|
|
814
|
+
_, _, r_value, _, _ = linregress(T_patch, Z_patch)
|
|
815
|
+
# try:
|
|
816
|
+
patch_results.append({
|
|
817
|
+
'r_squared': r_value**2, 'Z_patch': Z_patch,
|
|
818
|
+
'frame_num': frameNum, 'patch_number': patch_num,
|
|
819
|
+
'patch_x_lims': [i, i+patch_size_x], 'patch_y_lims': [j, j+patch_size_y],
|
|
820
|
+
'patch_z_idx': int(z_idx) - int(current_zpos), 'patch_z_pos': z_idx
|
|
821
|
+
})
|
|
822
|
+
if shift_patches:
|
|
823
|
+
# Append the best shift to the results
|
|
824
|
+
patch_results[-1]['best_shift'] = best_shift
|
|
825
|
+
|
|
826
|
+
# not needed: 'slope': slope, 'intercept': intercept,
|
|
827
|
+
|
|
828
|
+
# except FloatingPointError:
|
|
829
|
+
# print("Overflow Error")
|
|
830
|
+
# # print(f"z_idx: {z_idx}, current_zpos: {current_zpos}")
|
|
831
|
+
|
|
832
|
+
# Smooth R^2 values over valid depth indices
|
|
833
|
+
r_squares = [result['r_squared'] for result in patch_results]
|
|
834
|
+
r_squares = gaussian_filter(r_squares, sigma=1)
|
|
835
|
+
# Get the max R^2 value's index
|
|
836
|
+
max_r_square_idx = np.argmax(r_squares)
|
|
837
|
+
# Append the result corresponding to the max R^2 value to patch_regress_results
|
|
838
|
+
patch_regress_results.append(patch_results[max_r_square_idx])
|
|
839
|
+
|
|
840
|
+
# frame_end_time = time.time()
|
|
841
|
+
# print(f"Frame {frameNum} processed in {frame_end_time - fram_start_time:.2f} seconds")
|
|
842
|
+
|
|
843
|
+
return patch_regress_results
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def calculate_zones(patch_correlations, Ny, Nx):
|
|
847
|
+
"""
|
|
848
|
+
Find zones (i.e., how patches overlap) in the patch_correlations table.
|
|
849
|
+
|
|
850
|
+
Inputs:
|
|
851
|
+
- patch_correlations: DataFrame containing the patch correlations.
|
|
852
|
+
- Ny: int, the number of columns in the movie.
|
|
853
|
+
- Nx: int, the number of rows in the movie.
|
|
854
|
+
|
|
855
|
+
Outputs:
|
|
856
|
+
- zone_pattern_contig: 2D numpy array containing the zone pattern with continuous zone IDs.
|
|
857
|
+
- zone_pattern: 2D numpy array containing the zone pattern with non-contiguous unique zone IDs, for display purposes.
|
|
858
|
+
"""
|
|
859
|
+
start_time = time.time()
|
|
860
|
+
|
|
861
|
+
# Initialize a mask for zone calculation
|
|
862
|
+
mask = np.zeros((Ny, Nx), dtype=int)
|
|
863
|
+
|
|
864
|
+
# Use the first frame's patches to define zones
|
|
865
|
+
first_frame_patches = patch_correlations[patch_correlations['frame_num'] == patch_correlations['frame_num'].min()]
|
|
866
|
+
for _, row in first_frame_patches.iterrows():
|
|
867
|
+
x_start, x_end = row['patch_x_lims']
|
|
868
|
+
y_start, y_end = row['patch_y_lims']
|
|
869
|
+
mask[y_start:y_end, x_start:x_end] += 1
|
|
870
|
+
|
|
871
|
+
# Label zones based on overlapping patches. Assign a unique number to each zone
|
|
872
|
+
|
|
873
|
+
# Initialize the zone_pattern array to store zone identifiers
|
|
874
|
+
zone_pattern = np.zeros((Ny, Nx), dtype=int)
|
|
875
|
+
|
|
876
|
+
# Change the mask values from 4 to 3 (even to odd)
|
|
877
|
+
mask = np.where(mask == 4, 3, mask)
|
|
878
|
+
# Process for odd values (mask values that are odd become 1, others 0)
|
|
879
|
+
binary_mask_odd = (mask % 2 == 1).astype(int)
|
|
880
|
+
labeled_zones_odd, num_zones_odd = label(binary_mask_odd)
|
|
881
|
+
|
|
882
|
+
# Assign unique labels to zone_pattern for odd zones
|
|
883
|
+
zone_id_offset = 0 # Start zone IDs from 1 for odd zones
|
|
884
|
+
for i in range(1, num_zones_odd + 1):
|
|
885
|
+
zone_pattern[labeled_zones_odd == i] = zone_id_offset + i
|
|
886
|
+
|
|
887
|
+
# Update the offset for even zones to continue unique labeling
|
|
888
|
+
zone_id_offset += num_zones_odd
|
|
889
|
+
|
|
890
|
+
# Process for even values (mask values that are even and non-zero become 1, others 0)
|
|
891
|
+
binary_mask_even = ((mask % 2 == 0) & (mask != 0)).astype(int)
|
|
892
|
+
labeled_zones_even, num_zones_even = label(binary_mask_even)
|
|
893
|
+
|
|
894
|
+
# Assign unique labels to zone_pattern for even zones
|
|
895
|
+
for i in range(1, num_zones_even + 1):
|
|
896
|
+
zone_pattern[labeled_zones_even == i] = zone_id_offset + i
|
|
897
|
+
|
|
898
|
+
# Flatten the array column-wise
|
|
899
|
+
zone_pattern_col_flat = zone_pattern.flatten(order='F')
|
|
900
|
+
|
|
901
|
+
# Get the unique zones and their unique indices
|
|
902
|
+
_, inverse_indices = np.unique(zone_pattern_col_flat, return_inverse=True)
|
|
903
|
+
unique_indices = pd.unique(inverse_indices) + 1
|
|
904
|
+
|
|
905
|
+
# Re-label zones to have continuous IDs
|
|
906
|
+
zone_pattern_contig = np.zeros_like(zone_pattern, dtype=np.uint16)
|
|
907
|
+
for new_id, zone_id in enumerate(unique_indices):
|
|
908
|
+
zone_pattern_contig[zone_pattern == zone_id] = new_id
|
|
909
|
+
|
|
910
|
+
print(f"Patches divided into zones in {time.time() - start_time:.2f} seconds")
|
|
911
|
+
|
|
912
|
+
return zone_pattern_contig, zone_pattern
|
|
913
|
+
|
|
914
|
+
def make_composite_f_anat(patch_correlations, labeled_zones):
|
|
915
|
+
"""
|
|
916
|
+
Create a composite anatomical image from the patch correlations DataFrame.
|
|
917
|
+
|
|
918
|
+
Inputs:
|
|
919
|
+
- patch_correlations_df: DataFrame containing the patch correlations.
|
|
920
|
+
- labeled_zones: 2D numpy array containing the labeled zones.
|
|
921
|
+
|
|
922
|
+
Outputs:
|
|
923
|
+
- F_anat_non_rigid: 3D numpy array containing the composite anatomical image.
|
|
924
|
+
- zone_df: DataFrame containing the zone data.
|
|
925
|
+
"""
|
|
926
|
+
|
|
927
|
+
fields = ['r_squared', 'patch_number', 'patch_z_pos', 'Z_patch'] #'patch_x_shift', 'patch_y_shift',
|
|
928
|
+
|
|
929
|
+
# start_time = time.time()
|
|
930
|
+
|
|
931
|
+
zone_data = []
|
|
932
|
+
|
|
933
|
+
F_anat_non_rigid = np.zeros((len(np.unique(patch_correlations['frame_num'])), *labeled_zones.shape))
|
|
934
|
+
|
|
935
|
+
# Iterate over frames
|
|
936
|
+
for frame_idx, (frame_num, group) in enumerate(patch_correlations.groupby('frame_num')):
|
|
937
|
+
# `frame_num` is the current frame number
|
|
938
|
+
# `group` is a DataFrame containing all rows for `frame_num`
|
|
939
|
+
|
|
940
|
+
sums = {field: np.zeros((np.max(labeled_zones)+1,)) for field in fields if field != 'Z_patch'}
|
|
941
|
+
sums['Z_patch'] = {zone_id: [] for zone_id in np.unique(labeled_zones)}
|
|
942
|
+
|
|
943
|
+
count = np.zeros((np.max(labeled_zones)+1,))
|
|
944
|
+
|
|
945
|
+
composite_f_anat_frame = np.zeros(labeled_zones.shape)
|
|
946
|
+
|
|
947
|
+
# Populate sums and counts per zone
|
|
948
|
+
for _, row in group.iterrows():
|
|
949
|
+
# `index` is the index of the row in `patch_correlations_df`
|
|
950
|
+
# `row` is a Series containing the data for the current row
|
|
951
|
+
|
|
952
|
+
x_start, x_end = row['patch_x_lims']
|
|
953
|
+
y_start, y_end = row['patch_y_lims']
|
|
954
|
+
|
|
955
|
+
# Extract the patch area from the labeled_zones
|
|
956
|
+
patch_2D = row['Z_patch'].reshape(y_end - y_start, x_end - x_start)
|
|
957
|
+
|
|
958
|
+
patch_zone_labels = labeled_zones[y_start:y_end, x_start:x_end]
|
|
959
|
+
zone_ids = np.unique(patch_zone_labels)
|
|
960
|
+
|
|
961
|
+
for zone_id in zone_ids:
|
|
962
|
+
# Find coordinates of the current zone_id within the patch
|
|
963
|
+
zone_mask = (patch_zone_labels == zone_id)
|
|
964
|
+
# Get bounding box of the zone for 2d zone_specific_patch
|
|
965
|
+
rows, cols = np.where(zone_mask)
|
|
966
|
+
min_row, max_row = rows.min(), rows.max()
|
|
967
|
+
min_col, max_col = cols.min(), cols.max()
|
|
968
|
+
for field in fields:
|
|
969
|
+
if field == 'Z_patch':
|
|
970
|
+
# Extract the part of Z_patch that corresponds to the current zone_id.
|
|
971
|
+
# This returns a 1D array, ordered row-wise.
|
|
972
|
+
# zone_specific_patch = patch_2D[zone_mask]
|
|
973
|
+
# in 2D for sanity check
|
|
974
|
+
zone_specific_patch = patch_2D[min_row:max_row+1, min_col:max_col+1]
|
|
975
|
+
sums[field][zone_id].append(zone_specific_patch)
|
|
976
|
+
else:
|
|
977
|
+
sums[field][zone_id] += row[field]
|
|
978
|
+
count[zone_id] += 1
|
|
979
|
+
|
|
980
|
+
# Calculate averages for each zone
|
|
981
|
+
averages = {field: np.zeros((np.max(labeled_zones)+1,)) for field in fields if field != 'Z_patch'}
|
|
982
|
+
averages['Z_patch'] = {}
|
|
983
|
+
|
|
984
|
+
for field in sums:
|
|
985
|
+
if field == 'Z_patch':
|
|
986
|
+
for zone_id, arrays in sums[field].items():
|
|
987
|
+
try:
|
|
988
|
+
if arrays:
|
|
989
|
+
averages[field][zone_id] = np.mean(arrays, axis=0)
|
|
990
|
+
except Exception as e:
|
|
991
|
+
# print(e)
|
|
992
|
+
print(f"Frame {frame_num}, Zone {zone_id}")
|
|
993
|
+
else:
|
|
994
|
+
averages[field] = np.divide(sums[field], count, where=(count > 0))
|
|
995
|
+
|
|
996
|
+
# Collect data per zone
|
|
997
|
+
unique_zones = np.unique(labeled_zones)
|
|
998
|
+
for zone_id in unique_zones:
|
|
999
|
+
zone_info = {'frame_num': frame_num, 'zone_id': zone_id}
|
|
1000
|
+
if count[zone_id] > 0:
|
|
1001
|
+
y_indices, x_indices = np.where(labeled_zones == zone_id)
|
|
1002
|
+
y_min, y_max = y_indices.min(), y_indices.max() + 1
|
|
1003
|
+
x_min, x_max = x_indices.min(), x_indices.max() + 1
|
|
1004
|
+
for field in fields[:-1]:
|
|
1005
|
+
zone_info[field] = averages[field][zone_id]
|
|
1006
|
+
zone_data.append(zone_info)
|
|
1007
|
+
# reshape Zpatch to 2D and insert into composite_f_anat_frame
|
|
1008
|
+
if 'Z_patch' in averages and zone_id in averages['Z_patch']:
|
|
1009
|
+
# zone_2D = averages['Z_patch'][zone_id].reshape((y_max - y_min, x_max - x_min))
|
|
1010
|
+
composite_f_anat_frame[y_min:y_max, x_min:x_max] = averages['Z_patch'][zone_id]
|
|
1011
|
+
|
|
1012
|
+
# When this is the first zone in the first frame, print some info
|
|
1013
|
+
# if frame_idx == 0 and zone_id == unique_zones[0]:
|
|
1014
|
+
# print(f"Frame {frame_num}, Zone {zone_id}:")
|
|
1015
|
+
# print(f" R^2: {averages['r_squared'][zone_id]:.4f}")
|
|
1016
|
+
# print(f" Patch number: {averages['patch_number'][zone_id]}")
|
|
1017
|
+
# print(f" Patch z position: {averages['patch_z_pos'][zone_id]}")
|
|
1018
|
+
# print(f" Patch x limits: {x_min}, {x_max}")
|
|
1019
|
+
# print(f" Patch y limits: {y_min}, {y_max}")
|
|
1020
|
+
# print(f" Range of values: {np.min(averages['Z_patch'][zone_id]):.2f} to {np.max(averages['Z_patch'][zone_id]):.2f}")
|
|
1021
|
+
|
|
1022
|
+
F_anat_non_rigid[frame_idx, :, :] = composite_f_anat_frame
|
|
1023
|
+
|
|
1024
|
+
# print(f"Zone data computed in {time.time() - start_time:.2f} seconds")
|
|
1025
|
+
|
|
1026
|
+
zone_df = pd.DataFrame(zone_data)
|
|
1027
|
+
|
|
1028
|
+
return F_anat_non_rigid, zone_df
|
|
1029
|
+
|
|
1030
|
+
def format_patch_correl_for_mat(zone_df):
|
|
1031
|
+
|
|
1032
|
+
# Get fields from the dataframe
|
|
1033
|
+
fields = zone_df.columns[zone_df.columns != 'zone_id']
|
|
1034
|
+
|
|
1035
|
+
# Pivot to create a 3D structure: zones * fields * frames
|
|
1036
|
+
data_3d = zone_df.set_index(['zone_id', 'frame_num']).unstack(fill_value=0).to_numpy()
|
|
1037
|
+
data_3d = data_3d.reshape((len(zone_df['zone_id'].unique()), len(fields), -1))
|
|
1038
|
+
|
|
1039
|
+
# Create descriptors for each field
|
|
1040
|
+
field_descriptors = {field: 'Average ' + field for field in fields}
|
|
1041
|
+
|
|
1042
|
+
# Prepare data for MAT file
|
|
1043
|
+
mat_data = {
|
|
1044
|
+
'zone_data': data_3d,
|
|
1045
|
+
'descriptors': field_descriptors
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return mat_data
|
|
1049
|
+
|
|
1050
|
+
def compute_zone_mean_fluorescence(fov_image, labeled_zones, zone_data):
|
|
1051
|
+
"""
|
|
1052
|
+
Loop over each zones' pixel region and get the average fluorescence value for each zone from the FOV image
|
|
1053
|
+
"""
|
|
1054
|
+
zone_mean_fluorescence = []
|
|
1055
|
+
zone_rsquare = []
|
|
1056
|
+
|
|
1057
|
+
for zone_id in range(np.max(labeled_zones) + 1):
|
|
1058
|
+
zone_indices = np.where(labeled_zones == zone_id)
|
|
1059
|
+
# Get the mean fluorescence value for that zone
|
|
1060
|
+
zone_mean_fluorescence.append(np.mean(fov_image[zone_indices]))
|
|
1061
|
+
# Get the R2 value for that zone
|
|
1062
|
+
zone_rsquare.append(zone_data[zone_data['zone_id'] == zone_id]['r_squared'].values[0])
|
|
1063
|
+
|
|
1064
|
+
return zone_mean_fluorescence, zone_rsquare
|
|
1065
|
+
|
|
1066
|
+
def patch_correl_plots(patch_correlations_df, labeled_zones, zone_df, zone_pattern, fov_image, z_correlation, export_path):
|
|
1067
|
+
"""
|
|
1068
|
+
Summary plots
|
|
1069
|
+
Plot the zone pattern, the R^2 heatmap, the distribution of patch z indices,
|
|
1070
|
+
the distribution of R^2 values from compute_zcorrel, the R^2 histogram,
|
|
1071
|
+
the curve of average R2 values as a function of fluorescence, and an example zone.
|
|
1072
|
+
"""
|
|
1073
|
+
|
|
1074
|
+
# Create subplots
|
|
1075
|
+
fig, ax = plt.subplots(2, 4, figsize=(20, 10))
|
|
1076
|
+
|
|
1077
|
+
# Plot the zone pattern
|
|
1078
|
+
ax[0, 0].imshow(zone_pattern, cmap='plasma')
|
|
1079
|
+
ax[0, 0].set_xticks([])
|
|
1080
|
+
ax[0, 0].set_yticks([])
|
|
1081
|
+
ax[0, 0].set_title('Patch overlap zone pattern')
|
|
1082
|
+
|
|
1083
|
+
# Plot the R^2 heatmap
|
|
1084
|
+
# plot zone_data's average r_squared values as a heatmap
|
|
1085
|
+
dim_num_zones = int(np.sqrt(np.max(labeled_zones) + 1))
|
|
1086
|
+
|
|
1087
|
+
# Get zone_df data corresponding to the first frame
|
|
1088
|
+
frame_num = zone_df['frame_num'].min()
|
|
1089
|
+
zone_data = zone_df[zone_df['frame_num'] == frame_num]
|
|
1090
|
+
|
|
1091
|
+
r_squared_values = zone_data['r_squared'].values.reshape((dim_num_zones, dim_num_zones), order='F')
|
|
1092
|
+
im = ax[1, 0].imshow(r_squared_values, cmap='viridis')
|
|
1093
|
+
fig.colorbar(im, ax=ax[1, 0])
|
|
1094
|
+
ax[1, 0].set_title(f"Average $R^2$ values per zone for frame {frame_num}")
|
|
1095
|
+
|
|
1096
|
+
# Plot distribution of correlation for all patches across depth
|
|
1097
|
+
patch_z_idx = patch_correlations_df['patch_z_idx']
|
|
1098
|
+
bin_num = len(np.unique(patch_z_idx))
|
|
1099
|
+
bin_edges = np.arange(-bin_num/2, bin_num//2 + 1, 1)
|
|
1100
|
+
ax[0, 1].hist(patch_z_idx, bins=bin_edges, edgecolor='black')
|
|
1101
|
+
ax[0, 1].set_xlabel('Patch z index')
|
|
1102
|
+
ax[0, 1].set_ylabel('Frequency')
|
|
1103
|
+
ax[0, 1].set_title('Distribution of patch z indices, w/r to zpos')
|
|
1104
|
+
ax[0, 1].set_xticks(np.arange(-bin_num//2 + 1, bin_num//2 + 1, 1))
|
|
1105
|
+
|
|
1106
|
+
# Plot the distribution of R^2 values from compute_zcorrel
|
|
1107
|
+
zcorrel = z_correlation['zcorr']
|
|
1108
|
+
max_vals = np.max(zcorrel, axis=0)
|
|
1109
|
+
rsquare_corr = np.square(max_vals)
|
|
1110
|
+
ax[1, 1].hist(rsquare_corr, bins=50, edgecolor='black')
|
|
1111
|
+
ax[1, 1].set_xlabel('$R^2$ value')
|
|
1112
|
+
ax[1, 1].set_ylabel('Frequency')
|
|
1113
|
+
ax[1, 1].set_title('Distribution of $R^2$ values from compute_zcorrel (whole frames)')
|
|
1114
|
+
|
|
1115
|
+
# Plot the R2 histogram
|
|
1116
|
+
patch_rsquare_vals = patch_correlations_df['r_squared']
|
|
1117
|
+
ax[0, 2].hist(patch_rsquare_vals, bins=50, label='R-squared', edgecolor='black', color='blue') #alpha=0.5
|
|
1118
|
+
ax[0, 2].set_xlabel('$R^2$ value')
|
|
1119
|
+
ax[0, 2].set_ylabel('Frequency')
|
|
1120
|
+
ax[0, 2].set_title('Distribution of $R^2$ values for patches')
|
|
1121
|
+
|
|
1122
|
+
# Plot the curve of average R2 values as a function of fluorescence
|
|
1123
|
+
zone_mean_fluorescence, zone_rsquare = compute_zone_mean_fluorescence(fov_image, labeled_zones, zone_df)
|
|
1124
|
+
ax[1, 2].scatter(zone_mean_fluorescence, zone_rsquare, alpha=0.5, edgecolor='black', s=10, facecolors='none', marker='o')
|
|
1125
|
+
ax[1, 2].set_xlabel('Average Fluorescence')
|
|
1126
|
+
ax[1, 2].set_ylabel('Average $R^2$')
|
|
1127
|
+
ax[1, 2].set_title('Average $R^2$ vs Average Fluorescence')
|
|
1128
|
+
|
|
1129
|
+
# Plot an example zone
|
|
1130
|
+
zone_id = np.argmax(zone_mean_fluorescence)
|
|
1131
|
+
zone_indices = np.where(labeled_zones == zone_id)
|
|
1132
|
+
min_row, max_row = np.min(zone_indices[0]), np.max(zone_indices[0])
|
|
1133
|
+
min_col, max_col = np.min(zone_indices[1]), np.max(zone_indices[1])
|
|
1134
|
+
ax[0, 3].imshow(fov_image, cmap='gray')
|
|
1135
|
+
rect = patches.Rectangle((min_col, min_row), max_col-min_col+1, max_row-min_row+1, linewidth=1, edgecolor='r', facecolor='none')
|
|
1136
|
+
ax[0, 3].add_patch(rect)
|
|
1137
|
+
ax[0, 3].axis('off')
|
|
1138
|
+
ax[0, 3].set_title(f"FOV with Zone {zone_id} Highlighted")
|
|
1139
|
+
|
|
1140
|
+
# Plot the zone fluorescence
|
|
1141
|
+
zone_fluorescence = fov_image[zone_indices]
|
|
1142
|
+
fluorescence_image = np.zeros((max_row - min_row + 1, max_col - min_col + 1))
|
|
1143
|
+
for i, row in enumerate(zone_indices[0]):
|
|
1144
|
+
col = zone_indices[1][i]
|
|
1145
|
+
fluorescence_image[row - min_row, col - min_col] = zone_fluorescence[i]
|
|
1146
|
+
ax[1, 3].imshow(fluorescence_image, cmap='gray')
|
|
1147
|
+
ax[1, 3].axis('off')
|
|
1148
|
+
ax[1, 3].set_title(f"Zone {zone_id} Fluorescence")
|
|
1149
|
+
|
|
1150
|
+
# Save the figure
|
|
1151
|
+
plt.tight_layout()
|
|
1152
|
+
plot_export_path = export_path / 'plots'
|
|
1153
|
+
plot_export_path.mkdir(parents=True, exist_ok=True)
|
|
1154
|
+
plt.savefig(plot_export_path / 'patches_summary_plots.png')
|
|
1155
|
+
|
|
1156
|
+
# Close the figure
|
|
1157
|
+
plt.close(fig)
|
|
1158
|
+
|
|
1159
|
+
print(f"Summary plots saved to {plot_export_path}")
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def fit_huber_regressor(i_pixel, F_anat, F_anat_mean, F_func, F_func_mean):
|
|
1163
|
+
"""
|
|
1164
|
+
Fit a Huber regressor to find the scaling factor
|
|
1165
|
+
"""
|
|
1166
|
+
huber = HuberRegressor(fit_intercept=False)
|
|
1167
|
+
try:
|
|
1168
|
+
huber.fit(F_anat[i_pixel, :].reshape(-1, 1) - F_anat_mean[i_pixel], F_func[i_pixel, :] - F_func_mean[i_pixel])
|
|
1169
|
+
b = huber.coef_[0]
|
|
1170
|
+
except ValueError as e:
|
|
1171
|
+
if "HuberRegressor convergence failed" in str(e):
|
|
1172
|
+
b = 0
|
|
1173
|
+
print(f"Convergence warning for pixel {i_pixel}. Setting b[i_pixel] to 0.")
|
|
1174
|
+
else:
|
|
1175
|
+
raise e
|
|
1176
|
+
return b
|
|
1177
|
+
|
|
1178
|
+
# def fit_huber_regressor(F_anat_pixel, F_func_pixel):
|
|
1179
|
+
# """
|
|
1180
|
+
# Fit a Huber regressor to find the scaling factor for a given pixel
|
|
1181
|
+
# """
|
|
1182
|
+
# huber = HuberRegressor(fit_intercept=False)
|
|
1183
|
+
# try:
|
|
1184
|
+
# huber.fit(F_anat_pixel.reshape(-1, 1), F_func_pixel)
|
|
1185
|
+
# b = huber.coef_[0]
|
|
1186
|
+
# except ValueError as e:
|
|
1187
|
+
# if "HuberRegressor convergence failed" in str(e):
|
|
1188
|
+
# b = 0
|
|
1189
|
+
# print(f"Convergence warning for pixel. Setting b to 0.")
|
|
1190
|
+
# else:
|
|
1191
|
+
# raise e
|
|
1192
|
+
# return b
|
|
1193
|
+
|
|
1194
|
+
def fit_huber_regressor_on_region(F_anat, F_anat_mean, F_func, F_func_mean, pbar=None):
|
|
1195
|
+
"""
|
|
1196
|
+
Fit a Huber regressor to find the scaling factor for a given zone
|
|
1197
|
+
"""
|
|
1198
|
+
huber = HuberRegressor(fit_intercept=False)
|
|
1199
|
+
try:
|
|
1200
|
+
if np.isscalar(F_anat_mean) or len(F_anat_mean) == 1:
|
|
1201
|
+
X = F_anat.reshape(-1, 1) - F_anat_mean
|
|
1202
|
+
y = F_func - F_func_mean
|
|
1203
|
+
else:
|
|
1204
|
+
# Flatten the regions
|
|
1205
|
+
X = (F_anat - F_anat_mean[:, np.newaxis]).ravel().reshape(-1, 1)
|
|
1206
|
+
y = (F_func - F_func_mean[:, np.newaxis]).ravel()
|
|
1207
|
+
huber.fit(X, y)
|
|
1208
|
+
b = huber.coef_[0]
|
|
1209
|
+
except ValueError as e:
|
|
1210
|
+
if "HuberRegressor convergence failed" in str(e):
|
|
1211
|
+
b = 0
|
|
1212
|
+
print(f"Convergence warning. Setting b to 0.")
|
|
1213
|
+
else:
|
|
1214
|
+
raise e
|
|
1215
|
+
if pbar is not None:
|
|
1216
|
+
pbar.update()
|
|
1217
|
+
return b
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
# def subtract_z_motion_lr_frames(F_func, F_anat):
|
|
1221
|
+
# """
|
|
1222
|
+
# Subtract the movement-induced changes from the functional movie using linear regression.
|
|
1223
|
+
# """
|
|
1224
|
+
# # Compute fit between F_anat_non_rigid and F_func using linear regression
|
|
1225
|
+
# print("Computing fit between F_anat_non_rigid and F_func using linear regression")
|
|
1226
|
+
# F_func_flattened = F_func.ravel()
|
|
1227
|
+
# F_anat_non_rigid_flattened = F_anat.ravel()
|
|
1228
|
+
# F_func_mean = np.mean(F_func_flattened)
|
|
1229
|
+
# F_anat_non_rigid_mean = np.mean(F_anat_non_rigid_flattened)
|
|
1230
|
+
# # Use linregress to model F_func = slope * F_anat_non_rigid + intercept.
|
|
1231
|
+
# slope, intercept, r_value, _, _ = linregress(F_anat_non_rigid_flattened - F_anat_non_rigid_mean, F_func_flattened - F_func_mean)
|
|
1232
|
+
# z_motion_scaling_factors = {'slope': slope, 'intercept': intercept}
|
|
1233
|
+
# print(f"R^2 value between F_func and F_anat_non_rigid: {r_value**2}")
|
|
1234
|
+
# print(f"Slope: {slope}, Intercept: {intercept}")
|
|
1235
|
+
|
|
1236
|
+
# # Rescale and subtract the movement-induced changes from F_func
|
|
1237
|
+
# print("Rescaling and subtracting the movement-induced changes from F_func")
|
|
1238
|
+
# # We do not add intercept here, because that would bring the mean of F_corrected to 0, which is not desired when computing dF/F later.
|
|
1239
|
+
# # Instead, we subtract the mean of F_anat_non_rigid, which is a better approach.
|
|
1240
|
+
# F_anat_non_rigid_adjusted = slope * (F_anat - np.mean(F_anat, axis=0))
|
|
1241
|
+
# F_corrected = F_func - F_anat_non_rigid_adjusted
|
|
1242
|
+
# print(f"F_corrected shape: {F_corrected.shape}, data type: {F_corrected.dtype}, min: {F_corrected.min()}, max: {F_corrected.max()}")
|
|
1243
|
+
# # Clip F_corrected to uint16 range
|
|
1244
|
+
# F_corrected = np.clip(F_corrected, 0, 2**16-1)
|
|
1245
|
+
|
|
1246
|
+
# return F_corrected, z_motion_scaling_factors
|
|
1247
|
+
|
|
1248
|
+
# def subtract_z_motion_hr_frames(F_func, F_anat, smoothing_factor=1):
|
|
1249
|
+
|
|
1250
|
+
# # Get the dimensions of the F_func and F_anat movies (must be the same)
|
|
1251
|
+
# Nframe, Ny, Nx = F_func.shape
|
|
1252
|
+
|
|
1253
|
+
# if Nframe != F_anat.shape[0]:
|
|
1254
|
+
# raise ValueError("The number of frames in the functional and anatomical movies must be the same.")
|
|
1255
|
+
|
|
1256
|
+
# # Smooth the F_func and F_anat movies in x and y but not T
|
|
1257
|
+
# F_func = gaussian_filter(F_func, sigma=[0, smoothing_factor, smoothing_factor])
|
|
1258
|
+
# F_anat = gaussian_filter(F_anat, sigma=[0, smoothing_factor, smoothing_factor])
|
|
1259
|
+
|
|
1260
|
+
# # # Reshape the "functional" fluorescence movie
|
|
1261
|
+
# # F_func = F_func.reshape(Nframe, Ny * Nx).T
|
|
1262
|
+
|
|
1263
|
+
# # # Get the average image of the functional fluorescence movie
|
|
1264
|
+
# # F_func_mean = np.mean(F_func, axis=1)
|
|
1265
|
+
|
|
1266
|
+
# # # Reshape the "anatomical" fluorescence movie
|
|
1267
|
+
# # F_anat = F_anat.reshape(Nframe, Ny * Nx).T
|
|
1268
|
+
|
|
1269
|
+
# # # Get the average image of the z-stack movie
|
|
1270
|
+
# # F_anat_mean = np.mean(F_anat, axis=1)
|
|
1271
|
+
|
|
1272
|
+
# F_func_flattened = F_func.ravel()
|
|
1273
|
+
# F_anat_flattened = F_anat.ravel()
|
|
1274
|
+
# F_func_mean = np.mean(F_func_flattened)
|
|
1275
|
+
# F_anat_mean = np.mean(F_anat_flattened)
|
|
1276
|
+
|
|
1277
|
+
# # Fit a linear regression model between the non-rigid F_anat and the F_func movie, using fit_huber_regressor_on_region
|
|
1278
|
+
# with tqdm(total=1, desc='Fitting regressors on frames to find z motion scaling factors') as pbar:
|
|
1279
|
+
# z_motion_scaling_factors = fit_huber_regressor_on_region(F_anat_flattened, F_anat_mean, F_func_flattened, F_func_mean, pbar)
|
|
1280
|
+
|
|
1281
|
+
# z_motion_scaling_factors = np.array(z_motion_scaling_factors).astype(np.float32)
|
|
1282
|
+
# print(f"z_motion_scaling_factors: {z_motion_scaling_factors}")
|
|
1283
|
+
|
|
1284
|
+
# # Rescale and subtract the movement-induced changes from the original F matrix
|
|
1285
|
+
# F_anat_rescaled = z_motion_scaling_factors * (F_anat - F_anat_mean)
|
|
1286
|
+
# # F_anat_rescaled = z_motion_scaling_factors * (F_anat_flattened - F_anat_mean)
|
|
1287
|
+
|
|
1288
|
+
# # Subtract the rescaled F_anat from the F_func movie
|
|
1289
|
+
# print("Rescaling and subtracting the movement-induced changes from F_func")
|
|
1290
|
+
# F_corrected = F_func - F_anat_rescaled
|
|
1291
|
+
# # Fcorrected = F_func_flattened - F_anat_rescaled
|
|
1292
|
+
# # Fcorrected = Fcorrected.reshape(F_func.shape)
|
|
1293
|
+
|
|
1294
|
+
# print(f"F_corrected shape: {F_corrected.shape}, data type: {F_corrected.dtype}, min: {F_corrected.min()}, max: {F_corrected.max()}")
|
|
1295
|
+
# # Clip F_corrected to uint16 range
|
|
1296
|
+
# F_corrected = np.clip(F_corrected, 0, 2**16-1)
|
|
1297
|
+
|
|
1298
|
+
# # # # Compute R^2 of the correction by frames
|
|
1299
|
+
# # # # 1 – ( sum(Vcorr2) / sum( (Vfunc - <Vfunc>)2 ))
|
|
1300
|
+
# # # # Sum of squares of the residuals
|
|
1301
|
+
# # # sum_resid = np.sum((F_func - F_func_mean) ** 2)
|
|
1302
|
+
# # # # Sum of squares of the corrected values
|
|
1303
|
+
# # # sum_corrected = np.sum(F_corrected ** 2)
|
|
1304
|
+
# # # # Compute the R^2 value
|
|
1305
|
+
# # # R2 = 1 - sum_corrected / sum_resid
|
|
1306
|
+
|
|
1307
|
+
# # # Compute the sum of squared residuals
|
|
1308
|
+
# # sum_res = np.sum(((F_func - F_func_mean) - F_anat_rescaled) ** 2, axis=0)
|
|
1309
|
+
# # # Compute the total sum of squares
|
|
1310
|
+
# # sum_tot = np.sum((F_func - F_func_mean) ** 2, axis=0)
|
|
1311
|
+
# # # Compute the R^2 map
|
|
1312
|
+
# # r_square_map = 1 - sum_res / sum_tot
|
|
1313
|
+
|
|
1314
|
+
# return F_corrected, z_motion_scaling_factors
|
|
1315
|
+
|
|
1316
|
+
# def subtract_z_motion_hr_pixels(F_func, F_anat):
|
|
1317
|
+
# """
|
|
1318
|
+
# Subtract the movement-induced changes from the functional movie using pixel-wise subtraction.
|
|
1319
|
+
# """
|
|
1320
|
+
# # Get the dimensions of the F_func and F_anat movies (must be the same)
|
|
1321
|
+
# Nframe, Ny, Nx = F_func.shape
|
|
1322
|
+
|
|
1323
|
+
# # Reshape the "functional" fluorescence movie
|
|
1324
|
+
# F_func = F_func.reshape(Nframe, Ny * Nx).T
|
|
1325
|
+
|
|
1326
|
+
# # Get the average image of the functional fluorescence movie
|
|
1327
|
+
# F_func_mean = np.mean(F_func, axis=1)
|
|
1328
|
+
|
|
1329
|
+
# # Reshape the "anatomical" fluorescence movie
|
|
1330
|
+
# F_anat = F_anat.reshape(Nframe, Ny * Nx).T
|
|
1331
|
+
|
|
1332
|
+
# # Get the average image of the z-stack movie
|
|
1333
|
+
# F_anat_mean = np.mean(F_anat, axis=1)
|
|
1334
|
+
|
|
1335
|
+
# # Fit a linear regression model between the non-rigid F_anat and the F_func movie
|
|
1336
|
+
# z_motion_scaling_factors = Parallel(n_jobs=-1)(delayed(fit_huber_regressor)
|
|
1337
|
+
# (i_pixel, F_anat, F_anat_mean, F_func, F_func_mean)
|
|
1338
|
+
# for i_pixel in tqdm(range(Ny * Nx),
|
|
1339
|
+
# desc='Fitting regressors to find z motion scaling factors'))
|
|
1340
|
+
|
|
1341
|
+
# # # Subtract mean values before passing to the regressor
|
|
1342
|
+
# # F_anat_adjusted = [F_anat[i_pixel, :] - F_anat_mean[i_pixel] for i_pixel in range(Ny * Nx)]
|
|
1343
|
+
# # F_func_adjusted = [F_func[i_pixel, :] - F_func_mean[i_pixel] for i_pixel in range(Ny * Nx)]
|
|
1344
|
+
|
|
1345
|
+
# # # Fit a linear regression model between the non-rigid F_anat and the F_func movie
|
|
1346
|
+
# # z_motion_scaling_factors = Parallel(n_jobs=-1)(delayed(fit_huber_regressor)
|
|
1347
|
+
# # (F_anat_adjusted[i], F_func_adjusted[i])
|
|
1348
|
+
# # for i in tqdm(range(Ny * Nx),
|
|
1349
|
+
# # desc='Fitting regressors to find z motion scaling factors'))
|
|
1350
|
+
# # Run as regular loop for debugging purposes:
|
|
1351
|
+
# # z_motion_scaling_factors = [fit_huber_regressor(F_anat_adjusted[i], F_func_adjusted[i]) for i in range(Ny * Nx)]
|
|
1352
|
+
|
|
1353
|
+
# # Change the list to a numpy array and cast to float32
|
|
1354
|
+
# z_motion_scaling_factors = np.array(z_motion_scaling_factors).astype(np.float32)
|
|
1355
|
+
# # np.save(export_dir / 'z_motion_scaling_factors_hr_pixels.npy', z_motion_scaling_factors)
|
|
1356
|
+
# # Kill all LokyProcess workers (Windows only)
|
|
1357
|
+
# if os.name == 'nt':
|
|
1358
|
+
# for p in multiprocessing.active_children():
|
|
1359
|
+
# if 'LokyProcess' in p.name:
|
|
1360
|
+
# p.terminate()
|
|
1361
|
+
|
|
1362
|
+
# # Rescale and subtract the movement-induced changes from the original F matrix
|
|
1363
|
+
# F_anat_rescaled = z_motion_scaling_factors[:, np.newaxis] * (F_anat - F_anat_mean[:, np.newaxis])
|
|
1364
|
+
# Fcorrected = F_func - F_anat_rescaled
|
|
1365
|
+
|
|
1366
|
+
# # Reshape the corrected F matrix to the original shape
|
|
1367
|
+
# Fcorrected = Fcorrected.T.reshape(Nframe, Ny, Nx)
|
|
1368
|
+
# Fcorrected = np.clip(Fcorrected, 0, 2**16-1)
|
|
1369
|
+
|
|
1370
|
+
# # print(f"F_corrected shape: {F_corrected.shape}, data type: {F_corrected.dtype}, min: {F_corrected.min()}, max: {F_corrected.max()}")
|
|
1371
|
+
|
|
1372
|
+
# # # Compute R^2 of the correction for each pixel
|
|
1373
|
+
# # # # 1 – ( sum(Fxycorr(t)2)xyt / sum( (Fxyfunc(t) - <Fxyfunc(t)>t)2 )xyt )
|
|
1374
|
+
# # # # Total sum of squares, proportional to the variance of the data. We use the residuals of F_func when we remove the mean of F_func)
|
|
1375
|
+
# # # sum_tot = np.sum((F_func - F_func_mean[:, np.newaxis]) ** 2)
|
|
1376
|
+
# # # # Sum of squares of the corrected values residuals (meaning residuals of F_func when we remove the corrected F_anat)
|
|
1377
|
+
# # # sum_res = np.sum(Fcorrected ** 2)
|
|
1378
|
+
# # # # sum_res = np.sum((F_func - Fcorrected.T.reshape(Nframe, Ny * Nx).T) ** 2)
|
|
1379
|
+
# # # # Compute the R^2 value
|
|
1380
|
+
# # # R2 = 1 - sum_res / sum_tot
|
|
1381
|
+
|
|
1382
|
+
# # # R2 = 1 – sum((Fxyfunc(t) - <Fxyfunc(t)>t) - bxy * (Fxyanat(t) - <Fxyanat(t)>t))^2)
|
|
1383
|
+
# # # / sum((Fxyfunc(t) - <Fxyfunc(t)>t)^2)
|
|
1384
|
+
|
|
1385
|
+
# # # Compute the sum of squared residuals
|
|
1386
|
+
# # sum_res = np.sum(((F_func - F_func_mean[:, np.newaxis] ) - F_anat_rescaled) ** 2, axis=1)
|
|
1387
|
+
# # # Compute the total sum of squares
|
|
1388
|
+
# # sum_tot = np.sum((F_func - F_func_mean[:, np.newaxis]) ** 2, axis=1)
|
|
1389
|
+
# # # Compute the R^2 map
|
|
1390
|
+
# # r_square_map = 1 - sum_res / sum_tot
|
|
1391
|
+
# # # re-shape r_square_map to the original shape
|
|
1392
|
+
# # r_square_map = r_square_map.reshape(Ny, Nx)
|
|
1393
|
+
|
|
1394
|
+
# # # print("sum_res shape:", sum_res.shape)
|
|
1395
|
+
# # # print("sum_tot shape:", sum_tot.shape)
|
|
1396
|
+
# # # print("r_square_map shape:", r_square_map.shape)
|
|
1397
|
+
|
|
1398
|
+
# # # Print the formulas
|
|
1399
|
+
# # # print("Sum of squared residuals: sum_res = ∑ [ (F_func - F_func_mean) - z_motion_scaling_factors * (F_anat - F_anat_mean) ]^2")
|
|
1400
|
+
# # # print("Total sum of squares: sum_tot = ∑ (F_func - F_func_mean)^2")
|
|
1401
|
+
# # # print(f"R² map: r_square_map = 1 - sum_res / sum_tot")
|
|
1402
|
+
|
|
1403
|
+
# # # # Compute correlation coefficient between F_func and F_anat
|
|
1404
|
+
# # # # Compute the correlation coefficient for each pixel over time
|
|
1405
|
+
# # # corr_coef = np.array([np.corrcoef(F_func[i_pix], F_anat[i_pix])[0, 1] for i_pix in range(Ny * Nx)])
|
|
1406
|
+
# # # # Reshape the correlation coefficients back to the original 2D shape (y, x)
|
|
1407
|
+
# # # corr_coef_map = corr_coef.reshape(Ny, Nx)
|
|
1408
|
+
|
|
1409
|
+
# # # Save corr_coef_map as a png image
|
|
1410
|
+
# # # Reshape z_motion_scaling_factors
|
|
1411
|
+
# # z_motion_scaling_factors_map = z_motion_scaling_factors.reshape(Ny, Nx)
|
|
1412
|
+
|
|
1413
|
+
# # fov_image = np.mean(F_func.T.reshape(Nframe, Ny, Nx), axis=0)
|
|
1414
|
+
|
|
1415
|
+
# return Fcorrected, z_motion_scaling_factors
|
|
1416
|
+
|
|
1417
|
+
def subtract_z_motion_patches(movie_mmap_path, zstack_filepath, z_correlation, mcorr_params,
|
|
1418
|
+
subtract_method='huber_regression_pixels',
|
|
1419
|
+
save_tiffs=False,
|
|
1420
|
+
save_correl=False, save_format='parquet'):
|
|
1421
|
+
"""
|
|
1422
|
+
Find the correlation between patches in the F_func movie and the anat z-stack.
|
|
1423
|
+
Create a composite anatomical image, using patch correlations to identify the best depth for each zone.
|
|
1424
|
+
Fit a linear regression between the non-rigid F_anat and the F_func movie.
|
|
1425
|
+
Subtract the scaled F_anat from the F_func movie to correct for z-motion.
|
|
1426
|
+
"""
|
|
1427
|
+
print("Starting z-motion correction with patch correlations...")
|
|
1428
|
+
# Load the motion-corrected functional movie
|
|
1429
|
+
movie_16bit, dims, T = load_memmap(movie_mmap_path)
|
|
1430
|
+
F_func = np.reshape(movie_16bit.T, [T] + list(dims), order='F')
|
|
1431
|
+
# Get dimensions
|
|
1432
|
+
Nframe, Ny, Nx = F_func.shape
|
|
1433
|
+
# Clip the movie range to uint16
|
|
1434
|
+
F_func = np.clip(F_func, 0, 2**16-1)
|
|
1435
|
+
|
|
1436
|
+
# Load the shifted z-stack
|
|
1437
|
+
with TiffFile(zstack_filepath) as info_zstack:
|
|
1438
|
+
Nz = len(info_zstack.pages)
|
|
1439
|
+
Zstack = np.zeros((Ny, Nx, Nz), dtype=np.float32)
|
|
1440
|
+
for iz, page in enumerate(info_zstack.pages):
|
|
1441
|
+
Zstack[:, :, iz] = page.asarray()
|
|
1442
|
+
|
|
1443
|
+
# TODO: check why on Windows F_anat bit depth is 8 and not 16. Use other method to load z-stack (append to list)?
|
|
1444
|
+
|
|
1445
|
+
# Clip the z-stack range to uint16
|
|
1446
|
+
Zstack = np.clip(Zstack, 0, 2**16-1)
|
|
1447
|
+
|
|
1448
|
+
# Flip Zstack upside-down to match the orientation of the movie
|
|
1449
|
+
Zstack = np.flip(Zstack, axis=0)
|
|
1450
|
+
|
|
1451
|
+
# Get dimensions
|
|
1452
|
+
Zstack_shape = Zstack.shape
|
|
1453
|
+
|
|
1454
|
+
# Print F_func and Zstack shapes, data type and min/max range
|
|
1455
|
+
print(f"F_func shape: {F_func.shape}, data type: {F_func.dtype}, min: {F_func.min()}, max: {F_func.max()}")
|
|
1456
|
+
print(f"Zstack shape: {Zstack.shape}, data type: {Zstack.dtype}, min: {Zstack.min()}, max: {Zstack.max()}")
|
|
1457
|
+
|
|
1458
|
+
export_path = movie_mmap_path.parent.parent
|
|
1459
|
+
|
|
1460
|
+
# TODO: Update options to reload patch correlations:
|
|
1461
|
+
if (export_path / 'patch_correlations.parquet').exists():
|
|
1462
|
+
print(f"Loading patch correlations from {export_path / 'patch_correlations.parquet'}")
|
|
1463
|
+
patch_correlations_df = pd.read_parquet(export_path / 'patch_correlations.parquet')
|
|
1464
|
+
patch_correlations_df['Z_patch'] = patch_correlations_df['Z_patch'].apply(lambda x: np.frombuffer(x, dtype=np.uint16))
|
|
1465
|
+
else:
|
|
1466
|
+
# Define patch size based on Caiman's parameters. Width of patch = strides+overlaps.
|
|
1467
|
+
step_size = mcorr_params['strides']
|
|
1468
|
+
patch_overlap = mcorr_params['overlaps']
|
|
1469
|
+
# Calculate patch size for both x and y dimensions
|
|
1470
|
+
patch_size = [step + overlap for step, overlap in zip(step_size, patch_overlap)]
|
|
1471
|
+
print()
|
|
1472
|
+
print("Calling patch_regress to find the correlation between patches in the F_func movie and the anat z-stack over frames")
|
|
1473
|
+
print(f"Patch size: {patch_size}, Step size: {step_size}, Patch overlap: {patch_overlap}")
|
|
1474
|
+
|
|
1475
|
+
# Get zpos, x and y shifts
|
|
1476
|
+
zpos = z_correlation['zpos']
|
|
1477
|
+
|
|
1478
|
+
# Time the execution
|
|
1479
|
+
start_time = time.time()
|
|
1480
|
+
|
|
1481
|
+
# Prepare data for parallel processing
|
|
1482
|
+
frame_data_list = []
|
|
1483
|
+
for frameNum in range(Nframe):
|
|
1484
|
+
current_zpos = zpos[frameNum]
|
|
1485
|
+
# The number of z-stack frames to consider is 11 (+/- 5 frames around the current zpos)
|
|
1486
|
+
indices = [current_zpos + i for i in range(-5, 6)]
|
|
1487
|
+
# Check for valid z indices
|
|
1488
|
+
valid_indices = [index for index in indices if 0 <= index < Zstack_shape[2]]
|
|
1489
|
+
# Append the frame data to the list
|
|
1490
|
+
frame_data_list.append((frameNum, F_func[frameNum, :, :],
|
|
1491
|
+
Zstack, Nx, Ny, patch_size, step_size,
|
|
1492
|
+
zpos[frameNum], valid_indices))
|
|
1493
|
+
|
|
1494
|
+
if frameNum == 0:
|
|
1495
|
+
print(f"Displaying values for frame {frameNum}")
|
|
1496
|
+
print(f"Current zpos is: {current_zpos}")
|
|
1497
|
+
print(f"F_func shape: {F_func[frameNum, :, :].shape}, Zstack shape: {Zstack.shape}")
|
|
1498
|
+
print(f"Number of frames: {Nframe}, Number of z-stack frames: {Zstack_shape[2]}")
|
|
1499
|
+
print(f"Patch size: {patch_size}, Step size: {step_size}, Patch overlap: {patch_overlap}")
|
|
1500
|
+
print(f"Number of valid z indices per frame: {len(valid_indices)}")
|
|
1501
|
+
|
|
1502
|
+
# Call patch_regress in parallel
|
|
1503
|
+
patch_regress_results = Parallel(n_jobs=-1)(delayed(patch_regress)(frame_data) for frame_data in tqdm(frame_data_list, desc="Find the correlation between patches in the F_func movie and the anat z-stack over frames"))
|
|
1504
|
+
|
|
1505
|
+
# Flatten list of lists returned by map
|
|
1506
|
+
patch_correlations = [item for sublist in patch_regress_results for item in sublist]
|
|
1507
|
+
|
|
1508
|
+
# Kill all LokyProcess workers (Windows only)
|
|
1509
|
+
if os.name == 'nt':
|
|
1510
|
+
for p in multiprocessing.active_children():
|
|
1511
|
+
if 'LokyProcess' in p.name:
|
|
1512
|
+
p.terminate()
|
|
1513
|
+
|
|
1514
|
+
# Convert to DataFrame
|
|
1515
|
+
patch_correlations_df = pd.DataFrame(patch_correlations)
|
|
1516
|
+
|
|
1517
|
+
# Display execution time in mn and seconds
|
|
1518
|
+
exect_time = time.time() - start_time
|
|
1519
|
+
|
|
1520
|
+
print("Patch correlation computation took ", exect_time // 60, "mn and ", exect_time % 60, "seconds")
|
|
1521
|
+
|
|
1522
|
+
# Calculate patch overlap zones
|
|
1523
|
+
labeled_zones, zone_pattern= calculate_zones(patch_correlations_df, Ny, Nx)
|
|
1524
|
+
|
|
1525
|
+
print("Calculated labeled zones and zone pattern")
|
|
1526
|
+
print(f"Number of zones: {len(labeled_zones)}")
|
|
1527
|
+
print(f"Zone pattern shape: {zone_pattern.shape}, data type: {zone_pattern.dtype}, min: {zone_pattern.min()}, max: {zone_pattern.max()}")
|
|
1528
|
+
|
|
1529
|
+
# Create non-rigid F_anat, using ProcessPoolExecutor to parallelize
|
|
1530
|
+
groups = list(patch_correlations_df.groupby('frame_num'))
|
|
1531
|
+
with ProcessPoolExecutor() as executor:
|
|
1532
|
+
# Submit frames to make_composite_f_anat
|
|
1533
|
+
futures = {executor.submit(make_composite_f_anat, group, labeled_zones): frame_num for frame_num, group in groups}
|
|
1534
|
+
|
|
1535
|
+
# Collect results as they complete
|
|
1536
|
+
results = []
|
|
1537
|
+
frame_arrays = []
|
|
1538
|
+
frame_nums = []
|
|
1539
|
+
for future in tqdm(as_completed(futures), total=len(futures), desc="Generating composite F_anat"):
|
|
1540
|
+
frame_num = futures[future] # Retrieve frame number from the dictionary
|
|
1541
|
+
image_data, zone_data = future.result()
|
|
1542
|
+
results.append(zone_data)
|
|
1543
|
+
frame_arrays.append(image_data)
|
|
1544
|
+
frame_nums.append(frame_num)
|
|
1545
|
+
|
|
1546
|
+
# Concatenate all DataFrame results
|
|
1547
|
+
zone_df = pd.concat(results)
|
|
1548
|
+
|
|
1549
|
+
# F_anat_non_rigid = np.concatenate(frame_arrays, axis=0)
|
|
1550
|
+
# Just in case the frames are not in order, sort frames and arrays according to the frame number
|
|
1551
|
+
sorted_indices = np.argsort(frame_nums)
|
|
1552
|
+
F_anat_non_rigid = np.concatenate([frame_arrays[idx] for idx in sorted_indices], axis=0)
|
|
1553
|
+
# Clip to uint16 range
|
|
1554
|
+
F_anat_non_rigid = np.clip(F_anat_non_rigid, 0, 2**16-1).astype(np.float32)
|
|
1555
|
+
|
|
1556
|
+
# Plot the first frame of mcorr movie, the first frame of the z-stack, and the first frame of the composite F_anat side by side
|
|
1557
|
+
fig, ax = plt.subplots(1, 3, figsize=(15, 5))
|
|
1558
|
+
ax[0].imshow(F_func[0, :, :], cmap='gray')
|
|
1559
|
+
ax[0].set_title('First frame of F_func movie')
|
|
1560
|
+
ax[1].imshow(Zstack[:, :, 0], cmap='gray')
|
|
1561
|
+
ax[1].set_title('First frame of Zstack movie')
|
|
1562
|
+
ax[2].imshow(F_anat_non_rigid[0, :, :], cmap='gray')
|
|
1563
|
+
ax[2].set_title('First frame of composite F_anat movie')
|
|
1564
|
+
plt.tight_layout()
|
|
1565
|
+
# save the figure
|
|
1566
|
+
plot_dir = movie_mmap_path.parent.parent / 'plots'
|
|
1567
|
+
plot_dir.mkdir(parents=True, exist_ok=True)
|
|
1568
|
+
plt.savefig(plot_dir / 'first_frames_comparison.png')
|
|
1569
|
+
plt.close(fig)
|
|
1570
|
+
|
|
1571
|
+
###################################################
|
|
1572
|
+
### Subtract z motion from the functional movie ###
|
|
1573
|
+
###################################################
|
|
1574
|
+
|
|
1575
|
+
if subtract_method == 'linear_regression_frames':
|
|
1576
|
+
F_corrected, z_motion_scaling_factors = subtract_z_motion_lr_frames(F_func, F_anat_non_rigid)
|
|
1577
|
+
elif subtract_method == 'huber_regression_pixels':
|
|
1578
|
+
F_corrected, z_motion_scaling_factors = subtract_z_motion_hr_pixels(F_func, F_anat_non_rigid)
|
|
1579
|
+
elif subtract_method == 'huber_regression_frames':
|
|
1580
|
+
F_corrected, z_motion_scaling_factors = subtract_z_motion_hr_frames(F_func, F_anat_non_rigid)
|
|
1581
|
+
else:
|
|
1582
|
+
F_corrected = None
|
|
1583
|
+
z_motion_scaling_factors = None
|
|
1584
|
+
|
|
1585
|
+
# Make summary plots
|
|
1586
|
+
patch_correl_plots(patch_correlations_df, labeled_zones, zone_df, zone_pattern, np.mean(F_func, axis=0), z_correlation, export_path)
|
|
1587
|
+
|
|
1588
|
+
print("Saving labeled_zones")
|
|
1589
|
+
labeled_zones_df = pd.DataFrame(labeled_zones)
|
|
1590
|
+
labeled_zones_filepath = export_path / 'labeled_zones.csv'
|
|
1591
|
+
labeled_zones_df.to_csv(labeled_zones_filepath, index=False, header=False)
|
|
1592
|
+
|
|
1593
|
+
# Save r_square_map as heatmap figure
|
|
1594
|
+
# if subtract_method == 'huber_regression_pixels' or subtract_method == 'huber_regression_frames':
|
|
1595
|
+
# print("Saving R^2 heatmap")
|
|
1596
|
+
# fig, ax = plt.subplots(1, 1, figsize=(10, 10))
|
|
1597
|
+
# im = ax.imshow(r_square_map, cmap='viridis')
|
|
1598
|
+
# fig.colorbar(im, ax=ax)
|
|
1599
|
+
# ax.set_title("R^2 values for each pixel")
|
|
1600
|
+
# plt.tight_layout()
|
|
1601
|
+
# r_square_map_filepath = export_path / 'r_square_map.png'
|
|
1602
|
+
# plt.savefig(r_square_map_filepath)
|
|
1603
|
+
|
|
1604
|
+
# Save videos
|
|
1605
|
+
if save_tiffs:
|
|
1606
|
+
if F_corrected is not None:
|
|
1607
|
+
print("Saving F_corrected")
|
|
1608
|
+
# Save to tiff
|
|
1609
|
+
f_corrected_filepath = export_path / f"F_func_corrected_T_{Nframe}_Y_{Ny}_X_{Nx}.tiff"
|
|
1610
|
+
with TiffWriter(f_corrected_filepath, bigtiff=True) as tif:
|
|
1611
|
+
tif.write(F_corrected.astype(np.uint16), photometric='minisblack')
|
|
1612
|
+
|
|
1613
|
+
if F_anat_non_rigid is not None:
|
|
1614
|
+
print("Saving composite F_anat")
|
|
1615
|
+
# Save F_anat_non_rigid
|
|
1616
|
+
composite_f_anat_filepath = export_path / f"F_anat_non_rigid_T_{Nframe}_Y_{Ny}_X_{Nx}.tiff"
|
|
1617
|
+
# Save movie to a tiff file
|
|
1618
|
+
with TiffWriter(composite_f_anat_filepath, bigtiff=True) as tif:
|
|
1619
|
+
tif.write(F_anat_non_rigid.astype(np.uint16), photometric='minisblack')
|
|
1620
|
+
|
|
1621
|
+
if save_correl:
|
|
1622
|
+
print("Saving patch correlations")
|
|
1623
|
+
start_time = time.time()
|
|
1624
|
+
if save_format == 'mat':
|
|
1625
|
+
# # Save to a .mat file
|
|
1626
|
+
|
|
1627
|
+
patch_correlations_filepath = export_path / 'patch_correlations.mat'
|
|
1628
|
+
# zone_df_mat = format_patch_correl_for_mat(zone_df)
|
|
1629
|
+
# savemat(patch_correlations_filepath, zone_df_mat)
|
|
1630
|
+
|
|
1631
|
+
# Specifying the exact dtype
|
|
1632
|
+
dtype = [
|
|
1633
|
+
('frame_num', 'uint32'),
|
|
1634
|
+
('zone_id', 'int16'),
|
|
1635
|
+
('r_squared', 'float32'),
|
|
1636
|
+
('patch_number', 'uint16'),
|
|
1637
|
+
('patch_z_pos', 'int16'),
|
|
1638
|
+
]
|
|
1639
|
+
|
|
1640
|
+
# Convert zone_df to structured NumPy array
|
|
1641
|
+
structured_array = np.array(list(zone_df.itertuples(index=False, name=None)), dtype=dtype)
|
|
1642
|
+
|
|
1643
|
+
# Prepare data to save
|
|
1644
|
+
mat_dict = {
|
|
1645
|
+
'patch_data_averaged_by_zone': structured_array,
|
|
1646
|
+
'zone_index': labeled_zones
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
# Save to .mat file, displaying a counter for progress
|
|
1650
|
+
savemat(patch_correlations_filepath, mat_dict)
|
|
1651
|
+
|
|
1652
|
+
elif save_format == 'parquet':
|
|
1653
|
+
# Save to a Parquet file
|
|
1654
|
+
patch_correlations_filepath = export_path / 'patch_correlations.parquet'
|
|
1655
|
+
# Saving / loading the Z_patch column is tricky, as its quite large.
|
|
1656
|
+
# One option is to convert it to binary before saving
|
|
1657
|
+
patch_correlations_df['Z_patch'] = patch_correlations_df['Z_patch'].apply(lambda x: x.tobytes())
|
|
1658
|
+
patch_correlations_df.to_parquet(patch_correlations_filepath, index=False)
|
|
1659
|
+
# Another option would be to save it separately.
|
|
1660
|
+
|
|
1661
|
+
elif save_format == 'csv':
|
|
1662
|
+
# Save to a csv file
|
|
1663
|
+
patch_correlations_filepath = export_path / 'patch_correlations.csv'
|
|
1664
|
+
zone_df.to_csv(patch_correlations_filepath, index=False, header=True)
|
|
1665
|
+
|
|
1666
|
+
print(f"Patch correlations saved in {time.time() - start_time:.2f} seconds")
|
|
1667
|
+
|
|
1668
|
+
# clean up
|
|
1669
|
+
del F_func, movie_16bit, Zstack, F_anat_non_rigid, patch_regress_results, patch_correlations_df, labeled_zones_df, patch_correlations
|
|
1670
|
+
gc.collect()
|
|
1671
|
+
|
|
1672
|
+
return F_corrected, z_motion_scaling_factors
|
|
1673
|
+
|
|
1674
|
+
# def subtract_z_motion_neurons(components, fov_image, zpos, shifted_zstack_filename, zparams_path):
|
|
1675
|
+
# """
|
|
1676
|
+
# Subtract z motion from the fluorescence traces of neurons.
|
|
1677
|
+
# """
|
|
1678
|
+
|
|
1679
|
+
# F=components.C
|
|
1680
|
+
# spatial_components=components.A
|
|
1681
|
+
# Nneuron, Nframe = F.shape
|
|
1682
|
+
|
|
1683
|
+
# if not isinstance(zparams_path, dict):
|
|
1684
|
+
# with open(zparams_path, 'r') as file:
|
|
1685
|
+
# _ = json.load(file)
|
|
1686
|
+
|
|
1687
|
+
# with Image.open(shifted_zstack_filename) as info_zstack:
|
|
1688
|
+
# Nz = info_zstack.n_frames
|
|
1689
|
+
# Nx = info_zstack.width
|
|
1690
|
+
# Ny = info_zstack.height
|
|
1691
|
+
# Zstack = np.zeros((Ny, Nx, Nz), dtype=np.float32)
|
|
1692
|
+
# for iz in range(Nz):
|
|
1693
|
+
# info_zstack.seek(iz)
|
|
1694
|
+
# Zstack[:, :, iz] = np.array(info_zstack)
|
|
1695
|
+
|
|
1696
|
+
# # Assuming spatial_components is a sparse matrix
|
|
1697
|
+
# spatial_components_dense = spatial_components.toarray()
|
|
1698
|
+
# ROI = [{} for _ in range(Nneuron)]
|
|
1699
|
+
# for i_neuron in range(Nneuron):
|
|
1700
|
+
# ind = np.where(spatial_components_dense[:, i_neuron] > 0)[0]
|
|
1701
|
+
# pix_y, pix_x = np.unravel_index(ind, (Ny, Nx))
|
|
1702
|
+
# ROI[i_neuron]['pix_x'] = pix_x
|
|
1703
|
+
# ROI[i_neuron]['pix_y'] = pix_y
|
|
1704
|
+
# ROI[i_neuron]['pix_w'] = np.full(len(ROI[i_neuron]['pix_x']), 1 / len(ROI[i_neuron]['pix_x']))
|
|
1705
|
+
|
|
1706
|
+
# F0 = np.zeros(Nneuron)
|
|
1707
|
+
# Fz0 = np.zeros(Nneuron)
|
|
1708
|
+
# b = np.zeros(Nneuron)
|
|
1709
|
+
# Fz = np.zeros((Nneuron, Nframe))
|
|
1710
|
+
# Fz_rescaled = np.zeros((Nneuron, Nframe))
|
|
1711
|
+
# Fcorrected = np.zeros((Nneuron, Nframe))
|
|
1712
|
+
|
|
1713
|
+
# # Translate z-stack frames to minimize shift with mean of registered tseries frames
|
|
1714
|
+
# zpos_0 = mode(zpos).mode
|
|
1715
|
+
# Zstack_0 = Zstack[ :, :,zpos_0]
|
|
1716
|
+
# # Perform image registration
|
|
1717
|
+
# shift, error, _ = phase_cross_correlation(Zstack_0, fov_image, upsample_factor=10)
|
|
1718
|
+
|
|
1719
|
+
# # Create the transformation matrix (for translation only)
|
|
1720
|
+
# tform = np.eye(3)
|
|
1721
|
+
# tform[0, 2] = -shift[1] # x translation
|
|
1722
|
+
# tform[1, 2] = -shift[0] # y translation
|
|
1723
|
+
|
|
1724
|
+
# for iz in range(Nz):
|
|
1725
|
+
# Zstack[:, :, iz] = warp(Zstack[:, :, iz], tform)
|
|
1726
|
+
|
|
1727
|
+
# # Make a movie of the z-stack frames that best fit the registered tseries frames
|
|
1728
|
+
# F810_t = Zstack[:, :, zpos]
|
|
1729
|
+
|
|
1730
|
+
# # Apply the cell masks on this movie and find the movement-induced changes
|
|
1731
|
+
# # in fluorescence of each cell predicted from z changes, then rescale and
|
|
1732
|
+
# # subtract them from the original F matrix
|
|
1733
|
+
# for i_neuron in range(Nneuron):
|
|
1734
|
+
# F0[i_neuron] = np.mean(F[i_neuron, :])
|
|
1735
|
+
# for i_pix in range(len(ROI[i_neuron]['pix_x'])):
|
|
1736
|
+
# Fz[i_neuron, :] += ROI[i_neuron]['pix_w'][i_pix] * F810_t[ROI[i_neuron]['pix_y'][i_pix], ROI[i_neuron]['pix_x'][i_pix], :]
|
|
1737
|
+
# Fz0[i_neuron] = np.mean(Fz[i_neuron, :])
|
|
1738
|
+
|
|
1739
|
+
# huber = HuberRegressor(fit_intercept=False)
|
|
1740
|
+
# huber.fit(Fz[i_neuron, :].reshape(-1, 1) - Fz0[i_neuron], F[i_neuron, :] - F0[i_neuron])
|
|
1741
|
+
# b[i_neuron] = huber.coef_[0]
|
|
1742
|
+
|
|
1743
|
+
# Fz_rescaled[i_neuron, :] = b[i_neuron] * (Fz[i_neuron, :] - Fz0[i_neuron])
|
|
1744
|
+
# Fcorrected[i_neuron, :] = F[i_neuron, :] - Fz_rescaled[i_neuron, :]
|
|
1745
|
+
|
|
1746
|
+
# # If z is >+5um or <-5um away from mode, then replace F by NaN and interpolate linearly from non missing values
|
|
1747
|
+
# missing_F = np.abs(zpos - zpos_0) > 5
|
|
1748
|
+
# if np.any(missing_F):
|
|
1749
|
+
# Fcorrected[:, missing_F] = np.nan
|
|
1750
|
+
# Fcorrected = pd.DataFrame(Fcorrected).interpolate(method='linear', axis=1, limit_direction='both').values
|
|
1751
|
+
|
|
1752
|
+
# Fz_scale_factor = np.mean(b)
|
|
1753
|
+
|
|
1754
|
+
# # Plotting and visualization
|
|
1755
|
+
# # fig_z_drift = plt.figure()
|
|
1756
|
+
# # plt.plot(zpos)
|
|
1757
|
+
# # plt.xlabel('frames')
|
|
1758
|
+
# # plt.ylabel('z')
|
|
1759
|
+
|
|
1760
|
+
# # x,y,t
|
|
1761
|
+
# return Fcorrected, Fz_scale_factor
|
|
1762
|
+
|
|
1763
|
+
# def subtract_z_motion_pixels(movie_mmap_path, zpos, zstack_filepath, n_jobs=-1):
|
|
1764
|
+
# """
|
|
1765
|
+
# Fit a Huber regressor to find the scaling factor for each pixel.
|
|
1766
|
+
# Subtract the movement-induced changes from the original F matrix per pixel.
|
|
1767
|
+
# """
|
|
1768
|
+
# # Load the movie
|
|
1769
|
+
# movie_16bit, dims, T = load_memmap(movie_mmap_path)
|
|
1770
|
+
# pixels = np.reshape(movie_16bit.T, [T] + list(dims), order='F')
|
|
1771
|
+
# Nframe, Ny, Nx = pixels.shape
|
|
1772
|
+
|
|
1773
|
+
# # Load the FOV file
|
|
1774
|
+
# mcorr_batch_dir = movie_mmap_path.parent
|
|
1775
|
+
# fov_file_path = Path.joinpath(mcorr_batch_dir, f"{mcorr_batch_dir.stem}_mean_projection.npy")
|
|
1776
|
+
# fov_image = np.load(fov_file_path)
|
|
1777
|
+
|
|
1778
|
+
# # Load the z-stack
|
|
1779
|
+
# info_zstack = Image.open(zstack_filepath)
|
|
1780
|
+
# Nz = info_zstack.n_frames
|
|
1781
|
+
# Zstack = np.zeros((Ny, Nx, Nz), dtype=np.float32)
|
|
1782
|
+
# for iz in range(Nz):
|
|
1783
|
+
# info_zstack.seek(iz)
|
|
1784
|
+
# Zstack[:, :, iz] = np.array(info_zstack)
|
|
1785
|
+
# info_zstack.close()
|
|
1786
|
+
# # TODO: check why on Windows F_anat bit depth is 8 and not 16
|
|
1787
|
+
|
|
1788
|
+
# # Flip Zstack up-down to match the orientation of the movie
|
|
1789
|
+
# Zstack = np.flip(Zstack, axis=0)
|
|
1790
|
+
|
|
1791
|
+
# # Reshape the "functional" fluorescence movie
|
|
1792
|
+
# F_func = pixels.reshape(Nframe, Ny * Nx).T
|
|
1793
|
+
|
|
1794
|
+
# # Get the average image of the functional fluorescence movie
|
|
1795
|
+
# F_func_mean = np.mean(F_func, axis=1)
|
|
1796
|
+
|
|
1797
|
+
# # Translate zstack frames to minimize shift with mean of registered tseries frames
|
|
1798
|
+
# zpos_0 = mode(zpos).mode
|
|
1799
|
+
# Zstack_0 = Zstack[:, :, zpos_0]
|
|
1800
|
+
|
|
1801
|
+
# # Perform image registration
|
|
1802
|
+
# shift, _ , _ = phase_cross_correlation(Zstack_0, fov_image, upsample_factor=10)
|
|
1803
|
+
|
|
1804
|
+
# # Create the transformation matrix (for translation only)
|
|
1805
|
+
# tform = np.eye(3)
|
|
1806
|
+
# tform[0, 2] = -shift[1]
|
|
1807
|
+
# tform[1, 2] = -shift[0]
|
|
1808
|
+
|
|
1809
|
+
# # Translate z-stack frames to minimize shift with mean of registered tseries frames (FOV)
|
|
1810
|
+
# for iz in range(Nz):
|
|
1811
|
+
# Zstack[:, :, iz] = warp(Zstack[:, :, iz], tform)
|
|
1812
|
+
|
|
1813
|
+
# # Make a movie of the z-stack frames that follows zcorr (movement in depth)
|
|
1814
|
+
# F_anat = Zstack[:, :, zpos]
|
|
1815
|
+
|
|
1816
|
+
# # Reshape the "anatomical" fluorescence movie
|
|
1817
|
+
# F_anat = F_anat.reshape(Ny * Nx, Nframe)
|
|
1818
|
+
|
|
1819
|
+
# # Get the average image of the z-stack movie
|
|
1820
|
+
# F_anat_mean = np.mean(F_anat, axis=1)
|
|
1821
|
+
|
|
1822
|
+
# # Fit a Huber regressor to find the scaling factor
|
|
1823
|
+
# # with Parallel(n_jobs=n_jobs) as parallel:
|
|
1824
|
+
# # z_motion_scaling_factors = parallel(
|
|
1825
|
+
# # delayed(fit_huber_regressor)(
|
|
1826
|
+
# # i_pixel,
|
|
1827
|
+
# # F_anat,
|
|
1828
|
+
# # F_anat_mean,
|
|
1829
|
+
# # F_func,
|
|
1830
|
+
# # F_func_mean
|
|
1831
|
+
# # )
|
|
1832
|
+
# # for i_pixel in tqdm(
|
|
1833
|
+
# # range(Nx * Ny),
|
|
1834
|
+
# # desc='Fitting regressors to find z motion scaling factors'
|
|
1835
|
+
# # )
|
|
1836
|
+
# # )
|
|
1837
|
+
# # # Close and join workers
|
|
1838
|
+
# # parallel._backend.terminate()
|
|
1839
|
+
|
|
1840
|
+
# z_motion_scaling_factors = Parallel(n_jobs=n_jobs)(delayed(fit_huber_regressor)(i_pixel, F_anat, F_anat_mean, F_func, F_func_mean) for i_pixel in tqdm(range(Ny * Nx), desc='Fitting regressors to find z motion scaling factors'))
|
|
1841
|
+
|
|
1842
|
+
# # Change the list to a numpy array and cast to float32
|
|
1843
|
+
# z_motion_scaling_factors = np.array(z_motion_scaling_factors)
|
|
1844
|
+
# z_motion_scaling_factors = z_motion_scaling_factors.astype(np.float32)
|
|
1845
|
+
|
|
1846
|
+
# # Kill all LokyProcess workers (Windows only)
|
|
1847
|
+
# if os.name == 'nt':
|
|
1848
|
+
# for p in multiprocessing.active_children():
|
|
1849
|
+
# if 'LokyProcess' in p.name:
|
|
1850
|
+
# p.terminate()
|
|
1851
|
+
|
|
1852
|
+
# # Rescale and subtract the movement-induced changes from the original F matrix
|
|
1853
|
+
# F_anat_rescaled = z_motion_scaling_factors[:, np.newaxis] * (F_anat - F_anat_mean[:, np.newaxis])
|
|
1854
|
+
# Fcorrected = F_func - F_anat_rescaled
|
|
1855
|
+
# Fcorrected = np.clip(Fcorrected, 0, 2**16-1)
|
|
1856
|
+
|
|
1857
|
+
# # If z is >+5um or <-5um away from mode, then replace F by NaN and interpolate linearly from non missing values
|
|
1858
|
+
# missing_F = np.abs(zpos - zpos_0) > 5
|
|
1859
|
+
# if np.any(missing_F):
|
|
1860
|
+
# Fcorrected[:, missing_F] = np.nan
|
|
1861
|
+
# Fcorrected = pd.DataFrame(Fcorrected).interpolate(method='linear', axis=1, limit_direction='both').values
|
|
1862
|
+
# # TODO: check that it only interpolates NaNs
|
|
1863
|
+
|
|
1864
|
+
# # Reshape Fcorrected to the original shape
|
|
1865
|
+
# F_func_reshaped = F_func.T.reshape(Nframe, Ny, Nx)
|
|
1866
|
+
# F_anat_rescaled_reshaped = F_anat_rescaled.T.reshape(Nframe, Ny, Nx)
|
|
1867
|
+
# min_Fars = np.min(F_anat_rescaled_reshaped)
|
|
1868
|
+
# max_Fars = np.max(F_anat_rescaled_reshaped)
|
|
1869
|
+
# F_anat_rescaled_reshaped = (F_anat_rescaled_reshaped - min_Fars) / (max_Fars - min_Fars) * (2**16-1)
|
|
1870
|
+
# Fcorrected_reshaped = Fcorrected.T.reshape(Nframe, Ny, Nx)
|
|
1871
|
+
# Fcorrected_reshaped = np.clip(Fcorrected_reshaped, 0, 2**16-1)
|
|
1872
|
+
# F_anat_reshaped = F_anat.T.reshape(Nframe, Ny, Nx)
|
|
1873
|
+
|
|
1874
|
+
# # Save an excerpt of the corrected F matrix to a video file
|
|
1875
|
+
# # Take only the first 80 frames
|
|
1876
|
+
# F_anat_reshaped_excerpt = F_anat_reshaped[:80]
|
|
1877
|
+
# Fcorrected_reshaped_excerpt = Fcorrected_reshaped[:80]
|
|
1878
|
+
# F_anat_rescaled_reshaped_excerpt = F_anat_rescaled_reshaped[:80]
|
|
1879
|
+
# F_func_reshaped_excerpt = F_func_reshaped[:80]
|
|
1880
|
+
|
|
1881
|
+
# # Assign data type to uint16
|
|
1882
|
+
# F_func_reshaped_excerpt = F_func_reshaped_excerpt.astype(np.uint16)
|
|
1883
|
+
# F_anat_rescaled_reshaped_excerpt = F_anat_rescaled_reshaped_excerpt.astype(np.uint16)
|
|
1884
|
+
# Fcorrected_reshaped_excerpt = Fcorrected_reshaped_excerpt.astype(np.uint16)
|
|
1885
|
+
# F_anat_reshaped_excerpt = F_anat_reshaped_excerpt.astype(np.uint16)
|
|
1886
|
+
|
|
1887
|
+
# # Concatenate horizontally
|
|
1888
|
+
# top = np.concatenate((F_func_reshaped_excerpt, F_anat_reshaped_excerpt), axis=2)
|
|
1889
|
+
# bottom = np.concatenate((F_anat_rescaled_reshaped_excerpt, Fcorrected_reshaped_excerpt), axis=2)
|
|
1890
|
+
|
|
1891
|
+
# # Concatenate vertically
|
|
1892
|
+
# F_concat = np.concatenate((top, bottom), axis=1)
|
|
1893
|
+
|
|
1894
|
+
# # Save the concatenated array to a video file
|
|
1895
|
+
# F_concat_img = Image.fromarray(F_concat[0])
|
|
1896
|
+
# F_concat_img.save(movie_mmap_path.parent.parent / "F_concat.tif", save_all=True, append_images=[Image.fromarray(F_concat[i]) for i in range(1, 80)])
|
|
1897
|
+
|
|
1898
|
+
# # clean up
|
|
1899
|
+
# del pixels, movie_16bit
|
|
1900
|
+
# gc.collect()
|
|
1901
|
+
|
|
1902
|
+
# # Return the corrected F matrix and the scaling factor
|
|
1903
|
+
# return Fcorrected_reshaped, z_motion_scaling_factors
|
|
1904
|
+
|
|
1905
|
+
def z_motion(mcorr_movie_path, parameters, recompute=True, scale_range=False):
|
|
1906
|
+
"""
|
|
1907
|
+
Shifts the z-stack and performs z-motion correlation.
|
|
1908
|
+
|
|
1909
|
+
This function:
|
|
1910
|
+
1. Sets up and loads necessary parameters.
|
|
1911
|
+
2. Ensures the z-stack path is correctly formatted.
|
|
1912
|
+
3. Generates a shifted z-stack file if one does not already exist.
|
|
1913
|
+
4. Computes the z-correlation between the shifted z-stack and the movie.
|
|
1914
|
+
5. Optionally subtracts z-motion using various methods (patches, pixels, etc.).
|
|
1915
|
+
6. Saves the corrected movie as a memmap file if z-motion subtraction is performed.
|
|
1916
|
+
|
|
1917
|
+
Returns:
|
|
1918
|
+
movie_path: Path to the saved corrected movie (or None if not generated).
|
|
1919
|
+
z_motion_scaling_factors: The scaling factors used during z-motion subtraction (or None).
|
|
1920
|
+
z_correlation: The computed z-correlation data.
|
|
1921
|
+
"""
|
|
1922
|
+
|
|
1923
|
+
# --- Parameter Setup ---
|
|
1924
|
+
# Extract z-stack parameters and the z-stack file path from the input parameters.
|
|
1925
|
+
z_parameters = parameters['params_mcorr'].get('z_motion_correction', {})
|
|
1926
|
+
zstack_path = parameters.get('zstack_path', None)
|
|
1927
|
+
|
|
1928
|
+
# If zstack_path is not found, log an error and return None
|
|
1929
|
+
if zstack_path is None:
|
|
1930
|
+
log_and_print("Z-stack path not found in parameters.", level="error")
|
|
1931
|
+
return None, None, None
|
|
1932
|
+
|
|
1933
|
+
# If z_parameters is not already a dict, assume it is a path to a JSON file and load it.
|
|
1934
|
+
if not isinstance(z_parameters, dict):
|
|
1935
|
+
with open(z_parameters, 'r') as file:
|
|
1936
|
+
z_parameters = json.load(file)
|
|
1937
|
+
|
|
1938
|
+
# Retrieve the expected file name for the shifted z-stack.
|
|
1939
|
+
z_shifted_file = 'zstack_shifted.tif'# z_parameters['zstack_shift']['file_name']
|
|
1940
|
+
|
|
1941
|
+
# Ensure zstack_path is a Path object.
|
|
1942
|
+
if isinstance(zstack_path, str):
|
|
1943
|
+
zstack_path = Path(zstack_path)
|
|
1944
|
+
|
|
1945
|
+
# --- Generate Shifted Z-stack ---
|
|
1946
|
+
try:
|
|
1947
|
+
# If the shifted z-stack file doesn't exist, generate it using the shift_zstack function.
|
|
1948
|
+
# if not os.path.exists(zstack_path / z_shifted_file) or recompute:
|
|
1949
|
+
shift_zstack_path = shift_zstack(
|
|
1950
|
+
z_parameters['zstack_shift'],
|
|
1951
|
+
zstack_path,
|
|
1952
|
+
z_shifted_file,
|
|
1953
|
+
scale_range=scale_range,
|
|
1954
|
+
imaging_params=parameters.get('imaging')
|
|
1955
|
+
)
|
|
1956
|
+
# else:
|
|
1957
|
+
# shift_zstack_path = zstack_path / z_shifted_file
|
|
1958
|
+
except Exception as e:
|
|
1959
|
+
print(f"Error in compute_zcorr - generate zstack_shifted: {e}")
|
|
1960
|
+
return None, None, None
|
|
1961
|
+
|
|
1962
|
+
# --- Compute Z-correlation ---
|
|
1963
|
+
try:
|
|
1964
|
+
# Determine the directory where the z_correlation file should be saved.
|
|
1965
|
+
mesmerize_path = mcorr_movie_path.parents[1]
|
|
1966
|
+
# Compute z-correlation if the file doesn't already exist; otherwise, load it.
|
|
1967
|
+
if not os.path.exists(mesmerize_path / "z_correlation.npz") or recompute:
|
|
1968
|
+
z_correlation = compute_zcorrel(
|
|
1969
|
+
zstack_path / z_shifted_file,
|
|
1970
|
+
mcorr_movie_path,
|
|
1971
|
+
export_path=mesmerize_path,
|
|
1972
|
+
)
|
|
1973
|
+
else:
|
|
1974
|
+
z_correlation = np.load(mesmerize_path / "z_correlation.npz")
|
|
1975
|
+
except Exception as e:
|
|
1976
|
+
print(f"Error in compute_zcorr - compute z-correlation: {e}")
|
|
1977
|
+
return None, None, None
|
|
1978
|
+
|
|
1979
|
+
# --- Compute non-rigid z-motion computation if specified ---
|
|
1980
|
+
try:
|
|
1981
|
+
compute_non_rigid = z_parameters.get('non_rigid', None)
|
|
1982
|
+
if compute_non_rigid is None and 'subtract_z_motion' in z_parameters:
|
|
1983
|
+
legacy_value = z_parameters['subtract_z_motion']
|
|
1984
|
+
if isinstance(legacy_value, str):
|
|
1985
|
+
compute_non_rigid = legacy_value.lower() not in ('false', '0', 'none')
|
|
1986
|
+
else:
|
|
1987
|
+
compute_non_rigid = bool(legacy_value)
|
|
1988
|
+
|
|
1989
|
+
if compute_non_rigid is not None:
|
|
1990
|
+
|
|
1991
|
+
# Initialize variables for the z-motion subtraction results.
|
|
1992
|
+
zcorr_movie = None
|
|
1993
|
+
z_motion_scaling_factors = None
|
|
1994
|
+
movie_path = None
|
|
1995
|
+
|
|
1996
|
+
if compute_non_rigid:
|
|
1997
|
+
# Compute non-rigid functional-anatomical correlation
|
|
1998
|
+
if not os.path.exists(mesmerize_path / "non_rigid_z_motion_scaling_factors.npy"):
|
|
1999
|
+
zcorr_movie, z_motion_scaling_factors = subtract_z_motion_patches(
|
|
2000
|
+
mcorr_movie_path,
|
|
2001
|
+
zstack_path / z_shifted_file,
|
|
2002
|
+
z_correlation,
|
|
2003
|
+
parameters['params_mcorr']['main'],
|
|
2004
|
+
subtract_method=None,
|
|
2005
|
+
save_tiffs=True
|
|
2006
|
+
)
|
|
2007
|
+
# Save the scaling factors if they have been computed.
|
|
2008
|
+
if z_motion_scaling_factors is not None:
|
|
2009
|
+
np.save(mesmerize_path / "non_rigid_z_motion_scaling_factors.npy", z_motion_scaling_factors)
|
|
2010
|
+
else:
|
|
2011
|
+
print("Z-motion subtraction already performed.")
|
|
2012
|
+
|
|
2013
|
+
# --- Save Corrected Movie ---
|
|
2014
|
+
# If a corrected movie has been generated, save it to a memmap file.
|
|
2015
|
+
if zcorr_movie is not None:
|
|
2016
|
+
movie_path = save_mmap_movie(
|
|
2017
|
+
zcorr_movie,
|
|
2018
|
+
mcorr_movie_path.parent / f"zcorr_movie_{mcorr_movie_path.name}"
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
return movie_path, z_motion_scaling_factors, z_correlation
|
|
2022
|
+
|
|
2023
|
+
else:
|
|
2024
|
+
# If no subtraction is specified, return the computed z_correlation only.
|
|
2025
|
+
return None, None, z_correlation
|
|
2026
|
+
|
|
2027
|
+
except Exception as e:
|
|
2028
|
+
print(f"Error in compute_zcorr - computing non-rigid F_anat: {e}")
|
|
2029
|
+
return None, None, None
|
|
2030
|
+
|
|
2031
|
+
if __name__ == "__main__":
|
|
2032
|
+
# Get arguements from command line
|
|
2033
|
+
parser = argparse.ArgumentParser(description="Compute z-motion correction for a movie")
|
|
2034
|
+
parser.add_argument("mcorr_movie_path", type=str, help="Path to the motion-corrected movie")
|
|
2035
|
+
parser.add_argument("parameters", type=str, help="Path to the config file")
|
|
2036
|
+
args = parser.parse_args()
|
|
2037
|
+
mcorr_movie_path = Path(args.mcorr_movie_path)
|
|
2038
|
+
parameters = Path(args.parameters)
|
|
2039
|
+
# Call the function
|
|
2040
|
+
z_motion(mcorr_movie_path, parameters)
|