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
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Motion Correction Module for Analysis 2P Pipeline
|
|
3
|
+
|
|
4
|
+
This module handles the motion correction workflow including:
|
|
5
|
+
- Raw data concatenation
|
|
6
|
+
- Motion correction using CaImAn/Mesmerize
|
|
7
|
+
- Z-motion correction (optional)
|
|
8
|
+
- Movie output generation
|
|
9
|
+
- File optimization
|
|
10
|
+
|
|
11
|
+
Authors: Manuel Levy, Vincent Prevosto
|
|
12
|
+
Date: 2024-07-22
|
|
13
|
+
License: CC-BY-SA 4.0
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# Add project root to path
|
|
22
|
+
PROJECT_ROOT = Path(__file__).parent.parent
|
|
23
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
25
|
+
|
|
26
|
+
import time
|
|
27
|
+
import os
|
|
28
|
+
import numpy as np
|
|
29
|
+
import h5py
|
|
30
|
+
from tifffile import TiffWriter, TiffFile, imread
|
|
31
|
+
import matplotlib.pyplot as plt
|
|
32
|
+
import pandas as pd
|
|
33
|
+
from scipy.stats import mode
|
|
34
|
+
from sklearn.linear_model import HuberRegressor
|
|
35
|
+
import shutil
|
|
36
|
+
|
|
37
|
+
# Import CaImAn and Mesmerize components
|
|
38
|
+
import mesmerize_core as mc
|
|
39
|
+
# from caiman.mmapping import load_memmap
|
|
40
|
+
|
|
41
|
+
from modules import bruker_concat_tif as ct
|
|
42
|
+
from modules import compute_zcorr as cz
|
|
43
|
+
|
|
44
|
+
from pipeline.utils.pipeline_utils import (
|
|
45
|
+
log_and_print,
|
|
46
|
+
create_mp4_movie,
|
|
47
|
+
overwrite_movie_memmap,
|
|
48
|
+
load_mmap_movie,
|
|
49
|
+
clip_range,
|
|
50
|
+
cat_movies_to_mp4,
|
|
51
|
+
memory_manager,
|
|
52
|
+
load_caiman_memmap,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _copy_concat_sidecar(export_dir: Path, dest_movie_path: Path, allow_suite2p_dir: bool = False):
|
|
56
|
+
"""Copy concatenation sidecar JSON next to a converted movie.
|
|
57
|
+
|
|
58
|
+
Looks for sidecar written by bruker_concat_tif (cat_tiff_bt.tiff.json or
|
|
59
|
+
cat_tiff.h5.json) under export_dir, and writes a copy as
|
|
60
|
+
f"{dest_movie_path.name}.json" next to dest_movie_path.
|
|
61
|
+
Also creates extracted_parameters.json in plane0 folder.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
candidates = [
|
|
65
|
+
export_dir / "cat_tiff_bt.tiff.json",
|
|
66
|
+
export_dir / "cat_tiff.h5.json",
|
|
67
|
+
]
|
|
68
|
+
src = next((p for p in candidates if p.exists()), None)
|
|
69
|
+
if src is None:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Copy sidecar next to movie file
|
|
73
|
+
dst = dest_movie_path.with_name(dest_movie_path.name + ".json")
|
|
74
|
+
shutil.copy2(src, dst)
|
|
75
|
+
log_and_print(f"Copied sidecar JSON to {dst}")
|
|
76
|
+
|
|
77
|
+
if allow_suite2p_dir:
|
|
78
|
+
# Move XML parameter files to plane0 folder
|
|
79
|
+
try:
|
|
80
|
+
# Create plane0 directory if it doesn't exist
|
|
81
|
+
plane0_dir = export_dir / "suite2p" / "plane0"
|
|
82
|
+
plane0_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
# Check for consolidated sidecar and copy it if needed, or handle legacy files if they exist
|
|
85
|
+
# (Logic to move *_imaging_params.json removed to avoid clutter/redundancy)
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
log_and_print(f"Warning: failed to setup suite2p directory: {e}", level='warning')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
log_and_print(f"Warning: failed to copy sidecar JSON: {e}", level='warning')
|
|
93
|
+
|
|
94
|
+
def to_uint8_robust(arr, p_lo=0.1, p_hi=99.9):
|
|
95
|
+
# Compute robust range on the array (handles negatives, ignores outliers)
|
|
96
|
+
vmin, vmax = np.percentile(arr, (p_lo, p_hi))
|
|
97
|
+
if vmax <= vmin:
|
|
98
|
+
vmax = vmin + 1.0 # fallback to avoid divide-by-zero
|
|
99
|
+
arr = np.clip(arr, vmin, vmax)
|
|
100
|
+
arr = (arr - vmin) * (255.0 / (vmax - vmin))
|
|
101
|
+
return arr.astype(np.uint8)
|
|
102
|
+
|
|
103
|
+
def create_mcorr_movie(mcorr_movie_path, export_path, batch, index=0, format='mp4', diff_corr=True, to_uint8=True, excerpt=None):
|
|
104
|
+
"""
|
|
105
|
+
Save the motion corrected movie (memmaped array) as a BigTIFF file or a mp4 movie.
|
|
106
|
+
If diff_corr is true (default), concatenate the original movie and the motion corrected movie horizontally.
|
|
107
|
+
"""
|
|
108
|
+
# # Load the movie from the memmap file
|
|
109
|
+
# mcorr_movie_16bit , dims, T = load_memmap(mcorr_path)
|
|
110
|
+
# # Reshape the array to the desired dimensions
|
|
111
|
+
# mcorr_movie_16bit = np.reshape(mcorr_movie_16bit.T, [T] + list(dims), order='F')
|
|
112
|
+
# # At this point the images should already be transposed
|
|
113
|
+
# # mcorr_movie_16bit = mcorr_movie_16bit.transpose(0, 2, 1)
|
|
114
|
+
# # image = mcorr_movie_16bit[0,:,:]
|
|
115
|
+
|
|
116
|
+
# Load the movie from the memmap file
|
|
117
|
+
loaded_mcorr_movie = load_mmap_movie(mcorr_movie_path)
|
|
118
|
+
|
|
119
|
+
# If excerpt is not None, keep only the first x frames of the movie
|
|
120
|
+
if excerpt is not None:
|
|
121
|
+
loaded_mcorr_movie = loaded_mcorr_movie[:excerpt]
|
|
122
|
+
|
|
123
|
+
# Convert values to uint8
|
|
124
|
+
if to_uint8:
|
|
125
|
+
# Data is originally uint12, and is loaded as float 32, but is 16 bit range.
|
|
126
|
+
scale_factor = 255 / (2**16-1)
|
|
127
|
+
mcorr_movie_ = to_uint8_robust(loaded_mcorr_movie) #(loaded_mcorr_movie * scale_factor).astype('uint8')
|
|
128
|
+
else:
|
|
129
|
+
mcorr_movie_ = loaded_mcorr_movie.astype(np.uint16)
|
|
130
|
+
|
|
131
|
+
if format == 'mp4':
|
|
132
|
+
if diff_corr:
|
|
133
|
+
# Load the data frame from the batch
|
|
134
|
+
df = mc.load_batch(batch)
|
|
135
|
+
# Get the original movie as a memmaped numpy array
|
|
136
|
+
original_movie = df.iloc[index].caiman.get_input_movie()
|
|
137
|
+
if excerpt is not None:
|
|
138
|
+
original_movie = original_movie[:excerpt]
|
|
139
|
+
|
|
140
|
+
# Convert values to uint8
|
|
141
|
+
if to_uint8:
|
|
142
|
+
scale_factor = 255 / (2**16-1)
|
|
143
|
+
original_movie_ = to_uint8_robust(original_movie) #(original_movie * scale_factor).astype('uint8')
|
|
144
|
+
else:
|
|
145
|
+
original_movie_ = original_movie.astype(np.uint16)
|
|
146
|
+
# Set the path of the mp4 movie
|
|
147
|
+
movie_path = Path.joinpath(export_path, f"compare_og_mcorr.mp4")
|
|
148
|
+
# Concatenate the two movies horizontally
|
|
149
|
+
cat_movies_to_mp4(original_movie_, mcorr_movie_, movie_path)
|
|
150
|
+
log_and_print(f"Saved original vs motion corrected movie to {movie_path}.")
|
|
151
|
+
|
|
152
|
+
return movie_path
|
|
153
|
+
else:
|
|
154
|
+
# Save the motion corrected movie as a mp4 movie
|
|
155
|
+
# movie_path = Path.joinpath(export_path, f"mcorr.mp4")
|
|
156
|
+
create_mp4_movie(mcorr_movie_, export_path, 'mcorr.mp4')
|
|
157
|
+
log_and_print(f"Saved motion corrected movie to {export_path}/mcorr.mp4")
|
|
158
|
+
|
|
159
|
+
return Path.joinpath(export_path, 'mcorr.mp4')
|
|
160
|
+
else:
|
|
161
|
+
# Save the motion corrected movie as a BigTIFF file (8 bit by default)
|
|
162
|
+
if to_uint8:
|
|
163
|
+
mcorr_tif_path = Path.joinpath(export_path, f"mcorr_u8.tiff")
|
|
164
|
+
else:
|
|
165
|
+
mcorr_tif_path = Path.joinpath(export_path, f"mcorr_u16.tiff")
|
|
166
|
+
with TiffWriter(mcorr_tif_path, bigtiff=True) as tif:
|
|
167
|
+
tif.write(mcorr_movie_, photometric='minisblack')
|
|
168
|
+
if to_uint8:
|
|
169
|
+
log_and_print(f"Saved 8 bit motion corrected movie to {mcorr_tif_path}.")
|
|
170
|
+
else:
|
|
171
|
+
log_and_print(f"Saved 16 bit motion corrected movie to {mcorr_tif_path}.")
|
|
172
|
+
|
|
173
|
+
return mcorr_tif_path
|
|
174
|
+
|
|
175
|
+
def compute_movie_residuals(mcorr_movie_path, zcorr_movie, export_path):
|
|
176
|
+
"""
|
|
177
|
+
Compute the residuals between the motion corrected movie (x/y), and the z-motion corrected movie (z).
|
|
178
|
+
"""
|
|
179
|
+
# Load the motion corrected movie
|
|
180
|
+
# loaded_mcorr_movie , dims, T = load_memmap(clipped_mcorr_path)
|
|
181
|
+
# loaded_mcorr_movie = np.reshape(loaded_mcorr_movie.T, [T] + list(dims), order='F')
|
|
182
|
+
# loaded_mcorr_movie = loaded_mcorr_movie.transpose(0, 2, 1)
|
|
183
|
+
loaded_mcorr_movie = load_caiman_memmap(mcorr_movie_path)
|
|
184
|
+
|
|
185
|
+
# Compute the difference between the motion corrected movie and the z-motion corrected movie (residuals for each frame)
|
|
186
|
+
residual_movie = np.zeros_like(loaded_mcorr_movie)
|
|
187
|
+
for i, frame in enumerate(loaded_mcorr_movie):
|
|
188
|
+
residual_movie[i] = zcorr_movie[i] - frame
|
|
189
|
+
|
|
190
|
+
# Convert all three movies' values to uint8, then concatenate them horizontally and save as a mp4 movie
|
|
191
|
+
mcorr_movie_ = (loaded_mcorr_movie / (2**16-1) * 255).astype('uint8')
|
|
192
|
+
zcorr_movie_ = (zcorr_movie / (2**16-1) * 255).astype('uint8')
|
|
193
|
+
residual_movie_ = (residual_movie / (2**16-1) * 255).astype('uint8')
|
|
194
|
+
|
|
195
|
+
# Set the path of the mp4 movie
|
|
196
|
+
movie_path = Path.joinpath(export_path, f"compare_mcorr_zcorr_residuals.mp4")
|
|
197
|
+
|
|
198
|
+
# Concatenate the three movies horizontally
|
|
199
|
+
cat_movie = np.concatenate((mcorr_movie_, zcorr_movie_, residual_movie_), axis=2)
|
|
200
|
+
create_mp4_movie(cat_movie, export_path, 'compare_mcorr_zcorr_residuals.mp4')
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run_roi_zcorr(export_path, parameters):
|
|
204
|
+
"""Correct ROI fluorescence traces for z-motion artifacts.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
export_path : str or Path
|
|
209
|
+
Directory containing extraction outputs and z-motion files.
|
|
210
|
+
parameters : dict
|
|
211
|
+
Dictionary with processing parameters (unused, reserved for future use).
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
export_path = Path(export_path)
|
|
215
|
+
log_and_print("Starting ROI z-motion correction.")
|
|
216
|
+
|
|
217
|
+
zcorr_file = export_path / "z_correlation.npz"
|
|
218
|
+
f_anat_files = sorted(export_path.glob("F_anat_non_rigid*.tiff"))
|
|
219
|
+
|
|
220
|
+
cnmf_file = export_path / "cnmf_result.hdf5"
|
|
221
|
+
|
|
222
|
+
# Locate Suite2p outputs flexibly: root, suite2p/plane0, any suite2p/plane*, or stray plane0
|
|
223
|
+
s2p_dir = None
|
|
224
|
+
s2p_files = None
|
|
225
|
+
candidates = []
|
|
226
|
+
# Root-level outputs
|
|
227
|
+
candidates.append(export_path)
|
|
228
|
+
# Standard suite2p plane0
|
|
229
|
+
candidates.append(export_path / "suite2p" / "plane0")
|
|
230
|
+
# Any suite2p plane*
|
|
231
|
+
candidates.extend(sorted((export_path / "suite2p").glob("plane*"))) if (export_path / "suite2p").exists() else None
|
|
232
|
+
# Stray plane0 directly under export_path (seen in some save_mat behaviors)
|
|
233
|
+
candidates.append(export_path / "plane0")
|
|
234
|
+
|
|
235
|
+
for cdir in [c for c in candidates if c]:
|
|
236
|
+
Fp = cdir / "F.npy"
|
|
237
|
+
Sp = cdir / "stat.npy"
|
|
238
|
+
if Fp.exists() and Sp.exists():
|
|
239
|
+
s2p_dir = cdir
|
|
240
|
+
s2p_files = [Fp, Sp]
|
|
241
|
+
try:
|
|
242
|
+
log_and_print(f"Detected Suite2p outputs in: {s2p_dir}")
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
if not zcorr_file.exists() or not f_anat_files:
|
|
248
|
+
log_and_print(
|
|
249
|
+
"Required files for ROI z-motion correction missing. Skipping.",
|
|
250
|
+
level="warning",
|
|
251
|
+
)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
if cnmf_file.exists():
|
|
255
|
+
extractor = "cnmf"
|
|
256
|
+
elif s2p_dir is not None:
|
|
257
|
+
extractor = "suite2p"
|
|
258
|
+
else:
|
|
259
|
+
log_and_print(
|
|
260
|
+
"No supported extraction outputs found in export path. Skipping.",
|
|
261
|
+
level="warning",
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
with memory_manager("roi z-motion correction"):
|
|
266
|
+
zpos = np.load(zcorr_file)["zpos"]
|
|
267
|
+
f_anat_movie = imread(str(f_anat_files[0]))
|
|
268
|
+
# tifffile returns (T, Y, X); transpose to (Y, X, T)
|
|
269
|
+
if f_anat_movie.shape[0] == zpos.shape[0]:
|
|
270
|
+
f_anat_movie = np.transpose(f_anat_movie, (1, 2, 0))
|
|
271
|
+
|
|
272
|
+
if extractor == "cnmf":
|
|
273
|
+
from caiman.source_extraction.cnmf.cnmf import CNMF
|
|
274
|
+
|
|
275
|
+
cnm = CNMF()
|
|
276
|
+
cnm.load(str(cnmf_file))
|
|
277
|
+
F = cnm.estimates.F
|
|
278
|
+
A = cnm.estimates.A.tocsc()
|
|
279
|
+
d1 = cnm.estimates.d1
|
|
280
|
+
d2 = cnm.estimates.d2
|
|
281
|
+
n_neuron, n_frame = F.shape
|
|
282
|
+
|
|
283
|
+
Fz = np.zeros((n_neuron, n_frame))
|
|
284
|
+
Fz_rescaled = np.zeros_like(F)
|
|
285
|
+
Fcorrected = np.zeros_like(F)
|
|
286
|
+
b = np.zeros(n_neuron)
|
|
287
|
+
|
|
288
|
+
for i in range(n_neuron):
|
|
289
|
+
ind = A[:, i].nonzero()[0]
|
|
290
|
+
y, x = np.unravel_index(ind, (d1, d2))
|
|
291
|
+
weights = np.full(len(x), 1 / len(x))
|
|
292
|
+
for w, yy, xx in zip(weights, y, x):
|
|
293
|
+
Fz[i] += w * f_anat_movie[yy, xx, :]
|
|
294
|
+
F0 = F[i].mean()
|
|
295
|
+
Fz0 = Fz[i].mean()
|
|
296
|
+
huber = HuberRegressor(fit_intercept=False)
|
|
297
|
+
huber.fit((Fz[i] - Fz0).reshape(-1, 1), F[i] - F0)
|
|
298
|
+
b[i] = huber.coef_[0]
|
|
299
|
+
Fz_rescaled[i] = b[i] * (Fz[i] - Fz0)
|
|
300
|
+
Fcorrected[i] = F[i] - Fz_rescaled[i]
|
|
301
|
+
|
|
302
|
+
else: # Suite2p
|
|
303
|
+
F = np.load(str(s2p_files[0]))
|
|
304
|
+
stat = np.load(str(s2p_files[1]), allow_pickle=True)
|
|
305
|
+
n_neuron, n_frame = F.shape
|
|
306
|
+
Fz = np.zeros((n_neuron, n_frame))
|
|
307
|
+
Fz_rescaled = np.zeros_like(F)
|
|
308
|
+
Fcorrected = np.zeros_like(F)
|
|
309
|
+
b = np.zeros(n_neuron)
|
|
310
|
+
|
|
311
|
+
for i, cell in enumerate(stat):
|
|
312
|
+
ypix = cell["ypix"].astype(int)
|
|
313
|
+
xpix = cell["xpix"].astype(int)
|
|
314
|
+
lam = cell["lam"].astype(float)
|
|
315
|
+
weights = lam / lam.sum() if lam.sum() != 0 else np.full(len(lam), 1 / len(lam))
|
|
316
|
+
for w, yy, xx in zip(weights, ypix, xpix):
|
|
317
|
+
Fz[i] += w * f_anat_movie[yy, xx, :]
|
|
318
|
+
F0 = F[i].mean()
|
|
319
|
+
Fz0 = Fz[i].mean()
|
|
320
|
+
huber = HuberRegressor(fit_intercept=False)
|
|
321
|
+
huber.fit((Fz[i] - Fz0).reshape(-1, 1), F[i] - F0)
|
|
322
|
+
b[i] = huber.coef_[0]
|
|
323
|
+
Fz_rescaled[i] = b[i] * (Fz[i] - Fz0)
|
|
324
|
+
Fcorrected[i] = F[i] - Fz_rescaled[i]
|
|
325
|
+
|
|
326
|
+
z_mode = mode(zpos, keepdims=False).mode
|
|
327
|
+
if np.ndim(z_mode) > 0:
|
|
328
|
+
z_mode = z_mode[0]
|
|
329
|
+
missing = np.abs(zpos - z_mode) > 5
|
|
330
|
+
if np.any(missing):
|
|
331
|
+
Fcorrected[:, missing] = np.nan
|
|
332
|
+
Fcorrected = (
|
|
333
|
+
pd.DataFrame(Fcorrected)
|
|
334
|
+
.interpolate(method="linear", axis=1, limit_direction="both")
|
|
335
|
+
.to_numpy()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Save results
|
|
339
|
+
np.savez_compressed(export_path / "F_roi_zcorrected.npz",
|
|
340
|
+
F_roi_zcorrected=Fcorrected,
|
|
341
|
+
F_roi_zbaseline=Fz_rescaled,
|
|
342
|
+
roi_z_scaling=b)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
import matplotlib.pyplot as plt
|
|
346
|
+
|
|
347
|
+
plots_dir = export_path / "plots"
|
|
348
|
+
plots_dir.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
|
|
350
|
+
plt.figure()
|
|
351
|
+
plt.plot(zpos)
|
|
352
|
+
plt.xlabel("Frame")
|
|
353
|
+
plt.ylabel("Z position (µm)")
|
|
354
|
+
plt.tight_layout()
|
|
355
|
+
plt.savefig(plots_dir / "roi_z_drift.png")
|
|
356
|
+
plt.close()
|
|
357
|
+
|
|
358
|
+
plt.figure()
|
|
359
|
+
plt.hist(b, bins=30)
|
|
360
|
+
plt.xlabel("Scaling factor")
|
|
361
|
+
plt.ylabel("Count")
|
|
362
|
+
plt.tight_layout()
|
|
363
|
+
plt.savefig(plots_dir / "roi_z_scaling_hist.png")
|
|
364
|
+
plt.close()
|
|
365
|
+
except Exception as e:
|
|
366
|
+
log_and_print(f"Could not save diagnostic plots: {e}", level="warning")
|
|
367
|
+
|
|
368
|
+
log_and_print("ROI z-motion correction completed.")
|
|
369
|
+
return export_path
|
|
370
|
+
|
|
371
|
+
def save_movie_as_h5(memmap_path, h5_path, parameters, dtype_out='uint16', scale=None, chunk_size=256):
|
|
372
|
+
"""
|
|
373
|
+
Streamed save of motion-corrected movie as HDF5 (AIND / Suite2p extraction-ready).
|
|
374
|
+
|
|
375
|
+
Avoids loading full movie in RAM by iterating through CaImAn memmap frames.
|
|
376
|
+
"""
|
|
377
|
+
log_and_print(f"Saving final movie to {h5_path} (dtype_out={dtype_out})")
|
|
378
|
+
adapter = load_caiman_memmap(memmap_path)
|
|
379
|
+
T, Ly, Lx = adapter.shape
|
|
380
|
+
log_and_print(f"Memmap adapter: frames={T}, Ly={Ly}, Lx={Lx}, dtype={adapter.dtype}")
|
|
381
|
+
|
|
382
|
+
# Metadata extraction
|
|
383
|
+
try:
|
|
384
|
+
imaging = parameters.get('imaging', {}) if parameters else {}
|
|
385
|
+
except Exception:
|
|
386
|
+
imaging = {}
|
|
387
|
+
frame_rate = imaging.get('fr') or imaging.get('fs') or 30.0
|
|
388
|
+
pixel_size_um = imaging.get('microns_per_pixel', 1.0)
|
|
389
|
+
|
|
390
|
+
if dtype_out not in ('uint16','float32'):
|
|
391
|
+
raise ValueError("dtype_out must be 'uint16' or 'float32'")
|
|
392
|
+
|
|
393
|
+
running_min = np.inf
|
|
394
|
+
running_max = -np.inf
|
|
395
|
+
|
|
396
|
+
# Create a figure of pixel values histograms
|
|
397
|
+
# fig, axs = plt.subplots(1, 2, figsize=(12, 6))
|
|
398
|
+
# axs[0].hist(adapter.flatten(), bins=100, color='gray')
|
|
399
|
+
# axs[0].set_title('Histogram of Pixel Values (Memmap)')
|
|
400
|
+
# axs[0].set_xlabel('Pixel Value')
|
|
401
|
+
# axs[0].set_ylabel('Frequency')
|
|
402
|
+
|
|
403
|
+
with h5py.File(h5_path, 'w') as f:
|
|
404
|
+
if dtype_out == 'uint16':
|
|
405
|
+
dset = f.create_dataset('data', shape=(T, Ly, Lx), dtype='uint16', compression=None)
|
|
406
|
+
else:
|
|
407
|
+
dset = f.create_dataset('data', shape=(T, Ly, Lx), dtype='float32', compression=None)
|
|
408
|
+
dset.attrs['fs'] = frame_rate
|
|
409
|
+
dset.attrs['n_frames'] = int(T)
|
|
410
|
+
dset.attrs['Ly'] = int(Ly)
|
|
411
|
+
dset.attrs['Lx'] = int(Lx)
|
|
412
|
+
dset.attrs['pixel_size_um'] = float(pixel_size_um)
|
|
413
|
+
|
|
414
|
+
for start in range(0, T, chunk_size):
|
|
415
|
+
stop = min(T, start + chunk_size)
|
|
416
|
+
block = adapter[start:stop].astype(np.float32, copy=False) # (chunk, Ly, Lx)
|
|
417
|
+
if scale is not None:
|
|
418
|
+
block *= scale
|
|
419
|
+
# Update stats before conversion
|
|
420
|
+
blk_min = float(block.min())
|
|
421
|
+
blk_max = float(block.max())
|
|
422
|
+
if blk_min < running_min: running_min = blk_min
|
|
423
|
+
if blk_max > running_max: running_max = blk_max
|
|
424
|
+
|
|
425
|
+
if dtype_out == 'uint16':
|
|
426
|
+
if block.min() < 0:
|
|
427
|
+
# shift to positive per-chunk if needed
|
|
428
|
+
block = block - block.min()
|
|
429
|
+
block = clip_range(block, 'uint16').astype(np.uint16, copy=False)
|
|
430
|
+
else: # float32
|
|
431
|
+
block = block.astype(np.float32, copy=False)
|
|
432
|
+
|
|
433
|
+
dset[start:stop] = np.ascontiguousarray(block)
|
|
434
|
+
if (start // chunk_size) % 20 == 0 or stop == T:
|
|
435
|
+
log_and_print(f" Wrote frames {start}:{stop} (min={blk_min:.1f}, max={blk_max:.1f})")
|
|
436
|
+
|
|
437
|
+
log_and_print(f"H5 written: {h5_path} (range min={running_min:.2f}, max={running_max:.2f})")
|
|
438
|
+
|
|
439
|
+
# Quick verification (first frame only)
|
|
440
|
+
try:
|
|
441
|
+
with h5py.File(h5_path, 'r') as f:
|
|
442
|
+
test = f['data'][0]
|
|
443
|
+
if test.shape != (Ly, Lx):
|
|
444
|
+
log_and_print("WARNING: First frame shape mismatch on H5 read-back", level='warning')
|
|
445
|
+
else:
|
|
446
|
+
log_and_print("H5 read-back first frame OK")
|
|
447
|
+
except Exception as e:
|
|
448
|
+
log_and_print(f"H5 verification failed: {e}", level='warning')
|
|
449
|
+
|
|
450
|
+
# choose bin edges using running_min/running_max computed above
|
|
451
|
+
# nbins = 100
|
|
452
|
+
# bin_edges = np.linspace(running_min, running_max, nbins + 1)
|
|
453
|
+
# hist_counts = np.zeros(nbins, dtype=np.int64)
|
|
454
|
+
|
|
455
|
+
# # Compute histogram of pixel values on h5 file
|
|
456
|
+
# with h5py.File(h5_path, 'r') as f:
|
|
457
|
+
# data = f['data'][:]
|
|
458
|
+
# hist_counts, _ = np.histogram(data, bins=bin_edges)
|
|
459
|
+
|
|
460
|
+
# axs[1].bar(bin_edges[:-1], hist_counts, width=np.diff(bin_edges), color='gray')
|
|
461
|
+
# axs[1].set_title('Histogram of Pixel Values (H5)')
|
|
462
|
+
# axs[1].set_xlabel('Pixel Value')
|
|
463
|
+
# axs[1].set_ylabel('Frequency')
|
|
464
|
+
# plt.tight_layout()
|
|
465
|
+
|
|
466
|
+
# # Save figure
|
|
467
|
+
# histo_fig_path = h5_path.parent / "plots" / "pixel_value_histogram_h5.png"
|
|
468
|
+
# plt.savefig(histo_fig_path)
|
|
469
|
+
# plt.close(fig)
|
|
470
|
+
|
|
471
|
+
adapter.close()
|
|
472
|
+
# Copy concatenation sidecar JSON next to the new file
|
|
473
|
+
allow_suite2p_dir = (
|
|
474
|
+
parameters
|
|
475
|
+
and parameters.get("params_extraction", {}).get("method") in {"suite2p", "aind"}
|
|
476
|
+
)
|
|
477
|
+
_copy_concat_sidecar(Path(h5_path).parent, Path(h5_path), allow_suite2p_dir=allow_suite2p_dir)
|
|
478
|
+
return Path(h5_path)
|
|
479
|
+
|
|
480
|
+
def save_movie_as_bin(memmap_path, bin_path, parameters=None, chunk_size=512, scale=None):
|
|
481
|
+
"""
|
|
482
|
+
Stream-save motion-corrected movie as Suite2p-compatible .bin file (int16 C-order).
|
|
483
|
+
"""
|
|
484
|
+
log_and_print(f"Saving final movie to {bin_path} (chunk_size={chunk_size})")
|
|
485
|
+
adapter = load_caiman_memmap(memmap_path)
|
|
486
|
+
T, Ly, Lx = adapter.shape
|
|
487
|
+
log_and_print(f"Memmap adapter: frames={T}, Ly={Ly}, Lx={Lx}, dtype={adapter.dtype}")
|
|
488
|
+
|
|
489
|
+
running_min = np.inf
|
|
490
|
+
running_max = -np.inf
|
|
491
|
+
|
|
492
|
+
# Create a figure of pixel values histograms
|
|
493
|
+
# fig, axs = plt.subplots(1, 2, figsize=(12, 6))
|
|
494
|
+
# axs[0].hist(adapter.flatten(), bins=100, color='gray')
|
|
495
|
+
# axs[0].set_title('Histogram of Pixel Values (Memmap)')
|
|
496
|
+
# axs[0].set_xlabel('Pixel Value')
|
|
497
|
+
# axs[0].set_ylabel('Frequency')
|
|
498
|
+
|
|
499
|
+
with open(bin_path, 'wb') as f:
|
|
500
|
+
for start in range(0, T, chunk_size):
|
|
501
|
+
stop = min(T, start + chunk_size)
|
|
502
|
+
block = adapter[start:stop].astype(np.float32, copy=False)
|
|
503
|
+
if scale is not None:
|
|
504
|
+
block *= scale
|
|
505
|
+
blk_min = float(block.min()); blk_max = float(block.max())
|
|
506
|
+
if blk_min < running_min: running_min = blk_min
|
|
507
|
+
if blk_max > running_max: running_max = blk_max
|
|
508
|
+
block = clip_range(block, 'int16').astype(np.int16, copy=False)
|
|
509
|
+
f.write(np.ascontiguousarray(block).tobytes())
|
|
510
|
+
if (start // chunk_size) % 20 == 0 or stop == T:
|
|
511
|
+
log_and_print(f" Wrote frames {start}:{stop} (min={blk_min:.1f}, max={blk_max:.1f})")
|
|
512
|
+
|
|
513
|
+
# Verify size
|
|
514
|
+
expected_size = T * Ly * Lx * 2
|
|
515
|
+
actual_size = Path(bin_path).stat().st_size
|
|
516
|
+
log_and_print(f"Binary written: {bin_path} bytes={actual_size} expected={expected_size}")
|
|
517
|
+
if actual_size != expected_size:
|
|
518
|
+
log_and_print("WARNING: Size mismatch in .bin export", level='warning')
|
|
519
|
+
|
|
520
|
+
# Quick read-back of first frame
|
|
521
|
+
try:
|
|
522
|
+
first = np.fromfile(bin_path, dtype=np.int16, count=Ly*Lx).reshape(Ly, Lx)
|
|
523
|
+
log_and_print(f"First frame read-back: min={first.min()} max={first.max()}")
|
|
524
|
+
except Exception as e:
|
|
525
|
+
log_and_print(f"Read-back failed: {e}", level='warning')
|
|
526
|
+
|
|
527
|
+
# # Compute histogram of pixel values
|
|
528
|
+
# hist, bin_edges = np.histogram(np.fromfile(bin_path, dtype=np.int16), bins=100)
|
|
529
|
+
# axs[1].bar(bin_edges[:-1], hist, width=np.diff(bin_edges), color='gray')
|
|
530
|
+
# axs[1].set_title('Histogram of Pixel Values (Bin)')
|
|
531
|
+
# axs[1].set_xlabel('Pixel Value')
|
|
532
|
+
# axs[1].set_ylabel('Frequency')
|
|
533
|
+
# plt.tight_layout()
|
|
534
|
+
|
|
535
|
+
# # Save figure
|
|
536
|
+
# histo_fig_path = bin_path.parent / "plots" / "pixel_value_histogram_bin.png"
|
|
537
|
+
# plt.savefig(histo_fig_path)
|
|
538
|
+
# plt.close(fig)
|
|
539
|
+
|
|
540
|
+
log_and_print(f"Range across stream: min={running_min:.2f}, max={running_max:.2f}")
|
|
541
|
+
adapter.close()
|
|
542
|
+
log_and_print(f"✓ Successfully saved .bin movie to {bin_path}")
|
|
543
|
+
# Copy concatenation sidecar JSON next to the new file
|
|
544
|
+
allow_suite2p_dir = (
|
|
545
|
+
parameters
|
|
546
|
+
and parameters.get("params_extraction", {}).get("method") in {"suite2p", "aind"}
|
|
547
|
+
)
|
|
548
|
+
_copy_concat_sidecar(Path(bin_path).parent, Path(bin_path), allow_suite2p_dir=allow_suite2p_dir)
|
|
549
|
+
return Path(bin_path)
|
|
550
|
+
|
|
551
|
+
def save_movie_as_tiff(memmap_path, tiff_path, parameters=None, chunk_size=256, dtype_out='uint16', scale=None):
|
|
552
|
+
"""Stream-save motion-corrected movie as BigTIFF (multi-page).
|
|
553
|
+
|
|
554
|
+
Frames are written sequentially to avoid loading the full movie. By default, scales/clips to uint16.
|
|
555
|
+
"""
|
|
556
|
+
log_and_print(f"Saving final movie to {tiff_path} (dtype_out={dtype_out})")
|
|
557
|
+
adapter = load_caiman_memmap(memmap_path)
|
|
558
|
+
T, Ly, Lx = adapter.shape
|
|
559
|
+
log_and_print(f"Memmap adapter: frames={T}, Ly={Ly}, Lx={Lx}, dtype={adapter.dtype}")
|
|
560
|
+
|
|
561
|
+
running_min = np.inf
|
|
562
|
+
running_max = -np.inf
|
|
563
|
+
|
|
564
|
+
# # Create a figure of pixel values histograms
|
|
565
|
+
# fig, axs = plt.subplots(1, 2, figsize=(12, 6))
|
|
566
|
+
# axs[0].hist(adapter.flatten(), bins=100, color='gray')
|
|
567
|
+
# axs[0].set_title('Histogram of Pixel Values (Memmap)')
|
|
568
|
+
# axs[0].set_xlabel('Pixel Value')
|
|
569
|
+
# axs[0].set_ylabel('Frequency')
|
|
570
|
+
|
|
571
|
+
# choose converter
|
|
572
|
+
def to_dtype(frames: np.ndarray) -> np.ndarray:
|
|
573
|
+
arr = frames
|
|
574
|
+
if scale is not None:
|
|
575
|
+
arr = arr.astype(np.float32) * float(scale)
|
|
576
|
+
if dtype_out == 'uint16':
|
|
577
|
+
arr = clip_range(arr, 'uint16').astype(np.uint16)
|
|
578
|
+
elif dtype_out == 'float32':
|
|
579
|
+
arr = arr.astype(np.float32)
|
|
580
|
+
else:
|
|
581
|
+
raise ValueError("dtype_out must be 'uint16' or 'float32'")
|
|
582
|
+
return arr
|
|
583
|
+
|
|
584
|
+
with TiffWriter(str(tiff_path), bigtiff=True) as tif:
|
|
585
|
+
for start in range(0, T, chunk_size):
|
|
586
|
+
stop = min(T, start + chunk_size)
|
|
587
|
+
chunk = adapter[start:stop] # (chunk, Ly, Lx)
|
|
588
|
+
running_min = min(running_min, float(np.min(chunk)))
|
|
589
|
+
running_max = max(running_max, float(np.max(chunk)))
|
|
590
|
+
out = to_dtype(chunk)
|
|
591
|
+
for k in range(out.shape[0]):
|
|
592
|
+
tif.write(out[k], contiguous=True, photometric='minisblack')
|
|
593
|
+
|
|
594
|
+
log_and_print(f"TIFF written: {tiff_path} (range min={running_min:.2f}, max={running_max:.2f})")
|
|
595
|
+
adapter.close()
|
|
596
|
+
|
|
597
|
+
# Copy concatenation sidecar JSON next to the new file
|
|
598
|
+
allow_suite2p_dir = (
|
|
599
|
+
parameters
|
|
600
|
+
and parameters.get("params_extraction", {}).get("method") in {"suite2p", "aind"}
|
|
601
|
+
)
|
|
602
|
+
_copy_concat_sidecar(Path(tiff_path).parent, Path(tiff_path), allow_suite2p_dir=allow_suite2p_dir)
|
|
603
|
+
|
|
604
|
+
# # choose bin edges using running_min/running_max computed above
|
|
605
|
+
# nbins = 100
|
|
606
|
+
# bin_edges = np.linspace(running_min, running_max, nbins + 1)
|
|
607
|
+
# hist_counts = np.zeros(nbins, dtype=np.int64)
|
|
608
|
+
|
|
609
|
+
# with TiffFile(str(tiff_path)) as tif:
|
|
610
|
+
# for page in tif.pages:
|
|
611
|
+
# arr = page.asarray()
|
|
612
|
+
# c, _ = np.histogram(arr, bins=bin_edges)
|
|
613
|
+
# hist_counts += c
|
|
614
|
+
|
|
615
|
+
# # plot as bar (centers and widths)
|
|
616
|
+
# centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
617
|
+
# axs[1].bar(centers, hist_counts, width=np.diff(bin_edges), color='gray')
|
|
618
|
+
# axs[1].set_title('Histogram of Pixel Values (TIFF)')
|
|
619
|
+
# axs[1].set_xlabel('Pixel Value')
|
|
620
|
+
# axs[1].set_ylabel('Frequency')
|
|
621
|
+
# plt.tight_layout()
|
|
622
|
+
|
|
623
|
+
# # Save figure
|
|
624
|
+
# histo_fig_path = tiff_path.parent / "plots" / "pixel_value_histogram_tiff.png"
|
|
625
|
+
# fig.savefig(histo_fig_path, dpi=300)
|
|
626
|
+
# plt.close(fig)
|
|
627
|
+
|
|
628
|
+
return Path(tiff_path)
|
|
629
|
+
|
|
630
|
+
def run_mcorr(data_path, export_path, parameters, regex_pattern, recompute=True, scale_range=False):
|
|
631
|
+
"""
|
|
632
|
+
Run motion correction on a set of ome.tif files.
|
|
633
|
+
Concatenate the ome.tif files into a single multi-page tiff file.
|
|
634
|
+
Create a new batch.
|
|
635
|
+
Add the motion correction item to the batch.
|
|
636
|
+
Run the batch.
|
|
637
|
+
|
|
638
|
+
Arguments:
|
|
639
|
+
- data_path: Path to the folder containing the input ome.tif files.
|
|
640
|
+
- export_path: Path to the folder where the output files will be saved.
|
|
641
|
+
- parameters: Dictionary containing the parameters for motion correction.
|
|
642
|
+
- regex_pattern: Regular expression pattern to match the input files.
|
|
643
|
+
- recompute: Boolean indicating whether to recompute the motion correction.
|
|
644
|
+
- scale_range: Boolean indicating whether to scale the output to uint16 range.
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
- batch_path: Path to the created batch file.
|
|
648
|
+
- index: Index of the movie in the batch.
|
|
649
|
+
- movie_path: Path to the concatenated movie file.
|
|
650
|
+
|
|
651
|
+
"""
|
|
652
|
+
# Set movie path
|
|
653
|
+
movie_path = Path(export_path).joinpath('cat_tiff_bt.tiff')
|
|
654
|
+
|
|
655
|
+
# Set the parent raw data path before any batch operations
|
|
656
|
+
mc.set_parent_raw_data_path(Path(export_path))
|
|
657
|
+
|
|
658
|
+
if not recompute and movie_path.exists():
|
|
659
|
+
log_and_print(f"Concatenated movie already exists at {export_path}. Using existing file.")
|
|
660
|
+
else:
|
|
661
|
+
# Concatenate the ome.tif files into a single multi-page tiff file
|
|
662
|
+
# Using the concat_tif.py script to concatenate the tif files. If needed, install libtiff with: pip install pylibtiff
|
|
663
|
+
log_and_print(f"Loading and concatenating data from {data_path}.")
|
|
664
|
+
try:
|
|
665
|
+
# time0 = time.time()
|
|
666
|
+
##################
|
|
667
|
+
ct.concatenate_files(
|
|
668
|
+
input_paths=data_path,
|
|
669
|
+
output_path=export_path,
|
|
670
|
+
regex=regex_pattern,
|
|
671
|
+
scale_range=scale_range
|
|
672
|
+
)
|
|
673
|
+
##################
|
|
674
|
+
# formatted_time = time.strftime("%H:%M:%S", time.gmtime(time.time() - time0))
|
|
675
|
+
# print(f"Concatenation completed in {formatted_time}.")
|
|
676
|
+
|
|
677
|
+
# Check the tiff file dtype and value range
|
|
678
|
+
with TiffFile(movie_path) as tif:
|
|
679
|
+
dtype = tif.pages[0].asarray().dtype
|
|
680
|
+
min_val = tif.pages[0].asarray().min()
|
|
681
|
+
max_val = tif.pages[0].asarray().max()
|
|
682
|
+
mean_val = tif.pages[0].asarray().mean()
|
|
683
|
+
print(f"Concatenated TIFF file \n\
|
|
684
|
+
dtype: {dtype}, \n\
|
|
685
|
+
min: {min_val}, \n\
|
|
686
|
+
max: {max_val}, \n\
|
|
687
|
+
mean: {mean_val}")
|
|
688
|
+
|
|
689
|
+
# Verify that the concatenated movie is long enough for subsequent
|
|
690
|
+
# correlation computations. If the movie is too short, CaImAn's
|
|
691
|
+
# ``local_correlations_movie_parallel`` will fail silently.
|
|
692
|
+
with TiffFile(movie_path) as tif:
|
|
693
|
+
n_frames = len(tif.pages)
|
|
694
|
+
if n_frames < 1000:
|
|
695
|
+
raise ValueError(
|
|
696
|
+
f"Concatenated movie has {n_frames} frames which is below the safe "
|
|
697
|
+
"threshold (1000). Increase the '--max-frames' parameter when "
|
|
698
|
+
"creating test datasets or rerun with more data."
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
except Exception as e:
|
|
702
|
+
log_and_print(f"An error occurred while concatenating files: {e}")
|
|
703
|
+
|
|
704
|
+
# log_and_print(f"Concatenated movie path: {movie_path}.\n")
|
|
705
|
+
|
|
706
|
+
if not recompute:
|
|
707
|
+
# Check if a batch_*_pickle file exists in the export path
|
|
708
|
+
batch_files = list(Path(export_path).glob(f"batch_*.pickle"))
|
|
709
|
+
if len(batch_files) > 0:
|
|
710
|
+
# take the most recent batch file
|
|
711
|
+
batch_path = batch_files[-1]
|
|
712
|
+
log_and_print(f"Using existing batch file {batch_path}.")
|
|
713
|
+
# load the batch
|
|
714
|
+
df = mc.load_batch(batch_path)
|
|
715
|
+
|
|
716
|
+
# rows_keep = [df.iloc[0].uuid]
|
|
717
|
+
# for i, row in df.iterrows():
|
|
718
|
+
# if row.uuid not in rows_keep:
|
|
719
|
+
# df.caiman.remove_item(row.uuid)
|
|
720
|
+
|
|
721
|
+
# if batch_path is not defined, create a new batch
|
|
722
|
+
if 'batch_path' not in locals():
|
|
723
|
+
# create a new batch path, appendind timestamp to avoid overwriting
|
|
724
|
+
batch_path = Path.joinpath(export_path, f'batch_{time.strftime("%Y%m%d-%H%M%S")}.pickle')
|
|
725
|
+
log_and_print(f"Creating batch {batch_path}.")
|
|
726
|
+
df = mc.create_batch(batch_path)
|
|
727
|
+
|
|
728
|
+
# Add the input movie path to the batch
|
|
729
|
+
df.caiman.add_item(
|
|
730
|
+
algo='mcorr',
|
|
731
|
+
item_name=movie_path.stem,
|
|
732
|
+
input_movie_path=movie_path,
|
|
733
|
+
params=parameters
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
time0 = time.time()
|
|
737
|
+
mcorr_index = None
|
|
738
|
+
# Run batch items that haven't been run yet
|
|
739
|
+
for row_index, row in df.iterrows():
|
|
740
|
+
|
|
741
|
+
# If algo is not mcorr, skip (this is a safety check, in case df was reloaded from disk)
|
|
742
|
+
if row.algo != 'mcorr':
|
|
743
|
+
continue
|
|
744
|
+
|
|
745
|
+
# If already processed, skip
|
|
746
|
+
if row["outputs"] is not None:
|
|
747
|
+
log_and_print(f"Skipping batch item {row_index}, id {row.uuid}, algo {row.algo}. Already run.", level='warning')
|
|
748
|
+
mcorr_index = row_index
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
log_and_print(f"Running batch item {row_index}, id {row.uuid}, algo {row.algo}.")
|
|
752
|
+
try:
|
|
753
|
+
##########################
|
|
754
|
+
process = row.caiman.run()
|
|
755
|
+
mcorr_index = row_index
|
|
756
|
+
##########################
|
|
757
|
+
except Exception as e:
|
|
758
|
+
log_and_print(f"An error occurred while running caiman for batch item {row_index}: {e}")
|
|
759
|
+
|
|
760
|
+
# on Windows you MUST reload the batch dataframe after every iteration because it uses the `local` backend.
|
|
761
|
+
# this is unnecessary on Linux & Mac
|
|
762
|
+
# "DummyProcess" is used for local backend so this is automatic
|
|
763
|
+
if process.__class__.__name__ == "DummyProcess":
|
|
764
|
+
df = df.caiman.reload_from_disk()
|
|
765
|
+
|
|
766
|
+
log_and_print(f"Batch completed for motion correction. Results saved to {batch_path}.")
|
|
767
|
+
formatted_time = time.strftime("%H:%M:%S", time.gmtime(time.time() - time0))
|
|
768
|
+
print(f"mcorr completed in {formatted_time}.")
|
|
769
|
+
|
|
770
|
+
if mcorr_index is not None:
|
|
771
|
+
df = df.caiman.reload_from_disk()
|
|
772
|
+
# Get the path to the motion corrected movie
|
|
773
|
+
movie_path = Path(df.iloc[mcorr_index].mcorr.get_output_path())
|
|
774
|
+
# Get the motion corrected output as a memmaped numpy array
|
|
775
|
+
# mcorr_movie = df.iloc[mcorr_index].mcorr.get_output()
|
|
776
|
+
|
|
777
|
+
# Load motion corrected movie and check dtype, and mean pixel values
|
|
778
|
+
mcorr_movie = load_mmap_movie(movie_path)
|
|
779
|
+
if mcorr_movie is not None:
|
|
780
|
+
shape = mcorr_movie.shape
|
|
781
|
+
dtype = mcorr_movie.dtype
|
|
782
|
+
first_frame = mcorr_movie[0]
|
|
783
|
+
log_and_print(f"Motion corrected movie \n\
|
|
784
|
+
shape: {shape}, \n\
|
|
785
|
+
dtype: {dtype}, \n\
|
|
786
|
+
first frame min: {first_frame.min()}, \n\
|
|
787
|
+
first frame max: {first_frame.max()}, \n\
|
|
788
|
+
first frame mean: {first_frame.mean()}")
|
|
789
|
+
else:
|
|
790
|
+
log_and_print("Motion corrected movie could not be loaded.", level='error')
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
return batch_path, 0, movie_path
|
|
794
|
+
|
|
795
|
+
def run_motion_correction_workflow(
|
|
796
|
+
data_path,
|
|
797
|
+
export_path,
|
|
798
|
+
parameters,
|
|
799
|
+
regex_pattern='*_Ch2_*.ome.tif',
|
|
800
|
+
recompute=True,
|
|
801
|
+
create_movies=True,
|
|
802
|
+
output_format='memmap',
|
|
803
|
+
):
|
|
804
|
+
"""
|
|
805
|
+
Complete motion correction workflow including z-motion correction.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
data_path: List of paths containing input data
|
|
809
|
+
export_path: Output directory path
|
|
810
|
+
parameters: Dictionary containing processing parameters
|
|
811
|
+
regex_pattern: Pattern to match input files
|
|
812
|
+
recompute: Whether to recompute existing results
|
|
813
|
+
create_movies: Whether to create output movies
|
|
814
|
+
output_format: 'memmap' (default), 'h5', 'tiff', or 'bin' for final motion-corrected movie storage
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
dict: Results dictionary with paths and metadata
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
results = {
|
|
821
|
+
'batch_path': None,
|
|
822
|
+
'movie_path': None,
|
|
823
|
+
'export_path': export_path,
|
|
824
|
+
'success': False
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
try:
|
|
828
|
+
# Get motion correction parameters
|
|
829
|
+
parameters_mcorr = parameters['params_mcorr']
|
|
830
|
+
|
|
831
|
+
# Run the motion correction
|
|
832
|
+
scale_range_raw = parameters_mcorr.get('scale_range', False)
|
|
833
|
+
# Handle string representations of booleans
|
|
834
|
+
if isinstance(scale_range_raw, str):
|
|
835
|
+
scale_range = scale_range_raw.lower() in ('true', '1', 'yes')
|
|
836
|
+
else:
|
|
837
|
+
scale_range = bool(scale_range_raw)
|
|
838
|
+
|
|
839
|
+
batch_path, index, movie_path = run_mcorr(
|
|
840
|
+
data_path, export_path, parameters_mcorr, regex_pattern, recompute, scale_range=scale_range
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
results['batch_path'] = batch_path
|
|
844
|
+
results['movie_path'] = movie_path
|
|
845
|
+
|
|
846
|
+
# Determine whether to clip to uint16 range
|
|
847
|
+
clip_movie = output_format not in ('h5', 'bin', 'tiff')
|
|
848
|
+
|
|
849
|
+
if not recompute and movie_path.exists():
|
|
850
|
+
log_and_print(f"Motion corrected movie already exists at {movie_path}.")
|
|
851
|
+
else:
|
|
852
|
+
if clip_movie:
|
|
853
|
+
log_and_print("Optimizing motion corrected movie bit depth (clipping memmap to uint16).")
|
|
854
|
+
overwrite_movie_memmap(movie_path, movie_path, clip=True, movie_type='mcorr')
|
|
855
|
+
else:
|
|
856
|
+
log_and_print("Skipping memmap overwrite to preserve full dynamic range for downstream export.")
|
|
857
|
+
|
|
858
|
+
# Z-motion correction (optional)
|
|
859
|
+
if 'zstack_path' in parameters and 'z_motion_correction' in parameters.get('params_mcorr', {}):
|
|
860
|
+
log_and_print("Starting z-motion correction...")
|
|
861
|
+
time_z0 = time.time()
|
|
862
|
+
|
|
863
|
+
zcorr_movie_path, _, _ = cz.z_motion(
|
|
864
|
+
movie_path, parameters, scale_range=scale_range
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Save corrected movie, overwriting the original
|
|
868
|
+
if zcorr_movie_path is not None:
|
|
869
|
+
overwrite_movie_memmap(
|
|
870
|
+
zcorr_movie_path,
|
|
871
|
+
movie_path,
|
|
872
|
+
clip=clip_movie,
|
|
873
|
+
movie_type='zcorr',
|
|
874
|
+
save_original=False,
|
|
875
|
+
remove_input=True
|
|
876
|
+
)
|
|
877
|
+
results['z_corrected'] = True
|
|
878
|
+
else:
|
|
879
|
+
results['z_corrected'] = False
|
|
880
|
+
|
|
881
|
+
formatted_time = time.strftime("%H:%M:%S", time.gmtime(time.time() - time_z0))
|
|
882
|
+
if zcorr_movie_path is not None:
|
|
883
|
+
log_and_print(f"Z-motion correction completed in {formatted_time}.")
|
|
884
|
+
else:
|
|
885
|
+
log_and_print(f"Z-motion computation completed in {formatted_time}.")
|
|
886
|
+
|
|
887
|
+
# Create output movies (optional)
|
|
888
|
+
if create_movies:
|
|
889
|
+
log_and_print("Creating output movies...")
|
|
890
|
+
|
|
891
|
+
# Save as BigTIFF file
|
|
892
|
+
create_mcorr_movie(movie_path, export_path, None, None,
|
|
893
|
+
format='tiff', diff_corr=False)
|
|
894
|
+
|
|
895
|
+
# Create comparison movie (first 240 frames)
|
|
896
|
+
create_mcorr_movie(mcorr_movie_path=movie_path, export_path=export_path,
|
|
897
|
+
batch=batch_path, index=index, excerpt=240)
|
|
898
|
+
|
|
899
|
+
if output_format == 'memmap':
|
|
900
|
+
# This is already handled by run_mcorr() -> get_output_path()
|
|
901
|
+
# The memmap file exists in the batch folder.
|
|
902
|
+
# We don't need to duplicate it or link it, as the pipeline now preserves the batch folder.
|
|
903
|
+
pass
|
|
904
|
+
|
|
905
|
+
results['success'] = True
|
|
906
|
+
|
|
907
|
+
if output_format == 'h5':
|
|
908
|
+
print("Saving motion corrected movie as .h5 file...")
|
|
909
|
+
h5_path = export_path / 'mcorr_movie.h5'
|
|
910
|
+
# Save movie as Suite2p, AIND and ImageJ-compatible .h5
|
|
911
|
+
results['movie_path'] = save_movie_as_h5(
|
|
912
|
+
memmap_path=movie_path,
|
|
913
|
+
h5_path=h5_path,
|
|
914
|
+
parameters=parameters
|
|
915
|
+
)
|
|
916
|
+
elif output_format == 'bin':
|
|
917
|
+
log_and_print("Saving motion corrected movie as .bin file...")
|
|
918
|
+
bin_path = export_path / 'mcorr_movie.bin'
|
|
919
|
+
# Save movie as Suite2p-compatible .bin
|
|
920
|
+
results['movie_path'] = save_movie_as_bin(
|
|
921
|
+
memmap_path=movie_path,
|
|
922
|
+
bin_path=bin_path,
|
|
923
|
+
parameters=parameters
|
|
924
|
+
)
|
|
925
|
+
elif output_format == 'tiff':
|
|
926
|
+
log_and_print("Saving motion corrected movie as BigTIFF file...")
|
|
927
|
+
tiff_path = export_path / 'mcorr_movie.tiff'
|
|
928
|
+
results['movie_path'] = save_movie_as_tiff(
|
|
929
|
+
memmap_path=movie_path,
|
|
930
|
+
tiff_path=tiff_path,
|
|
931
|
+
parameters=parameters
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
log_and_print("Motion correction workflow completed successfully.")
|
|
935
|
+
|
|
936
|
+
except Exception as e:
|
|
937
|
+
log_and_print(f"Motion correction workflow failed: {e}", level='error')
|
|
938
|
+
results['error'] = str(e)
|
|
939
|
+
raise
|
|
940
|
+
|
|
941
|
+
return results
|
|
942
|
+
|
|
943
|
+
if __name__ == "__main__":
|
|
944
|
+
import argparse
|
|
945
|
+
|
|
946
|
+
parser = argparse.ArgumentParser(description="Motion Correction Module")
|
|
947
|
+
parser.add_argument('input_paths', nargs='+', help='Input data paths')
|
|
948
|
+
parser.add_argument('-o', '--output', required=True, help='Output directory')
|
|
949
|
+
parser.add_argument('-p', '--params', type=str, help='Path to parameters JSON file')
|
|
950
|
+
parser.add_argument('-t', '--pattern', default='*_Ch2_*.ome.tif', help='File pattern')
|
|
951
|
+
parser.add_argument('-r', '--recompute', action='store_true', help='Recompute existing results')
|
|
952
|
+
parser.add_argument('-c', '--create_movies', action='store_true', help='Create output movies')
|
|
953
|
+
parser.add_argument('-f', '--format', choices=['memmap', 'h5', 'bin', 'tiff'], default='memmap', help='Output format for final movie')
|
|
954
|
+
args = parser.parse_args()
|
|
955
|
+
|
|
956
|
+
# Convert input paths to Path objects
|
|
957
|
+
input_paths = [Path(p) for p in args.input_paths]
|
|
958
|
+
output_path = Path(args.output)
|
|
959
|
+
pattern = args.pattern
|
|
960
|
+
recompute = args.recompute
|
|
961
|
+
create_movies = args.create_movies
|
|
962
|
+
save_mcorr_movie = args.format
|
|
963
|
+
|
|
964
|
+
# Load parameters from JSON file if provided
|
|
965
|
+
parameters = {}
|
|
966
|
+
if args.params:
|
|
967
|
+
import json
|
|
968
|
+
with open(args.params, 'r') as f:
|
|
969
|
+
parameters = json.load(f)
|
|
970
|
+
else:
|
|
971
|
+
raise ValueError("Parameters file must be provided with -p option.")
|
|
972
|
+
|
|
973
|
+
# Run the motion correction workflow
|
|
974
|
+
results = run_motion_correction_workflow(
|
|
975
|
+
data_path=input_paths,
|
|
976
|
+
export_path=output_path,
|
|
977
|
+
parameters=parameters,
|
|
978
|
+
regex_pattern=pattern,
|
|
979
|
+
recompute=recompute,
|
|
980
|
+
create_movies=create_movies,
|
|
981
|
+
save_mcorr_movie=save_mcorr_movie
|
|
982
|
+
)
|