pycat-napari 1.5.332__tar.gz → 1.5.333__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.
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/CHANGELOG.md +36 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/PKG-INFO +1 -1
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/pyproject.toml +1 -1
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/vpt_tools.py +364 -14
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/vpt_ui.py +59 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/.gitignore +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/CONTRIBUTING.md +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/LICENSE +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/README.md +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/THIRD_PARTY_LICENSES.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/pycat-devbio-napari-env-arm-mac.yaml +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/pycat-devbio-napari-env-x86-windows.yaml +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/pycat-napari-env-arm-mac.yaml +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/pycat-napari-env-x86-windows.yaml +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/requirements-arm-mac.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/requirements-base.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/requirements-devbio-napari-arm-mac.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/requirements-devbio-napari.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/Makefile +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/audits/DEV_NOTES.md +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/audits/PyCAT_method_audit_2026-07-09.md +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/audits/PyCAT_napari_integration_audit_2026-07-09.md +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/logos/pycat_logo-2.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/logos/pycat_logo.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/logos/pycat_logo_1024.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/logos/pycat_logo_256.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/logos/pycat_logo_512.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/make.bat +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/requirements.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/dark_mode.css +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/examples/Image 1 dapi.tif +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/examples/Image 1.tif +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/pycat_logo_512.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/Viewer_and_menu_highlights.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/cell_analyzer.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/condensate_segmentation.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/measuring_lines.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/opened_image_in_viewer.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/preprocessed_images.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_static/screenshots/save_and_clear_popup.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/_templates/autosummary/module.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/index.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.central_manager.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.data.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.file_io.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.run_pycat.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.toolbox.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.ui.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/api/pycat.utils.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/conf.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/development/contributing.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/development/index.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/development/roadmap.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/development/support.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/features.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/index.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/installation.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/usage/index.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/source/usage/tutorials/nuclear_condensate_analysis_gui_tutorial.rst +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/docs/test.txt +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/notebooks/Synthtic Data Generation NB.ipynb +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/notebooks/int_truncated_dfs.ipynb +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/notebooks/pycat_plotting.ipynb +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/batch_processor.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/batch_step_registry.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/central_manager.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/data/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/data/data_modules.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/file_io.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/frap_io.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/metadata_extract.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/multidim_io.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/file_io/session_loader.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/fix_tifffile.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/icons/pycat_logo-2.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/icons/pycat_logo.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/icons/pycat_logo_1024.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/icons/pycat_logo_256.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/icons/pycat_logo_512.png +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/run_pycat.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/advanced_analysis_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/analysis_plots.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/batch_roi_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/benchmark_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/brightfield_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/brightfield_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/clean_spot_detection_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/condensate_physics_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/condensate_physics_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/contrast_cascade_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/contrast_cascade_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/correlation_func_analysis_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/data_qc_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/data_qc_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/data_viz_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/dynamic_spatial_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fd_curve_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fd_curve_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/feature_analysis_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fft_bandpass_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fibril_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fibril_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/frap_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/frap_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fusion_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/fusion_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/gaussian_localization_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/gpu_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/image_processing_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/intensity_profile_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/invitro_bf_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/invitro_fluor_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/invitro_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/label_and_mask_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/layer_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/molecular_counting_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/morphological_complexity_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/nb_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/nb_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/numba_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/obj_based_coloc_analysis_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/organizational_metrics_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/partition_enrichment_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/pipeline_diagnostic_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/pipeline_snr_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/pixel_wise_corr_analysis_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/segmentation_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spatial_acf_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spatial_metrology_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spatial_metrology_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spatial_randomness_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spida_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/spida_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/temperature_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/temperature_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/temporal_enhancement_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/timeseries_condensate_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/timeseries_invitro_fluor_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/timeseries_invitro_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/topology_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/trackmate_bridge.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/ts_cache_manager.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/ts_cellpose_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/two_channel_coloc_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/video_export_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/zstack_segmentation_tools.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/toolbox/zstack_segmentation_ui.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/coordinate_readout.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/field_status.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_analysis_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_diagnostics_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_filtering_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_imageops_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_labels_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_modules.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_segmentation_mixin.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/ui_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/ui/workflow_checklist.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/utils/__init__.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/utils/channel_naming.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/utils/general_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/utils/logging_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/src/pycat/utils/math_utils.py +0 -0
- {pycat_napari-1.5.332 → pycat_napari-1.5.333}/tests/README.md +0 -0
|
@@ -4,6 +4,42 @@ All notable changes to PyCAT-Napari will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.5.333] - 2026-07-10
|
|
8
|
+
### Added — VPT detection-variant staging + track-length histogram
|
|
9
|
+
- **Detection-variant staging framework.** `detect_beads_stack` gained a
|
|
10
|
+
`detection_variant` argument (default `'baseline'` = the 1.5.329-validated
|
|
11
|
+
path, byte-identical). New detection/classification approaches are opt-in
|
|
12
|
+
variants routed through their own branches, so the validated ~8.325-through-
|
|
13
|
+
TrackMate path stays selectable and any regression is a clean one-arg revert. A
|
|
14
|
+
`compare_detection_variants()` harness runs two variants on the same stack and
|
|
15
|
+
reports the classification diff, and the chosen variant is recorded on the
|
|
16
|
+
output DataFrame's attrs. This is the safety net for the detection rework —
|
|
17
|
+
every proposed change is A/B-measured against baseline before it is trusted.
|
|
18
|
+
- **`ring_merge` variant** (`dedup_detections_ring_merge`) — sigma-scaled merge
|
|
19
|
+
radius that folds DIM Airy-ring fragments into their BRIGHT centre while keeping
|
|
20
|
+
two genuinely-bright nearby beads as two. Built and kept in the codebase but
|
|
21
|
+
**not surfaced in the widget and flagged as needing validation data with
|
|
22
|
+
resolved Airy rings**: A/B on the current bead data showed it is a near no-op
|
|
23
|
+
there (beads well-separated, blob_log already ~one detection per bead). Reach it
|
|
24
|
+
via `detection_variant='ring_merge'`.
|
|
25
|
+
- **`hot_pixel_reject` variant** (`build_hot_pixel_mask` + harsher on-pixel NCC
|
|
26
|
+
gate) — identifies FIXED sensor hot/dead pixels from the stack's *temporal*
|
|
27
|
+
statistics (scene-independent: hot pixels are flat in time, tstd~3-4; beads are
|
|
28
|
+
variable, tstd~40-50), then applies a STRICTER acceptance test to detections
|
|
29
|
+
landing on them rather than a flat veto — so a real bead drifting over a hot/
|
|
30
|
+
dead pixel still survives on its template evidence. Validated correct and safe
|
|
31
|
+
(every confirmed bead survived, including one adjacent to a hot pixel). Nearly a
|
|
32
|
+
no-op on the current clean fluorescence data (~18 hot pixels found, blob_log
|
|
33
|
+
barely fires on them); earns its place on data/modes that turn hot pixels into
|
|
34
|
+
recurring false detections. Reach it via `detection_variant='hot_pixel_reject'`.
|
|
35
|
+
- **Track-length histogram in the linker widget.** Step 4 (Link Trajectories) now
|
|
36
|
+
shows an embedded histogram of trajectory lengths (frames per track) after each
|
|
37
|
+
link. A healthy link piles mass toward long tracks; a fragmentation-prone linker
|
|
38
|
+
shows a spike of very short tracks. The title reports the track count and the
|
|
39
|
+
fraction spanning ≥½ the movie, and a dashed line marks the median — an
|
|
40
|
+
at-a-glance linker-quality check. Fails safe if matplotlib-Qt embedding is
|
|
41
|
+
unavailable.
|
|
42
|
+
|
|
7
43
|
## [1.5.332] - 2026-07-10
|
|
8
44
|
### Fixed — VPT classifier green↔yellow flicker on bright, well-matched beads
|
|
9
45
|
- **A bright, high-NCC bead no longer flips singlet↔out_of_plane frame-to-frame.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycat-napari
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.333
|
|
4
4
|
Summary: Python Condensate Analysis Toolbox - A napari-based tool for biomolecular condensate analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/BanerjeeLab-repertoire/pycat-napari
|
|
6
6
|
Project-URL: Repository, https://github.com/BanerjeeLab-repertoire/pycat-napari
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycat-napari"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.333"
|
|
8
8
|
description = "Python Condensate Analysis Toolbox - A napari-based tool for biomolecular condensate analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12,<3.13"
|
|
@@ -340,6 +340,7 @@ def detect_beads_frame(
|
|
|
340
340
|
fit_window: int = 9,
|
|
341
341
|
fast_fit: bool = False,
|
|
342
342
|
use_gpu: bool = False,
|
|
343
|
+
return_sigma: bool = False,
|
|
343
344
|
) -> np.ndarray:
|
|
344
345
|
"""
|
|
345
346
|
Detect beads in a single frame via Laplacian-of-Gaussian blob detection.
|
|
@@ -381,20 +382,29 @@ def detect_beads_frame(
|
|
|
381
382
|
num_sigma=num_sigma, threshold=threshold)
|
|
382
383
|
|
|
383
384
|
if blobs.shape[0] == 0:
|
|
385
|
+
if return_sigma:
|
|
386
|
+
return np.empty((0, 2)), np.empty((0,))
|
|
384
387
|
return np.empty((0, 2))
|
|
385
388
|
|
|
386
389
|
coords = blobs[:, :2] # (y, x)
|
|
390
|
+
_sigmas = blobs[:, 2] if blobs.shape[1] >= 3 else None # detected scale
|
|
387
391
|
|
|
388
392
|
if host_mask is not None:
|
|
389
393
|
hm = np.asarray(host_mask) > 0
|
|
390
394
|
keep = []
|
|
391
|
-
|
|
395
|
+
keep_sig = []
|
|
396
|
+
for _i, (y, x) in enumerate(coords):
|
|
392
397
|
yi, xi = int(round(y)), int(round(x))
|
|
393
398
|
if 0 <= yi < hm.shape[0] and 0 <= xi < hm.shape[1] and hm[yi, xi]:
|
|
394
399
|
keep.append((y, x))
|
|
400
|
+
if _sigmas is not None:
|
|
401
|
+
keep_sig.append(_sigmas[_i])
|
|
395
402
|
coords = np.array(keep) if keep else np.empty((0, 2))
|
|
403
|
+
_sigmas = (np.array(keep_sig) if keep_sig else None) if _sigmas is not None else None
|
|
396
404
|
|
|
397
405
|
if not fit_quality:
|
|
406
|
+
if return_sigma:
|
|
407
|
+
return coords, _sigmas
|
|
398
408
|
return coords
|
|
399
409
|
|
|
400
410
|
# Per-bead 2D Gaussian quality fit
|
|
@@ -561,6 +571,211 @@ def build_airy_template(half, first_zero_px=None):
|
|
|
561
571
|
return tmpl_z
|
|
562
572
|
|
|
563
573
|
|
|
574
|
+
def build_hot_pixel_mask(bead_stack, cv_max=0.12, tstd_max=8.0,
|
|
575
|
+
local_excess_min=8.0, median_size=5,
|
|
576
|
+
max_frames=None):
|
|
577
|
+
"""Identify fixed-location sensor hot/dead pixels from a stack's TEMPORAL
|
|
578
|
+
statistics (detection_variant='hot_pixel_reject').
|
|
579
|
+
|
|
580
|
+
Physics. A sensor hot/dead pixel is a FIXED (r, c) whose value is set by the
|
|
581
|
+
detector, not the scene, so across the movie it is essentially CONSTANT in
|
|
582
|
+
time — high (or anomalous) temporal MEAN but very LOW temporal VARIANCE. A
|
|
583
|
+
real bead location has HIGH temporal variance because the bead moves through /
|
|
584
|
+
jitters (it comes and goes). Verified on Gable's fluorescence VPT data
|
|
585
|
+
(2026-07): hot pixels have temporal std ~3-4 (cv≈0.05) while bead locations
|
|
586
|
+
have temporal std ~40-50 (cv≈0.3-0.5) — a wide, clean gap. This temporal
|
|
587
|
+
signature is SCENE-INDEPENDENT, so unlike a per-frame spike test it (a)
|
|
588
|
+
catches hot pixels sitting DOWN NEAR THE NOISE FLOOR (this camera does this in
|
|
589
|
+
some modes), and (b) will not falsely reject a genuinely stable bead, which
|
|
590
|
+
still jitters in time. It is computed once over the stack, not per frame.
|
|
591
|
+
|
|
592
|
+
A pixel is flagged hot when ALL hold:
|
|
593
|
+
* it stands out from its local neighbourhood in temporal MEAN
|
|
594
|
+
(local-median-subtracted excess ≥ ``local_excess_min``), AND
|
|
595
|
+
* it is temporally FLAT — coefficient of variation
|
|
596
|
+
(temporal_std / temporal_mean) ≤ ``cv_max`` OR absolute temporal
|
|
597
|
+
std ≤ ``tstd_max``.
|
|
598
|
+
|
|
599
|
+
Parameters
|
|
600
|
+
----------
|
|
601
|
+
bead_stack : (T, H, W) stack (lazy or array). Streamed via iter_frames so the
|
|
602
|
+
whole movie is never held in memory.
|
|
603
|
+
cv_max : max coefficient of variation for a flat (hot) pixel.
|
|
604
|
+
tstd_max : max absolute temporal std for a flat (hot) pixel (catches
|
|
605
|
+
near-noise-floor hot pixels whose mean is low so cv is less reliable).
|
|
606
|
+
local_excess_min : min temporal-mean excess over the local median background
|
|
607
|
+
to be considered anomalous at all (avoids flagging ordinary background).
|
|
608
|
+
median_size : neighbourhood size for the local background estimate.
|
|
609
|
+
max_frames : cap frames used for the statistics (None = all). A few hundred
|
|
610
|
+
frames are plenty to estimate the temporal signature.
|
|
611
|
+
|
|
612
|
+
Returns
|
|
613
|
+
-------
|
|
614
|
+
(H, W) boolean mask, True at hot/dead sensor pixels.
|
|
615
|
+
|
|
616
|
+
STATUS (2026-07): mechanism validated CORRECT and SAFE on Gable's fluorescence
|
|
617
|
+
VPT data — the temporal signature cleanly separates hot pixels (temporal std
|
|
618
|
+
~3-4) from beads (temporal std ~40-50), and wired as detection_variant=
|
|
619
|
+
'hot_pixel_reject' it drops hot pixels via a harsher NCC gate WITHOUT rejecting
|
|
620
|
+
real beads (every confirmed bead survived, including one adjacent to a hot
|
|
621
|
+
pixel). HOWEVER on that specific data it is nearly a no-op (~18 hot pixels
|
|
622
|
+
found but blob_log barely fires on them, so ~1 detection removed) — the beads
|
|
623
|
+
are clean and detection is already good there. It earns its place on data where
|
|
624
|
+
a camera/mode DOES turn hot/dead pixels into recurring false detections (e.g.
|
|
625
|
+
the brightfield near-noise-floor hot pixels this camera can produce). Kept and
|
|
626
|
+
wired, low-risk (baseline untouched); expect little effect on clean
|
|
627
|
+
fluorescence bead movies.
|
|
628
|
+
"""
|
|
629
|
+
from pycat.file_io.file_io import iter_frames
|
|
630
|
+
# Streaming mean/variance (Welford) so we never hold the whole stack.
|
|
631
|
+
mean = None
|
|
632
|
+
M2 = None
|
|
633
|
+
n = 0
|
|
634
|
+
for t, frame in iter_frames(bead_stack):
|
|
635
|
+
f = np.asarray(frame, dtype=np.float64)
|
|
636
|
+
f = np.squeeze(f)
|
|
637
|
+
if f.ndim != 2:
|
|
638
|
+
continue
|
|
639
|
+
if mean is None:
|
|
640
|
+
mean = np.zeros_like(f)
|
|
641
|
+
M2 = np.zeros_like(f)
|
|
642
|
+
n += 1
|
|
643
|
+
delta = f - mean
|
|
644
|
+
mean += delta / n
|
|
645
|
+
M2 += delta * (f - mean)
|
|
646
|
+
if max_frames is not None and n >= int(max_frames):
|
|
647
|
+
break
|
|
648
|
+
if mean is None or n < 5:
|
|
649
|
+
# Not enough frames to estimate — flag nothing.
|
|
650
|
+
return np.zeros((1, 1), dtype=bool) if mean is None else \
|
|
651
|
+
np.zeros_like(mean, dtype=bool)
|
|
652
|
+
tvar = M2 / max(n - 1, 1)
|
|
653
|
+
tstd = np.sqrt(np.maximum(tvar, 0.0))
|
|
654
|
+
tmean = mean
|
|
655
|
+
|
|
656
|
+
from scipy.ndimage import median_filter
|
|
657
|
+
local_bg = median_filter(tmean, size=int(median_size))
|
|
658
|
+
excess = tmean - local_bg
|
|
659
|
+
cv = tstd / np.maximum(tmean, 1.0)
|
|
660
|
+
|
|
661
|
+
anomalous = excess >= float(local_excess_min)
|
|
662
|
+
flat = (cv <= float(cv_max)) | (tstd <= float(tstd_max))
|
|
663
|
+
hot = anomalous & flat
|
|
664
|
+
return hot
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def dedup_detections_ring_merge(coords, frame, sigmas=None,
|
|
668
|
+
k_sigma=2.5, ring_dim_ratio=0.6,
|
|
669
|
+
base_radius_px=None):
|
|
670
|
+
"""Ring-merge deduplication (detection_variant='ring_merge').
|
|
671
|
+
|
|
672
|
+
⚠ STATUS: BUILT BUT NOT YET VALIDATED — NEEDS DATA WITH RESOLVED AIRY RINGS.
|
|
673
|
+
---------------------------------------------------------------------------
|
|
674
|
+
A/B comparison against baseline on Gable's 2026-07 bead data (100x/~1.2 NA,
|
|
675
|
+
0.67 µm/px, 200 nm beads) showed this variant is a near no-op there: the
|
|
676
|
+
beads are well-separated (median nearest-neighbour ~17.5 px, only ~4% within
|
|
677
|
+
5 px) and blob_log already returns ~one detection per bead, so there are
|
|
678
|
+
essentially no ring fragments to merge (it changed ~2 of ~2000 detections).
|
|
679
|
+
On THAT data the real detection-quality lever is hot-pixel rejection, not
|
|
680
|
+
ring-merge. This function is kept because the logic is sound and there is
|
|
681
|
+
almost certainly a use case — data with genuinely RESOLVED Airy rings that
|
|
682
|
+
fire as separate blobs (denser sampling, lower NA relative to bead size, or a
|
|
683
|
+
lower detection threshold that picks up ring shoulders). It is deliberately
|
|
684
|
+
NOT exposed in the VPT widget; wire it in and validate against such a dataset
|
|
685
|
+
(center+ring must collapse to ONE bead, two bright peaks must stay TWO)
|
|
686
|
+
before trusting/surfacing it. Reach it programmatically via
|
|
687
|
+
detect_beads_stack(..., detection_variant='ring_merge').
|
|
688
|
+
|
|
689
|
+
Improves on ``dedup_detections`` for large, non-diffraction-limited Airy-disk
|
|
690
|
+
beads, where blob_log fires on both the bright CENTRE and the dim Airy RING /
|
|
691
|
+
multi-scale shoulders of a single bead. Two corrections over the baseline:
|
|
692
|
+
|
|
693
|
+
1. **Self-scaling merge radius.** The merge radius is ``k_sigma × sigma`` of
|
|
694
|
+
the detected blob (not a fixed pixel count), so it tracks the imaged
|
|
695
|
+
footprint and stays correct under low NA / undersampling / astigmatism.
|
|
696
|
+
At 0.67 µm/px a 200 nm bead is sub-pixel, so keying off physical µm is
|
|
697
|
+
wrong — the detected blob sigma is the robust length scale.
|
|
698
|
+
|
|
699
|
+
2. **Merge only the DIM companion into the BRIGHT centre; keep two bright
|
|
700
|
+
peaks as two beads.** A ring fragment is always the DIM companion of a
|
|
701
|
+
bright peak (never itself bright+compact). So a neighbour is merged into a
|
|
702
|
+
kept centre only if it is DIM relative to that centre
|
|
703
|
+
(``neighbour_intensity ≤ ring_dim_ratio × centre_intensity``). If a nearby
|
|
704
|
+
detection is comparably BRIGHT, it is a second real bead and is kept —
|
|
705
|
+
trajectory linking resolves two genuinely-separate beads far better than
|
|
706
|
+
detection can, and collapsing them (as the baseline does) destroys a real
|
|
707
|
+
track. This is the key behavioural difference from ``dedup_detections``.
|
|
708
|
+
|
|
709
|
+
Parameters
|
|
710
|
+
----------
|
|
711
|
+
coords : list/array of (y, x).
|
|
712
|
+
frame : the image, used for local intensity of each detection.
|
|
713
|
+
sigmas : per-detection blob sigma (from blob_log column 3). If None, falls
|
|
714
|
+
back to ``base_radius_px`` (behaves like a fixed-radius dedup that still
|
|
715
|
+
respects the bright-vs-dim rule).
|
|
716
|
+
k_sigma : merge radius = k_sigma × sigma (default 2.5).
|
|
717
|
+
ring_dim_ratio : a neighbour is a mergeable ring fragment only if its local
|
|
718
|
+
intensity ≤ this fraction of the centre's (default 0.6). Higher = merges
|
|
719
|
+
more aggressively; lower = keeps more separate detections.
|
|
720
|
+
base_radius_px : fallback merge radius when sigmas is None.
|
|
721
|
+
|
|
722
|
+
Returns
|
|
723
|
+
-------
|
|
724
|
+
Filtered list of (y, x) — bright bead centres, with dim ring fragments folded
|
|
725
|
+
in and genuinely-separate bright beads preserved.
|
|
726
|
+
"""
|
|
727
|
+
if coords is None or len(coords) == 0:
|
|
728
|
+
return coords
|
|
729
|
+
from scipy.spatial import cKDTree
|
|
730
|
+
pts = np.asarray([(float(y), float(x)) for (y, x) in coords], dtype=float)
|
|
731
|
+
raw = np.asarray(frame, dtype=np.float32)
|
|
732
|
+
raw = np.squeeze(raw)
|
|
733
|
+
if raw.ndim != 2:
|
|
734
|
+
return coords
|
|
735
|
+
H, W = raw.shape
|
|
736
|
+
|
|
737
|
+
def local_intensity(y, x, r=2):
|
|
738
|
+
yi, xi = int(round(y)), int(round(x))
|
|
739
|
+
y0, y1 = max(0, yi - r), min(H, yi + r + 1)
|
|
740
|
+
x0, x1 = max(0, xi - r), min(W, xi + r + 1)
|
|
741
|
+
if y1 <= y0 or x1 <= x0:
|
|
742
|
+
return -np.inf
|
|
743
|
+
return float(raw[y0:y1, x0:x1].max())
|
|
744
|
+
|
|
745
|
+
inten = np.array([local_intensity(y, x) for (y, x) in pts])
|
|
746
|
+
# Per-detection merge radius (sigma-scaled, or fixed fallback).
|
|
747
|
+
if sigmas is not None and len(sigmas) == len(pts):
|
|
748
|
+
radii = np.maximum(1.0, float(k_sigma) * np.asarray(sigmas, dtype=float))
|
|
749
|
+
elif base_radius_px:
|
|
750
|
+
radii = np.full(len(pts), float(base_radius_px))
|
|
751
|
+
else:
|
|
752
|
+
# No sigma and no fallback → nothing principled to merge on; keep all.
|
|
753
|
+
return [tuple(p) for p in pts]
|
|
754
|
+
|
|
755
|
+
tree = cKDTree(pts)
|
|
756
|
+
order = np.argsort(-inten) # brightest first
|
|
757
|
+
used = np.zeros(len(pts), dtype=bool)
|
|
758
|
+
kept = []
|
|
759
|
+
for idx in order:
|
|
760
|
+
if used[idx]:
|
|
761
|
+
continue
|
|
762
|
+
kept.append(idx)
|
|
763
|
+
centre_I = inten[idx]
|
|
764
|
+
# Query within this centre's radius; fold in only DIM neighbours.
|
|
765
|
+
neighbours = tree.query_ball_point(pts[idx], r=float(radii[idx]))
|
|
766
|
+
for n in neighbours:
|
|
767
|
+
if n == idx or used[n]:
|
|
768
|
+
continue
|
|
769
|
+
# Merge only if the neighbour is a DIM ring fragment of this centre.
|
|
770
|
+
# A comparably-bright neighbour is a second real bead → leave it for
|
|
771
|
+
# its own turn in the brightness-ordered loop (kept separately).
|
|
772
|
+
if inten[n] <= ring_dim_ratio * centre_I:
|
|
773
|
+
used[n] = True
|
|
774
|
+
used[idx] = True
|
|
775
|
+
kept.sort()
|
|
776
|
+
return [tuple(pts[i]) for i in kept]
|
|
777
|
+
|
|
778
|
+
|
|
564
779
|
def dedup_detections(coords, frame, merge_radius_px, keep='brightest'):
|
|
565
780
|
"""Merge detections that fall within merge_radius_px of one another, keeping
|
|
566
781
|
a single representative per cluster. blob_log can fire multiple times on one
|
|
@@ -689,11 +904,19 @@ def classify_beads(beads_df: pd.DataFrame,
|
|
|
689
904
|
aggregate_intensity_factor: float = 1.6,
|
|
690
905
|
defocus_r2_max: float = 0.85,
|
|
691
906
|
sigma_outlier_factor: float = 1.5,
|
|
692
|
-
strictness: float = 1.0
|
|
907
|
+
strictness: float = 1.0,
|
|
908
|
+
variant: str = 'baseline') -> pd.DataFrame:
|
|
693
909
|
"""
|
|
694
910
|
Classify fitted beads into singlet / aggregate / out-of-plane using the
|
|
695
911
|
2D-Gaussian quality metrics.
|
|
696
912
|
|
|
913
|
+
DETECTION-VARIANT STAGING (``variant``): 'baseline' is the 1.5.329-validated
|
|
914
|
+
classifier and is the default — it is never changed, so the validated
|
|
915
|
+
~8.325-through-TrackMate path stays selectable and a revert is a one-arg
|
|
916
|
+
change. New variants are opt-in and additive, each implemented as its own
|
|
917
|
+
branch so they can be A/B-compared against baseline on the same detections
|
|
918
|
+
without touching the baseline code path. See ``_classify_variant_*`` helpers.
|
|
919
|
+
|
|
697
920
|
The discriminating physics:
|
|
698
921
|
- A singlet has a characteristic PSF width (sigma) and integrated
|
|
699
922
|
intensity — the population modes.
|
|
@@ -770,6 +993,20 @@ def classify_beads(beads_df: pd.DataFrame,
|
|
|
770
993
|
NCC_FLOOR = 0.55
|
|
771
994
|
is_real = np.isfinite(ncc) & (ncc >= NCC_FLOOR)
|
|
772
995
|
|
|
996
|
+
# Hot-pixel reject variant: on a FIXED sensor hot pixel, apply a HARSHER
|
|
997
|
+
# acceptance test instead of a flat veto — a real bead can drift over a
|
|
998
|
+
# hot/dead pixel and must still be accepted if it brings genuine template
|
|
999
|
+
# evidence. A bare hot pixel is a flat/spiky single pixel that matches the
|
|
1000
|
+
# bead PSF template poorly (low NCC), so a raised NCC floor on suspect
|
|
1001
|
+
# pixels drops the naked hot pixel while a bead passing over (high NCC)
|
|
1002
|
+
# survives. Baseline is untouched (no 'on_hot_pixel' column there).
|
|
1003
|
+
if variant == 'hot_pixel_reject' and 'on_hot_pixel' in df.columns:
|
|
1004
|
+
HOT_NCC_FLOOR = 0.75 # stricter than the 0.55 baseline floor
|
|
1005
|
+
on_hot = df['on_hot_pixel'].fillna(False).to_numpy(dtype=bool)
|
|
1006
|
+
# A detection on a hot pixel must clear the higher floor to be real.
|
|
1007
|
+
harsh_ok = ~on_hot | (np.isfinite(ncc) & (ncc >= HOT_NCC_FLOOR))
|
|
1008
|
+
is_real = is_real & harsh_ok
|
|
1009
|
+
|
|
773
1010
|
# References computed over REAL beads only (so garbage doesn't skew them).
|
|
774
1011
|
rii = ii[is_real & np.isfinite(ii)]
|
|
775
1012
|
ramp = amp[is_real & np.isfinite(amp)]
|
|
@@ -1077,6 +1314,7 @@ def detect_beads_stack(
|
|
|
1077
1314
|
n_workers: Optional[int] = None,
|
|
1078
1315
|
strictness: float = 1.0,
|
|
1079
1316
|
use_gpu: str = 'auto',
|
|
1317
|
+
detection_variant: str = 'baseline',
|
|
1080
1318
|
) -> pd.DataFrame:
|
|
1081
1319
|
"""
|
|
1082
1320
|
Detect beads across all frames of a (T, H, W) stack.
|
|
@@ -1181,8 +1419,28 @@ def detect_beads_stack(
|
|
|
1181
1419
|
except Exception:
|
|
1182
1420
|
gpu_on = False
|
|
1183
1421
|
|
|
1422
|
+
_variant = (detection_variant or 'baseline').lower()
|
|
1423
|
+
# Hot-pixel reject: build the fixed-sensor-pixel mask ONCE from the stack's
|
|
1424
|
+
# temporal statistics (scene-independent), then drop detections landing on
|
|
1425
|
+
# those pixels. Needs all frames, so it's a stack-level pre-pass.
|
|
1426
|
+
_hot_mask = None
|
|
1427
|
+
if _variant == 'hot_pixel_reject':
|
|
1428
|
+
try:
|
|
1429
|
+
_hot_mask = build_hot_pixel_mask(bead_stack)
|
|
1430
|
+
_n_hot = int(_hot_mask.sum()) if _hot_mask is not None else 0
|
|
1431
|
+
if progress_callback is None:
|
|
1432
|
+
print(f"[PyCAT VPT] hot_pixel_reject: flagged {_n_hot} fixed "
|
|
1433
|
+
f"sensor pixels from temporal statistics.")
|
|
1434
|
+
except Exception as _e:
|
|
1435
|
+
print(f"[PyCAT VPT] hot-pixel mask failed ({_e}); proceeding without.")
|
|
1436
|
+
_hot_mask = None
|
|
1184
1437
|
precomputed_coords = None
|
|
1185
|
-
|
|
1438
|
+
# Ring-merge needs per-detection sigma from blob_log, which the parallel
|
|
1439
|
+
# worker path doesn't carry, so run detection serially for that variant.
|
|
1440
|
+
# Hot-pixel reject also runs serially (it filters coords against the mask).
|
|
1441
|
+
if quality_mode == 'fast' and not gpu_on \
|
|
1442
|
+
and _variant not in ('ring_merge', 'hot_pixel_reject') \
|
|
1443
|
+
and parallel in ('auto', 'cpu', 'process'):
|
|
1186
1444
|
src_desc = _bead_source_descriptor(bead_stack)
|
|
1187
1445
|
try:
|
|
1188
1446
|
import os as _os
|
|
@@ -1236,14 +1494,24 @@ def detect_beads_stack(
|
|
|
1236
1494
|
# template building + scoring (both cheap).
|
|
1237
1495
|
coords = precomputed_coords[t]
|
|
1238
1496
|
else:
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
coords =
|
|
1497
|
+
if _variant == 'ring_merge':
|
|
1498
|
+
coords, _sig = detect_beads_frame(
|
|
1499
|
+
frame, min_sigma=min_sigma, max_sigma=max_sigma,
|
|
1500
|
+
num_sigma=num_sigma, threshold=threshold,
|
|
1501
|
+
host_mask=host_mask, use_gpu=gpu_on, return_sigma=True)
|
|
1502
|
+
# Ring-merge: sigma-scaled radius, merge dim ring fragments
|
|
1503
|
+
# into bright centres, keep two bright beads as two.
|
|
1504
|
+
coords = dedup_detections_ring_merge(
|
|
1505
|
+
coords, frame, sigmas=_sig,
|
|
1506
|
+
base_radius_px=merge_radius_px)
|
|
1507
|
+
else:
|
|
1508
|
+
coords = detect_beads_frame(
|
|
1509
|
+
frame, min_sigma=min_sigma, max_sigma=max_sigma,
|
|
1510
|
+
num_sigma=num_sigma, threshold=threshold,
|
|
1511
|
+
host_mask=host_mask, use_gpu=gpu_on)
|
|
1512
|
+
# Baseline de-dup (fixed radius, keep brightest per cluster).
|
|
1513
|
+
if merge_radius_px:
|
|
1514
|
+
coords = dedup_detections(coords, frame, merge_radius_px)
|
|
1247
1515
|
if template_z is None or template_mode == 'per_frame':
|
|
1248
1516
|
if template_type == 'airy':
|
|
1249
1517
|
# Analytic Airy (Bessel J1) template — matches beads that
|
|
@@ -1256,14 +1524,23 @@ def detect_beads_stack(
|
|
|
1256
1524
|
scored = score_beads_template(frame, coords, template_z,
|
|
1257
1525
|
half=half, subpixel=subpixel)
|
|
1258
1526
|
for i, b in enumerate(scored):
|
|
1259
|
-
|
|
1527
|
+
_row = {
|
|
1260
1528
|
'frame': t, 'object_id': i,
|
|
1261
1529
|
'y_um': float(b['y']) * microns_per_pixel,
|
|
1262
1530
|
'x_um': float(b['x']) * microns_per_pixel,
|
|
1263
1531
|
'area_um2': nominal_area,
|
|
1264
1532
|
'ncc': b['ncc'], 'snr': b['snr'], 'symmetry': b['symmetry'],
|
|
1265
1533
|
'amplitude': b['amplitude'],
|
|
1266
|
-
'integrated_intensity': b['integrated_intensity']}
|
|
1534
|
+
'integrated_intensity': b['integrated_intensity']}
|
|
1535
|
+
# Flag detections sitting on a fixed sensor hot pixel so the
|
|
1536
|
+
# classifier can apply a HARSHER acceptance test there (not a flat
|
|
1537
|
+
# reject — a real bead can drift over a hot/dead pixel and should
|
|
1538
|
+
# still be accepted if it brings real PSF/template evidence).
|
|
1539
|
+
if _hot_mask is not None:
|
|
1540
|
+
yi = int(round(b['y'])); xi = int(round(b['x']))
|
|
1541
|
+
if 0 <= yi < _hot_mask.shape[0] and 0 <= xi < _hot_mask.shape[1]:
|
|
1542
|
+
_row['on_hot_pixel'] = bool(_hot_mask[yi, xi])
|
|
1543
|
+
rows.append(_row)
|
|
1267
1544
|
else:
|
|
1268
1545
|
beads = detect_beads_frame(
|
|
1269
1546
|
frame, min_sigma=min_sigma, max_sigma=max_sigma,
|
|
@@ -1308,7 +1585,13 @@ def detect_beads_stack(
|
|
|
1308
1585
|
return pd.DataFrame(columns=cols)
|
|
1309
1586
|
|
|
1310
1587
|
df = pd.DataFrame(rows)
|
|
1311
|
-
|
|
1588
|
+
# ── Detection-variant staging ────────────────────────────────────────────
|
|
1589
|
+
# 'baseline' = the 1.5.329-validated classifier (recovers ~8.325 through
|
|
1590
|
+
# TrackMate). New variants are OPT-IN and additive so the validated path
|
|
1591
|
+
# stays selectable and a revert is a clean single-arg change. The variant is
|
|
1592
|
+
# recorded on the frame for auditability and downstream comparison.
|
|
1593
|
+
df = classify_beads(df, strictness=strictness, variant=_variant)
|
|
1594
|
+
df.attrs['detection_variant'] = _variant
|
|
1312
1595
|
|
|
1313
1596
|
if exclude_aggregates:
|
|
1314
1597
|
df = df[df['bead_class'] != 'aggregate'].reset_index(drop=True)
|
|
@@ -1770,3 +2053,70 @@ def _link(detections, linker, max_dist_um, max_gap, mpp, progress_callback=None)
|
|
|
1770
2053
|
progress_callback=progress_callback)
|
|
1771
2054
|
from pycat.toolbox.dynamic_spatial_tools import link_trajectories
|
|
1772
2055
|
return link_trajectories(detections, max_dist_um, max_gap)
|
|
2056
|
+
|
|
2057
|
+
|
|
2058
|
+
def compare_detection_variants(
|
|
2059
|
+
bead_stack,
|
|
2060
|
+
variant_a: str = 'baseline',
|
|
2061
|
+
variant_b: str = 'baseline',
|
|
2062
|
+
microns_per_pixel: float = 1.0,
|
|
2063
|
+
max_frames: Optional[int] = None,
|
|
2064
|
+
**detect_kwargs,
|
|
2065
|
+
) -> dict:
|
|
2066
|
+
"""
|
|
2067
|
+
Run bead detection under TWO variants on the SAME stack and report how the
|
|
2068
|
+
detections/classifications differ. This is the staging safety net for the
|
|
2069
|
+
detection rework: every proposed change is measured against the
|
|
2070
|
+
1.5.329-validated baseline before it is trusted, and a regression is visible
|
|
2071
|
+
immediately rather than only surfacing in the final viscosity.
|
|
2072
|
+
|
|
2073
|
+
Parameters
|
|
2074
|
+
----------
|
|
2075
|
+
bead_stack : (T, H, W) stack (lazy or array).
|
|
2076
|
+
variant_a, variant_b : detection_variant names to compare (default both
|
|
2077
|
+
'baseline'; pass e.g. variant_b='ring_merge' to A/B a new variant).
|
|
2078
|
+
microns_per_pixel : pixel size (passed through to detection).
|
|
2079
|
+
max_frames : cap the number of frames for a fast comparison (None = all).
|
|
2080
|
+
**detect_kwargs : forwarded to detect_beads_stack (quality_mode, parallel…).
|
|
2081
|
+
|
|
2082
|
+
Returns
|
|
2083
|
+
-------
|
|
2084
|
+
dict with:
|
|
2085
|
+
counts_a / counts_b : total detections per variant
|
|
2086
|
+
class_counts_a / _b : bead_class value-counts per variant
|
|
2087
|
+
n_frames : frames compared
|
|
2088
|
+
summary : human-readable one-line diff
|
|
2089
|
+
det_a / det_b : the two detection DataFrames (for deeper analysis)
|
|
2090
|
+
"""
|
|
2091
|
+
import numpy as np
|
|
2092
|
+
frame_indices = None
|
|
2093
|
+
if max_frames is not None:
|
|
2094
|
+
try:
|
|
2095
|
+
T = int(np.asarray(bead_stack).shape[0]) if not hasattr(bead_stack, 'shape') \
|
|
2096
|
+
else int(bead_stack.shape[0])
|
|
2097
|
+
frame_indices = list(range(min(T, int(max_frames))))
|
|
2098
|
+
except Exception:
|
|
2099
|
+
frame_indices = None
|
|
2100
|
+
|
|
2101
|
+
det_a = detect_beads_stack(
|
|
2102
|
+
bead_stack, microns_per_pixel=microns_per_pixel,
|
|
2103
|
+
frame_indices=frame_indices, detection_variant=variant_a, **detect_kwargs)
|
|
2104
|
+
det_b = detect_beads_stack(
|
|
2105
|
+
bead_stack, microns_per_pixel=microns_per_pixel,
|
|
2106
|
+
frame_indices=frame_indices, detection_variant=variant_b, **detect_kwargs)
|
|
2107
|
+
|
|
2108
|
+
cc_a = (det_a['bead_class'].value_counts().to_dict()
|
|
2109
|
+
if 'bead_class' in det_a else {})
|
|
2110
|
+
cc_b = (det_b['bead_class'].value_counts().to_dict()
|
|
2111
|
+
if 'bead_class' in det_b else {})
|
|
2112
|
+
n_frames = int(det_a['frame'].nunique()) if 'frame' in det_a else 0
|
|
2113
|
+
|
|
2114
|
+
summary = (
|
|
2115
|
+
f"[{variant_a}] {len(det_a)} dets {cc_a} vs "
|
|
2116
|
+
f"[{variant_b}] {len(det_b)} dets {cc_b} over {n_frames} frames")
|
|
2117
|
+
|
|
2118
|
+
return dict(
|
|
2119
|
+
counts_a=len(det_a), counts_b=len(det_b),
|
|
2120
|
+
class_counts_a=cc_a, class_counts_b=cc_b,
|
|
2121
|
+
n_frames=n_frames, summary=summary,
|
|
2122
|
+
det_a=det_a, det_b=det_b)
|
|
@@ -825,6 +825,24 @@ class VideoParticleTrackingUI:
|
|
|
825
825
|
btn.clicked.connect(self._on_link)
|
|
826
826
|
form.addRow(self._track_prog); from pycat.ui.field_status import button_with_circle as _bwc
|
|
827
827
|
form.addRow(_bwc(btn))
|
|
828
|
+
|
|
829
|
+
# Embedded track-length histogram — shows the distribution of trajectory
|
|
830
|
+
# lengths (in frames) after linking. A healthy result has many long
|
|
831
|
+
# tracks (mass toward the right / spanning most of the movie); a
|
|
832
|
+
# fragmentation-prone linker shows a spike of very short tracks. This is
|
|
833
|
+
# useful every run, and doubles as an at-a-glance linker-quality check.
|
|
834
|
+
try:
|
|
835
|
+
from matplotlib.figure import Figure
|
|
836
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
|
837
|
+
self._tracklen_fig = Figure(figsize=(3.2, 1.7), tight_layout=True)
|
|
838
|
+
self._tracklen_canvas = FigureCanvasQTAgg(self._tracklen_fig)
|
|
839
|
+
self._tracklen_canvas.setMinimumHeight(150)
|
|
840
|
+
self._tracklen_canvas.setVisible(False) # shown after first link
|
|
841
|
+
form.addRow(self._tracklen_canvas)
|
|
842
|
+
except Exception:
|
|
843
|
+
self._tracklen_fig = None
|
|
844
|
+
self._tracklen_canvas = None
|
|
845
|
+
|
|
828
846
|
layout.addWidget(grp)
|
|
829
847
|
|
|
830
848
|
def _linker_name(self):
|
|
@@ -974,6 +992,7 @@ class VideoParticleTrackingUI:
|
|
|
974
992
|
f"(drift-corrected) from the {pop_which} population.")
|
|
975
993
|
if n_agg_tracks:
|
|
976
994
|
msg += f" Aggregate population: {n_agg_tracks} tracks."
|
|
995
|
+
self._update_tracklen_hist(tracks)
|
|
977
996
|
napari_show_info(msg)
|
|
978
997
|
def _err(msg):
|
|
979
998
|
self._track_prog.setVisible(False)
|
|
@@ -982,6 +1001,46 @@ class VideoParticleTrackingUI:
|
|
|
982
1001
|
w.progress.connect(lambda i, n: self._track_prog.setValue(i))
|
|
983
1002
|
self._track_worker = w; w.start()
|
|
984
1003
|
|
|
1004
|
+
def _update_tracklen_hist(self, tracks):
|
|
1005
|
+
"""Draw the track-length (frames-per-track) histogram in the linker
|
|
1006
|
+
widget. A healthy link has many long tracks; a fragmentation-prone
|
|
1007
|
+
linker piles up very short ones. Called after each link."""
|
|
1008
|
+
if getattr(self, '_tracklen_canvas', None) is None \
|
|
1009
|
+
or getattr(self, '_tracklen_fig', None) is None:
|
|
1010
|
+
return
|
|
1011
|
+
try:
|
|
1012
|
+
if tracks is None or 'track_id' not in tracks or tracks.empty:
|
|
1013
|
+
return
|
|
1014
|
+
lengths = tracks.groupby('track_id').size().values
|
|
1015
|
+
n_frames = int(tracks['frame'].nunique()) if 'frame' in tracks else 0
|
|
1016
|
+
fig = self._tracklen_fig
|
|
1017
|
+
fig.clear()
|
|
1018
|
+
ax = fig.add_subplot(111)
|
|
1019
|
+
import numpy as _np
|
|
1020
|
+
Lmax = int(lengths.max()) if len(lengths) else 1
|
|
1021
|
+
nbins = int(_np.clip(Lmax, 5, 40))
|
|
1022
|
+
ax.hist(lengths, bins=nbins, color='#4c72b0',
|
|
1023
|
+
edgecolor='#2b4a72', linewidth=0.4)
|
|
1024
|
+
med = float(_np.median(lengths)) if len(lengths) else 0.0
|
|
1025
|
+
ax.axvline(med, color='#c44e52', ls='--', lw=1,
|
|
1026
|
+
label=f"median {med:.0f}")
|
|
1027
|
+
# Fraction of tracks spanning most of the movie — the quality signal.
|
|
1028
|
+
if n_frames > 0:
|
|
1029
|
+
frac_long = float(_np.mean(lengths >= 0.5 * n_frames))
|
|
1030
|
+
ax.set_title(
|
|
1031
|
+
f"{len(lengths)} tracks · {frac_long*100:.0f}% span ≥½ movie",
|
|
1032
|
+
fontsize=8)
|
|
1033
|
+
else:
|
|
1034
|
+
ax.set_title(f"{len(lengths)} tracks", fontsize=8)
|
|
1035
|
+
ax.set_xlabel("track length (frames)", fontsize=8)
|
|
1036
|
+
ax.set_ylabel("count", fontsize=8)
|
|
1037
|
+
ax.tick_params(labelsize=7)
|
|
1038
|
+
ax.legend(fontsize=7, frameon=False)
|
|
1039
|
+
self._tracklen_canvas.setVisible(True)
|
|
1040
|
+
self._tracklen_canvas.draw_idle()
|
|
1041
|
+
except Exception as _e:
|
|
1042
|
+
print(f"[PyCAT VPT] track-length histogram skipped: {_e}")
|
|
1043
|
+
|
|
985
1044
|
# ── Step 5: microrheology ──────────────────────────────────────────
|
|
986
1045
|
def _add_microrheology(self, layout):
|
|
987
1046
|
grp = QGroupBox("Step 5 — Microrheology (MSD → Viscosity)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pycat_napari-1.5.332 → pycat_napari-1.5.333}/config/pycat-devbio-napari-env-x86-windows.yaml
RENAMED
|
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
|
|
File without changes
|