lbm_suite2p_python 3.0.1__tar.gz → 3.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lbm_suite2p_python-3.0.1/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.0.3}/PKG-INFO +3 -3
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/__init__.py +2 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/cellpose.py +2 -5
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/cli.py +7 -5
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/conversion.py +0 -2
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/db_settings.py +58 -9
- lbm_suite2p_python-3.0.3/lbm_suite2p_python/default_ops.py +84 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/gui.py +0 -1
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/postprocessing.py +226 -5
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/run_lsp.py +281 -405
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/volume.py +220 -11
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/zplane.py +307 -315
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3/lbm_suite2p_python.egg-info}/PKG-INFO +3 -3
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -1
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python.egg-info/requires.txt +1 -1
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/pyproject.toml +106 -107
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/tests/test_frame_count_aliases.py +315 -317
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/tests/test_pipeline_parameters.py +2 -2
- lbm_suite2p_python-3.0.1/lbm_suite2p_python/_padding_shim.py +0 -140
- lbm_suite2p_python-3.0.1/lbm_suite2p_python/default_ops.py +0 -230
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/LICENSE.md +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/MANIFEST.in +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/README.md +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/__main__.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/_benchmarking.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/grid_search.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/merging.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python/utils.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/setup.cfg +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/tests/test_refactored_pipeline.py +0 -0
- {lbm_suite2p_python-3.0.1 → lbm_suite2p_python-3.0.3}/tests/test_run_volume.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lbm_suite2p_python
|
|
3
|
-
Version: 3.0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 3.0.3
|
|
4
|
+
Summary: Calcium Imaging Pipeline built with Suite2p, Cellpose and Rastermap
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
|
|
7
7
|
Keywords: Pipeline,Numpy,Microscopy,ScanImage,Suite2p,tiff
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
11
11
|
Requires-Python: <3.14,>=3.12.7
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE.md
|
|
14
|
-
Requires-Dist: mbo_utilities>=3.0.
|
|
14
|
+
Requires-Dist: mbo_utilities>=3.0.2
|
|
15
15
|
Requires-Dist: suite2p>=1.0.0.1
|
|
16
16
|
Requires-Dist: setuptools<81
|
|
17
17
|
Provides-Extra: rastermap
|
|
@@ -48,6 +48,7 @@ from lbm_suite2p_python.volume import (
|
|
|
48
48
|
plot_orthoslices,
|
|
49
49
|
plot_3d_roi_map,
|
|
50
50
|
plot_3d_rastermap_clusters,
|
|
51
|
+
plot_volume_trace_figures,
|
|
51
52
|
plot_volume_signal,
|
|
52
53
|
plot_volume_neuron_counts,
|
|
53
54
|
consolidate_volume,
|
|
@@ -149,6 +150,7 @@ __all__ = [
|
|
|
149
150
|
"plot_orthoslices",
|
|
150
151
|
"plot_3d_roi_map",
|
|
151
152
|
"plot_3d_rastermap_clusters",
|
|
153
|
+
"plot_volume_trace_figures",
|
|
152
154
|
"plot_projection",
|
|
153
155
|
"plot_volume_signal",
|
|
154
156
|
"plot_volume_neuron_counts",
|
|
@@ -194,10 +194,8 @@ def _masks_to_stat(masks, img=None, compute_overlap=True):
|
|
|
194
194
|
|
|
195
195
|
# Build pixel count map for overlap detection
|
|
196
196
|
if compute_overlap and masks.ndim == 2:
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
from scipy import ndimage
|
|
200
|
-
# Dilate each mask slightly to find potential overlaps at boundaries
|
|
197
|
+
# cellpose masks are non-overlapping by construction; track per-pixel
|
|
198
|
+
# claim count so callers can detect dilated-boundary overlaps
|
|
201
199
|
overlap_map = np.zeros(masks.shape, dtype=np.int32)
|
|
202
200
|
for roi_id in range(1, n_rois + 1):
|
|
203
201
|
roi_mask = masks == roi_id
|
|
@@ -1611,7 +1609,6 @@ def prepare_training_data(
|
|
|
1611
1609
|
train_cellpose : Train model on prepared data
|
|
1612
1610
|
save_gui_results : Save results in GUI-compatible format
|
|
1613
1611
|
"""
|
|
1614
|
-
import shutil
|
|
1615
1612
|
import tifffile
|
|
1616
1613
|
|
|
1617
1614
|
output_dir = Path(output_dir)
|
|
@@ -23,13 +23,10 @@ Examples:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import argparse
|
|
26
|
-
import json
|
|
27
26
|
import sys
|
|
28
27
|
from pathlib import Path
|
|
29
28
|
from typing import Any
|
|
30
29
|
|
|
31
|
-
import numpy as np
|
|
32
|
-
|
|
33
30
|
|
|
34
31
|
def _snake_to_kebab(name: str) -> str:
|
|
35
32
|
"""convert snake_case to kebab-case for CLI args."""
|
|
@@ -95,7 +92,6 @@ def _get_ops_help() -> dict[str, str]:
|
|
|
95
92
|
def build_parser() -> argparse.ArgumentParser:
|
|
96
93
|
"""build the argument parser with all pipeline and ops parameters."""
|
|
97
94
|
from lbm_suite2p_python.default_ops import s2p_ops
|
|
98
|
-
from lbm_suite2p_python import __version__
|
|
99
95
|
|
|
100
96
|
parser = argparse.ArgumentParser(
|
|
101
97
|
prog="lsp",
|
|
@@ -193,6 +189,11 @@ Examples:
|
|
|
193
189
|
"--dff-smooth", type=int, dest="dff_smooth_window",
|
|
194
190
|
help="smoothing window for dF/F"
|
|
195
191
|
)
|
|
192
|
+
dff.add_argument(
|
|
193
|
+
"--correct-neuropil", dest="correct_neuropil",
|
|
194
|
+
action=argparse.BooleanOptionalAction, default=True,
|
|
195
|
+
help="subtract 0.7*Fneu before dF/F (default: on; use --no-correct-neuropil to disable)"
|
|
196
|
+
)
|
|
196
197
|
|
|
197
198
|
# cell filter options
|
|
198
199
|
filters = parser.add_argument_group("cell filter options")
|
|
@@ -491,7 +492,7 @@ def main():
|
|
|
491
492
|
|
|
492
493
|
# run pipeline
|
|
493
494
|
try:
|
|
494
|
-
|
|
495
|
+
lsp.pipeline(
|
|
495
496
|
input_data=input_path,
|
|
496
497
|
save_path=output_path,
|
|
497
498
|
ops=ops,
|
|
@@ -505,6 +506,7 @@ def main():
|
|
|
505
506
|
dff_window_size=args.dff_window_size,
|
|
506
507
|
dff_percentile=args.dff_percentile,
|
|
507
508
|
dff_smooth_window=args.dff_smooth_window,
|
|
509
|
+
correct_neuropil=args.correct_neuropil,
|
|
508
510
|
cell_filters=cell_filters,
|
|
509
511
|
accept_all_cells=args.accept_all_cells,
|
|
510
512
|
save_json=args.save_json,
|
|
@@ -746,8 +746,6 @@ def compare_detections(path_a, path_b, iou_threshold=0.5):
|
|
|
746
746
|
dict
|
|
747
747
|
Comparison results with matched pairs and unique ROIs.
|
|
748
748
|
"""
|
|
749
|
-
from scipy.ndimage import label
|
|
750
|
-
|
|
751
749
|
path_a, path_b = Path(path_a), Path(path_b)
|
|
752
750
|
|
|
753
751
|
# load masks from both
|
|
@@ -189,6 +189,31 @@ _ANATOMICAL_ONLY_TO_IMG: dict[int, str] = {
|
|
|
189
189
|
}
|
|
190
190
|
_UPSTREAM_TO_FORK_RENAMES = {v: k for k, v in _FORK_TO_UPSTREAM_RENAMES.items()}
|
|
191
191
|
|
|
192
|
+
# per-section flat-key disambiguation. Several upstream sections share
|
|
193
|
+
# an upstream key. When flattened to ops, the second iteration clobbers
|
|
194
|
+
# the first; on the reverse derivation, the surviving value gets fanned
|
|
195
|
+
# back into BOTH sections — silently corrupting one of them.
|
|
196
|
+
# This map gives the colliding (section, upstream_key) pair its own
|
|
197
|
+
# distinct flat-ops key so the round-trip preserves both values.
|
|
198
|
+
#
|
|
199
|
+
# Discovered collisions (audit via _SETTINGS_SECTIONS + detection groups):
|
|
200
|
+
# "batch_size" — registration vs extraction.
|
|
201
|
+
# registration keeps the legacy flat name "batch_size"
|
|
202
|
+
# (matches the historical fork ops shape);
|
|
203
|
+
# extraction gets "extract_batch_size".
|
|
204
|
+
# "block_size" — registration vs detection (suite2p uses the same
|
|
205
|
+
# upstream key for the rigid-block geometry AND the
|
|
206
|
+
# pca-denoise / sparsery block size).
|
|
207
|
+
# registration keeps the legacy flat name "block_size";
|
|
208
|
+
# detection gets "det_block_size".
|
|
209
|
+
#
|
|
210
|
+
# Both rename targets match the field names that mbo's GUI dataclass
|
|
211
|
+
# already uses (`extract_batch_size`, `det_block_size_y/_x`).
|
|
212
|
+
_SECTION_FLAT_RENAMES: dict[tuple[str, str], str] = {
|
|
213
|
+
("extraction", "batch_size"): "extract_batch_size",
|
|
214
|
+
("detection", "block_size"): "det_block_size",
|
|
215
|
+
}
|
|
216
|
+
|
|
192
217
|
# baseline values differ between fork and upstream:
|
|
193
218
|
# fork dcnv.preprocess branches on "maximin" / "constant" / "constant_prctile"
|
|
194
219
|
# upstream dcnv.preprocess branches on "maximin" / "constant" / "prctile"
|
|
@@ -284,9 +309,15 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
|
|
|
284
309
|
for section, keys in _SETTINGS_SECTIONS.items():
|
|
285
310
|
bucket: dict[str, Any] = {}
|
|
286
311
|
for key in keys:
|
|
287
|
-
|
|
312
|
+
# per-section flat-key disambiguation. e.g. extraction reads
|
|
313
|
+
# its batch_size from ops["extract_batch_size"] (not
|
|
314
|
+
# ops["batch_size"]) so registration's batch_size doesn't get
|
|
315
|
+
# spread into both sections during the flat→structured
|
|
316
|
+
# derivation. Mirror of the write side in db_settings_to_ops.
|
|
317
|
+
flat_key = _SECTION_FLAT_RENAMES.get((section, key), key)
|
|
318
|
+
if flat_key not in lookup:
|
|
288
319
|
continue
|
|
289
|
-
value = lookup[
|
|
320
|
+
value = lookup[flat_key]
|
|
290
321
|
if section == "run" and key == "do_registration":
|
|
291
322
|
value = _ensure_do_registration_int(value)
|
|
292
323
|
if section == "registration" and key == "block_size" and isinstance(value, list):
|
|
@@ -316,11 +347,16 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
|
|
|
316
347
|
for key in _DETECTION_TOP_KEYS:
|
|
317
348
|
if key == "algorithm":
|
|
318
349
|
continue
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
350
|
+
# per-section flat-key disambiguation. detection.block_size reads
|
|
351
|
+
# from ops["det_block_size"] (not ops["block_size"], which holds
|
|
352
|
+
# the registration block size). Mirror of the write side.
|
|
353
|
+
flat_key = _SECTION_FLAT_RENAMES.get(("detection", key), key)
|
|
354
|
+
if flat_key not in lookup:
|
|
355
|
+
continue
|
|
356
|
+
value = lookup[flat_key]
|
|
357
|
+
if key == "block_size" and isinstance(value, list):
|
|
358
|
+
value = tuple(value)
|
|
359
|
+
detection[key] = value
|
|
324
360
|
|
|
325
361
|
sparsery: dict[str, Any] = {}
|
|
326
362
|
for key in _DETECTION_SPARSERY_KEYS:
|
|
@@ -410,7 +446,14 @@ def db_settings_to_ops(db: dict | None, settings: dict | None) -> dict:
|
|
|
410
446
|
value = section_dict[key]
|
|
411
447
|
if section == "dcnv_preprocess" and key == "baseline":
|
|
412
448
|
value = _BASELINE_UPSTREAM_TO_FORK.get(str(value), value)
|
|
413
|
-
|
|
449
|
+
# per-section disambiguation first (e.g. extraction.batch_size
|
|
450
|
+
# → extract_batch_size to avoid colliding with
|
|
451
|
+
# registration.batch_size). Fall back to the global
|
|
452
|
+
# upstream→fork rename map.
|
|
453
|
+
target_name = _SECTION_FLAT_RENAMES.get(
|
|
454
|
+
(section, key),
|
|
455
|
+
_UPSTREAM_TO_FORK_RENAMES.get(key, key),
|
|
456
|
+
)
|
|
414
457
|
ops[target_name] = value
|
|
415
458
|
|
|
416
459
|
# mirror align_by_chan2 back to align_by_chan for fork consumers
|
|
@@ -423,7 +466,13 @@ def db_settings_to_ops(db: dict | None, settings: dict | None) -> dict:
|
|
|
423
466
|
for key in _DETECTION_TOP_KEYS:
|
|
424
467
|
if key in detection:
|
|
425
468
|
value = detection[key]
|
|
426
|
-
|
|
469
|
+
# per-section disambiguation first (e.g. detection.block_size
|
|
470
|
+
# → det_block_size to avoid colliding with
|
|
471
|
+
# registration.block_size).
|
|
472
|
+
target_name = _SECTION_FLAT_RENAMES.get(
|
|
473
|
+
("detection", key),
|
|
474
|
+
_UPSTREAM_TO_FORK_RENAMES.get(key, key),
|
|
475
|
+
)
|
|
427
476
|
ops[target_name] = value
|
|
428
477
|
|
|
429
478
|
# reverse the algorithm → sparse_mode derivation for fork consumers
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Default ops for the LBM pipeline.
|
|
2
|
+
|
|
3
|
+
Historically this module hard-coded a flat ops dict whose values
|
|
4
|
+
diverged from suite2p's own defaults in a handful of fields
|
|
5
|
+
(`batch_size=500`, `chan2_thres=0.65`, `tau=1.3`, `diameter=4`, etc.).
|
|
6
|
+
That divergence made the round-trip through `settings.npy` confusing —
|
|
7
|
+
on-disk values that matched the lsp default were flagged as "modified"
|
|
8
|
+
against suite2p's schema, and vice versa.
|
|
9
|
+
|
|
10
|
+
This module now exposes exactly suite2p's defaults (the values from
|
|
11
|
+
`suite2p.default_settings()` + `suite2p.default_db()`), flattened to the
|
|
12
|
+
fork's flat-ops shape via the same `db_settings_to_ops` translation
|
|
13
|
+
that the rest of lsp uses. There is now ONE source of truth for "what
|
|
14
|
+
default means": suite2p's parameter schema. Any LBM-specific tweaks
|
|
15
|
+
(diameter, tau, etc.) belong in user code, not in the default.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from mbo_utilities.metadata import get_param, get_voxel_size
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def s2p_ops() -> dict:
|
|
22
|
+
"""Suite2p's default ops in flat (fork-style) form.
|
|
23
|
+
|
|
24
|
+
Composed from `suite2p.default_settings()` + `suite2p.default_db()`
|
|
25
|
+
via `db_settings_to_ops`, so the rename map and per-section
|
|
26
|
+
flat-key disambiguation (e.g. extraction.batch_size →
|
|
27
|
+
extract_batch_size) are applied consistently.
|
|
28
|
+
"""
|
|
29
|
+
from suite2p import default_settings, default_db
|
|
30
|
+
from lbm_suite2p_python.db_settings import db_settings_to_ops
|
|
31
|
+
|
|
32
|
+
return db_settings_to_ops(default_db(), default_settings())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_ops(metadata: dict | None = None, ops: dict | None = None) -> dict:
|
|
36
|
+
"""Return default ops for the LBM Suite2p pipeline.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
metadata : dict, optional
|
|
41
|
+
Source-data metadata. When provided, `fs` and the (`dx`, `dy`)
|
|
42
|
+
voxel-size pair are pulled in from the metadata and overlaid
|
|
43
|
+
onto the suite2p defaults.
|
|
44
|
+
ops : dict, optional
|
|
45
|
+
A user-supplied ops dict to start from. When None, starts from
|
|
46
|
+
`s2p_ops()` (suite2p's defaults).
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
dict
|
|
51
|
+
Flat ops dict ready to be passed to `pipeline()` / `run_plane()`.
|
|
52
|
+
|
|
53
|
+
Notes
|
|
54
|
+
-----
|
|
55
|
+
`nplanes=1` and `nchannels=1` are forced at the end. The lsp
|
|
56
|
+
pipeline always processes one plane at a time, so these stay fixed
|
|
57
|
+
regardless of caller input.
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
>>> import lbm_suite2p_python as lsp
|
|
62
|
+
>>> ops = lsp.default_ops()
|
|
63
|
+
>>> lsp.run_plane(
|
|
64
|
+
... ops=ops,
|
|
65
|
+
... input_tiff="D:/demo/raw_data/raw_file_00001.tif",
|
|
66
|
+
... save_path="D:/demo/results",
|
|
67
|
+
... save_folder="v1",
|
|
68
|
+
... )
|
|
69
|
+
"""
|
|
70
|
+
if ops is None:
|
|
71
|
+
ops = s2p_ops()
|
|
72
|
+
|
|
73
|
+
if metadata is not None:
|
|
74
|
+
fs = get_param(metadata, "fs")
|
|
75
|
+
if fs is not None:
|
|
76
|
+
ops["fs"] = fs
|
|
77
|
+
voxel = get_voxel_size(metadata)
|
|
78
|
+
if voxel.dx != 1.0 or voxel.dy != 1.0:
|
|
79
|
+
ops["dx"] = voxel.dx
|
|
80
|
+
ops["dy"] = voxel.dy
|
|
81
|
+
|
|
82
|
+
ops["nplanes"] = 1
|
|
83
|
+
ops["nchannels"] = 1
|
|
84
|
+
return ops
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Callable
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
6
5
|
from scipy.ndimage import percentile_filter
|
|
@@ -514,6 +513,222 @@ def filter_by_eccentricity(
|
|
|
514
513
|
}
|
|
515
514
|
|
|
516
515
|
|
|
516
|
+
def _load_F_Fneu(plane_dir, F, Fneu):
|
|
517
|
+
"""Load F.npy / Fneu.npy from plane_dir if not already provided."""
|
|
518
|
+
if F is None and plane_dir is not None:
|
|
519
|
+
f_path = plane_dir / "F.npy"
|
|
520
|
+
if f_path.exists():
|
|
521
|
+
F = np.load(f_path, allow_pickle=True)
|
|
522
|
+
if Fneu is None and plane_dir is not None:
|
|
523
|
+
fn_path = plane_dir / "Fneu.npy"
|
|
524
|
+
if fn_path.exists():
|
|
525
|
+
Fneu = np.load(fn_path, allow_pickle=True)
|
|
526
|
+
return F, Fneu
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _resolve_dff_window(window_size, ops):
|
|
530
|
+
"""Same auto-window rule that dff_rolling_percentile uses."""
|
|
531
|
+
if window_size is not None:
|
|
532
|
+
return max(3, int(window_size))
|
|
533
|
+
fs = float((ops or {}).get("fs", 0) or 0)
|
|
534
|
+
tau = float((ops or {}).get("tau", 0) or 0)
|
|
535
|
+
w = int(10 * tau * fs) if (fs > 0 and tau > 0) else 300
|
|
536
|
+
return max(3, w)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _compute_dff_baseline(F, Fneu, correct_neuropil, neuropil_coef, percentile, window_size):
|
|
540
|
+
"""
|
|
541
|
+
Compute the rolling-percentile baseline that dff_rolling_percentile
|
|
542
|
+
will see, given the neuropil-correction toggle. Returns the per-cell
|
|
543
|
+
f0 array (same shape as F).
|
|
544
|
+
"""
|
|
545
|
+
if correct_neuropil:
|
|
546
|
+
if Fneu is None:
|
|
547
|
+
raise ValueError("correct_neuropil=True requires Fneu")
|
|
548
|
+
f_in = F - neuropil_coef * Fneu
|
|
549
|
+
else:
|
|
550
|
+
f_in = F
|
|
551
|
+
return np.array(
|
|
552
|
+
[percentile_filter(f, percentile, size=window_size, mode="nearest") for f in f_in]
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def filter_by_negative_baseline(
|
|
557
|
+
plane_dir=None,
|
|
558
|
+
iscell=None,
|
|
559
|
+
stat=None,
|
|
560
|
+
ops=None,
|
|
561
|
+
F=None,
|
|
562
|
+
Fneu=None,
|
|
563
|
+
correct_neuropil: bool = True,
|
|
564
|
+
neuropil_coef: float = 0.7,
|
|
565
|
+
percentile: int = 20,
|
|
566
|
+
window_size: int = None,
|
|
567
|
+
save: bool = False,
|
|
568
|
+
):
|
|
569
|
+
"""
|
|
570
|
+
Reject ROIs whose rolling-percentile baseline ever goes negative.
|
|
571
|
+
|
|
572
|
+
Real fluorescence is nonnegative. A negative rolling baseline means the
|
|
573
|
+
trace dropped below zero somewhere — usually neuropil over-subtraction
|
|
574
|
+
or registration drop-out. These cells produce divide-by-near-zero
|
|
575
|
+
blowups in dF/F.
|
|
576
|
+
|
|
577
|
+
Parameters mirror :func:`dff_rolling_percentile` so the baseline matches
|
|
578
|
+
what the dF/F plotting will see. Returns the standard
|
|
579
|
+
``(iscell_filtered, removed_mask, info)`` triple.
|
|
580
|
+
"""
|
|
581
|
+
iscell, stat, ops, plane_dir = _load_plane_data(plane_dir, iscell, stat, ops)
|
|
582
|
+
iscell_orig = _normalize_iscell(iscell)
|
|
583
|
+
F, Fneu = _load_F_Fneu(plane_dir, F, Fneu)
|
|
584
|
+
if F is None:
|
|
585
|
+
raise ValueError("filter_by_negative_baseline requires F (pass F or plane_dir with F.npy)")
|
|
586
|
+
|
|
587
|
+
window_size = _resolve_dff_window(window_size, ops)
|
|
588
|
+
f0 = _compute_dff_baseline(F, Fneu, correct_neuropil, neuropil_coef, percentile, window_size)
|
|
589
|
+
f0_min = f0.min(axis=1)
|
|
590
|
+
|
|
591
|
+
valid = f0_min >= 0
|
|
592
|
+
removed_mask = ~valid & iscell_orig
|
|
593
|
+
iscell_filtered = iscell_orig & valid
|
|
594
|
+
n_removed = int(removed_mask.sum())
|
|
595
|
+
|
|
596
|
+
if n_removed > 0:
|
|
597
|
+
print(f"filter_by_negative_baseline: removed {n_removed} ROIs (rolling p{percentile} F0 < 0)")
|
|
598
|
+
|
|
599
|
+
if save and plane_dir is not None:
|
|
600
|
+
_save_filtered_iscell(plane_dir, iscell_filtered, iscell_orig)
|
|
601
|
+
|
|
602
|
+
return iscell_filtered, removed_mask, {
|
|
603
|
+
"f0_min": f0_min,
|
|
604
|
+
"window_size": int(window_size),
|
|
605
|
+
"percentile": int(percentile),
|
|
606
|
+
"correct_neuropil": bool(correct_neuropil),
|
|
607
|
+
"neuropil_coef": float(neuropil_coef) if correct_neuropil else 0.0,
|
|
608
|
+
"n_removed": n_removed,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def filter_by_min_baseline_abs(
|
|
613
|
+
plane_dir=None,
|
|
614
|
+
iscell=None,
|
|
615
|
+
stat=None,
|
|
616
|
+
ops=None,
|
|
617
|
+
F=None,
|
|
618
|
+
Fneu=None,
|
|
619
|
+
correct_neuropil: bool = True,
|
|
620
|
+
neuropil_coef: float = 0.7,
|
|
621
|
+
percentile: int = 20,
|
|
622
|
+
window_size: int = None,
|
|
623
|
+
min_F0_abs: float = 1.0,
|
|
624
|
+
save: bool = False,
|
|
625
|
+
):
|
|
626
|
+
"""
|
|
627
|
+
Reject ROIs whose median rolling baseline is below an absolute floor.
|
|
628
|
+
|
|
629
|
+
Catches very dim cells where any small fluctuation dominates dF/F.
|
|
630
|
+
The threshold is in raw photon-count units (same units as F.npy).
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
min_F0_abs : float, default 1.0
|
|
635
|
+
Reject cells whose median(rolling p-th percentile baseline) is
|
|
636
|
+
below this value.
|
|
637
|
+
"""
|
|
638
|
+
iscell, stat, ops, plane_dir = _load_plane_data(plane_dir, iscell, stat, ops)
|
|
639
|
+
iscell_orig = _normalize_iscell(iscell)
|
|
640
|
+
F, Fneu = _load_F_Fneu(plane_dir, F, Fneu)
|
|
641
|
+
if F is None:
|
|
642
|
+
raise ValueError("filter_by_min_baseline_abs requires F (pass F or plane_dir with F.npy)")
|
|
643
|
+
|
|
644
|
+
window_size = _resolve_dff_window(window_size, ops)
|
|
645
|
+
f0 = _compute_dff_baseline(F, Fneu, correct_neuropil, neuropil_coef, percentile, window_size)
|
|
646
|
+
f0_median = np.median(f0, axis=1)
|
|
647
|
+
|
|
648
|
+
valid = f0_median >= min_F0_abs
|
|
649
|
+
removed_mask = ~valid & iscell_orig
|
|
650
|
+
iscell_filtered = iscell_orig & valid
|
|
651
|
+
n_removed = int(removed_mask.sum())
|
|
652
|
+
|
|
653
|
+
if n_removed > 0:
|
|
654
|
+
print(f"filter_by_min_baseline_abs: removed {n_removed} ROIs (median F0 < {min_F0_abs:g})")
|
|
655
|
+
|
|
656
|
+
if save and plane_dir is not None:
|
|
657
|
+
_save_filtered_iscell(plane_dir, iscell_filtered, iscell_orig)
|
|
658
|
+
|
|
659
|
+
return iscell_filtered, removed_mask, {
|
|
660
|
+
"f0_median": f0_median,
|
|
661
|
+
"min_F0_abs": float(min_F0_abs),
|
|
662
|
+
"window_size": int(window_size),
|
|
663
|
+
"percentile": int(percentile),
|
|
664
|
+
"correct_neuropil": bool(correct_neuropil),
|
|
665
|
+
"neuropil_coef": float(neuropil_coef) if correct_neuropil else 0.0,
|
|
666
|
+
"n_removed": n_removed,
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def filter_by_min_baseline_rel(
|
|
671
|
+
plane_dir=None,
|
|
672
|
+
iscell=None,
|
|
673
|
+
stat=None,
|
|
674
|
+
ops=None,
|
|
675
|
+
F=None,
|
|
676
|
+
Fneu=None,
|
|
677
|
+
correct_neuropil: bool = True,
|
|
678
|
+
neuropil_coef: float = 0.7,
|
|
679
|
+
percentile: int = 20,
|
|
680
|
+
window_size: int = None,
|
|
681
|
+
min_F0_rel: float = 0.05,
|
|
682
|
+
save: bool = False,
|
|
683
|
+
):
|
|
684
|
+
"""
|
|
685
|
+
Reject ROIs whose minimum rolling baseline collapses far below the
|
|
686
|
+
cell's own typical raw brightness.
|
|
687
|
+
|
|
688
|
+
Catches transient baseline drop-outs (e.g. registration / motion
|
|
689
|
+
artifacts) that don't manifest as a low median baseline but still
|
|
690
|
+
cause dF/F spikes when the divisor briefly approaches zero.
|
|
691
|
+
|
|
692
|
+
Parameters
|
|
693
|
+
----------
|
|
694
|
+
min_F0_rel : float, default 0.05
|
|
695
|
+
Reject cells whose ``min(rolling baseline) < min_F0_rel * median(F_raw)``.
|
|
696
|
+
"""
|
|
697
|
+
iscell, stat, ops, plane_dir = _load_plane_data(plane_dir, iscell, stat, ops)
|
|
698
|
+
iscell_orig = _normalize_iscell(iscell)
|
|
699
|
+
F, Fneu = _load_F_Fneu(plane_dir, F, Fneu)
|
|
700
|
+
if F is None:
|
|
701
|
+
raise ValueError("filter_by_min_baseline_rel requires F (pass F or plane_dir with F.npy)")
|
|
702
|
+
|
|
703
|
+
window_size = _resolve_dff_window(window_size, ops)
|
|
704
|
+
f0 = _compute_dff_baseline(F, Fneu, correct_neuropil, neuropil_coef, percentile, window_size)
|
|
705
|
+
f0_min = f0.min(axis=1)
|
|
706
|
+
f_raw_median = np.median(F, axis=1)
|
|
707
|
+
threshold = min_F0_rel * f_raw_median
|
|
708
|
+
|
|
709
|
+
valid = f0_min >= threshold
|
|
710
|
+
removed_mask = ~valid & iscell_orig
|
|
711
|
+
iscell_filtered = iscell_orig & valid
|
|
712
|
+
n_removed = int(removed_mask.sum())
|
|
713
|
+
|
|
714
|
+
if n_removed > 0:
|
|
715
|
+
print(f"filter_by_min_baseline_rel: removed {n_removed} ROIs (min F0 < {min_F0_rel:g} * median(F_raw))")
|
|
716
|
+
|
|
717
|
+
if save and plane_dir is not None:
|
|
718
|
+
_save_filtered_iscell(plane_dir, iscell_filtered, iscell_orig)
|
|
719
|
+
|
|
720
|
+
return iscell_filtered, removed_mask, {
|
|
721
|
+
"f0_min": f0_min,
|
|
722
|
+
"f_raw_median": f_raw_median,
|
|
723
|
+
"min_F0_rel": float(min_F0_rel),
|
|
724
|
+
"window_size": int(window_size),
|
|
725
|
+
"percentile": int(percentile),
|
|
726
|
+
"correct_neuropil": bool(correct_neuropil),
|
|
727
|
+
"neuropil_coef": float(neuropil_coef) if correct_neuropil else 0.0,
|
|
728
|
+
"n_removed": n_removed,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
517
732
|
def apply_filters(
|
|
518
733
|
plane_dir=None,
|
|
519
734
|
iscell=None,
|
|
@@ -540,7 +755,7 @@ def apply_filters(
|
|
|
540
755
|
Suite2p ops dictionary.
|
|
541
756
|
filters : list of dict
|
|
542
757
|
List of filter configurations. Each dict must have:
|
|
543
|
-
- 'name': str - filter function name (e.g., 'max_diameter', 'area', 'eccentricity')
|
|
758
|
+
- 'name': str - filter function name (e.g., 'max_diameter', 'area', 'eccentricity', 'baseline')
|
|
544
759
|
- Additional keys are passed as kwargs to the filter function.
|
|
545
760
|
|
|
546
761
|
Available filters:
|
|
@@ -548,6 +763,9 @@ def apply_filters(
|
|
|
548
763
|
- 'max_diameter': filter_by_max_diameter (max_diameter_um, max_diameter_px, min_diameter_um, min_diameter_px)
|
|
549
764
|
- 'area': filter_by_area (min_area_px, max_area_px, min_mult, max_mult)
|
|
550
765
|
- 'eccentricity': filter_by_eccentricity (max_ratio, min_ratio)
|
|
766
|
+
- 'negative_baseline': filter_by_negative_baseline (correct_neuropil, percentile, window_size, neuropil_coef)
|
|
767
|
+
- 'min_baseline_abs': filter_by_min_baseline_abs (min_F0_abs, correct_neuropil, percentile, window_size, neuropil_coef)
|
|
768
|
+
- 'min_baseline_rel': filter_by_min_baseline_rel (min_F0_rel, correct_neuropil, percentile, window_size, neuropil_coef)
|
|
551
769
|
|
|
552
770
|
save : bool, default False
|
|
553
771
|
If True, save final filtered iscell.npy to plane_dir.
|
|
@@ -586,6 +804,9 @@ def apply_filters(
|
|
|
586
804
|
"max_diameter": filter_by_max_diameter,
|
|
587
805
|
"area": filter_by_area,
|
|
588
806
|
"eccentricity": filter_by_eccentricity,
|
|
807
|
+
"negative_baseline": filter_by_negative_baseline,
|
|
808
|
+
"min_baseline_abs": filter_by_min_baseline_abs,
|
|
809
|
+
"min_baseline_rel": filter_by_min_baseline_rel,
|
|
589
810
|
}
|
|
590
811
|
|
|
591
812
|
iscell, stat, ops, plane_dir = _load_plane_data(plane_dir, iscell, stat, ops)
|
|
@@ -613,8 +834,10 @@ def apply_filters(
|
|
|
613
834
|
# Don't save intermediate results, only final
|
|
614
835
|
config["save"] = False
|
|
615
836
|
|
|
616
|
-
# Apply filter
|
|
837
|
+
# Apply filter — pass plane_dir so filters that need extra files
|
|
838
|
+
# (e.g. F.npy / Fneu.npy for baseline) can find them.
|
|
617
839
|
iscell_current, removed, info = filter_fn(
|
|
840
|
+
plane_dir=plane_dir,
|
|
618
841
|
iscell=iscell_current,
|
|
619
842
|
stat=stat,
|
|
620
843
|
ops=ops,
|
|
@@ -1049,8 +1272,6 @@ def compute_trace_quality_score(
|
|
|
1049
1272
|
if weights is None:
|
|
1050
1273
|
weights = {'snr': 1.0, 'skewness': 0.8, 'shot_noise': 0.5}
|
|
1051
1274
|
|
|
1052
|
-
n_neurons = F.shape[0]
|
|
1053
|
-
|
|
1054
1275
|
# neuropil correction and rectification
|
|
1055
1276
|
if Fneu is not None:
|
|
1056
1277
|
F_corr = F - 0.7 * Fneu
|