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/extraction.py ADDED
@@ -0,0 +1,559 @@
1
+ # -*- coding: utf-8 -*-
2
+ """extraction
3
+
4
+ Module implementing the constrained nonnegative matrix factorization (CNMF)
5
+ step of the Analysis 2P pipeline.
6
+
7
+ This module was extracted from :mod:`Mesmerize.pipeline_mcorr_cnmf` so that the
8
+ CNMF logic can be reused independently of the full pipeline.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ import re
15
+ from pathlib import Path
16
+
17
+ # Add project root to path
18
+ PROJECT_ROOT = Path(__file__).parent.parent
19
+ if str(PROJECT_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(PROJECT_ROOT))
21
+
22
+ import json
23
+ import multiprocessing
24
+ import shutil
25
+ import threading
26
+ import time
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+ import mesmerize_core as mc
31
+ from scipy import io
32
+
33
+ from caiman.mmapping import load_memmap, prepare_shape
34
+ from caiman import save_memmap as save_memmap
35
+
36
+ from pipeline.utils.pipeline_utils import (
37
+ log_and_print,
38
+ clip_range,
39
+ cat_movies_to_mp4,
40
+ load_caiman_memmap,
41
+ )
42
+
43
+ __all__ = [
44
+ "run_cnmf",
45
+ "save_processing_parameters",
46
+ "prepare_cnmf_object",
47
+ "copy_mean_intensity_template",
48
+ "export_cnmf_results",
49
+ ]
50
+
51
+
52
+ def _prepare_cnmf_movie(mcorr_movie_path: str | Path, export_path: str | Path) -> Path:
53
+ mcorr_movie_path = Path(mcorr_movie_path)
54
+ export_path = Path(export_path)
55
+
56
+ # Ensure export directory exists for saving memmaps
57
+ if not export_path.exists():
58
+ export_path.mkdir(parents=True, exist_ok=True)
59
+
60
+ if mcorr_movie_path.suffix.lower() in {".h5", ".hdf5", ".tif", ".tiff"}:
61
+
62
+ # Save memmap directly to export_path/mcorr to avoid tmp directory issues
63
+ mcorr_movie_path = Path(
64
+ save_memmap(
65
+ [str(mcorr_movie_path)],
66
+ base_name=str(export_path / "mcorr"),
67
+ order="C",
68
+ dview=None
69
+ )
70
+ )
71
+
72
+ try:
73
+ RelPath = mcorr_movie_path.relative_to(export_path)
74
+ except ValueError:
75
+ # Path is not relative to export path (e.g. it's in /tmp)
76
+ new_path = export_path / mcorr_movie_path.name
77
+ if new_path.exists():
78
+ if new_path.stat().st_size == mcorr_movie_path.stat().st_size:
79
+ mcorr_movie_path = new_path
80
+ else:
81
+ shutil.move(mcorr_movie_path, new_path)
82
+ mcorr_movie_path = new_path
83
+ else:
84
+ shutil.move(mcorr_movie_path, new_path)
85
+ mcorr_movie_path = new_path
86
+ log_and_print(f"Moved temporary memmap to {mcorr_movie_path}")
87
+
88
+ return mcorr_movie_path
89
+
90
+
91
+ def _mean_projection_from_memmap(
92
+ movie_path: Path,
93
+ chunk_size: int = 200,
94
+ max_frames: int | None = None,
95
+ ) -> np.ndarray:
96
+ adapter = load_caiman_memmap(movie_path)
97
+ T, Ly, Lx = adapter.shape
98
+ if max_frames is not None:
99
+ T = min(T, max_frames)
100
+ running_sum = np.zeros((Ly, Lx), dtype=np.float64)
101
+ for start in range(0, T, chunk_size):
102
+ stop = min(T, start + chunk_size)
103
+ running_sum += adapter[start:stop].sum(axis=0, dtype=np.float64)
104
+ adapter.close()
105
+ return (running_sum / float(T)).astype(np.float32)
106
+
107
+
108
+ def _mean_projection_from_tiff(
109
+ movie_path: Path,
110
+ max_frames: int | None = None,
111
+ ) -> np.ndarray:
112
+ import tifffile
113
+
114
+ running_sum = None
115
+ count = 0
116
+ with tifffile.TiffFile(movie_path) as tif:
117
+ for page in tif.pages:
118
+ frame = page.asarray()
119
+ if running_sum is None:
120
+ running_sum = np.zeros_like(frame, dtype=np.float64)
121
+ running_sum += frame.astype(np.float64, copy=False)
122
+ count += 1
123
+ if max_frames is not None and count >= max_frames:
124
+ break
125
+ if running_sum is None or count == 0:
126
+ raise ValueError(f"No frames found in {movie_path}")
127
+ return (running_sum / float(count)).astype(np.float32)
128
+
129
+
130
+ def _mean_projection_from_h5(
131
+ movie_path: Path,
132
+ max_frames: int | None = None,
133
+ ) -> np.ndarray:
134
+ import h5py
135
+
136
+ with h5py.File(movie_path, "r") as f:
137
+ if "data" in f:
138
+ dset = f["data"]
139
+ else:
140
+ keys = [k for k in f.keys()]
141
+ if not keys:
142
+ raise ValueError(f"No datasets found in {movie_path}")
143
+ dset = f[keys[0]]
144
+ T = dset.shape[0]
145
+ if max_frames is not None:
146
+ T = min(T, max_frames)
147
+ running_sum = np.zeros(dset.shape[1:], dtype=np.float64)
148
+ for start in range(0, T, 200):
149
+ stop = min(T, start + 200)
150
+ running_sum += dset[start:stop].sum(axis=0, dtype=np.float64)
151
+ return (running_sum / float(T)).astype(np.float32)
152
+
153
+
154
+ def _mean_projection_from_path(
155
+ movie_path: str | Path,
156
+ max_frames: int | None = None,
157
+ ) -> np.ndarray:
158
+ if isinstance(movie_path, np.memmap):
159
+ if movie_path.ndim == 3:
160
+ return np.mean(movie_path, axis=0).astype(np.float32)
161
+ filename = getattr(movie_path, "filename", None)
162
+ if filename:
163
+ path = Path(filename)
164
+ if path.suffix.lower() in {".mmap", ".npy"}:
165
+ return _mean_projection_from_memmap(path, max_frames=max_frames)
166
+ raise TypeError("Unsupported memmap input for mean projection")
167
+ else:
168
+ movie_path = Path(movie_path)
169
+ suffix = movie_path.suffix.lower()
170
+ if suffix in {".mmap", ".npy"}:
171
+ return _mean_projection_from_memmap(movie_path, max_frames=max_frames)
172
+ if suffix in {".tif", ".tiff", ".btf"}:
173
+ return _mean_projection_from_tiff(movie_path, max_frames=max_frames)
174
+ if suffix in {".h5", ".hdf5", ".n5", ".zarr"}:
175
+ return _mean_projection_from_h5(movie_path, max_frames=max_frames)
176
+
177
+ import caiman as cm
178
+ movie = cm.load(str(movie_path))
179
+ return np.mean(movie, axis=0).astype(np.float32)
180
+
181
+
182
+ def _coerce_matlab_value(value):
183
+ if value is None:
184
+ return np.array([])
185
+ return value
186
+
187
+
188
+ def countdown(n: int) -> None:
189
+ """Simple countdown timer used when prompting the user."""
190
+ while n > 0:
191
+ mins, secs = divmod(n, 60)
192
+ timer = f"{mins:02d}:{secs:02d}"
193
+ print(timer, end="\r")
194
+ time.sleep(1)
195
+ n -= 1
196
+
197
+ def create_components_movie(batch_path, export_path, mcorr_movie_path=None, cnmf_obj=None, index=-1, excerpt=None):
198
+ """
199
+ Create a movie concatenating the components and the residuals.
200
+ """
201
+ # if mcorr_movie is not a string, load the data frame from the batch
202
+ # if not isinstance(mcorr_movie, str):
203
+ if batch_path is not None:
204
+ df = mc.load_batch(batch_path)
205
+ # Load the motion corrected movie
206
+ # mcorr_movie = df.iloc[index].caiman.get_input_movie()
207
+ # Load the components (reconstructed movie with no background)
208
+ neural_activity_movie = df.iloc[index].cnmf.get_rcm()
209
+ # image_neural_activity = neural_activity_movie[0,:,:]
210
+ # Load the residuals
211
+ residuals_movie = df.iloc[index].cnmf.get_residuals()
212
+ # image_residuals = residuals_movie[0,:,:]
213
+ else:
214
+ mcorr_movie_path = _prepare_cnmf_movie(mcorr_movie_path, export_path)
215
+ mcorr_movie, dims, T = load_memmap(str(mcorr_movie_path))
216
+ mcorr_movie = np.reshape(mcorr_movie.T, [T] + list(dims), order='F')
217
+ mcorr_movie = mcorr_movie.transpose(0, 2, 1)
218
+
219
+ # # If cnmf_obj is not a CNMF object, load the data frame from the batch path
220
+ # if isinstance(cnmf_obj, str):
221
+ # # Load the data frame from the batch
222
+ # df = mc.load_batch(batch_path)
223
+ # # Load the CNMF results
224
+ # cnmf_obj = df.iloc[index].cnmf.get_output()
225
+ # # Keep only accepted components
226
+ # cnmf_obj.estimates.select_components(use_object=True)
227
+ # # Compute dF/F
228
+ # if cnmf_obj.estimates.F_dff is None:
229
+ # print('Calculating estimates.F_dff')
230
+ # cnmf_obj.estimates.detrend_df_f(quantileMin=8,
231
+ # frames_window=400,
232
+ # use_residuals=False)
233
+
234
+ # reconstruct denoised components movie
235
+ neural_activity = cnmf_obj.estimates.A @ cnmf_obj.estimates.C # A ⊗ C
236
+ # reshape neural activity to movie dimensions
237
+ dims = cnmf_obj.dims
238
+ T = cnmf_obj.estimates.C.shape[1]
239
+ neural_activity_movie = np.reshape(neural_activity.T, [T] + list(dims), order='F')
240
+
241
+ # If residuals_movie is not defined, reconstruct it
242
+ # if 'residuals_movie' not in locals():
243
+
244
+ # reconstruct background movie
245
+ background = cnmf_obj.estimates.b @ cnmf_obj.estimates.f # b ⊗ f
246
+ # reconstruct denoised movie
247
+ denoised_movie = neural_activity + background # AC + bf
248
+ # reshape denoised movie to movie dimensions
249
+ denoised_movie = np.reshape(denoised_movie.T, [T] + list(dims), order='F')
250
+ # reconstruct residuals movie
251
+ residuals_movie = mcorr_movie - denoised_movie # mcorr_movie - AC - bf
252
+ # turn into a movie object
253
+ # denoised_movie = cm.movie(denoised_movie).reshape(dims + (-1,), order='F').transpose([2, 0, 1])
254
+
255
+ # If excerpt requested, keep only the first x frames of the movie
256
+ if excerpt is not None:
257
+ neural_activity_movie = neural_activity_movie[:excerpt]
258
+ residuals_movie = residuals_movie[:excerpt]
259
+
260
+ # Data is originally uint16, but values may extend beyond range, and converted to float at this point.
261
+ # Clip the values to the uint16 range, and convert to uint16 data type
262
+ neural_activity_movie = clip_range(neural_activity_movie, 'uint16').astype('uint16')
263
+ residuals_movie = clip_range(residuals_movie, 'uint16').astype('uint16')
264
+
265
+ # # Convert to uint8 to reduce file size further
266
+ # neural_activity_movie = (neural_activity_movie / (2**16-1) * 255).astype('uint8')
267
+ # residuals_movie = (residuals_movie / (2**16-1) * 255).astype('uint8')
268
+
269
+ # Set the path of the mp4 movie
270
+ movie_path = Path.joinpath(export_path, f"compare_components_residuals.mp4")
271
+
272
+ # Concatenate the two movies horizontally
273
+ cat_movies_to_mp4(neural_activity_movie, residuals_movie, movie_path)
274
+
275
+ log_and_print(f"Saved components movie to {export_path}/components_movie.mp4")
276
+
277
+ def run_cnmf(
278
+ batch: Path,
279
+ index: int | None,
280
+ export_path: Path,
281
+ params_extraction: dict,
282
+ data_path,
283
+ input_movie_path: str | Path | None = None,
284
+ z_correlation=None,
285
+ z_motion_scaling_factors=None,
286
+ ):
287
+ """Run CNMF on a motion corrected movie and export results.
288
+
289
+ Parameters
290
+ ----------
291
+ batch : Path
292
+ Path to the Mesmerize batch file produced by motion correction.
293
+ index : int | None
294
+ Index of the motion correction item within the batch. None if using input_movie_path.
295
+ export_path : Path
296
+ Directory where outputs should be written.
297
+ params_extraction : dict
298
+ Parameters for the CNMF algorithm.
299
+ data_path : str or Path or list
300
+ Original data location (used only for metadata in the saved parameters).
301
+ input_movie_path : str | Path, optional
302
+ Direct path to input movie if not using a batch item.
303
+ z_correlation : np.ndarray, optional
304
+ Pre-computed z correlation data.
305
+ z_motion_scaling_factors : np.ndarray, optional
306
+ Scaling factors from z motion subtraction (unused but kept for backwards
307
+ compatibility).
308
+ """
309
+ # Set the parent raw data path before any batch operations
310
+ mc.set_parent_raw_data_path(Path(export_path))
311
+
312
+ # Load batch dataframe
313
+ df = mc.load_batch(batch)
314
+
315
+ if input_movie_path is not None:
316
+ mcorr_movie_path = Path(input_movie_path)
317
+ item_name = mcorr_movie_path.stem
318
+ elif index is not None:
319
+ mcorr_movie_path=df.iloc[index]
320
+ item_name = df.iloc[index]["item_name"]
321
+ else:
322
+ raise ValueError("Either index or input_movie_path must be provided.")
323
+
324
+ if str(mcorr_movie_path).endswith((".h5", ".hdf5", ".tif", ".tiff")):
325
+ mcorr_movie_path = _prepare_cnmf_movie(mcorr_movie_path, export_path)
326
+
327
+
328
+ # Add CNMF item using the motion corrected result
329
+ df.caiman.add_item(
330
+ algo="cnmf",
331
+ input_movie_path=mcorr_movie_path,
332
+ params=params_extraction,
333
+ item_name=item_name,
334
+ )
335
+
336
+ # Handle existing multiprocessing children from previous runs
337
+ if len(multiprocessing.active_children()) > 0:
338
+ import select
339
+
340
+ print("There are active processes : ")
341
+ for p in multiprocessing.active_children():
342
+ print(p)
343
+ print("Do you want to kill them ? (y/n)")
344
+
345
+ t = threading.Thread(target=countdown, args=(300,))
346
+ t.start()
347
+
348
+ i, _, _ = select.select([sys.stdin], [], [], 300)
349
+ if i:
350
+ answer = sys.stdin.readline().strip()
351
+ else:
352
+ print("Timeout, killing the processes.")
353
+ answer = "y"
354
+
355
+ if answer == "y":
356
+ print("Killing processes")
357
+ for p in multiprocessing.active_children():
358
+ p.terminate()
359
+ p.join()
360
+ else:
361
+ print("CNMF may not run properly if a cluster is already running.")
362
+
363
+ # Run CNMF for items that haven't been processed yet
364
+ for _, row in df.iterrows():
365
+ if row.algo != "cnmf":
366
+ continue
367
+ if row["outputs"] is not None:
368
+ continue
369
+
370
+ log_and_print(
371
+ f"Running batch item {row.name}, id {row.uuid}, algo {row.algo}."
372
+ )
373
+ process = row.caiman.run()
374
+
375
+ if process.__class__.__name__ == "DummyProcess":
376
+ df = df.caiman.reload_from_disk()
377
+
378
+ log_and_print(f"Batch completed for CNMF. Results saved to {batch}.")
379
+
380
+ # Reload to ensure we have the latest results
381
+ df = df.caiman.reload_from_disk()
382
+
383
+ # Save parameters and export results
384
+ save_processing_parameters(df, export_path, data_path, params_extraction)
385
+
386
+ # Get CNMF object and prepare it for export
387
+ cnmf_obj = prepare_cnmf_object(df)
388
+
389
+ # Copy the mean intensity template if available
390
+ copy_mean_intensity_template(df, batch, export_path)
391
+
392
+ # Export results to MATLAB format
393
+ export_cnmf_results(df, cnmf_obj, export_path, z_correlation)
394
+
395
+ return cnmf_obj
396
+
397
+
398
+ def save_processing_parameters(df, export_path, data_path, params_extraction):
399
+ """Save the parameters used for motion correction and CNMF processing.
400
+
401
+ Parameters
402
+ ----------
403
+ df : pandas.DataFrame
404
+ Mesmerize batch dataframe containing processing information.
405
+ export_path : Path
406
+ Directory where parameter file should be saved.
407
+ data_path : str, Path, or list
408
+ Original data location.
409
+ params_extraction : dict
410
+ CNMF parameters used for processing.
411
+ """
412
+ params_path = export_path / "caiman_params.json"
413
+
414
+ if isinstance(data_path, list):
415
+ data_path = data_path[0]
416
+
417
+ # Use the first batch item to identify the original input movie if possible
418
+ # This preserves the logic to point to the raw data (input of mcorr) rather than the mcorr output
419
+ movie_name = str(df.iloc[0]["input_movie_path"])
420
+
421
+ caiman_params = {
422
+ "data_path": str(data_path),
423
+ "export_path": str(export_path),
424
+ "movie_name": movie_name,
425
+ }
426
+
427
+ # Attempt to retrieve motion correction parameters from the batch history
428
+ caiman_mcorr_params = {}
429
+
430
+ # Check if we have mcorr items in the batch
431
+ # We filter by algo to be safe
432
+ mcorr_items = df[df["algo"] == "mcorr"]
433
+ if not mcorr_items.empty:
434
+ # Use the first mcorr item found
435
+ caiman_mcorr_params = mcorr_items.iloc[0]["params"]["main"]
436
+ caiman_mcorr_params["timestamp_mcorr"] = mcorr_items.iloc[0]["ran_time"]
437
+
438
+ # Get CNMF parameters
439
+ # Merge passed params with what's in the dataframe to be sure
440
+ caiman_cnmf_params = params_extraction["main"]
441
+ if "params" in df.columns and "main" in df.iloc[-1]["params"]:
442
+ caiman_cnmf_params = caiman_cnmf_params | df.iloc[-1]["params"]["main"]
443
+
444
+ caiman_cnmf_params["timestamp_cnmf"] = df.iloc[-1]["ran_time"]
445
+
446
+ caiman_params = caiman_params | caiman_mcorr_params | caiman_cnmf_params
447
+
448
+ with open(params_path, "w") as f:
449
+ json.dump(caiman_params, f, indent=4)
450
+
451
+ log_and_print(f"Saved parameters to {params_path}.")
452
+
453
+
454
+ def prepare_cnmf_object(df):
455
+ """Prepare the CNMF object for export by selecting components and calculating dF/F.
456
+
457
+ Parameters
458
+ ----------
459
+ df : pandas.DataFrame
460
+ Mesmerize batch dataframe containing processing information.
461
+
462
+ Returns
463
+ -------
464
+ cnmf_obj : caiman.source_extraction.cnmf.cnmf.CNMF
465
+ Prepared CNMF object with selected components and calculated F_dff.
466
+ """
467
+ cnmf_obj = df.iloc[-1].cnmf.get_output()
468
+
469
+ # Select components
470
+ cnmf_obj.estimates.select_components(use_object=True)
471
+
472
+ # Calculate dF/F if not already calculated
473
+ if cnmf_obj.estimates.F_dff is None:
474
+ log_and_print("Calculating estimates.F_dff")
475
+ cnmf_obj.estimates.detrend_df_f(
476
+ quantileMin=8, frames_window=400, use_residuals=False
477
+ )
478
+
479
+ return cnmf_obj
480
+
481
+
482
+ def copy_mean_intensity_template(df, batch, export_path):
483
+ """Copy the mean intensity template file to the export directory.
484
+
485
+ Parameters
486
+ ----------
487
+ df : pandas.DataFrame
488
+ Mesmerize batch dataframe containing processing information.
489
+ batch : Path
490
+ Path to the Mesmerize batch file.
491
+ export_path : Path
492
+ Directory where template file should be copied.
493
+ """
494
+ cnmf_uuid = df[df["algo"] == "cnmf"]["uuid"].values[0]
495
+ mean_intensity_template_path = batch.parent / cnmf_uuid / f"{cnmf_uuid}_cn.npy"
496
+
497
+ if mean_intensity_template_path.exists():
498
+ shutil.copy(mean_intensity_template_path, export_path / "mean_intensity_template.npy")
499
+ log_and_print(f"Saved mean_intensity_template.npy to {export_path}.")
500
+ else:
501
+ log_and_print(
502
+ f"Could not find cn.npy file at {mean_intensity_template_path}.")
503
+
504
+
505
+ def export_cnmf_results(df, cnmf_obj, export_path, z_correlation=None):
506
+ """Export CNMF results to MATLAB format.
507
+
508
+ Parameters
509
+ ----------
510
+ df : pandas.DataFrame
511
+ Mesmerize batch dataframe containing processing information.
512
+ cnmf_obj : caiman.source_extraction.cnmf.cnmf.CNMF
513
+ CNMF object containing processing results.
514
+ export_path : Path
515
+ Directory where results should be saved.
516
+ z_correlation : np.ndarray, optional
517
+ Z correlation data if available.
518
+ """
519
+ # Get Z position information if available
520
+ if z_correlation is None:
521
+ zcorr_file = export_path / "z_correlation.npz"
522
+ if zcorr_file.exists():
523
+ z_correlation = np.load(zcorr_file)
524
+ zpos = z_correlation["zpos"]
525
+ else:
526
+ zpos = np.array([np.nan])
527
+ else:
528
+ # Assume z_correlation is a dict-like object with "zpos" key
529
+ zpos = z_correlation.get("zpos", np.array([np.nan]))
530
+
531
+ mcorr_rows = df[df["algo"] == "mcorr"]
532
+ if not mcorr_rows.empty:
533
+ mean_map = mcorr_rows.iloc[0].caiman.get_projection("mean")
534
+ else:
535
+ # Use the input movie of the CNMF item
536
+ movie_path = df.iloc[-1].caiman.get_input_movie()
537
+ mean_map = _mean_projection_from_path(movie_path)
538
+
539
+ # Export to MATLAB format
540
+ io.savemat(
541
+ export_path / "results_caiman.mat",
542
+ mdict={
543
+ "mean_map_motion_corrected": _coerce_matlab_value(mean_map),
544
+ "spatial_components": _coerce_matlab_value(cnmf_obj.estimates.A),
545
+ "temporal_components": _coerce_matlab_value(cnmf_obj.estimates.C),
546
+ "background_spatial_component": _coerce_matlab_value(cnmf_obj.estimates.b),
547
+ "background_temporal_component": _coerce_matlab_value(cnmf_obj.estimates.f),
548
+ "residuals": _coerce_matlab_value(cnmf_obj.estimates.R),
549
+ "df_wo_bckgrnd": _coerce_matlab_value(cnmf_obj.estimates.F_dff),
550
+ "deconv_spk": _coerce_matlab_value(cnmf_obj.estimates.S),
551
+ "SNR_comp": _coerce_matlab_value(cnmf_obj.estimates.SNR_comp),
552
+ "baseline": _coerce_matlab_value(np.array(cnmf_obj.estimates.bl, dtype=np.float32) if cnmf_obj.estimates.bl is not None else None),
553
+ "noise": _coerce_matlab_value(np.array(cnmf_obj.estimates.neurons_sn, dtype=np.float32) if cnmf_obj.estimates.neurons_sn is not None else None),
554
+ "zpos": zpos,
555
+ },
556
+ )
557
+
558
+ log_and_print(f"Saved results to {export_path}/results_caiman.mat.")
559
+