AutoMorphoTrack 2.4.0__tar.gz → 2.5.0__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.
Files changed (126) hide show
  1. automorphotrack-2.5.0/CITATION.cff +12 -0
  2. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/MANIFEST.in +2 -0
  3. automorphotrack-2.5.0/PKG-INFO +112 -0
  4. automorphotrack-2.5.0/README.md +68 -0
  5. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/__init__.py +1 -1
  6. automorphotrack-2.5.0/automorphotrack/refined/__init__.py +8 -0
  7. automorphotrack-2.5.0/automorphotrack/refined/__main__.py +20 -0
  8. automorphotrack-2.5.0/automorphotrack/refined/pipeline.py +134 -0
  9. automorphotrack-2.5.0/automorphotrack/refined/segmentation.py +85 -0
  10. automorphotrack-2.5.0/automorphotrack/refined/tracking.py +93 -0
  11. automorphotrack-2.5.0/automorphotrack.egg-info/PKG-INFO +112 -0
  12. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack.egg-info/SOURCES.txt +35 -1
  13. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack.egg-info/entry_points.txt +2 -0
  14. automorphotrack-2.5.0/docs/PARTICLE_VALIDATION.md +15 -0
  15. automorphotrack-2.5.0/examples/Refined_Pipeline.ipynb +71 -0
  16. automorphotrack-2.5.0/examples/refined_verified.py +52 -0
  17. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/pyproject.toml +3 -1
  18. automorphotrack-2.5.0/tests/test_refined.py +141 -0
  19. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/core_manifest.json +2 -2
  20. automorphotrack-2.5.0/validation/platform_check.py +108 -0
  21. automorphotrack-2.5.0/validation/refined_manifest.json +71 -0
  22. automorphotrack-2.5.0/validation/refined_motion.ome.tif +0 -0
  23. automorphotrack-2.5.0/validation/refined_motion_config.json +28 -0
  24. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_labels.tif +0 -0
  25. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_motility.csv +3 -0
  26. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_motility_per_frame.csv +6 -0
  27. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_raw_labels.tif +0 -0
  28. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_raw_tracks.csv +11 -0
  29. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_shapes.csv +11 -0
  30. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_steps.csv +11 -0
  31. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_thresholds.csv +6 -0
  32. automorphotrack-2.5.0/validation/refined_motion_expected/channel_0_tracks.csv +11 -0
  33. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_labels.tif +0 -0
  34. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_motility.csv +3 -0
  35. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_motility_per_frame.csv +6 -0
  36. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_raw_labels.tif +0 -0
  37. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_raw_tracks.csv +11 -0
  38. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_shapes.csv +11 -0
  39. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_steps.csv +11 -0
  40. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_thresholds.csv +6 -0
  41. automorphotrack-2.5.0/validation/refined_motion_expected/channel_1_tracks.csv +11 -0
  42. automorphotrack-2.5.0/validation/refined_motion_expected/colocalization.csv +6 -0
  43. automorphotrack-2.5.0/validation/refined_motion_expected/run_manifest.json +99 -0
  44. automorphotrack-2.4.0/PKG-INFO +0 -93
  45. automorphotrack-2.4.0/README.md +0 -49
  46. automorphotrack-2.4.0/automorphotrack.egg-info/PKG-INFO +0 -93
  47. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/LICENSE.md +0 -0
  48. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/adaptive_segmentation.py +0 -0
  49. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/benchmarking.py +0 -0
  50. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/cli.py +0 -0
  51. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/colocalization.py +0 -0
  52. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/detection.py +0 -0
  53. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/lyso_count.py +0 -0
  54. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/mcp_server.py +0 -0
  55. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/morphology.py +0 -0
  56. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/motility.py +0 -0
  57. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/napari.yaml +0 -0
  58. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/napari_plugin.py +0 -0
  59. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/network_analysis.py +0 -0
  60. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/shape_features.py +0 -0
  61. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/shape_profiling.py +0 -0
  62. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/spatial_statistics.py +0 -0
  63. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/summary.py +0 -0
  64. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/temporal_dynamics.py +0 -0
  65. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/tracking.py +0 -0
  66. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/tracking_overlay.py +0 -0
  67. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/utils.py +0 -0
  68. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/__init__.py +0 -0
  69. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/__main__.py +0 -0
  70. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/colocalization.py +0 -0
  71. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/config.py +0 -0
  72. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/core.py +0 -0
  73. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/detection.py +0 -0
  74. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/io.py +0 -0
  75. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/lyso_count.py +0 -0
  76. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/morphology.py +0 -0
  77. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/motility.py +0 -0
  78. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/pipeline.py +0 -0
  79. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/plotting.py +0 -0
  80. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/shape_features.py +0 -0
  81. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/shape_profiling.py +0 -0
  82. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/summary.py +0 -0
  83. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/tracking.py +0 -0
  84. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/tracking_overlay.py +0 -0
  85. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validated/utils.py +0 -0
  86. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack/validation.py +0 -0
  87. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack.egg-info/dependency_links.txt +0 -0
  88. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack.egg-info/requires.txt +0 -0
  89. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/automorphotrack.egg-info/top_level.txt +0 -0
  90. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/docs/GUI.md +0 -0
  91. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/docs/MCP.md +0 -0
  92. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/docs/V2_INTERFACE.md +0 -0
  93. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/docs/VALIDATED_CORE.md +0 -0
  94. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/docs/reviewer_concerns.md +0 -0
  95. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/setup.cfg +0 -0
  96. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/setup.py +0 -0
  97. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/__init__.py +0 -0
  98. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/test_benchmarking.py +0 -0
  99. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/test_gui_import.py +0 -0
  100. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/test_install_and_imports.py +0 -0
  101. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/test_validated_revision.py +0 -0
  102. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/tests/test_validation_and_segmentation.py +0 -0
  103. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/api_reconciliation.json +0 -0
  104. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/check_installed_core.py +0 -0
  105. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion.ome.tif +0 -0
  106. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_config.json +0 -0
  107. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Colocalization.csv +0 -0
  108. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Frame_Correlation_N.csv +0 -0
  109. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Frame_Descriptive_Spearman.csv +0 -0
  110. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Frame_Summary.csv +0 -0
  111. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_InstanceLabels.tif +0 -0
  112. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_Masks.tif +0 -0
  113. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_MotilitySteps.csv +0 -0
  114. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_MotilitySummary.csv +0 -0
  115. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_ShapeMetrics.csv +0 -0
  116. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Lyso_Tracks.csv +0 -0
  117. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_InstanceLabels.tif +0 -0
  118. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_Masks.tif +0 -0
  119. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_MotilitySteps.csv +0 -0
  120. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_MotilitySummary.csv +0 -0
  121. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_ShapeMetrics.csv +0 -0
  122. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Mito_Tracks.csv +0 -0
  123. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Motility_PerFrame.csv +0 -0
  124. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Sample_Summary.csv +0 -0
  125. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_expected/Thresholds.csv +0 -0
  126. {automorphotrack-2.4.0 → automorphotrack-2.5.0}/validation/known_motion_truth.json +0 -0
@@ -0,0 +1,12 @@
1
+ cff-version: 1.2.0
2
+ message: "Cite this release for the particle revision and reproduction materials."
3
+ type: software
4
+ title: "AutoMorphoTrack 2.5.0: particle refinement and manuscript revision"
5
+ authors:
6
+ - family-names: Bayati
7
+ given-names: Armin
8
+ version: 2.5.0
9
+ date-released: 2026-11-14
10
+ repository-code: "https://github.com/abayatibrain/AutoMorphoTrack"
11
+ url: "https://github.com/abayatibrain/AutoMorphoTrack/releases/tag/v2.5.0"
12
+ license: MIT
@@ -1,6 +1,8 @@
1
1
  include README.md
2
2
  include LICENSE.md
3
3
  include pyproject.toml
4
+ include CITATION.cff
5
+ recursive-include examples *.py *.ipynb
4
6
  recursive-include automorphotrack *.py
5
7
 
6
8
  recursive-include validation *.json *.py *.tif *.csv
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: automorphotrack
3
+ Version: 2.5.0
4
+ Summary: Automated pipeline for mitochondrial and lysosomal detection, tracking, morphology, and colocalization analysis in microscopy images.
5
+ Author-email: "Armin Bayati, Ph.D." <a.bayati.brain@gmail.com>
6
+ License: MIT
7
+ Project-URL: Documentation, https://github.com/abayatibrain/AutoMorphoTrack
8
+ Project-URL: Source, https://github.com/abayatibrain/AutoMorphoTrack
9
+ Project-URL: Tracker, https://github.com/abayatibrain/AutoMorphoTrack/issues
10
+ Keywords: image-processing,microscopy,organelle-tracking,mitochondria,lysosomes,colocalization,live-cell-imaging
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
18
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE.md
22
+ Requires-Dist: numpy>=2.0
23
+ Requires-Dist: pandas>=2.2
24
+ Requires-Dist: matplotlib>=3.6.0
25
+ Requires-Dist: seaborn>=0.12.0
26
+ Requires-Dist: opencv-python>=4.6.0
27
+ Requires-Dist: scikit-image>=0.25
28
+ Requires-Dist: scipy>=1.13
29
+ Requires-Dist: tifffile>=2024.8
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
32
+ Provides-Extra: compressed-tiff
33
+ Requires-Dist: imagecodecs>=2024.1; extra == "compressed-tiff"
34
+ Provides-Extra: napari
35
+ Requires-Dist: napari>=0.4.18; extra == "napari"
36
+ Requires-Dist: magicgui>=0.7.0; extra == "napari"
37
+ Requires-Dist: qtpy>=2.3.0; extra == "napari"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.0; extra == "dev"
40
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
41
+ Requires-Dist: build>=1.0; extra == "dev"
42
+ Requires-Dist: twine>=4.0; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # AutoMorphoTrack 2.5.0
46
+
47
+ AutoMorphoTrack 2.5.0 requires Python 3.11 or later and has two explicitly versioned analysis routes. The automorphotrack.validated namespace conserves the 18 modules of core 1.1.0rc2. The automorphotrack.refined namespace includes five additional modules that provide particle detection and association, described below, and make use of the unchanged baseline measurement modules. NumPy, pandas, SciPy, scikit-image, and tifffile are the main dependencies. The two implementations are distinguishable by their source, and examples show which route produced a given result.
48
+
49
+ ## Installation
50
+
51
+ ```sh
52
+ python -m pip install "automorphotrack==2.5.0"
53
+ ```
54
+
55
+ Minimal Debian and Ubuntu systems may need libxcb1, libgl1, and libglib2.0-0 for the OpenCV import. The GitHub CI matrix completed wheel and source installations on Windows, Linux, and macOS with Python 3.11 and 3.12, and retained test reports, dependency versions, source checks, and known-motion outputs.
56
+
57
+ ## Particle pipeline
58
+
59
+ ```python
60
+ from automorphotrack.refined import RefinedConfig, run_pipeline
61
+ config = RefinedConfig(channels=(0, 1), sample_id="recording")
62
+ result = run_pipeline("recording.ome.tif", "new_results", config)
63
+ ```
64
+
65
+ ```sh
66
+ amt-refined recording.ome.tif --out new_results --config configuration.json
67
+ ```
68
+
69
+ The particle example uses a two-channel synthetic movie with known motion. Run the guarded adapter from the release directory after installing the 2.5.0 wheel. It checks the 18 baseline and five particle modules before importing the numerical implementation. Use a new output directory for every run. For a static image, set min_observations to 1. Particle support masks have not been validated to be mitochondrial membrane boundaries. The default setting requires three observations, thus short-lived events will not be retained and retained masks and motion summaries will not reflect these. Raw outputs preserve these events.
70
+
71
+ The input must identify time, channel, and spatial axes. Calibration requires a recorded source; otherwise motion remains in pixels per frame. Label masks and track IDs are linked through each frame's Label column. A different configuration requires its own evaluation.
72
+
73
+ ## Recorded validation
74
+
75
+ Six reserved movies contain 145 annotated trajectories and 2,764 adjacent reference links. Mean accepted-link recall for the frozen default and fixed top-hat configurations was 0.014 and 0.212, respectively. With the particle pipeline, mean accepted-link recall was 0.696, and mean localization recall was 0.768. The new pipeline recovered 2,024 accepted reference links, and four additional accepted links related to different annotated identities, giving conditional precision of 2,024/2,028 among the links with two reference-matched endpoints. Remaining predictions were left unverified. Thirteen trajectories were completely missed, and 33 were fully recovered with accepted links, compared to 41 missed and two fully recovered with the top-hat configuration. Accepted-link recall was 0.739 with a 5-pixel tolerance and 0.587 with a 2-pixel tolerance. Individual reserved movies yielded accepted-link recall from 0.408 to 0.911. Supplementary Fig. 10 and Table 9 show the paired comparisons and their shortcomings.
76
+
77
+ The original 50 synthetic sequences, when rerun, yielded mean Dice scores of 0.877, 0.752 and 0.532, for the new particle masks, the fixed top-hat configuration and the frozen default, respectively. Mean instance precision and accepted-link recall increased from 0.407 to 0.966 and 0.178 to 0.965, respectively. The two baselines reproduced their earlier results to within an absolute error of 10^-12. Transfer to the external mitochondrial masks was limited. Without the length filter, the selected detector yielded mean Dice scores of 0.369 and 0.060 for the seven MoDL SIM patches and the 13 Myofibril patches, respectively. The MoDL score was still below the earlier top-hat result of 0.421. The particle pipeline thus does not replace target-specific segmentation validation for elongated or densely packed mitochondria.
78
+
79
+ AutoMorphoTrack 2.5.0 passed 72 tests in each of 12 GitHub CI installations. It built wheels and source distributions for Windows, Linux, and macOS, with Python 3.11 and 3.12. All installations kept the 18 baseline and five particle source modules. Each test compared 19 baseline and 19 particle CSV or TIFFs with recorded references. Integer masks were exact, and for baseline and particle outputs, numerical tolerances were 10^-12 and 10^-9, respectively. The test failed initially on Windows due to Git line-ending conversion which changed the source hashes. Enforcing LF line endings solved this, and the numerical code remained the same. The OpenCV import failure and its system-library remedy documented for minimal Linux remain documented.
80
+
81
+ Supplementary figures and tables are in the manuscript files attached to [release v2.5.0](https://github.com/abayatibrain/AutoMorphoTrack/releases/tag/v2.5.0). The [reproduction archive](https://github.com/abayatibrain/AutoMorphoTrack/releases/download/v2.5.0/AMT_Revision_Reproducibility_v2.5.0.zip) includes the development protocol, every candidate result, reserved-movie outputs, controls, and prior analyses. [The 12-job CI run](https://github.com/abayatibrain/AutoMorphoTrack/actions/runs/43602083124) records Windows, Linux, and macOS installations.
82
+
83
+ Biological source: [Richens et al., PLOS Biology 2025](https://doi.org/10.1371/journal.pbio.3003099), [data version 1](https://doi.org/10.17863/CAM.114338), CC BY 4.0. All 12 movies had been assessed with the baseline before six were reserved from the new parameter selection. This is a within-study split. Manual annotations cover a subset of vesicles; unmatched predictions remain unverified.
84
+
85
+ ## Reproducible integration
86
+
87
+ For the 2.5.0 route, Gemini wrote a new adapter and Codex ran the captured source against the installed wheel. The first adapter returned a file-hash mapping where the contract required a digest string. A focused change resolved that metadata error. The corrected adapter, the Codex adapter, a standalone command line, and an executed Jupyter notebook reproduced the 19 reference particle files exactly on the local host. Altered manifests and source files were rejected before analysis. The Codex guard verifies all 23 modules across both numerical namespaces. Prior to the requests, Argon High was selected in Gemini, but the interface changed its model label after the responses; the underlying model identity was not verified. These records test software integration, and numerical reproducibility, without constituting independent biological validation by either assistant.
88
+
89
+ The runnable adapter is `examples/refined_verified.py`. The notebook is `examples/Refined_Pipeline.ipynb`. Keep both source manifests under version control. Run a fixture after changing an adapter, configuration, preprocessing step, environment, or downstream calculation. Source integrity alone does not validate those changes.
90
+
91
+ ```sh
92
+ python -I examples/refined_verified.py --input validation/refined_motion.ome.tif --out new_fixture_results --config validation/refined_motion_config.json --validation-dir validation
93
+ ```
94
+
95
+ ## Baseline route
96
+
97
+ Install the pinned 2.5.0 wheel in a virtual environment. Use amt-refined, or import RefinedConfig and run_pipeline from automorphotrack.refined, for the particle pipeline. The amt-validated command retains the baseline implementation for Figures 1-17 and Supplementary Figures 1-9. Supplementary Figure 10 reports the new particle evaluation. The automorphotrack and amt commands remain unchanged. The compressed-tiff extra supplies a TIFF decoder.
98
+
99
+ ```sh
100
+ amt-validated recording.ome.tif --out baseline_results --config baseline_configuration.json
101
+ ```
102
+
103
+ | Implementation | Modules | Source digest |
104
+ | --- | ---: | --- |
105
+ | automorphotrack.validated, core 1.1.0rc2 | 18 | 8a4125a5ebe2b49e019f79b783503f31403a24ea3134d2d10b1bd4610ac3bce8 |
106
+ | automorphotrack.refined, package 2.5.0 | 5 | dcacbe6eede1cfef2cfa165418f8e0c9428aafebf3632a27a6a7655e3605f3a6 |
107
+
108
+ ## License and citation
109
+
110
+ Software: MIT. External datasets retain their individual licenses, recorded in the reproduction archive. See `CITATION.cff` for this release.
111
+
112
+ Bayati A, Schumacher JG, Chen X. AutoMorphoTrack. [eLife reviewed preprint 109936, version 1](https://doi.org/10.7554/eLife.109936.1). This is the older preprint, preceding the v2.5.0 particle revision. The revised manuscript is deposited with the current GitHub release.
@@ -0,0 +1,68 @@
1
+ # AutoMorphoTrack 2.5.0
2
+
3
+ AutoMorphoTrack 2.5.0 requires Python 3.11 or later and has two explicitly versioned analysis routes. The automorphotrack.validated namespace conserves the 18 modules of core 1.1.0rc2. The automorphotrack.refined namespace includes five additional modules that provide particle detection and association, described below, and make use of the unchanged baseline measurement modules. NumPy, pandas, SciPy, scikit-image, and tifffile are the main dependencies. The two implementations are distinguishable by their source, and examples show which route produced a given result.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ python -m pip install "automorphotrack==2.5.0"
9
+ ```
10
+
11
+ Minimal Debian and Ubuntu systems may need libxcb1, libgl1, and libglib2.0-0 for the OpenCV import. The GitHub CI matrix completed wheel and source installations on Windows, Linux, and macOS with Python 3.11 and 3.12, and retained test reports, dependency versions, source checks, and known-motion outputs.
12
+
13
+ ## Particle pipeline
14
+
15
+ ```python
16
+ from automorphotrack.refined import RefinedConfig, run_pipeline
17
+ config = RefinedConfig(channels=(0, 1), sample_id="recording")
18
+ result = run_pipeline("recording.ome.tif", "new_results", config)
19
+ ```
20
+
21
+ ```sh
22
+ amt-refined recording.ome.tif --out new_results --config configuration.json
23
+ ```
24
+
25
+ The particle example uses a two-channel synthetic movie with known motion. Run the guarded adapter from the release directory after installing the 2.5.0 wheel. It checks the 18 baseline and five particle modules before importing the numerical implementation. Use a new output directory for every run. For a static image, set min_observations to 1. Particle support masks have not been validated to be mitochondrial membrane boundaries. The default setting requires three observations, thus short-lived events will not be retained and retained masks and motion summaries will not reflect these. Raw outputs preserve these events.
26
+
27
+ The input must identify time, channel, and spatial axes. Calibration requires a recorded source; otherwise motion remains in pixels per frame. Label masks and track IDs are linked through each frame's Label column. A different configuration requires its own evaluation.
28
+
29
+ ## Recorded validation
30
+
31
+ Six reserved movies contain 145 annotated trajectories and 2,764 adjacent reference links. Mean accepted-link recall for the frozen default and fixed top-hat configurations was 0.014 and 0.212, respectively. With the particle pipeline, mean accepted-link recall was 0.696, and mean localization recall was 0.768. The new pipeline recovered 2,024 accepted reference links, and four additional accepted links related to different annotated identities, giving conditional precision of 2,024/2,028 among the links with two reference-matched endpoints. Remaining predictions were left unverified. Thirteen trajectories were completely missed, and 33 were fully recovered with accepted links, compared to 41 missed and two fully recovered with the top-hat configuration. Accepted-link recall was 0.739 with a 5-pixel tolerance and 0.587 with a 2-pixel tolerance. Individual reserved movies yielded accepted-link recall from 0.408 to 0.911. Supplementary Fig. 10 and Table 9 show the paired comparisons and their shortcomings.
32
+
33
+ The original 50 synthetic sequences, when rerun, yielded mean Dice scores of 0.877, 0.752 and 0.532, for the new particle masks, the fixed top-hat configuration and the frozen default, respectively. Mean instance precision and accepted-link recall increased from 0.407 to 0.966 and 0.178 to 0.965, respectively. The two baselines reproduced their earlier results to within an absolute error of 10^-12. Transfer to the external mitochondrial masks was limited. Without the length filter, the selected detector yielded mean Dice scores of 0.369 and 0.060 for the seven MoDL SIM patches and the 13 Myofibril patches, respectively. The MoDL score was still below the earlier top-hat result of 0.421. The particle pipeline thus does not replace target-specific segmentation validation for elongated or densely packed mitochondria.
34
+
35
+ AutoMorphoTrack 2.5.0 passed 72 tests in each of 12 GitHub CI installations. It built wheels and source distributions for Windows, Linux, and macOS, with Python 3.11 and 3.12. All installations kept the 18 baseline and five particle source modules. Each test compared 19 baseline and 19 particle CSV or TIFFs with recorded references. Integer masks were exact, and for baseline and particle outputs, numerical tolerances were 10^-12 and 10^-9, respectively. The test failed initially on Windows due to Git line-ending conversion which changed the source hashes. Enforcing LF line endings solved this, and the numerical code remained the same. The OpenCV import failure and its system-library remedy documented for minimal Linux remain documented.
36
+
37
+ Supplementary figures and tables are in the manuscript files attached to [release v2.5.0](https://github.com/abayatibrain/AutoMorphoTrack/releases/tag/v2.5.0). The [reproduction archive](https://github.com/abayatibrain/AutoMorphoTrack/releases/download/v2.5.0/AMT_Revision_Reproducibility_v2.5.0.zip) includes the development protocol, every candidate result, reserved-movie outputs, controls, and prior analyses. [The 12-job CI run](https://github.com/abayatibrain/AutoMorphoTrack/actions/runs/43602083124) records Windows, Linux, and macOS installations.
38
+
39
+ Biological source: [Richens et al., PLOS Biology 2025](https://doi.org/10.1371/journal.pbio.3003099), [data version 1](https://doi.org/10.17863/CAM.114338), CC BY 4.0. All 12 movies had been assessed with the baseline before six were reserved from the new parameter selection. This is a within-study split. Manual annotations cover a subset of vesicles; unmatched predictions remain unverified.
40
+
41
+ ## Reproducible integration
42
+
43
+ For the 2.5.0 route, Gemini wrote a new adapter and Codex ran the captured source against the installed wheel. The first adapter returned a file-hash mapping where the contract required a digest string. A focused change resolved that metadata error. The corrected adapter, the Codex adapter, a standalone command line, and an executed Jupyter notebook reproduced the 19 reference particle files exactly on the local host. Altered manifests and source files were rejected before analysis. The Codex guard verifies all 23 modules across both numerical namespaces. Prior to the requests, Argon High was selected in Gemini, but the interface changed its model label after the responses; the underlying model identity was not verified. These records test software integration, and numerical reproducibility, without constituting independent biological validation by either assistant.
44
+
45
+ The runnable adapter is `examples/refined_verified.py`. The notebook is `examples/Refined_Pipeline.ipynb`. Keep both source manifests under version control. Run a fixture after changing an adapter, configuration, preprocessing step, environment, or downstream calculation. Source integrity alone does not validate those changes.
46
+
47
+ ```sh
48
+ python -I examples/refined_verified.py --input validation/refined_motion.ome.tif --out new_fixture_results --config validation/refined_motion_config.json --validation-dir validation
49
+ ```
50
+
51
+ ## Baseline route
52
+
53
+ Install the pinned 2.5.0 wheel in a virtual environment. Use amt-refined, or import RefinedConfig and run_pipeline from automorphotrack.refined, for the particle pipeline. The amt-validated command retains the baseline implementation for Figures 1-17 and Supplementary Figures 1-9. Supplementary Figure 10 reports the new particle evaluation. The automorphotrack and amt commands remain unchanged. The compressed-tiff extra supplies a TIFF decoder.
54
+
55
+ ```sh
56
+ amt-validated recording.ome.tif --out baseline_results --config baseline_configuration.json
57
+ ```
58
+
59
+ | Implementation | Modules | Source digest |
60
+ | --- | ---: | --- |
61
+ | automorphotrack.validated, core 1.1.0rc2 | 18 | 8a4125a5ebe2b49e019f79b783503f31403a24ea3134d2d10b1bd4610ac3bce8 |
62
+ | automorphotrack.refined, package 2.5.0 | 5 | dcacbe6eede1cfef2cfa165418f8e0c9428aafebf3632a27a6a7655e3605f3a6 |
63
+
64
+ ## License and citation
65
+
66
+ Software: MIT. External datasets retain their individual licenses, recorded in the reproduction archive. See `CITATION.cff` for this release.
67
+
68
+ Bayati A, Schumacher JG, Chen X. AutoMorphoTrack. [eLife reviewed preprint 109936, version 1](https://doi.org/10.7554/eLife.109936.1). This is the older preprint, preceding the v2.5.0 particle revision. The revised manuscript is deposited with the current GitHub release.
@@ -33,7 +33,7 @@
33
33
  # )
34
34
  # ============================================================
35
35
 
36
- __version__ = "2.4.0"
36
+ __version__ = "2.5.0"
37
37
  __author__ = "Armin Bayati"
38
38
 
39
39
  # --- Utility Imports ---
@@ -0,0 +1,8 @@
1
+ """Versioned particle segmentation and association; frozen validated API is unchanged."""
2
+
3
+ from .segmentation import SpotConfig, segment_spots
4
+ from .tracking import TrackingConfig, link_spots
5
+ from .pipeline import RefinedConfig, analyze_movie, run_pipeline
6
+
7
+ __all__ = ["SpotConfig", "segment_spots", "TrackingConfig", "link_spots",
8
+ "RefinedConfig", "analyze_movie", "run_pipeline"]
@@ -0,0 +1,20 @@
1
+ """Command-line access to the separately versioned particle pipeline."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+ from .pipeline import RefinedConfig, run_pipeline
6
+
7
+
8
+ def main():
9
+ parser=argparse.ArgumentParser(description='AutoMorphoTrack 2.5 particle analysis')
10
+ parser.add_argument('input',help='One TIFF movie')
11
+ parser.add_argument('--out',required=True,help='New output directory')
12
+ parser.add_argument('--config',help='Explicit JSON configuration')
13
+ args=parser.parse_args()
14
+ config=RefinedConfig.from_dict(json.loads(Path(args.config).read_text())) if args.config else RefinedConfig()
15
+ result=run_pipeline(args.input,args.out,config)
16
+ print(json.dumps({'status':'completed','source_sha256':result['manifest']['source_sha256'],
17
+ 'observations':result['manifest']['observations']},indent=2))
18
+
19
+
20
+ if __name__=='__main__':main()
@@ -0,0 +1,134 @@
1
+ """Recorded single- or multichannel particle analysis, separate from the frozen core."""
2
+
3
+ from dataclasses import dataclass, field, asdict
4
+ from pathlib import Path
5
+ from datetime import datetime, timezone
6
+ import hashlib
7
+ import json
8
+ import platform
9
+ from importlib import metadata
10
+ import numpy as np
11
+ import pandas as pd
12
+ import tifffile
13
+ from .segmentation import SpotConfig, segment_spots
14
+ from .tracking import TrackingConfig, link_spots
15
+ from ..validated.io import normalize_axes, sha256_file
16
+ from ..validated.config import PipelineConfig as MeasurementConfig
17
+ from ..validated.core import measure_shapes, motility_from_tracks, colocalization_metrics
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class RefinedConfig:
22
+ axes: str | None = None
23
+ axes_override_reason: str | None = None
24
+ channels: tuple[int, ...] = (0,)
25
+ spot: SpotConfig = field(default_factory=SpotConfig)
26
+ tracking: TrackingConfig = field(default_factory=TrackingConfig)
27
+ min_observations: int = 3
28
+ pixel_size_um: float | None = None
29
+ frame_interval_s: float | None = None
30
+ calibration_source: str | None = None
31
+ sample_id: str = "sample"
32
+
33
+ def __post_init__(self):
34
+ if not isinstance(self.spot, SpotConfig) or not isinstance(self.tracking, TrackingConfig):
35
+ raise TypeError("Expected SpotConfig and TrackingConfig")
36
+ if (not self.channels or len(set(self.channels)) != len(self.channels)
37
+ or any(isinstance(x, bool) or not isinstance(x, int) or x < 0 for x in self.channels)):
38
+ raise ValueError("channels must contain distinct nonnegative integers")
39
+ if isinstance(self.min_observations, bool) or not isinstance(self.min_observations, int) or self.min_observations < 1:
40
+ raise ValueError("min_observations must be a positive integer")
41
+ if not self.sample_id:
42
+ raise ValueError("sample_id must be nonempty")
43
+ MeasurementConfig(pixel_size_um=self.pixel_size_um, frame_interval_s=self.frame_interval_s)
44
+ if (self.pixel_size_um is not None or self.frame_interval_s is not None) and not self.calibration_source:
45
+ raise ValueError("Physical calibration requires calibration_source")
46
+
47
+ @classmethod
48
+ def from_dict(cls, values):
49
+ values=dict(values)
50
+ if 'spot' in values: values['spot']=SpotConfig(**values['spot'])
51
+ if 'tracking' in values: values['tracking']=TrackingConfig(**values['tracking'])
52
+ if 'channels' in values: values['channels']=tuple(values['channels'])
53
+ return cls(**values)
54
+
55
+
56
+ def analyze_movie(images, config=None):
57
+ """Analyze a nonnegative TYX array; label IDs remain tied to recorded detections."""
58
+ config=config or RefinedConfig()
59
+ images=np.asarray(images)
60
+ if images.ndim != 3 or not all(images.shape):
61
+ raise ValueError("Expected a nonempty TYX array")
62
+ if len(images) < config.min_observations:
63
+ raise ValueError("Movie is shorter than min_observations; set that value explicitly for brief or static inputs")
64
+ raw_labels=[];points=[];thresholds=[]
65
+ for frame, im in enumerate(images):
66
+ lab,xy,meta=segment_spots(im,config.spot)
67
+ raw_labels.append(lab);points.append(xy);thresholds.append(dict(Frame=frame,**meta))
68
+ raw_labels=np.asarray(raw_labels)
69
+ raw_tracks=link_spots(points,config.tracking)
70
+ counts=raw_tracks.groupby('Track_ID').size()
71
+ keep=counts[counts>=config.min_observations].index
72
+ tracks=raw_tracks[raw_tracks.Track_ID.isin(keep)].copy()
73
+ labels=np.zeros_like(raw_labels)
74
+ for frame in range(len(images)):
75
+ allowed=tracks.loc[tracks.Frame.eq(frame),'Label'].to_numpy()
76
+ labels[frame]=np.where(np.isin(raw_labels[frame],allowed),raw_labels[frame],0)
77
+ measurement=MeasurementConfig(pixel_size_um=config.pixel_size_um,frame_interval_s=config.frame_interval_s)
78
+ shapes,_=measure_shapes(labels,measurement,labeled=True)
79
+ shapes=shapes.rename(columns={'X':'Mask_Centroid_X','Y':'Mask_Centroid_Y'})
80
+ shapes=shapes.merge(tracks[['Frame','Label','X','Y','Track_ID']],on=['Frame','Label'],how='left',validate='one_to_one')
81
+ # The frozen measurement routine independently computes actual frame intervals.
82
+ steps,motility,per_frame=motility_from_tracks(tracks,measurement)
83
+ return dict(labels=labels,raw_labels=raw_labels,tracks=tracks,raw_tracks=raw_tracks,
84
+ shapes=shapes,steps=steps,motility=motility,motility_per_frame=per_frame,
85
+ thresholds=pd.DataFrame(thresholds))
86
+
87
+
88
+ def run_pipeline(input_path, out_dir, config=None):
89
+ """Write all inputs, settings, source identities, and quantitative outputs.
90
+
91
+ For exactly two selected channels, retained foreground masks also define the
92
+ full-field colocalization measurements. This does not establish contact.
93
+ """
94
+ config=config or RefinedConfig()
95
+ if not isinstance(config,RefinedConfig): raise TypeError("config must be RefinedConfig")
96
+ out=Path(out_dir)
97
+ if out.exists() and any(out.iterdir()): raise FileExistsError(f"Output directory is not empty: {out}")
98
+ with tifffile.TiffFile(input_path) as tf:
99
+ if len(tf.series)!=1: raise ValueError("Extract one TIFF series before analysis")
100
+ series=tf.series[0];native_axes=series.axes;array=series.asarray()
101
+ axes=config.axes or native_axes
102
+ if axes!=native_axes and not config.axes_override_reason:
103
+ raise ValueError("An axes override requires axes_override_reason")
104
+ stack=normalize_axes(array,axes)
105
+ if max(config.channels)>=stack.shape[-1]: raise ValueError("Selected channel is outside the image")
106
+ channels={channel:analyze_movie(stack[...,channel],config) for channel in config.channels}
107
+ out.mkdir(parents=True,exist_ok=True)
108
+ for channel,result in channels.items():
109
+ for name,value in result.items():
110
+ path=out/f'channel_{channel}_{name}'
111
+ if isinstance(value,pd.DataFrame): value.to_csv(path.with_suffix('.csv'),index=False)
112
+ else: tifffile.imwrite(path.with_suffix('.tif'),value,compression='deflate',metadata={'axes':'TYX'},photometric='minisblack')
113
+ if len(config.channels)==2:
114
+ a,b=config.channels
115
+ values=[dict(Frame=f,**colocalization_metrics(stack[f,...,a],stack[f,...,b],channels[a]['labels'][f]>0,channels[b]['labels'][f]>0)) for f in range(len(stack))]
116
+ pd.DataFrame(values).to_csv(out/'colocalization.csv',index=False)
117
+ source={p.name:sha256_file(p) for p in sorted(Path(__file__).parent.glob('*.py'))}
118
+ frozen={p.name:sha256_file(p) for p in sorted((Path(__file__).parent.parent/'validated').glob('*.py'))}
119
+ manifest=dict(schema_version=1,algorithm_version='2.5.0',namespace='automorphotrack.refined',
120
+ created_utc=datetime.now(timezone.utc).isoformat(),input_sha256=sha256_file(input_path),
121
+ input_name=Path(input_path).name,native_axes=native_axes,effective_axes=axes,
122
+ canonical_shape=list(stack.shape),configuration=asdict(config),
123
+ source_files_sha256=source,source_sha256=hashlib.sha256(json.dumps(source,sort_keys=True).encode()).hexdigest(),
124
+ frozen_measurement_sha256=hashlib.sha256(json.dumps(frozen,sort_keys=True).encode()).hexdigest(),
125
+ python=platform.python_version(),platform=platform.platform(),
126
+ dependencies={name:metadata.version(name) for name in ['numpy','scipy','pandas','scikit-image','tifffile']},
127
+ observations={str(k):dict(raw=len(v['raw_tracks']),retained=len(v['tracks'])) for k,v in channels.items()},
128
+ limitations=['Intensity-support masks are not validated organelle membrane boundaries.',
129
+ 'Minimum track length can exclude real brief events.',
130
+ 'Ambiguous and gap links are excluded from adjacent-step speeds.',
131
+ 'No inferred fission, fusion, or membrane contact.'],
132
+ output_files={p.name:sha256_file(p) for p in sorted(out.iterdir()) if p.is_file()})
133
+ (out/'run_manifest.json').write_text(json.dumps(manifest,indent=2,allow_nan=False)+'\n')
134
+ return dict(channels=channels,manifest=manifest)
@@ -0,0 +1,85 @@
1
+ """Noise-scaled spot detection with explicit, bounded instance masks."""
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ import numpy as np
6
+ from scipy.ndimage import gaussian_filter, gaussian_laplace
7
+ from skimage.feature import peak_local_max
8
+ from skimage.segmentation import watershed
9
+ from skimage.measure import regionprops
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SpotConfig:
14
+ sigma_px: float = 1.5
15
+ background_sigma_px: float = 6.0
16
+ threshold_snr: float = 3.0
17
+ min_distance_px: int = 3
18
+ support_fraction: float = 0.2
19
+ max_radius_px: float = 4.5
20
+ min_area_px: int = 3
21
+
22
+ def __post_init__(self):
23
+ for name in ("sigma_px", "background_sigma_px", "threshold_snr", "max_radius_px"):
24
+ value = getattr(self, name)
25
+ if not math.isfinite(value) or value <= 0:
26
+ raise ValueError(f"{name} must be positive and finite")
27
+ if self.background_sigma_px <= self.sigma_px:
28
+ raise ValueError("background_sigma_px must exceed sigma_px")
29
+ if not 0 < self.support_fraction < 1:
30
+ raise ValueError("support_fraction must lie between zero and one")
31
+ for name in ("min_distance_px", "min_area_px"):
32
+ value = getattr(self, name)
33
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
34
+ raise ValueError(f"{name} must be a positive integer")
35
+
36
+
37
+ def segment_spots(image, config=None):
38
+ """Return instance labels, intensity-weighted positions, and detector metadata.
39
+
40
+ Positions use positive background-subtracted intensity inside each mask.
41
+ These intensity support regions are not validated physical organelle borders.
42
+ No temporal information or reference annotations enter segmentation.
43
+ """
44
+ config = config or SpotConfig()
45
+ image = np.asarray(image, dtype=float)
46
+ if image.ndim != 2 or not image.size or not np.isfinite(image).all() or np.any(image < 0):
47
+ raise ValueError("Expected a finite, nonnegative, nonempty YX image")
48
+ empty = np.zeros(image.shape, np.uint32)
49
+ if np.ptp(image) == 0:
50
+ return empty, np.empty((0, 2)), {"noise_response": 0.0, "threshold": 0.0, "detections": 0}
51
+ background = gaussian_filter(image, config.background_sigma_px)
52
+ foreground = image - background
53
+ response = -gaussian_laplace(foreground, config.sigma_px) * config.sigma_px ** 2
54
+ # A robust response-scale estimate includes correlated acquisition noise.
55
+ median = float(np.median(response))
56
+ noise = float(np.median(np.abs(response - median)) / 0.6744897501960817)
57
+ noise = max(noise, np.finfo(float).eps * max(float(image.max()), 1.0))
58
+ threshold = config.threshold_snr * noise
59
+ peaks = peak_local_max(response, min_distance=config.min_distance_px,
60
+ threshold_abs=threshold, exclude_border=False)
61
+ if not len(peaks):
62
+ return empty, np.empty((0, 2)), {"noise_response": noise, "threshold": threshold, "detections": 0}
63
+ markers = np.zeros(image.shape, np.int32)
64
+ markers[tuple(peaks.T)] = np.arange(1, len(peaks) + 1)
65
+ smooth = gaussian_filter(foreground, max(config.sigma_px / 2, 0.5))
66
+ labels = watershed(-smooth, markers, mask=smooth > 0)
67
+ coords = []
68
+ result = empty.copy()
69
+ for region in regionprops(labels):
70
+ index = region.label - 1
71
+ y, x = peaks[index]
72
+ pixels = region.coords
73
+ py, px = pixels.T
74
+ keep = ((py - y) ** 2 + (px - x) ** 2 <= config.max_radius_px ** 2)
75
+ keep &= smooth[py, px] >= max(smooth[y, x] * config.support_fraction, 0)
76
+ py, px = py[keep], px[keep]
77
+ if len(py) < config.min_area_px:
78
+ continue
79
+ weights = np.maximum(smooth[py, px], 0)
80
+ if weights.sum() <= 0:
81
+ continue
82
+ coords.append([float(np.average(py, weights=weights)), float(np.average(px, weights=weights))])
83
+ result[py, px] = len(coords)
84
+ return result, np.asarray(coords).reshape(-1, 2), {
85
+ "noise_response": noise, "threshold": threshold, "detections": len(coords)}
@@ -0,0 +1,93 @@
1
+ """One-to-one, motion-assisted particle association with explicit ambiguity."""
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy.optimize import linear_sum_assignment
8
+ from scipy.spatial.distance import cdist
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class TrackingConfig:
13
+ max_distance_px: float = 6.0
14
+ ambiguity_margin_px: float = 0.5
15
+ prediction_weight: float = 0.0
16
+ max_gap_frames: int = 0
17
+
18
+ def __post_init__(self):
19
+ if not math.isfinite(self.max_distance_px) or self.max_distance_px <= 0:
20
+ raise ValueError("max_distance_px must be positive and finite")
21
+ if not math.isfinite(self.ambiguity_margin_px) or self.ambiguity_margin_px < 0:
22
+ raise ValueError("ambiguity_margin_px must be nonnegative and finite")
23
+ if not math.isfinite(self.prediction_weight) or not 0 <= self.prediction_weight <= 1:
24
+ raise ValueError("prediction_weight must lie between zero and one")
25
+ if isinstance(self.max_gap_frames, bool) or not isinstance(self.max_gap_frames, int) or not 0 <= self.max_gap_frames <= 2:
26
+ raise ValueError("max_gap_frames must be an integer from zero through two")
27
+
28
+
29
+ COLUMNS = ["Frame", "Track_ID", "Label", "X", "Y", "Link_Status", "Link_Distance_px",
30
+ "Previous_Candidate_Count", "Current_Candidate_Count", "Delta_Frame",
31
+ "Prediction_Error_px", "Assignment_Margin_px"]
32
+
33
+
34
+ def link_spots(centroids, config=None, labels_per_frame=None):
35
+ """Keep gaps and ambiguous links visible; never interpolate missing positions.
36
+
37
+ Accepted adjacent links require a clear distance margin in both directions.
38
+ Gap links have a separate status and are excluded from speed estimation.
39
+ """
40
+ config = config or TrackingConfig()
41
+ active, next_id, rows = {}, 0, []
42
+ for frame, points in enumerate(centroids):
43
+ current = np.asarray(points, dtype=float)
44
+ if current.size == 0:
45
+ current = np.empty((0, 2))
46
+ if current.ndim != 2 or current.shape[1] != 2 or not np.isfinite(current).all():
47
+ raise ValueError("Each frame must contain finite N by 2 row/column positions")
48
+ if labels_per_frame is not None and len(labels_per_frame[frame]) != len(current):
49
+ raise ValueError("Labels and positions must have equal lengths")
50
+ active = {k:v for k,v in active.items() if frame-v['frame'] <= config.max_gap_frames+1}
51
+ ids = sorted(active)
52
+ links = {}
53
+ allowed = np.zeros((len(ids),len(current)),bool)
54
+ distances = np.empty(allowed.shape)
55
+ raw = np.empty(allowed.shape)
56
+ if ids and len(current):
57
+ old = np.array([active[k]['point'] for k in ids])
58
+ dt = np.array([frame-active[k]['frame'] for k in ids])
59
+ predicted = old + np.array([active[k]['velocity'] for k in ids]) * dt[:,None] * config.prediction_weight
60
+ distances = cdist(predicted,current)
61
+ raw = cdist(old,current)
62
+ gates = config.max_distance_px * dt
63
+ allowed = (raw <= gates[:,None]) & (distances <= gates[:,None])
64
+ n,m=allowed.shape
65
+ # Dummy assignments permit births and losses instead of forcing links.
66
+ costs=np.full((n+m,m+n),1e12)
67
+ costs[:n,:m]=np.where(allowed,(distances/gates[:,None])**2,1e12)
68
+ costs[:n,m:]=0.5
69
+ costs[n:,:m]=0.5
70
+ costs[n:,m:]=0
71
+ a,b=linear_sum_assignment(costs)
72
+ links={int(j):int(i) for i,j in zip(a,b) if i<n and j<m and allowed[i,j]}
73
+ for j, point in enumerate(current):
74
+ if j in links:
75
+ i=links[j];tid=ids[i];previous=active[tid]
76
+ dt=frame-previous['frame']
77
+ other_row=np.where(allowed[i],distances[i],np.inf).copy();other_row[j]=np.inf
78
+ other_col=np.where(allowed[:,j],distances[:,j],np.inf).copy();other_col[i]=np.inf
79
+ margin=min(other_row.min(initial=np.inf),other_col.min(initial=np.inf))-distances[i,j]
80
+ clear=margin > config.ambiguity_margin_px
81
+ status=('matched' if dt==1 else 'gap') if clear else 'ambiguous'
82
+ velocity=(point-previous['point'])/dt if clear else np.zeros(2)
83
+ distance=float(raw[i,j]);error=float(distances[i,j])
84
+ pc=int(allowed[i].sum());cc=int(allowed[:,j].sum())
85
+ else:
86
+ tid=next_id;next_id+=1;dt=0
87
+ status='start' if frame==0 else 'birth_or_unmatched'
88
+ velocity=np.zeros(2);distance=error=margin=np.nan;pc=0
89
+ cc=int(allowed[:,j].sum())
90
+ active[tid]=dict(frame=frame,point=point.copy(),velocity=velocity)
91
+ label=labels_per_frame[frame][j] if labels_per_frame is not None else j+1
92
+ rows.append([frame,tid,label,float(point[1]),float(point[0]),status,distance,pc,cc,dt,error,margin])
93
+ return pd.DataFrame(rows,columns=COLUMNS)