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