lbm_suite2p_python 3.0.0__tar.gz → 3.0.1__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.0/lbm_suite2p_python.egg-info → lbm_suite2p_python-3.0.1}/PKG-INFO +3 -6
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/__init__.py +4 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/conversion.py +3 -1
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/db_settings.py +60 -19
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/default_ops.py +10 -2
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/postprocessing.py +9 -1
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/run_lsp.py +104 -22
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/volume.py +22 -12
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/zplane.py +382 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1/lbm_suite2p_python.egg-info}/PKG-INFO +3 -6
- lbm_suite2p_python-3.0.1/lbm_suite2p_python.egg-info/requires.txt +12 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/pyproject.toml +4 -9
- lbm_suite2p_python-3.0.0/lbm_suite2p_python.egg-info/requires.txt +0 -16
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/LICENSE.md +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/MANIFEST.in +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/README.md +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/__main__.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/_benchmarking.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/_padding_shim.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/cellpose.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/cli.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/grid_search.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/gui.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/merging.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python/utils.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/setup.cfg +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/tests/test_frame_count_aliases.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/tests/test_pipeline_parameters.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/tests/test_refactored_pipeline.py +0 -0
- {lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/tests/test_run_volume.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lbm_suite2p_python
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Light Beads Microscopy Pipeline using Suite2p
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
|
|
@@ -11,18 +11,15 @@ 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>=
|
|
14
|
+
Requires-Dist: mbo_utilities>=3.0.0
|
|
15
15
|
Requires-Dist: suite2p>=1.0.0.1
|
|
16
16
|
Requires-Dist: setuptools<81
|
|
17
17
|
Provides-Extra: rastermap
|
|
18
18
|
Requires-Dist: rastermap; extra == "rastermap"
|
|
19
19
|
Provides-Extra: cellpose
|
|
20
20
|
Requires-Dist: cellpose>=4.0.6; extra == "cellpose"
|
|
21
|
-
Provides-Extra: torch
|
|
22
|
-
Requires-Dist: torch>=2.7.0; extra == "torch"
|
|
23
|
-
Requires-Dist: torchvision>=0.22.0; extra == "torch"
|
|
24
21
|
Provides-Extra: all
|
|
25
|
-
Requires-Dist: lbm_suite2p_python[cellpose,rastermap
|
|
22
|
+
Requires-Dist: lbm_suite2p_python[cellpose,rastermap]; extra == "all"
|
|
26
23
|
Dynamic: license-file
|
|
27
24
|
|
|
28
25
|
<p align="center">
|
|
@@ -39,6 +39,8 @@ from lbm_suite2p_python.zplane import (
|
|
|
39
39
|
plot_filtered_cells,
|
|
40
40
|
plot_diameter_histogram,
|
|
41
41
|
plot_projection,
|
|
42
|
+
plot_accepted_rejected_overlay,
|
|
43
|
+
plot_volume_accepted_rejected_overlay,
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
from lbm_suite2p_python.volume import (
|
|
@@ -151,4 +153,6 @@ __all__ = [
|
|
|
151
153
|
"plot_volume_signal",
|
|
152
154
|
"plot_volume_neuron_counts",
|
|
153
155
|
"consolidate_volume",
|
|
156
|
+
"plot_accepted_rejected_overlay",
|
|
157
|
+
"plot_volume_accepted_rejected_overlay",
|
|
154
158
|
]
|
|
@@ -20,6 +20,8 @@ from pathlib import Path
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
from mbo_utilities.file_io import load_npy
|
|
22
22
|
|
|
23
|
+
from lbm_suite2p_python.db_settings import _ensure_diameter_array
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
# file signatures for format detection
|
|
25
27
|
SUITE2P_REQUIRED = ["stat.npy", "iscell.npy", "ops.npy"]
|
|
@@ -436,7 +438,7 @@ def cellpose_to_suite2p(
|
|
|
436
438
|
meta_path = cellpose_dir / "cellpose_meta.npy"
|
|
437
439
|
if meta_path.exists():
|
|
438
440
|
cp_meta = np.load(meta_path, allow_pickle=True).item()
|
|
439
|
-
ops["diameter"] = cp_meta.get("diameter", ops.get("diameter"))
|
|
441
|
+
ops["diameter"] = _ensure_diameter_array(cp_meta.get("diameter", ops.get("diameter")))
|
|
440
442
|
ops["cellprob_threshold"] = cp_meta.get("cellprob_threshold")
|
|
441
443
|
ops["flow_threshold"] = cp_meta.get("flow_threshold")
|
|
442
444
|
|
|
@@ -172,6 +172,20 @@ _FORK_TO_UPSTREAM_RENAMES: dict[str, str] = {
|
|
|
172
172
|
"nbinned": "nbins",
|
|
173
173
|
"high_pass": "highpass_time",
|
|
174
174
|
"spatial_hp_cp": "highpass_spatial",
|
|
175
|
+
"pretrained_model": "cellpose_model",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# fork's anatomical_only int (1-4) selects which image cellpose runs on.
|
|
179
|
+
# upstream replaced this with a string in cellpose_settings.img. mapping:
|
|
180
|
+
# 1 -> 'max_proj / meanImg' (log ratio)
|
|
181
|
+
# 2 -> 'meanImg'
|
|
182
|
+
# 3 -> enhanced_mean_img (REMOVED upstream — no equivalent string;
|
|
183
|
+
# falls through to max_proj branch)
|
|
184
|
+
# 4 -> 'max_proj' (anything not matching the two strings hits else: img=max_proj)
|
|
185
|
+
_ANATOMICAL_ONLY_TO_IMG: dict[int, str] = {
|
|
186
|
+
1: "max_proj / meanImg",
|
|
187
|
+
2: "meanImg",
|
|
188
|
+
4: "max_proj",
|
|
175
189
|
}
|
|
176
190
|
_UPSTREAM_TO_FORK_RENAMES = {v: k for k, v in _FORK_TO_UPSTREAM_RENAMES.items()}
|
|
177
191
|
|
|
@@ -189,17 +203,24 @@ _BASELINE_UPSTREAM_TO_FORK = {
|
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
|
|
192
|
-
def
|
|
193
|
-
"""
|
|
206
|
+
def _ensure_diameter_array(value: Any) -> np.ndarray:
|
|
207
|
+
"""Coerce diameter to an `np.ndarray` of shape (2,), mirroring suite2p
|
|
208
|
+
`pipeline_s2p.py:150-160`: scalar -> [d, d]; list/tuple -> array; size-1
|
|
209
|
+
array -> [d, d]; size>=2 ndarray passes through. None falls back to
|
|
210
|
+
[6., 6.] (fork-only — upstream doesn't accept None at this point).
|
|
211
|
+
"""
|
|
194
212
|
if value is None:
|
|
195
|
-
return [
|
|
213
|
+
return np.array([6.0, 6.0])
|
|
214
|
+
if not isinstance(value, (list, tuple, np.ndarray)):
|
|
215
|
+
return np.array([value, value])
|
|
196
216
|
if isinstance(value, (list, tuple)):
|
|
197
217
|
if len(value) == 0:
|
|
198
|
-
return [
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
return np.array([6.0, 6.0])
|
|
219
|
+
value = np.array(value)
|
|
220
|
+
if value.size == 1:
|
|
221
|
+
v = value.item()
|
|
222
|
+
return np.array([v, v])
|
|
223
|
+
return value
|
|
203
224
|
|
|
204
225
|
|
|
205
226
|
def _ensure_do_registration_int(value: Any) -> int:
|
|
@@ -255,7 +276,7 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
|
|
|
255
276
|
for key in _SETTINGS_TOP_LEVEL:
|
|
256
277
|
if key in lookup:
|
|
257
278
|
if key == "diameter":
|
|
258
|
-
settings[key] =
|
|
279
|
+
settings[key] = _ensure_diameter_array(lookup[key])
|
|
259
280
|
else:
|
|
260
281
|
settings[key] = lookup[key]
|
|
261
282
|
|
|
@@ -319,6 +340,33 @@ def ops_to_db_settings(ops: dict) -> tuple[dict, dict]:
|
|
|
319
340
|
for key in _DETECTION_CELLPOSE_KEYS:
|
|
320
341
|
if key in lookup:
|
|
321
342
|
cellpose[key] = lookup[key]
|
|
343
|
+
|
|
344
|
+
# fork's anatomical_only int picks which image cellpose runs on.
|
|
345
|
+
# upstream encodes this via cellpose_settings.img; without this
|
|
346
|
+
# mapping anatomical_only=4 silently degrades to anatomical_only=1
|
|
347
|
+
# (the upstream default 'max_proj / meanImg' = log ratio image)
|
|
348
|
+
# because the translator only sets algorithm='cellpose' otherwise.
|
|
349
|
+
anat = lookup.get("anatomical_only")
|
|
350
|
+
if anat and "img" not in cellpose:
|
|
351
|
+
try:
|
|
352
|
+
anat_int = int(anat)
|
|
353
|
+
except (TypeError, ValueError):
|
|
354
|
+
anat_int = None
|
|
355
|
+
if anat_int in _ANATOMICAL_ONLY_TO_IMG:
|
|
356
|
+
cellpose["img"] = _ANATOMICAL_ONLY_TO_IMG[anat_int]
|
|
357
|
+
elif anat_int == 3:
|
|
358
|
+
# upstream removed enhanced_mean_img — closest fallback is
|
|
359
|
+
# 'meanImg' (same source family, no median-ratio enhancement).
|
|
360
|
+
# warn the caller so they know something changed.
|
|
361
|
+
import warnings
|
|
362
|
+
warnings.warn(
|
|
363
|
+
"anatomical_only=3 (enhanced_mean_img) is no longer supported "
|
|
364
|
+
"upstream; falling back to cellpose_settings.img='meanImg'. "
|
|
365
|
+
"Use anatomical_only in {1, 2, 4} to avoid this fallback.",
|
|
366
|
+
stacklevel=2,
|
|
367
|
+
)
|
|
368
|
+
cellpose["img"] = "max_proj"
|
|
369
|
+
|
|
322
370
|
if cellpose:
|
|
323
371
|
detection["cellpose_settings"] = cellpose
|
|
324
372
|
|
|
@@ -348,16 +396,9 @@ def db_settings_to_ops(db: dict | None, settings: dict | None) -> dict:
|
|
|
348
396
|
if key in settings:
|
|
349
397
|
value = settings[key]
|
|
350
398
|
if key == "diameter":
|
|
351
|
-
#
|
|
352
|
-
# scalar
|
|
353
|
-
|
|
354
|
-
# value for fork consumers — use dy if equal, else the
|
|
355
|
-
# mean rounded to 1 decimal.
|
|
356
|
-
if isinstance(value, (list, tuple)) and len(value) >= 1:
|
|
357
|
-
if len(value) >= 2 and float(value[0]) != float(value[1]):
|
|
358
|
-
value = round((float(value[0]) + float(value[1])) / 2, 1)
|
|
359
|
-
else:
|
|
360
|
-
value = float(value[0])
|
|
399
|
+
# keep upstream's [dy, dx] np.ndarray shape — fork consumers
|
|
400
|
+
# that need a scalar should use np.mean / [0] explicitly.
|
|
401
|
+
value = _ensure_diameter_array(value)
|
|
361
402
|
ops[key] = value
|
|
362
403
|
|
|
363
404
|
# flat sections
|
|
@@ -119,6 +119,14 @@ def s2p_ops():
|
|
|
119
119
|
1.0, # adjust the automatically determined threshold by this scalar multiplier
|
|
120
120
|
"max_overlap":
|
|
121
121
|
0.75, # cells with more overlap than this get removed during triage, before refinement
|
|
122
|
+
# disable upstream's npix_norm filter (suite2p>=1.0.0.x) — its
|
|
123
|
+
# detect.py call site applies 0.0/100.0 if these aren't set, which
|
|
124
|
+
# culls 100x-median-sized ROIs. suite2p_mbo had no such filter and
|
|
125
|
+
# LBM cells at diameter=4 trip the upper bound when many small
|
|
126
|
+
# cells skew the median low. -1 / inf mirrors roi_stats's own
|
|
127
|
+
# function-level defaults (effectively off).
|
|
128
|
+
"npix_norm_min": -1.0,
|
|
129
|
+
"npix_norm_max": float("inf"),
|
|
122
130
|
"high_pass":
|
|
123
131
|
100, # running mean subtraction across bins with a window of size "high_pass" (use low values for 1P)
|
|
124
132
|
"spatial_hp_detect":
|
|
@@ -130,7 +138,7 @@ def s2p_ops():
|
|
|
130
138
|
3,
|
|
131
139
|
# run cellpose to get masks on 1: max_proj / mean_img; 2: mean_img; 3: mean_img enhanced, 4: max_proj
|
|
132
140
|
"diameter": 4, # LBM default cell diameter in pixels (cellpose estimates if 0)
|
|
133
|
-
"cellprob_threshold":
|
|
141
|
+
"cellprob_threshold": -6, # permissive LBM default for cellpose (upstream is 0.0)
|
|
134
142
|
"flow_threshold": 0, # flow_threshold for cellpose (0 = flow-check disabled)
|
|
135
143
|
"spatial_hp_cp": 0.5, # high-pass image spatially by a multiple of the diameter
|
|
136
144
|
# cellpose model: 'cpsam' (default) or path to custom model trained with lsp.train_cellpose()
|
|
@@ -176,7 +184,7 @@ def default_ops(metadata=None, ops=None):
|
|
|
176
184
|
anatomical_only=3
|
|
177
185
|
diameter=4
|
|
178
186
|
spatial_hp_cp=0.5
|
|
179
|
-
cellprob_threshold
|
|
187
|
+
cellprob_threshold=-6
|
|
180
188
|
flow_threshold=0
|
|
181
189
|
spatial_scale=1
|
|
182
190
|
tau=1.3
|
|
@@ -166,7 +166,15 @@ def filter_by_diameter(
|
|
|
166
166
|
|
|
167
167
|
radii = np.array([s["radius"] for s in stat])
|
|
168
168
|
diameters_px = 2 * radii
|
|
169
|
-
|
|
169
|
+
# ops["diameter"] is np.ndarray([dy, dx]) post-pipeline; collapse to scalar
|
|
170
|
+
# for the multiplier comparison below.
|
|
171
|
+
raw_diam = ops.get("diameter")
|
|
172
|
+
if raw_diam is None:
|
|
173
|
+
median_diam = float(np.median(diameters_px))
|
|
174
|
+
elif isinstance(raw_diam, (list, tuple, np.ndarray)) and np.size(raw_diam) >= 1:
|
|
175
|
+
median_diam = float(np.mean(raw_diam))
|
|
176
|
+
else:
|
|
177
|
+
median_diam = float(raw_diam)
|
|
170
178
|
lower, upper = min_mult * median_diam, max_mult * median_diam
|
|
171
179
|
|
|
172
180
|
valid = (diameters_px >= lower) & (diameters_px <= upper)
|
|
@@ -99,13 +99,10 @@ def _call_upstream_pipeline(ops, f_reg, f_raw, f_reg_chan2, f_raw_chan2,
|
|
|
99
99
|
for k, v in reg_outputs.items():
|
|
100
100
|
ops[k] = v
|
|
101
101
|
if isinstance(detect_outputs, dict):
|
|
102
|
+
from lbm_suite2p_python.db_settings import _ensure_diameter_array
|
|
102
103
|
for k, v in detect_outputs.items():
|
|
103
|
-
# don't clobber diameter — keep whatever was passed in unless upstream
|
|
104
|
-
# actually refined it (and collapse list to scalar for fork consumers)
|
|
105
104
|
if k == "diameter" and v is not None:
|
|
106
|
-
|
|
107
|
-
v = float(v[0]) if len(v) < 2 or v[0] == v[1] else (float(v[0]) + float(v[1])) / 2
|
|
108
|
-
ops[k] = v
|
|
105
|
+
ops[k] = _ensure_diameter_array(v)
|
|
109
106
|
elif k != "diameter":
|
|
110
107
|
ops[k] = v
|
|
111
108
|
ops["plane_times"] = plane_times
|
|
@@ -420,6 +417,7 @@ from lbm_suite2p_python.zplane import (
|
|
|
420
417
|
plot_filtered_cells,
|
|
421
418
|
plot_filter_exclusions,
|
|
422
419
|
plot_cell_filter_summary,
|
|
420
|
+
plot_volume_accepted_rejected_overlay,
|
|
423
421
|
)
|
|
424
422
|
|
|
425
423
|
DEFAULT_CELL_FILTERS = []
|
|
@@ -432,6 +430,43 @@ from mbo_utilities.metadata import (
|
|
|
432
430
|
|
|
433
431
|
logger = get_logger("run_lsp")
|
|
434
432
|
|
|
433
|
+
|
|
434
|
+
_external_logging_attached = False
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _attach_external_loggers(level: int = logging.INFO) -> None:
|
|
438
|
+
"""Surface suite2p / cellpose progress messages on stdout.
|
|
439
|
+
|
|
440
|
+
Mainline suite2p (`suite2p.detection.anatomical`,
|
|
441
|
+
`suite2p.registration.*`) and cellpose (`cellpose.models`) use plain
|
|
442
|
+
`logging.getLogger(__name__)` loggers with no handlers attached. Their
|
|
443
|
+
`logger.info(...)` calls — registration progress, ">>>> CELLPOSE finding
|
|
444
|
+
masks", median-diameter reports — propagate to root and get silently
|
|
445
|
+
dropped by the default WARNING-level lastResort handler. Without this
|
|
446
|
+
hookup the user can't tell whether detection is running or what params
|
|
447
|
+
cellpose actually picked. Idempotent, called once per pipeline entry.
|
|
448
|
+
"""
|
|
449
|
+
global _external_logging_attached
|
|
450
|
+
if _external_logging_attached:
|
|
451
|
+
return
|
|
452
|
+
fmt = logging.Formatter("%(name)s: %(message)s")
|
|
453
|
+
for name in ("suite2p", "cellpose"):
|
|
454
|
+
lg = logging.getLogger(name)
|
|
455
|
+
if lg.level == logging.NOTSET or lg.level > level:
|
|
456
|
+
lg.setLevel(level)
|
|
457
|
+
has_stream = any(
|
|
458
|
+
isinstance(h, logging.StreamHandler) and not isinstance(h, logging.FileHandler)
|
|
459
|
+
for h in lg.handlers
|
|
460
|
+
)
|
|
461
|
+
if not has_stream:
|
|
462
|
+
h = logging.StreamHandler()
|
|
463
|
+
h.setFormatter(fmt)
|
|
464
|
+
h.setLevel(level)
|
|
465
|
+
lg.addHandler(h)
|
|
466
|
+
lg.propagate = False
|
|
467
|
+
_external_logging_attached = True
|
|
468
|
+
|
|
469
|
+
|
|
435
470
|
from lbm_suite2p_python._benchmarking import get_cpu_percent, get_ram_used
|
|
436
471
|
from lbm_suite2p_python.volume import (
|
|
437
472
|
plot_volume_diagnostics,
|
|
@@ -644,6 +679,8 @@ def pipeline(
|
|
|
644
679
|
from mbo_utilities import imread
|
|
645
680
|
from mbo_utilities.arrays import supports_roi
|
|
646
681
|
|
|
682
|
+
_attach_external_loggers()
|
|
683
|
+
|
|
647
684
|
# 1. Handle Deprecations
|
|
648
685
|
if roi is not None:
|
|
649
686
|
import warnings
|
|
@@ -1204,6 +1241,9 @@ def run_volume(
|
|
|
1204
1241
|
plot_3d_roi_map(
|
|
1205
1242
|
ops_files, save_path / "roi_map_3d_plane.png", color_by="plane"
|
|
1206
1243
|
)
|
|
1244
|
+
plot_volume_accepted_rejected_overlay(
|
|
1245
|
+
ops_files, save_path / "volume_segmentation_overlay.png"
|
|
1246
|
+
)
|
|
1207
1247
|
except Exception as e:
|
|
1208
1248
|
print(f"Warning: Volume plots failed: {e}")
|
|
1209
1249
|
traceback.print_exc()
|
|
@@ -1445,12 +1485,31 @@ def run_plane_bin(ops) -> bool:
|
|
|
1445
1485
|
n_func = ops.get("nframes_chan1") or ops.get("nframes") or ops.get("n_frames")
|
|
1446
1486
|
if n_func is None:
|
|
1447
1487
|
raise KeyError("Missing nframes_chan1 / nframes / n_frames in ops")
|
|
1488
|
+
|
|
1489
|
+
# Graceful fallback for "reload a completed plane" workflow:
|
|
1490
|
+
# - keep_raw=False (default) deletes data_raw.bin after a run.
|
|
1491
|
+
# - When the user reloads that plane_dir and clicks Run again, the
|
|
1492
|
+
# GUI hands us an ops where raw_file is missing but reg_file (the
|
|
1493
|
+
# registered data.bin) is present. Re-registration needs raw data,
|
|
1494
|
+
# but detection/extraction can still run against data.bin.
|
|
1495
|
+
# - Instead of crashing, downgrade to detection-only and log why.
|
|
1448
1496
|
if run_registration and raw_file is None:
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1497
|
+
_existing_reg = ops.get("reg_file")
|
|
1498
|
+
if _existing_reg and Path(_existing_reg).exists():
|
|
1499
|
+
print(
|
|
1500
|
+
"NOTE: raw_file missing but reg_file exists — downgrading "
|
|
1501
|
+
"do_registration=1 → 0 (detection/extraction only against "
|
|
1502
|
+
f"{Path(_existing_reg).name}). Pass keep_raw=True on the "
|
|
1503
|
+
"first run if you want to re-register a reloaded plane."
|
|
1504
|
+
)
|
|
1505
|
+
ops["do_registration"] = 0
|
|
1506
|
+
run_registration = False
|
|
1507
|
+
else:
|
|
1508
|
+
raise KeyError(
|
|
1509
|
+
"Missing raw_file in ops — required when do_registration=1. "
|
|
1510
|
+
"Set do_registration=0 to run detection-only against an "
|
|
1511
|
+
"existing data.bin."
|
|
1512
|
+
)
|
|
1454
1513
|
n_func = int(n_func)
|
|
1455
1514
|
|
|
1456
1515
|
# reg_file may already point at a linked source binary; only pin it
|
|
@@ -1488,16 +1547,12 @@ def run_plane_bin(ops) -> bool:
|
|
|
1488
1547
|
ops["nframes_chan2"] = n_align
|
|
1489
1548
|
|
|
1490
1549
|
if "diameter" in ops:
|
|
1491
|
-
# save user's input diameter
|
|
1492
|
-
#
|
|
1550
|
+
# save user's input diameter for provenance — cellpose later overwrites
|
|
1551
|
+
# ops["diameter"] with the estimated median from detection.
|
|
1552
|
+
# do not default/coerce here: db_settings._ensure_diameter_list
|
|
1553
|
+
# converts to [dy, dx] at the upstream-pipeline boundary, and mainline
|
|
1554
|
+
# suite2p / cellpose handle None / scalar / list / aspect-pair natively.
|
|
1493
1555
|
ops["diameter_user"] = ops["diameter"]
|
|
1494
|
-
if ops["diameter"] is not None and np.isnan(ops["diameter"]):
|
|
1495
|
-
ops["diameter"] = 8
|
|
1496
|
-
ops["diameter_user"] = 8
|
|
1497
|
-
if (ops["diameter"] in (None, 0)) and ops.get("anatomical_only", 0) > 0:
|
|
1498
|
-
ops["diameter"] = 8
|
|
1499
|
-
ops["diameter_user"] = 8
|
|
1500
|
-
print("Warning: diameter was not set, defaulting to 8.")
|
|
1501
1556
|
|
|
1502
1557
|
# reset detection-derived parameters when re-running registration or detection
|
|
1503
1558
|
# so compute_enhanced_mean_image() reinitializes them from diameter.
|
|
@@ -1506,8 +1561,16 @@ def run_plane_bin(ops) -> bool:
|
|
|
1506
1561
|
# (run_registration / run_detection were resolved earlier, near the
|
|
1507
1562
|
# raw_file check — keep them in scope here.)
|
|
1508
1563
|
if run_registration:
|
|
1509
|
-
# full reset: clear both registration and detection intermediates
|
|
1510
|
-
|
|
1564
|
+
# full reset: clear both registration and detection intermediates.
|
|
1565
|
+
# registration outputs (badframes/xoff/yoff/corrXY) are cleared too —
|
|
1566
|
+
# otherwise a prior divergent run leaves badframes=True for most
|
|
1567
|
+
# frames and detection silently excludes them on the rerun even
|
|
1568
|
+
# though the new registration succeeded.
|
|
1569
|
+
for key in [
|
|
1570
|
+
"spatscale_pix", "Vcorr", "Vmax", "Vmap", "Vsplit", "ihop",
|
|
1571
|
+
"badframes", "xoff", "yoff", "corrXY",
|
|
1572
|
+
"xoff1", "yoff1", "corrXY1",
|
|
1573
|
+
]:
|
|
1511
1574
|
if key in ops:
|
|
1512
1575
|
del ops[key]
|
|
1513
1576
|
elif run_detection:
|
|
@@ -2102,10 +2165,29 @@ def run_plane(
|
|
|
2102
2165
|
"data_path": str(input_path.resolve()),
|
|
2103
2166
|
}
|
|
2104
2167
|
|
|
2168
|
+
# auto-resolve source_mode based on user intent + disk state:
|
|
2169
|
+
#
|
|
2170
|
+
# - explicit `do_registration=0` from the user means "I'm reusing
|
|
2171
|
+
# the registered binary, just sweeping detection/extraction".
|
|
2172
|
+
# the safe + cheap mode here is `link` — zero copies, source
|
|
2173
|
+
# binary is read-only, and N sweeps over the same plane don't
|
|
2174
|
+
# produce N redundant copies of data.bin.
|
|
2175
|
+
# - otherwise fall back to the original disk-based heuristic:
|
|
2176
|
+
# `copy` if raw exists (registration can proceed locally), else
|
|
2177
|
+
# `copy_reg` (no raw on disk, raw_file gets popped, downstream
|
|
2178
|
+
# gracefully downgrades to detection-only).
|
|
2179
|
+
_effective_mode = source_mode
|
|
2180
|
+
if _effective_mode == "auto":
|
|
2181
|
+
if ops_user.get("do_registration", 1) == 0:
|
|
2182
|
+
_effective_mode = "link"
|
|
2183
|
+
else:
|
|
2184
|
+
_effective_mode = (
|
|
2185
|
+
"copy" if (src_dir / "data_raw.bin").exists() else "copy_reg"
|
|
2186
|
+
)
|
|
2187
|
+
|
|
2105
2188
|
# link mode can't safely co-exist with do_registration=1 because
|
|
2106
2189
|
# registration would need to write the source data.bin. flip to
|
|
2107
2190
|
# copy_reg and warn instead of corrupting the source binary.
|
|
2108
|
-
_effective_mode = source_mode
|
|
2109
2191
|
if _effective_mode == "link" and ops_user.get("do_registration", 1):
|
|
2110
2192
|
logger.warning(
|
|
2111
2193
|
"source_mode='link' requires do_registration=0 (link mode "
|
|
@@ -1068,13 +1068,13 @@ def plot_orthoslices(
|
|
|
1068
1068
|
save_path: str | Path = None,
|
|
1069
1069
|
figsize: tuple = (16, 6),
|
|
1070
1070
|
use_mean: bool = True,
|
|
1071
|
+
interpolate: bool = False,
|
|
1071
1072
|
) -> plt.Figure:
|
|
1072
1073
|
"""
|
|
1073
1074
|
Generate orthogonal maximum intensity projections (XY, XZ, YZ) of the volume.
|
|
1074
1075
|
|
|
1075
|
-
Creates a 3-panel figure showing the volume from three orthogonal views
|
|
1076
|
-
|
|
1077
|
-
in micrometers using voxel size metadata.
|
|
1076
|
+
Creates a 3-panel figure showing the volume from three orthogonal views.
|
|
1077
|
+
Axes are displayed in micrometers using voxel size metadata.
|
|
1078
1078
|
|
|
1079
1079
|
Parameters
|
|
1080
1080
|
----------
|
|
@@ -1086,13 +1086,17 @@ def plot_orthoslices(
|
|
|
1086
1086
|
Figure size in inches.
|
|
1087
1087
|
use_mean : bool, default True
|
|
1088
1088
|
If True, use meanImg. If False, use refImg (registered reference).
|
|
1089
|
+
interpolate : bool, default False
|
|
1090
|
+
If True, resample the volume in Z to isotropic resolution and use
|
|
1091
|
+
bilinear interpolation in the XZ/YZ panels. Off by default since
|
|
1092
|
+
LBM volumes are typically very thin in Z (e.g. 14 planes), where
|
|
1093
|
+
interpolation can be misleading.
|
|
1089
1094
|
|
|
1090
1095
|
Returns
|
|
1091
1096
|
-------
|
|
1092
1097
|
fig : matplotlib.figure.Figure
|
|
1093
1098
|
The generated figure object.
|
|
1094
1099
|
"""
|
|
1095
|
-
from scipy.ndimage import zoom
|
|
1096
1100
|
from lbm_suite2p_python.postprocessing import load_ops
|
|
1097
1101
|
|
|
1098
1102
|
if not ops_files:
|
|
@@ -1159,15 +1163,21 @@ def plot_orthoslices(
|
|
|
1159
1163
|
vol_y_um = ny * dy_um
|
|
1160
1164
|
vol_z_um = (nz - 1) * dz_um if nz > 1 else dz_um
|
|
1161
1165
|
|
|
1162
|
-
#
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1166
|
+
# optionally resample volume in z to isotropic resolution
|
|
1167
|
+
if interpolate:
|
|
1168
|
+
from scipy.ndimage import zoom
|
|
1169
|
+
xy_res = (dx_um + dy_um) / 2
|
|
1170
|
+
z_zoom = dz_um / xy_res if xy_res > 0 else 1.0
|
|
1171
|
+
z_zoom = min(z_zoom, 10.0) # cap to avoid memory issues
|
|
1172
|
+
if z_zoom > 1.1:
|
|
1173
|
+
volume_resampled = zoom(volume, (z_zoom, 1, 1), order=1)
|
|
1174
|
+
else:
|
|
1175
|
+
volume_resampled = volume
|
|
1168
1176
|
else:
|
|
1169
1177
|
volume_resampled = volume
|
|
1170
1178
|
|
|
1179
|
+
imshow_interp = "bilinear" if interpolate else "nearest"
|
|
1180
|
+
|
|
1171
1181
|
# compute projections
|
|
1172
1182
|
xy_proj = np.max(volume, axis=0)
|
|
1173
1183
|
xz_proj = np.max(volume_resampled, axis=1)
|
|
@@ -1199,7 +1209,7 @@ def plot_orthoslices(
|
|
|
1199
1209
|
ax2.set_facecolor("black")
|
|
1200
1210
|
im2 = ax2.imshow(xz_proj, cmap="magma", aspect="auto", extent=xz_extent,
|
|
1201
1211
|
vmin=np.percentile(xz_proj, 1), vmax=np.percentile(xz_proj, 99.5),
|
|
1202
|
-
interpolation=
|
|
1212
|
+
interpolation=imshow_interp)
|
|
1203
1213
|
ax2.set_xlabel("X (μm)", fontsize=10, fontweight="bold", color="white")
|
|
1204
1214
|
ax2.set_ylabel("Z (μm)", fontsize=10, fontweight="bold", color="white")
|
|
1205
1215
|
ax2.set_title("XZ Projection", fontsize=11, fontweight="bold", color="white")
|
|
@@ -1212,7 +1222,7 @@ def plot_orthoslices(
|
|
|
1212
1222
|
ax3.set_facecolor("black")
|
|
1213
1223
|
im3 = ax3.imshow(yz_proj.T, cmap="magma", aspect="auto", extent=yz_extent,
|
|
1214
1224
|
vmin=np.percentile(yz_proj, 1), vmax=np.percentile(yz_proj, 99.5),
|
|
1215
|
-
interpolation=
|
|
1225
|
+
interpolation=imshow_interp)
|
|
1216
1226
|
ax3.set_xlabel("Z (μm)", fontsize=10, fontweight="bold", color="white")
|
|
1217
1227
|
ax3.set_ylabel("Y (μm)", fontsize=10, fontweight="bold", color="white")
|
|
1218
1228
|
ax3.set_title("YZ Projection", fontsize=11, fontweight="bold", color="white")
|
|
@@ -946,6 +946,339 @@ def plot_masks(
|
|
|
946
946
|
plt.show()
|
|
947
947
|
|
|
948
948
|
|
|
949
|
+
def _build_accepted_rejected_canvas(
|
|
950
|
+
img: np.ndarray,
|
|
951
|
+
stat,
|
|
952
|
+
iscell_mask: np.ndarray,
|
|
953
|
+
*,
|
|
954
|
+
ops: dict = None,
|
|
955
|
+
proj_key: str = None,
|
|
956
|
+
accepted_color=(0.0, 1.0, 0.0),
|
|
957
|
+
rejected_color=(1.0, 0.0, 0.0),
|
|
958
|
+
):
|
|
959
|
+
"""
|
|
960
|
+
Build an RGB canvas of a projection with accepted/rejected ROIs blended
|
|
961
|
+
on top using suite2p ``lam`` weights — the same per-pixel opacity used
|
|
962
|
+
by :func:`plot_masks`.
|
|
963
|
+
|
|
964
|
+
Returns
|
|
965
|
+
-------
|
|
966
|
+
canvas : ndarray (Ly, Lx, 3)
|
|
967
|
+
n_accepted, n_rejected : int
|
|
968
|
+
"""
|
|
969
|
+
stat_yoff = 0
|
|
970
|
+
stat_xoff = 0
|
|
971
|
+
if ops is not None and proj_key is not None:
|
|
972
|
+
img, stat_yoff, stat_xoff = _crop_projection_to_valid(ops, img, proj_key)
|
|
973
|
+
|
|
974
|
+
vmin = np.nanpercentile(img, 1)
|
|
975
|
+
vmax = np.nanpercentile(img, 99)
|
|
976
|
+
normalized = (img - vmin) / (vmax - vmin + 1e-6)
|
|
977
|
+
normalized = np.clip(normalized, 0, 1)
|
|
978
|
+
normalized = np.nan_to_num(normalized, nan=0.0)
|
|
979
|
+
canvas = np.tile(normalized, (3, 1, 1)).transpose(1, 2, 0).astype(np.float32)
|
|
980
|
+
|
|
981
|
+
Ly, Lx = img.shape[:2]
|
|
982
|
+
iscell_mask = np.asarray(iscell_mask, dtype=bool)
|
|
983
|
+
|
|
984
|
+
accepted_color = np.asarray(accepted_color, dtype=np.float32)
|
|
985
|
+
rejected_color = np.asarray(rejected_color, dtype=np.float32)
|
|
986
|
+
|
|
987
|
+
for n, s in enumerate(stat):
|
|
988
|
+
ypix = np.asarray(s.get("ypix", []), dtype=int) - stat_yoff
|
|
989
|
+
xpix = np.asarray(s.get("xpix", []), dtype=int) - stat_xoff
|
|
990
|
+
lam = np.asarray(s.get("lam", []), dtype=np.float32)
|
|
991
|
+
if ypix.size == 0:
|
|
992
|
+
continue
|
|
993
|
+
valid = (ypix >= 0) & (ypix < Ly) & (xpix >= 0) & (xpix < Lx)
|
|
994
|
+
if not np.any(valid):
|
|
995
|
+
continue
|
|
996
|
+
ypix, xpix, lam = ypix[valid], xpix[valid], lam[valid]
|
|
997
|
+
lam = lam / (lam.max() + 1e-10)
|
|
998
|
+
col = accepted_color if iscell_mask[n] else rejected_color
|
|
999
|
+
for k in range(3):
|
|
1000
|
+
canvas[ypix, xpix, k] = (
|
|
1001
|
+
0.5 * canvas[ypix, xpix, k] + 0.5 * col[k] * lam
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
n_accepted = int(iscell_mask.sum())
|
|
1005
|
+
n_rejected = int((~iscell_mask).sum())
|
|
1006
|
+
return canvas, n_accepted, n_rejected
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def plot_accepted_rejected_overlay(
|
|
1010
|
+
img: np.ndarray,
|
|
1011
|
+
stat,
|
|
1012
|
+
iscell_mask: np.ndarray,
|
|
1013
|
+
savepath: str | Path = None,
|
|
1014
|
+
title: str = None,
|
|
1015
|
+
*,
|
|
1016
|
+
ops: dict = None,
|
|
1017
|
+
proj_key: str = None,
|
|
1018
|
+
figsize: tuple = (6, 6),
|
|
1019
|
+
dpi: int = 300,
|
|
1020
|
+
):
|
|
1021
|
+
"""
|
|
1022
|
+
Draw accepted (green) and rejected (red) ROI overlays on a projection.
|
|
1023
|
+
|
|
1024
|
+
Uses the same lam-weighted per-pixel opacity as :func:`plot_masks` so
|
|
1025
|
+
feathering matches the rest of the segmentation figures. Adds dark
|
|
1026
|
+
formatting and ``Accepted``/``Rejected`` count labels at the top.
|
|
1027
|
+
|
|
1028
|
+
Parameters
|
|
1029
|
+
----------
|
|
1030
|
+
img : ndarray (Ly x Lx)
|
|
1031
|
+
Background projection image.
|
|
1032
|
+
stat : list[dict]
|
|
1033
|
+
Suite2p ROI stat dictionaries (full-frame coordinates).
|
|
1034
|
+
iscell_mask : ndarray[bool]
|
|
1035
|
+
Boolean array, True for accepted ROIs.
|
|
1036
|
+
savepath : str or Path, optional
|
|
1037
|
+
Path to save the figure. If None, displays with plt.show().
|
|
1038
|
+
title : str, optional
|
|
1039
|
+
Projection-name title rendered above the count labels.
|
|
1040
|
+
ops : dict, optional
|
|
1041
|
+
Suite2p ops dictionary. With ``proj_key``, the image is cropped to
|
|
1042
|
+
the valid (non-padded) region and stat coordinates are translated
|
|
1043
|
+
accordingly.
|
|
1044
|
+
proj_key : str, optional
|
|
1045
|
+
Projection key matching ``img`` (e.g. ``"meanImg"``, ``"max_proj"``).
|
|
1046
|
+
figsize : tuple, optional
|
|
1047
|
+
Figure size. Default (6, 6) to match other segmentation figures.
|
|
1048
|
+
dpi : int, optional
|
|
1049
|
+
Output DPI. Default 300.
|
|
1050
|
+
"""
|
|
1051
|
+
canvas, n_accepted, n_rejected = _build_accepted_rejected_canvas(
|
|
1052
|
+
img, stat, iscell_mask, ops=ops, proj_key=proj_key,
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
fig, ax = plt.subplots(figsize=figsize, facecolor="black")
|
|
1056
|
+
ax.set_facecolor("black")
|
|
1057
|
+
ax.imshow(canvas, interpolation="nearest")
|
|
1058
|
+
|
|
1059
|
+
label_y = 1.02
|
|
1060
|
+
if title:
|
|
1061
|
+
ax.text(
|
|
1062
|
+
0.5, 1.02, title,
|
|
1063
|
+
transform=ax.transAxes,
|
|
1064
|
+
fontsize=12, fontweight="bold", fontname="Courier New",
|
|
1065
|
+
color="white", ha="center", va="bottom",
|
|
1066
|
+
)
|
|
1067
|
+
label_y = 1.10
|
|
1068
|
+
ax.text(
|
|
1069
|
+
0.37, label_y,
|
|
1070
|
+
f"Accepted: {n_accepted:03d}",
|
|
1071
|
+
transform=ax.transAxes,
|
|
1072
|
+
fontsize=14, fontweight="bold", fontname="Courier New",
|
|
1073
|
+
color="lime", ha="right", va="bottom",
|
|
1074
|
+
)
|
|
1075
|
+
ax.text(
|
|
1076
|
+
0.63, label_y,
|
|
1077
|
+
f"Rejected: {n_rejected:03d}",
|
|
1078
|
+
transform=ax.transAxes,
|
|
1079
|
+
fontsize=14, fontweight="bold", fontname="Courier New",
|
|
1080
|
+
color="red", ha="left", va="bottom",
|
|
1081
|
+
)
|
|
1082
|
+
ax.set_xticks([])
|
|
1083
|
+
ax.set_yticks([])
|
|
1084
|
+
ax.axis("off")
|
|
1085
|
+
plt.tight_layout()
|
|
1086
|
+
|
|
1087
|
+
if savepath:
|
|
1088
|
+
if Path(savepath).is_dir():
|
|
1089
|
+
raise ValueError("savepath must be a file path, not a directory.")
|
|
1090
|
+
plt.savefig(savepath, dpi=dpi, facecolor="black")
|
|
1091
|
+
plt.close(fig)
|
|
1092
|
+
else:
|
|
1093
|
+
plt.show()
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def plot_volume_accepted_rejected_overlay(
|
|
1097
|
+
ops_files,
|
|
1098
|
+
savepath: str | Path = None,
|
|
1099
|
+
*,
|
|
1100
|
+
ncols: int = None,
|
|
1101
|
+
figsize: tuple = None,
|
|
1102
|
+
dpi: int = 200,
|
|
1103
|
+
):
|
|
1104
|
+
"""
|
|
1105
|
+
Volumetric version of :func:`plot_accepted_rejected_overlay`.
|
|
1106
|
+
|
|
1107
|
+
One subplot per z-plane showing the projection used by cellpose with
|
|
1108
|
+
accepted ROIs in green and rejected in red. Uses the same lam-weighted
|
|
1109
|
+
opacity as :func:`plot_masks`. Per-subplot titles show the plane's
|
|
1110
|
+
``Accepted: N`` (lime) and ``Rejected: N`` (red) counts; an overall
|
|
1111
|
+
title at the very top reports total accepted/rejected across the volume.
|
|
1112
|
+
|
|
1113
|
+
Parameters
|
|
1114
|
+
----------
|
|
1115
|
+
ops_files : list of str or Path
|
|
1116
|
+
Paths to per-plane ``ops.npy`` files.
|
|
1117
|
+
savepath : str or Path, optional
|
|
1118
|
+
Where to save the figure. If None, calls plt.show().
|
|
1119
|
+
ncols : int, optional
|
|
1120
|
+
Number of columns in the subplot grid. Defaults to a near-square
|
|
1121
|
+
layout.
|
|
1122
|
+
figsize : tuple, optional
|
|
1123
|
+
Figure size. Defaults based on the grid shape.
|
|
1124
|
+
dpi : int, optional
|
|
1125
|
+
Output DPI. Default 200.
|
|
1126
|
+
"""
|
|
1127
|
+
ops_files = [Path(p) for p in ops_files]
|
|
1128
|
+
n_planes = len(ops_files)
|
|
1129
|
+
if n_planes == 0:
|
|
1130
|
+
raise ValueError("ops_files is empty")
|
|
1131
|
+
|
|
1132
|
+
if ncols is None:
|
|
1133
|
+
ncols = int(np.ceil(np.sqrt(n_planes)))
|
|
1134
|
+
nrows = int(np.ceil(n_planes / ncols))
|
|
1135
|
+
if figsize is None:
|
|
1136
|
+
figsize = (4 * ncols, 4.4 * nrows)
|
|
1137
|
+
|
|
1138
|
+
proj_lookup = {
|
|
1139
|
+
0: ("Vcorr", "Correlation Image"),
|
|
1140
|
+
1: ("max_proj", "Max Projection"),
|
|
1141
|
+
2: ("meanImg", "Mean Image"),
|
|
1142
|
+
3: ("meanImgE", "Enhanced Mean Image"),
|
|
1143
|
+
4: ("max_proj", "Max Projection"),
|
|
1144
|
+
}
|
|
1145
|
+
fallback_titles = {
|
|
1146
|
+
"meanImg": "Mean Image",
|
|
1147
|
+
"max_proj": "Max Projection",
|
|
1148
|
+
"meanImgE": "Enhanced Mean Image",
|
|
1149
|
+
"Vcorr": "Correlation Image",
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
def _is_valid_image(im):
|
|
1153
|
+
if im is None:
|
|
1154
|
+
return False
|
|
1155
|
+
if isinstance(im, (int, float)) and im == 0:
|
|
1156
|
+
return False
|
|
1157
|
+
if isinstance(im, np.ndarray) and im.size == 0:
|
|
1158
|
+
return False
|
|
1159
|
+
return True
|
|
1160
|
+
|
|
1161
|
+
def _plane_num(ops, fallback):
|
|
1162
|
+
raw = ops.get("plane", None)
|
|
1163
|
+
if raw is None:
|
|
1164
|
+
return fallback
|
|
1165
|
+
if isinstance(raw, (int, np.integer)):
|
|
1166
|
+
return int(raw)
|
|
1167
|
+
digits = "".join(c for c in str(raw) if c.isdigit())
|
|
1168
|
+
return int(digits) if digits else fallback
|
|
1169
|
+
|
|
1170
|
+
plane_entries = []
|
|
1171
|
+
for i, ops_file in enumerate(ops_files):
|
|
1172
|
+
try:
|
|
1173
|
+
ops = load_ops(ops_file)
|
|
1174
|
+
except Exception as e:
|
|
1175
|
+
print(f" Warning: could not load {ops_file}: {e}")
|
|
1176
|
+
continue
|
|
1177
|
+
try:
|
|
1178
|
+
res = load_planar_results(ops)
|
|
1179
|
+
except Exception as e:
|
|
1180
|
+
print(f" Warning: could not load planar results for {ops_file}: {e}")
|
|
1181
|
+
continue
|
|
1182
|
+
|
|
1183
|
+
stat = res["stat"]
|
|
1184
|
+
iscell_mask = res["iscell"][:, 0].astype(bool)
|
|
1185
|
+
|
|
1186
|
+
anatomical_only = int(ops.get("anatomical_only", 0) or 0)
|
|
1187
|
+
proj_key, proj_title = proj_lookup.get(anatomical_only, ("meanImg", "Mean Image"))
|
|
1188
|
+
img = ops.get(proj_key)
|
|
1189
|
+
if not _is_valid_image(img):
|
|
1190
|
+
for fk in ("meanImg", "max_proj", "meanImgE", "Vcorr"):
|
|
1191
|
+
if _is_valid_image(ops.get(fk)):
|
|
1192
|
+
proj_key = fk
|
|
1193
|
+
proj_title = fallback_titles[fk]
|
|
1194
|
+
img = ops.get(fk)
|
|
1195
|
+
break
|
|
1196
|
+
if not _is_valid_image(img):
|
|
1197
|
+
continue
|
|
1198
|
+
|
|
1199
|
+
plane_entries.append({
|
|
1200
|
+
"plane": _plane_num(ops, i),
|
|
1201
|
+
"ops": ops,
|
|
1202
|
+
"img": img,
|
|
1203
|
+
"stat": stat,
|
|
1204
|
+
"iscell_mask": iscell_mask,
|
|
1205
|
+
"proj_key": proj_key,
|
|
1206
|
+
"proj_title": proj_title,
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
if not plane_entries:
|
|
1210
|
+
raise RuntimeError("No valid plane data found in ops_files")
|
|
1211
|
+
|
|
1212
|
+
plane_entries.sort(key=lambda e: e["plane"])
|
|
1213
|
+
|
|
1214
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=figsize, facecolor="black")
|
|
1215
|
+
axes = np.atleast_1d(axes).ravel()
|
|
1216
|
+
|
|
1217
|
+
total_accepted = 0
|
|
1218
|
+
total_rejected = 0
|
|
1219
|
+
|
|
1220
|
+
for idx, ax in enumerate(axes):
|
|
1221
|
+
ax.set_facecolor("black")
|
|
1222
|
+
if idx >= len(plane_entries):
|
|
1223
|
+
ax.axis("off")
|
|
1224
|
+
continue
|
|
1225
|
+
e = plane_entries[idx]
|
|
1226
|
+
canvas, n_acc, n_rej = _build_accepted_rejected_canvas(
|
|
1227
|
+
e["img"], e["stat"], e["iscell_mask"],
|
|
1228
|
+
ops=e["ops"], proj_key=e["proj_key"],
|
|
1229
|
+
)
|
|
1230
|
+
total_accepted += n_acc
|
|
1231
|
+
total_rejected += n_rej
|
|
1232
|
+
|
|
1233
|
+
ax.imshow(canvas, interpolation="nearest")
|
|
1234
|
+
ax.set_xticks([])
|
|
1235
|
+
ax.set_yticks([])
|
|
1236
|
+
for spine in ax.spines.values():
|
|
1237
|
+
spine.set_visible(False)
|
|
1238
|
+
|
|
1239
|
+
ax.text(
|
|
1240
|
+
0.02, 1.10, f"plane {e['plane']:02d}",
|
|
1241
|
+
transform=ax.transAxes,
|
|
1242
|
+
fontsize=10, fontweight="bold", fontname="Courier New",
|
|
1243
|
+
color="white", ha="left", va="bottom",
|
|
1244
|
+
)
|
|
1245
|
+
ax.text(
|
|
1246
|
+
0.48, 1.10, f"Accepted: {n_acc:03d}",
|
|
1247
|
+
transform=ax.transAxes,
|
|
1248
|
+
fontsize=10, fontweight="bold", fontname="Courier New",
|
|
1249
|
+
color="lime", ha="right", va="bottom",
|
|
1250
|
+
)
|
|
1251
|
+
ax.text(
|
|
1252
|
+
0.52, 1.10, f"Rejected: {n_rej:03d}",
|
|
1253
|
+
transform=ax.transAxes,
|
|
1254
|
+
fontsize=10, fontweight="bold", fontname="Courier New",
|
|
1255
|
+
color="red", ha="left", va="bottom",
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
fig.text(
|
|
1259
|
+
0.49, 0.985,
|
|
1260
|
+
f"Total Accepted: {total_accepted:04d}",
|
|
1261
|
+
color="lime", fontsize=16, fontweight="bold", fontname="Courier New",
|
|
1262
|
+
ha="right", va="top",
|
|
1263
|
+
)
|
|
1264
|
+
fig.text(
|
|
1265
|
+
0.51, 0.985,
|
|
1266
|
+
f"Total Rejected: {total_rejected:04d}",
|
|
1267
|
+
color="red", fontsize=16, fontweight="bold", fontname="Courier New",
|
|
1268
|
+
ha="left", va="top",
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
plt.tight_layout(rect=(0, 0, 1, 0.96))
|
|
1272
|
+
|
|
1273
|
+
if savepath:
|
|
1274
|
+
if Path(savepath).is_dir():
|
|
1275
|
+
raise ValueError("savepath must be a file path, not a directory.")
|
|
1276
|
+
plt.savefig(savepath, dpi=dpi, facecolor="black")
|
|
1277
|
+
plt.close(fig)
|
|
1278
|
+
else:
|
|
1279
|
+
plt.show()
|
|
1280
|
+
|
|
1281
|
+
|
|
949
1282
|
def plot_projection(
|
|
950
1283
|
ops,
|
|
951
1284
|
output_directory=None,
|
|
@@ -2873,6 +3206,8 @@ def plot_zplane_figures(
|
|
|
2873
3206
|
"meanImg_segmentation": plane_dir / "03_mean_segmentation.png",
|
|
2874
3207
|
"meanImgE": plane_dir / "04_mean_enhanced.png",
|
|
2875
3208
|
"meanImgE_segmentation": plane_dir / "04_mean_enhanced_segmentation.png",
|
|
3209
|
+
# rejected-cell overlay on the projection used for cellpose detection
|
|
3210
|
+
"rejected_segmentation": plane_dir / "04b_rejected_segmentation.png",
|
|
2876
3211
|
# Diagnostics and analysis
|
|
2877
3212
|
"quality_diagnostics": plane_dir / "05_quality_diagnostics.png",
|
|
2878
3213
|
"registration": plane_dir / "06_registration.png",
|
|
@@ -2910,6 +3245,7 @@ def plot_zplane_figures(
|
|
|
2910
3245
|
"meanImg_segmentation",
|
|
2911
3246
|
"meanImgE",
|
|
2912
3247
|
"meanImgE_segmentation",
|
|
3248
|
+
"rejected_segmentation",
|
|
2913
3249
|
"quality_diagnostics",
|
|
2914
3250
|
"registration",
|
|
2915
3251
|
"traces_raw_20",
|
|
@@ -3004,6 +3340,52 @@ def plot_zplane_figures(
|
|
|
3004
3340
|
except Exception as e:
|
|
3005
3341
|
print(f" Warning: {img_key} segmentation failed: {e}")
|
|
3006
3342
|
|
|
3343
|
+
# rejected-cell overlay on the projection actually used for cellpose
|
|
3344
|
+
# detection. anatomical_only mapping (from default_ops.py):
|
|
3345
|
+
# 0 -> Vcorr (functional sparse mode)
|
|
3346
|
+
# 1 -> max_proj / meanImg (combined; we display max_proj as the
|
|
3347
|
+
# closest visualizable proxy since the ratio isn't stored)
|
|
3348
|
+
# 2 -> meanImg
|
|
3349
|
+
# 3 -> meanImgE
|
|
3350
|
+
# 4 -> max_proj
|
|
3351
|
+
try:
|
|
3352
|
+
if n_rejected > 0:
|
|
3353
|
+
anatomical_only = int(output_ops.get("anatomical_only", 0) or 0)
|
|
3354
|
+
proj_lookup = {
|
|
3355
|
+
0: ("Vcorr", "Correlation Image"),
|
|
3356
|
+
1: ("max_proj", "Max Projection (max_proj / meanImg)"),
|
|
3357
|
+
2: ("meanImg", "Mean Image"),
|
|
3358
|
+
3: ("meanImgE", "Enhanced Mean Image"),
|
|
3359
|
+
4: ("max_proj", "Max Projection"),
|
|
3360
|
+
}
|
|
3361
|
+
rej_key, rej_title = proj_lookup.get(anatomical_only, ("meanImg", "Mean Image"))
|
|
3362
|
+
rej_img = output_ops.get(rej_key)
|
|
3363
|
+
# fallback chain if the chosen projection isn't available
|
|
3364
|
+
if not _is_valid_image(rej_img):
|
|
3365
|
+
for fk in ("meanImg", "max_proj", "meanImgE", "Vcorr"):
|
|
3366
|
+
if _is_valid_image(output_ops.get(fk)):
|
|
3367
|
+
rej_key = fk
|
|
3368
|
+
rej_title = {
|
|
3369
|
+
"meanImg": "Mean Image",
|
|
3370
|
+
"max_proj": "Max Projection",
|
|
3371
|
+
"meanImgE": "Enhanced Mean Image",
|
|
3372
|
+
"Vcorr": "Correlation Image",
|
|
3373
|
+
}[fk]
|
|
3374
|
+
rej_img = output_ops.get(fk)
|
|
3375
|
+
break
|
|
3376
|
+
if _is_valid_image(rej_img):
|
|
3377
|
+
plot_accepted_rejected_overlay(
|
|
3378
|
+
img=rej_img,
|
|
3379
|
+
stat=stat_full,
|
|
3380
|
+
iscell_mask=iscell_mask,
|
|
3381
|
+
savepath=expected_files["rejected_segmentation"],
|
|
3382
|
+
title=rej_title,
|
|
3383
|
+
ops=output_ops,
|
|
3384
|
+
proj_key=rej_key,
|
|
3385
|
+
)
|
|
3386
|
+
except Exception as e:
|
|
3387
|
+
print(f" Warning: rejected segmentation failed: {e}")
|
|
3388
|
+
|
|
3007
3389
|
# correlation image (Vcorr) - cropped space. Render the no-mask
|
|
3008
3390
|
# version through the cropping helper so it matches the mask
|
|
3009
3391
|
# variant's extent.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lbm_suite2p_python
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Light Beads Microscopy Pipeline using Suite2p
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
|
|
@@ -11,18 +11,15 @@ 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>=
|
|
14
|
+
Requires-Dist: mbo_utilities>=3.0.0
|
|
15
15
|
Requires-Dist: suite2p>=1.0.0.1
|
|
16
16
|
Requires-Dist: setuptools<81
|
|
17
17
|
Provides-Extra: rastermap
|
|
18
18
|
Requires-Dist: rastermap; extra == "rastermap"
|
|
19
19
|
Provides-Extra: cellpose
|
|
20
20
|
Requires-Dist: cellpose>=4.0.6; extra == "cellpose"
|
|
21
|
-
Provides-Extra: torch
|
|
22
|
-
Requires-Dist: torch>=2.7.0; extra == "torch"
|
|
23
|
-
Requires-Dist: torchvision>=0.22.0; extra == "torch"
|
|
24
21
|
Provides-Extra: all
|
|
25
|
-
Requires-Dist: lbm_suite2p_python[cellpose,rastermap
|
|
22
|
+
Requires-Dist: lbm_suite2p_python[cellpose,rastermap]; extra == "all"
|
|
26
23
|
Dynamic: license-file
|
|
27
24
|
|
|
28
25
|
<p align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lbm_suite2p_python"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.1"
|
|
8
8
|
description = "Light Beads Microscopy Pipeline using Suite2p"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -18,7 +18,7 @@ classifiers=[
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
dependencies = [
|
|
21
|
-
"mbo_utilities>=
|
|
21
|
+
"mbo_utilities>=3.0.0",
|
|
22
22
|
"suite2p>=1.0.0.1",
|
|
23
23
|
"setuptools<81",
|
|
24
24
|
]
|
|
@@ -35,14 +35,9 @@ rastermap = [
|
|
|
35
35
|
cellpose = [
|
|
36
36
|
"cellpose>=4.0.6",
|
|
37
37
|
]
|
|
38
|
-
# PyTorch for neural network operations (Cellpose, etc.)
|
|
39
|
-
torch = [
|
|
40
|
-
"torch>=2.7.0",
|
|
41
|
-
"torchvision>=0.22.0",
|
|
42
|
-
]
|
|
43
38
|
# All optional dependencies
|
|
44
39
|
all = [
|
|
45
|
-
"lbm_suite2p_python[rastermap,cellpose
|
|
40
|
+
"lbm_suite2p_python[rastermap,cellpose]",
|
|
46
41
|
]
|
|
47
42
|
|
|
48
43
|
[dependency-groups]
|
|
@@ -70,7 +65,7 @@ docs = [
|
|
|
70
65
|
"scikit-image",
|
|
71
66
|
"scipy",
|
|
72
67
|
"pandas",
|
|
73
|
-
"
|
|
68
|
+
"suite2p",
|
|
74
69
|
]
|
|
75
70
|
|
|
76
71
|
# https://github.com/charliermarsh/ruff
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{lbm_suite2p_python-3.0.0 → lbm_suite2p_python-3.0.1}/lbm_suite2p_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|