AutoMorphoTrack 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AutoMorphoTrack-1.0.0.dist-info/LICENSE.md +21 -0
- AutoMorphoTrack-1.0.0.dist-info/METADATA +125 -0
- AutoMorphoTrack-1.0.0.dist-info/RECORD +17 -0
- AutoMorphoTrack-1.0.0.dist-info/WHEEL +5 -0
- AutoMorphoTrack-1.0.0.dist-info/top_level.txt +1 -0
- automorphotrack/__init__.py +75 -0
- automorphotrack/colocalization.py +128 -0
- automorphotrack/detection.py +65 -0
- automorphotrack/lyso_count.py +84 -0
- automorphotrack/morphology.py +140 -0
- automorphotrack/motility.py +102 -0
- automorphotrack/shape_features.py +100 -0
- automorphotrack/shape_profiling.py +51 -0
- automorphotrack/summary.py +72 -0
- automorphotrack/tracking.py +121 -0
- automorphotrack/tracking_overlay.py +123 -0
- automorphotrack/utils.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Armin Bayati
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: AutoMorphoTrack
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AutoMorphoTrack is an open-source Python package for automated detection, morphology classification, motility tracking, and colocalization analysis of organelles in multichannel fluorescence microscopy data.
|
|
5
|
+
Home-page: https://github.com/abayatibrain/AMTpackage
|
|
6
|
+
Author: Armin Bayati
|
|
7
|
+
Author-email: a.bayati.brain@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Documentation, https://github.com/abayatibrain/AMTpackage
|
|
10
|
+
Project-URL: Source, https://github.com/abayatibrain/AMTpackage
|
|
11
|
+
Project-URL: Tracker, https://github.com/abayatibrain/AMTpackage/issues
|
|
12
|
+
Platform: UNKNOWN
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
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
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: numpy (>=1.23.0)
|
|
21
|
+
Requires-Dist: pandas (>=1.5.0)
|
|
22
|
+
Requires-Dist: matplotlib (>=3.6.0)
|
|
23
|
+
Requires-Dist: seaborn (>=0.12.0)
|
|
24
|
+
Requires-Dist: opencv-python (>=4.6.0)
|
|
25
|
+
Requires-Dist: scikit-image (>=0.19.0)
|
|
26
|
+
Requires-Dist: scipy (>=1.9.0)
|
|
27
|
+
Requires-Dist: tifffile (>=2022.8.12)
|
|
28
|
+
|
|
29
|
+
# AutoMorphoTrack
|
|
30
|
+
|
|
31
|
+
**AutoMorphoTrack** is an open-source Python package for automated detection, morphology classification, shape profiling, motility tracking, and colocalization analysis of subcellular organelles in multichannel fluorescence microscopy data.
|
|
32
|
+
|
|
33
|
+
This package represents the fully modular version of the **AutoMorphoTrack pipeline**, originally developed as a Jupyter Notebook by **Armin Bayati, Ph.D.**, and now distributed as an installable Python framework.
|
|
34
|
+
Repository: [https://github.com/abayatibrain/automorphotrack](https://github.com/abayatibrain/automorphotrack)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🧬 Overview
|
|
39
|
+
|
|
40
|
+
AutoMorphoTrack automates quantitative analysis of time-lapse `.tif` image stacks—typically dual-channel recordings of mitochondria and lysosomes—and generates publication-ready visual and numerical outputs at every step of the analysis pipeline.
|
|
41
|
+
|
|
42
|
+
**Pipeline stages:**
|
|
43
|
+
|
|
44
|
+
1. **Detection** – Organelle segmentation and outline visualization
|
|
45
|
+
2. **Lysosomal Counting** – Per-frame lysosome enumeration and temporal profiling
|
|
46
|
+
3. **Morphology Classification** – Elongated vs. punctate mitochondria
|
|
47
|
+
4. **Shape Feature Extraction** – Area, circularity, solidity, aspect ratio, orientation
|
|
48
|
+
5. **Tracking** – Cumulative organelle trajectories (mitochondria, lysosomes, composite)
|
|
49
|
+
6. **Motility Analysis** – Velocity, displacement, and trajectory-based statistics
|
|
50
|
+
7. **Colocalization** – Bright-blue overlap overlays and correlation metrics
|
|
51
|
+
8. **Integrated Summary** – Correlation matrices linking morphology, motility, and overlap
|
|
52
|
+
|
|
53
|
+
Each stage outputs high-resolution figures, videos, and CSV data tables stored in standardized subdirectories, ensuring reproducibility and compatibility with any image dataset.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📦 Installation
|
|
58
|
+
|
|
59
|
+
Install directly from PyPI:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install AutoMorphoTrack
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or from source:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/abayatibrain/automorphotrack.git
|
|
69
|
+
cd automorphotrack
|
|
70
|
+
pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🚀 Basic Usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from automorphotrack import *
|
|
79
|
+
|
|
80
|
+
tif_path = "Composite.tif"
|
|
81
|
+
|
|
82
|
+
detect_organelles(tif_path)
|
|
83
|
+
count_lysosomes_per_frame(tif_path)
|
|
84
|
+
classify_morphology(tif_path)
|
|
85
|
+
analyze_shape_features(tif_path)
|
|
86
|
+
track_organelles(tif_path)
|
|
87
|
+
analyze_motility()
|
|
88
|
+
analyze_colocalization(tif_path)
|
|
89
|
+
summarize_integrated_data()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each function automatically generates visual and quantitative outputs at every step.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 📁 Outputs
|
|
97
|
+
|
|
98
|
+
| Step | Output Type | Example Files |
|
|
99
|
+
|------|--------------|---------------|
|
|
100
|
+
| Detection | PNG + MP4 | `Mito_Frame0.png`, `Mitochondria_Detection.mp4` |
|
|
101
|
+
| Lysosome Count | PNG + CSV + MP4 | `Lyso_Count_Plot.png`, `Lysosome_Counts.csv` |
|
|
102
|
+
| Morphology | PNG + MP4 + CSV | `Morphology_Frame0_Labeled.png`, `Morphology_Labeled.mp4` |
|
|
103
|
+
| Shape Features | PNG + CSV | `Shape_Distributions.png`, `Mito_ShapeMetrics.csv` |
|
|
104
|
+
| Tracking | PNG + MP4 + CSV | `Cumulative_Mito.png`, `Mito_Tracks.csv` |
|
|
105
|
+
| Motility | PNG + CSV | `Motility_Distributions.png`, `Motility_Scatter.png` |
|
|
106
|
+
| Colocalization | PNG + MP4 + CSV | `Colocalization_Frame0.png`, `Colocalization.csv` |
|
|
107
|
+
| Summary | PNG + CSV | `Integrated_CorrelationMatrix.png`, `Integrated_Merged_Data.csv` |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🔧 Dependencies
|
|
112
|
+
|
|
113
|
+
- Python ≥ 3.9
|
|
114
|
+
- numpy, pandas, matplotlib, seaborn, opencv-python, scikit-image, scipy, tifffile
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧩 Citation
|
|
119
|
+
|
|
120
|
+
If you use **AutoMorphoTrack** in your research, please cite:
|
|
121
|
+
|
|
122
|
+
> **Bayati, A. (2025)**. AutoMorphoTrack: An accessible Python framework for automated analysis of organelle morphology and motility.
|
|
123
|
+
> *GitHub Repository:* [https://github.com/abayatibrain/automorphotrack](https://github.com/abayatibrain/automorphotrack)
|
|
124
|
+
|
|
125
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
automorphotrack/__init__.py,sha256=DsfyrpqWJhF3J2QtP0fz7td1GkTT-kYRazKX8zg3m90,2570
|
|
2
|
+
automorphotrack/colocalization.py,sha256=ot1Gmnx_x9Bmhshnhi8irf3SFIFPnbqSJzyOLv7y_-U,5026
|
|
3
|
+
automorphotrack/detection.py,sha256=DBKHLXBD_6yphaB4_msUbAVP7h9VfUrgrnI8-5oE9Yk,2880
|
|
4
|
+
automorphotrack/lyso_count.py,sha256=4Z2OUGZO5On7Q4NHniQUJV2sgU-vHfZV92mlEUO3VZc,3395
|
|
5
|
+
automorphotrack/morphology.py,sha256=b0HXCUY79zaCeZL0qlFoUrYvwhD0X2ivefqz7RwxqEg,5576
|
|
6
|
+
automorphotrack/motility.py,sha256=NNf_G68IMRMctNbv0gzsTW6u_vXJL_BrO08fKfjvJwY,4272
|
|
7
|
+
automorphotrack/shape_features.py,sha256=pKFQcJvlKwk8HNY9AjuVMLWCsnhDqnnGelExv0oWv0A,4203
|
|
8
|
+
automorphotrack/shape_profiling.py,sha256=XXYEUFByzybXcNZR57hYnNC17jVxuZGhIqaXyL6jqnQ,1863
|
|
9
|
+
automorphotrack/summary.py,sha256=aNE0JAe-QJf06YAyiLO4BQTk2ckS1sftH5ImpzhnHGI,2940
|
|
10
|
+
automorphotrack/tracking.py,sha256=NgjjmxiZulyuOCpuQnE0WNSaKbw7tGNu_gv4FSGPvIQ,5681
|
|
11
|
+
automorphotrack/tracking_overlay.py,sha256=LgBwLOI6rj9D71DxdjJIexyKG6R6WqFjaRsz-V0q9Wo,5426
|
|
12
|
+
automorphotrack/utils.py,sha256=2BjQnNTetzUNuHgoseAaktZ1EbBZiHzSktvQZ6sBWZE,1207
|
|
13
|
+
AutoMorphoTrack-1.0.0.dist-info/LICENSE.md,sha256=C2loTUG_HnWtD719BDxgquwzwWYCpJtpMILJEktYUos,1069
|
|
14
|
+
AutoMorphoTrack-1.0.0.dist-info/METADATA,sha256=81YtYarqVqx_0NMxUbOff_SEYuGkdG7ZhMEX4s6QShk,4877
|
|
15
|
+
AutoMorphoTrack-1.0.0.dist-info/WHEEL,sha256=LRS-GSFNyYPLBJaJJ6kxdk4Urtb4kEVXWc5DpA8iv6o,97
|
|
16
|
+
AutoMorphoTrack-1.0.0.dist-info/top_level.txt,sha256=t2gMCx-ewdtS91D7S7vPeSrRV5OnidZvXAowk5Cbros,16
|
|
17
|
+
AutoMorphoTrack-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
automorphotrack
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Automated Organelle Detection and Tracking
|
|
3
|
+
# ============================================================
|
|
4
|
+
# Author: Armin Bayati, Ph.D.
|
|
5
|
+
# Description:
|
|
6
|
+
# AutoMorphoTrack is a modular image-analysis package for
|
|
7
|
+
# mitochondria and lysosome tracking, morphology, shape,
|
|
8
|
+
# motility, and colocalization analysis in fluorescence microscopy.
|
|
9
|
+
#
|
|
10
|
+
# Core Pipeline:
|
|
11
|
+
# 1. Detection (detection.py)
|
|
12
|
+
# 2. Lysosome counting (lyso_count.py)
|
|
13
|
+
# 3. Morphology classification (morphology.py)
|
|
14
|
+
# 4. Shape feature extraction (shape_features.py)
|
|
15
|
+
# 5. Shape profiling (shape_profiling.py)
|
|
16
|
+
# 6. Organelle tracking (tracking.py / tracking_overlay.py)
|
|
17
|
+
# 7. Motility analysis (motility.py)
|
|
18
|
+
# 8. Colocalization analysis (colocalization.py)
|
|
19
|
+
# 9. Integrated summary (summary.py)
|
|
20
|
+
#
|
|
21
|
+
# Package structure follows:
|
|
22
|
+
# from automorphotrack import (
|
|
23
|
+
# detect_organelles,
|
|
24
|
+
# count_lysosomes_per_frame,
|
|
25
|
+
# classify_morphology,
|
|
26
|
+
# analyze_shape_features,
|
|
27
|
+
# profile_shape_data,
|
|
28
|
+
# track_organelles,
|
|
29
|
+
# track_overlay,
|
|
30
|
+
# analyze_motility,
|
|
31
|
+
# analyze_colocalization,
|
|
32
|
+
# summarize_integrated_data
|
|
33
|
+
# )
|
|
34
|
+
# ============================================================
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.0"
|
|
37
|
+
__author__ = "Armin Bayati"
|
|
38
|
+
|
|
39
|
+
# --- Utility Imports ---
|
|
40
|
+
from automorphotrack.utils import (
|
|
41
|
+
ensure_dir,
|
|
42
|
+
save_high_dpi,
|
|
43
|
+
upscale_frame,
|
|
44
|
+
write_video
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# --- Core Functional Imports ---
|
|
48
|
+
from automorphotrack.detection import detect_organelles
|
|
49
|
+
from automorphotrack.lyso_count import count_lysosomes_per_frame
|
|
50
|
+
from automorphotrack.morphology import classify_morphology
|
|
51
|
+
from automorphotrack.shape_features import analyze_shape_features
|
|
52
|
+
from automorphotrack.shape_profiling import profile_shape_data
|
|
53
|
+
from automorphotrack.tracking import track_organelles
|
|
54
|
+
from automorphotrack.tracking_overlay import track_overlay
|
|
55
|
+
from automorphotrack.motility import analyze_motility
|
|
56
|
+
from automorphotrack.colocalization import analyze_colocalization
|
|
57
|
+
from automorphotrack.summary import summarize_integrated_data
|
|
58
|
+
|
|
59
|
+
# --- Convenience Alias (optional shortcut API) ---
|
|
60
|
+
__all__ = [
|
|
61
|
+
"ensure_dir",
|
|
62
|
+
"save_high_dpi",
|
|
63
|
+
"upscale_frame",
|
|
64
|
+
"write_video",
|
|
65
|
+
"detect_organelles",
|
|
66
|
+
"count_lysosomes_per_frame",
|
|
67
|
+
"classify_morphology",
|
|
68
|
+
"analyze_shape_features",
|
|
69
|
+
"profile_shape_data",
|
|
70
|
+
"track_organelles",
|
|
71
|
+
"track_overlay",
|
|
72
|
+
"analyze_motility",
|
|
73
|
+
"analyze_colocalization",
|
|
74
|
+
"summarize_integrated_data"
|
|
75
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Colocalization Analysis (Manders + Pearson)
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import tifffile, cv2, numpy as np, pandas as pd, matplotlib.pyplot as plt
|
|
6
|
+
from skimage.filters import threshold_otsu
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from automorphotrack.utils import ensure_dir, save_high_dpi
|
|
9
|
+
|
|
10
|
+
def analyze_colocalization(
|
|
11
|
+
tif_path="Composite.tif",
|
|
12
|
+
mito_channel=0,
|
|
13
|
+
lyso_channel=1,
|
|
14
|
+
out_dir="Colocalization_Outputs",
|
|
15
|
+
fps=5,
|
|
16
|
+
upscale=2.0,
|
|
17
|
+
overlay_alpha=0.6):
|
|
18
|
+
|
|
19
|
+
ensure_dir(out_dir)
|
|
20
|
+
stack = tifffile.imread(tif_path)
|
|
21
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
22
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
23
|
+
n_frames = stack.shape[0]
|
|
24
|
+
print(f"Loaded {n_frames} frames for colocalization analysis")
|
|
25
|
+
|
|
26
|
+
# ---------- Helpers ----------
|
|
27
|
+
def upscale_frame(img):
|
|
28
|
+
h, w = img.shape[:2]
|
|
29
|
+
return cv2.resize(img, (int(w * upscale), int(h * upscale)), interpolation=cv2.INTER_CUBIC)
|
|
30
|
+
|
|
31
|
+
def write_video(frames, out_path, fps):
|
|
32
|
+
h, w = frames[0].shape[:2]
|
|
33
|
+
out = cv2.VideoWriter(str(out_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
|
|
34
|
+
for fr in frames:
|
|
35
|
+
out.write(cv2.cvtColor(fr, cv2.COLOR_RGB2BGR))
|
|
36
|
+
out.release()
|
|
37
|
+
|
|
38
|
+
frames_out = []
|
|
39
|
+
overlap_percent, m1_list, m2_list = [], [], []
|
|
40
|
+
pearson_r, overlap_R = [], []
|
|
41
|
+
mito_sum, lyso_sum = [], []
|
|
42
|
+
|
|
43
|
+
for f in range(n_frames):
|
|
44
|
+
mito = stack[f][..., mito_channel].astype(float)
|
|
45
|
+
lyso = stack[f][..., lyso_channel].astype(float)
|
|
46
|
+
|
|
47
|
+
# Background correction
|
|
48
|
+
mito -= np.percentile(mito, 5)
|
|
49
|
+
lyso -= np.percentile(lyso, 5)
|
|
50
|
+
mito, lyso = np.clip(mito, 0, None), np.clip(lyso, 0, None)
|
|
51
|
+
|
|
52
|
+
# Binary masks
|
|
53
|
+
thr_m, thr_l = threshold_otsu(mito), threshold_otsu(lyso)
|
|
54
|
+
mito_mask, lyso_mask = mito > thr_m, lyso > thr_l
|
|
55
|
+
overlap_mask, union_mask = mito_mask & lyso_mask, mito_mask | lyso_mask
|
|
56
|
+
|
|
57
|
+
# Metrics
|
|
58
|
+
overlap_ratio = (overlap_mask.sum() / union_mask.sum() * 100) if union_mask.sum() else 0
|
|
59
|
+
overlap_percent.append(overlap_ratio)
|
|
60
|
+
M1 = mito[lyso_mask].sum() / mito.sum() if mito.sum() > 0 else 0
|
|
61
|
+
M2 = lyso[mito_mask].sum() / lyso.sum() if lyso.sum() > 0 else 0
|
|
62
|
+
m1_list.append(M1)
|
|
63
|
+
m2_list.append(M2)
|
|
64
|
+
|
|
65
|
+
flat_m, flat_l = mito.flatten(), lyso.flatten()
|
|
66
|
+
if np.std(flat_m) > 0 and np.std(flat_l) > 0:
|
|
67
|
+
pearson_r.append(np.corrcoef(flat_m, flat_l)[0, 1])
|
|
68
|
+
else:
|
|
69
|
+
pearson_r.append(0)
|
|
70
|
+
denom = np.sqrt((mito**2).sum() * (lyso**2).sum())
|
|
71
|
+
overlap_R.append((mito * lyso).sum() / denom if denom > 0 else 0)
|
|
72
|
+
|
|
73
|
+
mito_sum.append(mito.sum())
|
|
74
|
+
lyso_sum.append(lyso.sum())
|
|
75
|
+
|
|
76
|
+
# Visualization with bright-blue overlay
|
|
77
|
+
mito_norm = (mito / mito.max() * 255).astype(np.uint8)
|
|
78
|
+
lyso_norm = (lyso / lyso.max() * 255).astype(np.uint8)
|
|
79
|
+
rgb = np.zeros((*mito.shape, 3), np.uint8)
|
|
80
|
+
rgb[..., 0], rgb[..., 1] = mito_norm, lyso_norm
|
|
81
|
+
|
|
82
|
+
mask = overlap_mask.astype(bool)
|
|
83
|
+
bright_blue_overlay = np.full_like(rgb[mask], (0, 150, 255), dtype=np.uint8)
|
|
84
|
+
rgb[mask] = cv2.addWeighted(rgb[mask], 1 - overlay_alpha, bright_blue_overlay, overlay_alpha, 0)
|
|
85
|
+
|
|
86
|
+
frames_out.append(upscale_frame(rgb))
|
|
87
|
+
|
|
88
|
+
# ---------- Save metrics ----------
|
|
89
|
+
csv_path = Path(out_dir) / "Colocalization.csv"
|
|
90
|
+
pd.DataFrame({
|
|
91
|
+
"Frame": np.arange(n_frames),
|
|
92
|
+
"Percent_Overlap": overlap_percent,
|
|
93
|
+
"Manders_M1": m1_list,
|
|
94
|
+
"Manders_M2": m2_list,
|
|
95
|
+
"Pearson_r": pearson_r,
|
|
96
|
+
"Overlap_R": overlap_R,
|
|
97
|
+
"Mito_TotalIntensity": mito_sum,
|
|
98
|
+
"Lyso_TotalIntensity": lyso_sum
|
|
99
|
+
}).to_csv(csv_path, index=False)
|
|
100
|
+
print(f"Saved metrics → {csv_path}")
|
|
101
|
+
|
|
102
|
+
# ---------- Save video ----------
|
|
103
|
+
video_path = Path(out_dir) / "Colocalization_BrightBlueOverlay.mp4"
|
|
104
|
+
write_video(frames_out, video_path, fps=fps)
|
|
105
|
+
|
|
106
|
+
# ---------- Save frame 0 still ----------
|
|
107
|
+
frame0_path = Path(out_dir) / "Colocalization_Frame0.png"
|
|
108
|
+
cv2.imwrite(str(frame0_path),
|
|
109
|
+
cv2.cvtColor(frames_out[0], cv2.COLOR_RGB2BGR),
|
|
110
|
+
[cv2.IMWRITE_PNG_COMPRESSION, 0])
|
|
111
|
+
|
|
112
|
+
# ---------- Plot metrics ----------
|
|
113
|
+
plot_path = Path(out_dir) / "Colocalization_MetricsPlot.png"
|
|
114
|
+
fig, ax = plt.subplots(figsize=(10, 9))
|
|
115
|
+
ax.plot(m1_list, 'r-', label='Manders M1 (mito→lyso)')
|
|
116
|
+
ax.plot(m2_list, 'g-', label='Manders M2 (lyso→mito)')
|
|
117
|
+
ax.plot(np.array(overlap_percent) / 100, 'b--', label='Overlap fraction')
|
|
118
|
+
ax.plot(pearson_r, 'k-.', label='Pearson r')
|
|
119
|
+
ax.plot(overlap_R, 'm:', label='Normalized R')
|
|
120
|
+
ax.set_xlabel("Frame")
|
|
121
|
+
ax.set_ylabel("Coefficient / Fraction (0–1)")
|
|
122
|
+
ax.set_ylim(0, 1)
|
|
123
|
+
ax.legend()
|
|
124
|
+
ax.set_title("Colocalization Metrics Over Time")
|
|
125
|
+
plt.tight_layout()
|
|
126
|
+
save_high_dpi(fig, plot_path)
|
|
127
|
+
|
|
128
|
+
print(f"Colocalization analysis complete — results saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Organelle Detection
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import tifffile, numpy as np, cv2
|
|
6
|
+
from skimage.filters import threshold_otsu
|
|
7
|
+
from skimage.morphology import remove_small_objects, binary_opening, disk
|
|
8
|
+
from skimage.segmentation import clear_border
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from automorphotrack.utils import ensure_dir, write_video, upscale_frame
|
|
11
|
+
|
|
12
|
+
def detect_mask(frame, ch, thr_factor=0.8, min_size=3):
|
|
13
|
+
"""Threshold and clean a single channel."""
|
|
14
|
+
img = frame[..., ch].astype(float)
|
|
15
|
+
img = (img - img.min()) / (img.ptp() + 1e-12)
|
|
16
|
+
thr = threshold_otsu(img) * thr_factor
|
|
17
|
+
mask = clear_border(binary_opening(img > thr, footprint=disk(1)))
|
|
18
|
+
mask = remove_small_objects(mask, min_size)
|
|
19
|
+
return (mask * 255).astype(np.uint8), img
|
|
20
|
+
|
|
21
|
+
def detect_organelles(
|
|
22
|
+
tif_path="Composite.tif",
|
|
23
|
+
out_dir="Detection_Outputs",
|
|
24
|
+
mito_channel=0,
|
|
25
|
+
lyso_channel=1,
|
|
26
|
+
upscale_factor=2,
|
|
27
|
+
fps=5):
|
|
28
|
+
|
|
29
|
+
ensure_dir(out_dir)
|
|
30
|
+
stack = tifffile.imread(str(tif_path))
|
|
31
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
32
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
33
|
+
n_frames = stack.shape[0]
|
|
34
|
+
print(f"Loaded {n_frames} frames for detection")
|
|
35
|
+
|
|
36
|
+
mito_frames, lyso_frames = [], []
|
|
37
|
+
|
|
38
|
+
for f in range(n_frames):
|
|
39
|
+
# ----- Mitochondria (relaxed detection) -----
|
|
40
|
+
mito_mask, mito_img = detect_mask(stack[f], mito_channel, thr_factor=0.6, min_size=0.5)
|
|
41
|
+
mito_gray = (mito_img * 255).astype(np.uint8)
|
|
42
|
+
mito_rgb = cv2.cvtColor(mito_gray, cv2.COLOR_GRAY2RGB)
|
|
43
|
+
cnts, _ = cv2.findContours(mito_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
44
|
+
cv2.drawContours(mito_rgb, cnts, -1, (255, 0, 0), 1, cv2.LINE_AA)
|
|
45
|
+
mito_frames.append(upscale_frame(mito_rgb, upscale_factor))
|
|
46
|
+
|
|
47
|
+
# ----- Lysosomes (standard detection) -----
|
|
48
|
+
lyso_mask, lyso_img = detect_mask(stack[f], lyso_channel, thr_factor=0.8, min_size=2)
|
|
49
|
+
lyso_gray = (lyso_img * 255).astype(np.uint8)
|
|
50
|
+
lyso_rgb = cv2.cvtColor(lyso_gray, cv2.COLOR_GRAY2RGB)
|
|
51
|
+
cnts, _ = cv2.findContours(lyso_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
52
|
+
cv2.drawContours(lyso_rgb, cnts, -1, (0, 255, 0), 1, cv2.LINE_AA)
|
|
53
|
+
lyso_frames.append(upscale_frame(lyso_rgb, upscale_factor))
|
|
54
|
+
|
|
55
|
+
# Save representative frames
|
|
56
|
+
cv2.imwrite(str(Path(out_dir) / "Mito_Frame0.png"),
|
|
57
|
+
cv2.cvtColor(mito_frames[0], cv2.COLOR_RGB2BGR))
|
|
58
|
+
cv2.imwrite(str(Path(out_dir) / "Lyso_Frame0.png"),
|
|
59
|
+
cv2.cvtColor(lyso_frames[0], cv2.COLOR_RGB2BGR))
|
|
60
|
+
|
|
61
|
+
# Save videos
|
|
62
|
+
write_video(mito_frames, Path(out_dir) / "Mitochondria_Detection.mp4", fps=fps)
|
|
63
|
+
write_video(lyso_frames, Path(out_dir) / "Lysosomes_Detection.mp4", fps=fps)
|
|
64
|
+
|
|
65
|
+
print(f"Detection complete — outputs saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Lysosomal Counting per Frame
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import tifffile, numpy as np, cv2, pandas as pd, matplotlib.pyplot as plt
|
|
6
|
+
from skimage.filters import threshold_otsu
|
|
7
|
+
from skimage.morphology import remove_small_objects, binary_opening, disk
|
|
8
|
+
from skimage.segmentation import clear_border
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from automorphotrack.utils import ensure_dir, write_video, upscale_frame
|
|
11
|
+
|
|
12
|
+
def detect_mask(frame, ch, thr_factor=0.8, min_size=2):
|
|
13
|
+
"""Detect lysosomes in a single channel frame."""
|
|
14
|
+
img = frame[..., ch].astype(float)
|
|
15
|
+
img = (img - img.min()) / (img.ptp() + 1e-12)
|
|
16
|
+
thr = threshold_otsu(img) * thr_factor
|
|
17
|
+
mask = clear_border(binary_opening(img > thr, footprint=disk(1)))
|
|
18
|
+
mask = remove_small_objects(mask, min_size)
|
|
19
|
+
return (mask * 255).astype(np.uint8), img
|
|
20
|
+
|
|
21
|
+
def count_lysosomes_per_frame(
|
|
22
|
+
tif_path="Composite.tif",
|
|
23
|
+
out_dir="Lyso_Count_Outputs",
|
|
24
|
+
lyso_channel=1,
|
|
25
|
+
upscale_factor=2,
|
|
26
|
+
fps=5):
|
|
27
|
+
|
|
28
|
+
ensure_dir(out_dir)
|
|
29
|
+
stack = tifffile.imread(str(tif_path))
|
|
30
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
31
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
32
|
+
n_frames = stack.shape[0]
|
|
33
|
+
print(f"Loaded {n_frames} frames for lysosomal counting")
|
|
34
|
+
|
|
35
|
+
lyso_counts, lyso_frames = [], []
|
|
36
|
+
|
|
37
|
+
for f in range(n_frames):
|
|
38
|
+
lyso_mask, lyso_img = detect_mask(stack[f], lyso_channel, thr_factor=0.8, min_size=2)
|
|
39
|
+
cnts, _ = cv2.findContours(lyso_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
40
|
+
lyso_count = len(cnts)
|
|
41
|
+
lyso_counts.append({"Frame": f, "Lysosome_Count": lyso_count})
|
|
42
|
+
|
|
43
|
+
lyso_gray = (lyso_img * 255).astype(np.uint8)
|
|
44
|
+
lyso_rgb = cv2.cvtColor(lyso_gray, cv2.COLOR_GRAY2RGB)
|
|
45
|
+
cv2.drawContours(lyso_rgb, cnts, -1, (0, 255, 0), 1, cv2.LINE_AA)
|
|
46
|
+
|
|
47
|
+
# Add small green numbers at each lysosome position
|
|
48
|
+
for i, c in enumerate(cnts):
|
|
49
|
+
M = cv2.moments(c)
|
|
50
|
+
if M["m00"] != 0:
|
|
51
|
+
cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
|
|
52
|
+
cv2.putText(lyso_rgb, str(i + 1), (cx, cy),
|
|
53
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (0, 255, 0), 1, cv2.LINE_AA)
|
|
54
|
+
|
|
55
|
+
# Add total count label in white (small font)
|
|
56
|
+
cv2.putText(lyso_rgb, f"Count: {lyso_count}", (10, 18),
|
|
57
|
+
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA)
|
|
58
|
+
|
|
59
|
+
lyso_frames.append(upscale_frame(lyso_rgb, upscale_factor))
|
|
60
|
+
|
|
61
|
+
# Save representative frame
|
|
62
|
+
cv2.imwrite(str(Path(out_dir) / "Lyso_Frame0_Count.png"),
|
|
63
|
+
cv2.cvtColor(lyso_frames[0], cv2.COLOR_RGB2BGR))
|
|
64
|
+
|
|
65
|
+
# Save video
|
|
66
|
+
write_video(lyso_frames, Path(out_dir) / "Lyso_Count_Video.mp4", fps=fps)
|
|
67
|
+
|
|
68
|
+
# Save CSV
|
|
69
|
+
df = pd.DataFrame(lyso_counts)
|
|
70
|
+
csv_path = Path(out_dir) / "Lysosome_Counts.csv"
|
|
71
|
+
df.to_csv(csv_path, index=False)
|
|
72
|
+
|
|
73
|
+
# Plot counts over frames
|
|
74
|
+
plt.figure(figsize=(6, 4))
|
|
75
|
+
plt.plot(df["Frame"], df["Lysosome_Count"], "-o", color="green")
|
|
76
|
+
plt.xlabel("Frame")
|
|
77
|
+
plt.ylabel("Lysosome Count")
|
|
78
|
+
plt.title("Lysosome Count per Frame")
|
|
79
|
+
plt.grid(True, linestyle="--", alpha=0.5)
|
|
80
|
+
plt.tight_layout()
|
|
81
|
+
plt.savefig(Path(out_dir) / "Lyso_Count_Plot.png", dpi=200)
|
|
82
|
+
plt.close()
|
|
83
|
+
|
|
84
|
+
print(f"Lysosomal counting complete — outputs saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Mitochondrial Morphology Classification
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import tifffile, numpy as np, cv2, matplotlib.pyplot as plt, pandas as pd
|
|
6
|
+
from skimage.measure import regionprops, label
|
|
7
|
+
from skimage.filters import threshold_otsu
|
|
8
|
+
from skimage.morphology import remove_small_objects, binary_opening, disk
|
|
9
|
+
from skimage.segmentation import clear_border
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from automorphotrack.utils import ensure_dir, upscale_frame, save_high_dpi
|
|
12
|
+
|
|
13
|
+
def detect_mask(frame, ch, thr_factor=0.8, min_size=10):
|
|
14
|
+
"""Threshold a channel to obtain clean mitochondrial mask."""
|
|
15
|
+
img = frame[..., ch].astype(float)
|
|
16
|
+
img = (img - img.min()) / (img.ptp() + 1e-12)
|
|
17
|
+
thr = threshold_otsu(img) * thr_factor
|
|
18
|
+
mask = clear_border(binary_opening(img > thr, footprint=disk(1)))
|
|
19
|
+
mask = remove_small_objects(mask, min_size)
|
|
20
|
+
return (mask * 255).astype(np.uint8), img
|
|
21
|
+
|
|
22
|
+
def draw_text_with_outline(img, text, pos, font_scale, color, thickness, outline_color):
|
|
23
|
+
x, y = int(pos[0]), int(pos[1])
|
|
24
|
+
cv2.putText(img, text, (x, y), cv2.FONT_HERSHEY_SIMPLEX,
|
|
25
|
+
font_scale, outline_color, thickness + 2, cv2.LINE_AA)
|
|
26
|
+
cv2.putText(img, text, (x, y), cv2.FONT_HERSHEY_SIMPLEX,
|
|
27
|
+
font_scale, color, thickness, cv2.LINE_AA)
|
|
28
|
+
|
|
29
|
+
def classify_morphology(
|
|
30
|
+
tif_path="Composite.tif",
|
|
31
|
+
out_dir="Morphology_Outputs",
|
|
32
|
+
mito_channel=0,
|
|
33
|
+
area_thresh_elongated=0.12,
|
|
34
|
+
ecc_thresh_elongated=0.80,
|
|
35
|
+
area_thresh_punctate=0.03,
|
|
36
|
+
ecc_thresh_punctate=0.60,
|
|
37
|
+
upscale_factor=2,
|
|
38
|
+
fps=5):
|
|
39
|
+
|
|
40
|
+
ensure_dir(out_dir)
|
|
41
|
+
stack = tifffile.imread(tif_path)
|
|
42
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
43
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
44
|
+
if stack.shape[-1] == 3:
|
|
45
|
+
stack = stack[..., :2]
|
|
46
|
+
n_frames = stack.shape[0]
|
|
47
|
+
print(f"Loaded {n_frames} frames for morphology classification")
|
|
48
|
+
|
|
49
|
+
thr_stack, gray_stack = [], []
|
|
50
|
+
for f in range(n_frames):
|
|
51
|
+
m, g = detect_mask(stack[f], mito_channel)
|
|
52
|
+
thr_stack.append(m)
|
|
53
|
+
gray_stack.append(g)
|
|
54
|
+
|
|
55
|
+
results, summary_counts = [], []
|
|
56
|
+
|
|
57
|
+
for f in range(n_frames):
|
|
58
|
+
lbl = label(thr_stack[f])
|
|
59
|
+
props = regionprops(lbl, intensity_image=gray_stack[f])
|
|
60
|
+
frame_res = []
|
|
61
|
+
for p in props:
|
|
62
|
+
ecc, area = p.eccentricity, p.area
|
|
63
|
+
if area >= area_thresh_elongated and ecc >= ecc_thresh_elongated:
|
|
64
|
+
morph = "Elongated"
|
|
65
|
+
elif area >= area_thresh_punctate and ecc <= ecc_thresh_punctate:
|
|
66
|
+
morph = "Punctate"
|
|
67
|
+
else:
|
|
68
|
+
morph = "Intermediate"
|
|
69
|
+
frame_res.append({
|
|
70
|
+
"Frame": f,
|
|
71
|
+
"Label": p.label,
|
|
72
|
+
"Area": area,
|
|
73
|
+
"Eccentricity": ecc,
|
|
74
|
+
"Morphology": morph
|
|
75
|
+
})
|
|
76
|
+
results += frame_res
|
|
77
|
+
summary_counts.append({
|
|
78
|
+
"Frame": f,
|
|
79
|
+
"Elongated": sum(r["Morphology"] == "Elongated" for r in frame_res),
|
|
80
|
+
"Punctate": sum(r["Morphology"] == "Punctate" for r in frame_res)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
# Save CSV outputs
|
|
84
|
+
pd.DataFrame(results).to_csv(Path(out_dir) / "Morphology_AllFrames.csv", index=False)
|
|
85
|
+
pd.DataFrame(summary_counts).to_csv(Path(out_dir) / "Morphology_Summary.csv", index=False)
|
|
86
|
+
print("Saved morphology classification CSVs")
|
|
87
|
+
|
|
88
|
+
# Visualization: E/P-labeled video and frame 0
|
|
89
|
+
video_frames = []
|
|
90
|
+
font_scale = 0.45
|
|
91
|
+
font_thickness = 1
|
|
92
|
+
font_color_E = (0, 255, 0)
|
|
93
|
+
font_color_P = (255, 255, 0)
|
|
94
|
+
outline_color = (0, 0, 0)
|
|
95
|
+
|
|
96
|
+
for f in range(n_frames):
|
|
97
|
+
mito_gray = gray_stack[f]
|
|
98
|
+
lbl = label(thr_stack[f])
|
|
99
|
+
props = regionprops(lbl, intensity_image=mito_gray)
|
|
100
|
+
base = (mito_gray * 255).astype(np.uint8)
|
|
101
|
+
base_rgb = cv2.cvtColor(base, cv2.COLOR_GRAY2BGR)
|
|
102
|
+
base_up = upscale_frame(base_rgb, upscale_factor)
|
|
103
|
+
|
|
104
|
+
elong_cnts, punct_cnts = [], []
|
|
105
|
+
for p in props:
|
|
106
|
+
cnt_mask = (lbl == p.label).astype(np.uint8)
|
|
107
|
+
cnts, _ = cv2.findContours(cnt_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
|
|
108
|
+
for c in cnts:
|
|
109
|
+
c[:] *= upscale_factor
|
|
110
|
+
if p.eccentricity >= ecc_thresh_elongated:
|
|
111
|
+
elong_cnts.append(c)
|
|
112
|
+
else:
|
|
113
|
+
punct_cnts.append(c)
|
|
114
|
+
|
|
115
|
+
overlay = base_up.copy()
|
|
116
|
+
cv2.drawContours(overlay, elong_cnts, -1, font_color_E, 2, cv2.LINE_AA)
|
|
117
|
+
cv2.drawContours(overlay, punct_cnts, -1, font_color_P, 2, cv2.LINE_AA)
|
|
118
|
+
|
|
119
|
+
for p in props:
|
|
120
|
+
y, x = p.centroid
|
|
121
|
+
x, y = int(x * upscale_factor), int(y * upscale_factor)
|
|
122
|
+
morph = "E" if p.eccentricity >= ecc_thresh_elongated else "P"
|
|
123
|
+
col = font_color_E if morph == "E" else font_color_P
|
|
124
|
+
draw_text_with_outline(overlay, morph, (x, y),
|
|
125
|
+
font_scale, col, font_thickness, outline_color)
|
|
126
|
+
|
|
127
|
+
video_frames.append(cv2.cvtColor(overlay, cv2.COLOR_RGB2BGR))
|
|
128
|
+
|
|
129
|
+
# Write labeled video
|
|
130
|
+
video_path = Path(out_dir) / "Morphology_Labeled.mp4"
|
|
131
|
+
h, w = video_frames[0].shape[:2]
|
|
132
|
+
writer = cv2.VideoWriter(str(video_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
|
|
133
|
+
for fr in video_frames:
|
|
134
|
+
writer.write(fr)
|
|
135
|
+
writer.release()
|
|
136
|
+
|
|
137
|
+
# Save labeled frame 0 still
|
|
138
|
+
out_path = Path(out_dir) / "Morphology_Frame0_Labeled.png"
|
|
139
|
+
cv2.imwrite(str(out_path), cv2.cvtColor(video_frames[0], cv2.COLOR_RGB2BGR))
|
|
140
|
+
print(f"Morphology visualization saved → {out_path}")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Motility Analysis
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import pandas as pd, numpy as np, matplotlib.pyplot as plt, seaborn as sns
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from automorphotrack.utils import ensure_dir, save_high_dpi
|
|
8
|
+
|
|
9
|
+
def analyze_motility(
|
|
10
|
+
mito_tracks_path="Tracking_Outputs/Mito_Tracks.csv",
|
|
11
|
+
lyso_tracks_path="Tracking_Outputs/Lyso_Tracks.csv",
|
|
12
|
+
out_dir="Motility_Outputs",
|
|
13
|
+
fps=5):
|
|
14
|
+
|
|
15
|
+
ensure_dir(out_dir)
|
|
16
|
+
|
|
17
|
+
# ---------- Load data ----------
|
|
18
|
+
mito_df = pd.read_csv(mito_tracks_path)
|
|
19
|
+
lyso_df = pd.read_csv(lyso_tracks_path)
|
|
20
|
+
print(f"Loaded {len(mito_df)} mitochondrial and {len(lyso_df)} lysosomal coordinates")
|
|
21
|
+
|
|
22
|
+
# ---------- Compute displacement & velocity ----------
|
|
23
|
+
def compute_motility(df, label):
|
|
24
|
+
df = df.sort_values(["Organelle", "Frame"])
|
|
25
|
+
df["DX"] = df.groupby("Organelle")["X"].diff()
|
|
26
|
+
df["DY"] = df.groupby("Organelle")["Y"].diff()
|
|
27
|
+
df["Displacement"] = np.sqrt(df["DX"]**2 + df["DY"]**2)
|
|
28
|
+
df["Velocity"] = df["Displacement"]
|
|
29
|
+
|
|
30
|
+
# Per-organelle summary
|
|
31
|
+
summary = (
|
|
32
|
+
df.groupby("Organelle")
|
|
33
|
+
.agg({
|
|
34
|
+
"Displacement": ["mean", "sum"],
|
|
35
|
+
"Velocity": "mean"
|
|
36
|
+
})
|
|
37
|
+
.reset_index()
|
|
38
|
+
)
|
|
39
|
+
summary.columns = ["Organelle", "Mean_Displacement",
|
|
40
|
+
"Total_Displacement", "Mean_Velocity"]
|
|
41
|
+
summary["Organelle_Type"] = label
|
|
42
|
+
|
|
43
|
+
# Per-frame mean values
|
|
44
|
+
frame_summary = (
|
|
45
|
+
df.groupby("Frame")[["Displacement", "Velocity"]]
|
|
46
|
+
.mean()
|
|
47
|
+
.reset_index()
|
|
48
|
+
.rename(columns={
|
|
49
|
+
"Displacement": "Mean_Displacement",
|
|
50
|
+
"Velocity": "Mean_Velocity"
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
frame_summary["Organelle_Type"] = label
|
|
54
|
+
return df, summary, frame_summary
|
|
55
|
+
|
|
56
|
+
mito_full, mito_summary, mito_frame = compute_motility(mito_df, "Mitochondria")
|
|
57
|
+
lyso_full, lyso_summary, lyso_frame = compute_motility(lyso_df, "Lysosomes")
|
|
58
|
+
|
|
59
|
+
# ---------- Combine summaries ----------
|
|
60
|
+
combined_summary = pd.concat([mito_summary, lyso_summary], ignore_index=True)
|
|
61
|
+
combined_frame = pd.concat([mito_frame, lyso_frame], ignore_index=True)
|
|
62
|
+
|
|
63
|
+
# ---------- Save CSVs ----------
|
|
64
|
+
summary_csv = Path(out_dir) / "Motility_Summary.csv"
|
|
65
|
+
frame_csv = Path(out_dir) / "Motility_PerFrame.csv"
|
|
66
|
+
combined_summary.to_csv(summary_csv, index=False)
|
|
67
|
+
combined_frame.to_csv(frame_csv, index=False)
|
|
68
|
+
|
|
69
|
+
print(f"Saved organelle summary → {summary_csv}")
|
|
70
|
+
print(f"Saved per-frame summary → {frame_csv}")
|
|
71
|
+
|
|
72
|
+
# ---------- Distribution Plots ----------
|
|
73
|
+
fig, axes = plt.subplots(1, 2, figsize=(20, 8))
|
|
74
|
+
sns.kdeplot(mito_summary["Mean_Velocity"], color="red", fill=True, ax=axes[0], label="Mitochondria")
|
|
75
|
+
sns.kdeplot(lyso_summary["Mean_Velocity"], color="green", fill=True, alpha=0.4, ax=axes[0], label="Lysosomes")
|
|
76
|
+
axes[0].set_title("Mean Velocity Distribution")
|
|
77
|
+
axes[0].set_xlabel("Velocity (px/frame)")
|
|
78
|
+
axes[0].legend()
|
|
79
|
+
|
|
80
|
+
sns.kdeplot(mito_summary["Total_Displacement"], color="red", fill=True, ax=axes[1], label="Mitochondria")
|
|
81
|
+
sns.kdeplot(lyso_summary["Total_Displacement"], color="green", fill=True, alpha=0.4, ax=axes[1], label="Lysosomes")
|
|
82
|
+
axes[1].set_title("Total Displacement Distribution")
|
|
83
|
+
axes[1].set_xlabel("Displacement (px)")
|
|
84
|
+
axes[1].legend()
|
|
85
|
+
|
|
86
|
+
plt.tight_layout()
|
|
87
|
+
save_high_dpi(fig, Path(out_dir) / "Motility_Distributions.png")
|
|
88
|
+
|
|
89
|
+
# ---------- Scatter Plot ----------
|
|
90
|
+
fig, ax = plt.subplots(figsize=(12, 10))
|
|
91
|
+
sns.scatterplot(data=mito_summary, x="Total_Displacement", y="Mean_Velocity",
|
|
92
|
+
color="red", s=30, label="Mitochondria")
|
|
93
|
+
sns.scatterplot(data=lyso_summary, x="Total_Displacement", y="Mean_Velocity",
|
|
94
|
+
color="green", s=30, alpha=0.6, label="Lysosomes")
|
|
95
|
+
ax.set_xlabel("Total Displacement (px)")
|
|
96
|
+
ax.set_ylabel("Mean Velocity (px/frame)")
|
|
97
|
+
ax.set_title("Motility Scatter Plot")
|
|
98
|
+
ax.legend()
|
|
99
|
+
plt.tight_layout()
|
|
100
|
+
save_high_dpi(fig, Path(out_dir) / "Motility_Scatter.png")
|
|
101
|
+
|
|
102
|
+
print(f"Motility analysis complete — outputs saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Organelle Shape Feature Extraction
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import tifffile, numpy as np, pandas as pd, cv2, seaborn as sns, matplotlib.pyplot as plt
|
|
6
|
+
from skimage.measure import regionprops, label
|
|
7
|
+
from skimage.filters import threshold_otsu
|
|
8
|
+
from skimage.morphology import remove_small_objects, binary_opening, disk
|
|
9
|
+
from skimage.segmentation import clear_border
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from automorphotrack.utils import ensure_dir, save_high_dpi, upscale_frame
|
|
12
|
+
|
|
13
|
+
def detect_mask(frame, ch, min_size=3, thr_factor=0.8):
|
|
14
|
+
"""Threshold and clean a single channel."""
|
|
15
|
+
img = frame[..., ch].astype(float)
|
|
16
|
+
img = (img - img.min()) / (img.ptp() + 1e-12)
|
|
17
|
+
thr = threshold_otsu(img) * thr_factor
|
|
18
|
+
mask = clear_border(binary_opening(img > thr, footprint=disk(1)))
|
|
19
|
+
mask = remove_small_objects(mask, min_size)
|
|
20
|
+
return (mask > 0).astype(np.uint8), img
|
|
21
|
+
|
|
22
|
+
def analyze_shape_features(
|
|
23
|
+
tif_path="Composite.tif",
|
|
24
|
+
out_dir="Shape_Feature_Outputs",
|
|
25
|
+
mito_channel=0,
|
|
26
|
+
lyso_channel=1):
|
|
27
|
+
|
|
28
|
+
ensure_dir(out_dir)
|
|
29
|
+
stack = tifffile.imread(tif_path)
|
|
30
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
31
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
32
|
+
n_frames = stack.shape[0]
|
|
33
|
+
print(f"Loaded {n_frames} frames for shape feature extraction")
|
|
34
|
+
|
|
35
|
+
mito_records, lyso_records = [], []
|
|
36
|
+
|
|
37
|
+
for f in range(n_frames):
|
|
38
|
+
# ---------- Mitochondria ----------
|
|
39
|
+
mito_mask, mito_img = detect_mask(stack[f], mito_channel, min_size=5, thr_factor=0.85)
|
|
40
|
+
lbl_m = label(mito_mask)
|
|
41
|
+
for p in regionprops(lbl_m):
|
|
42
|
+
if p.area < 5:
|
|
43
|
+
continue
|
|
44
|
+
circ = (4 * np.pi * p.area) / (p.perimeter ** 2 + 1e-9)
|
|
45
|
+
aspect = p.major_axis_length / (p.minor_axis_length + 1e-9)
|
|
46
|
+
mito_records.append({
|
|
47
|
+
"Frame": f,
|
|
48
|
+
"Area": p.area,
|
|
49
|
+
"Eccentricity": p.eccentricity,
|
|
50
|
+
"Solidity": p.solidity,
|
|
51
|
+
"Circularity": circ,
|
|
52
|
+
"Aspect_Ratio": aspect,
|
|
53
|
+
"Orientation": p.orientation,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# ---------- Lysosomes ----------
|
|
57
|
+
lyso_mask, lyso_img = detect_mask(stack[f], lyso_channel, min_size=2, thr_factor=0.85)
|
|
58
|
+
lbl_l = label(lyso_mask)
|
|
59
|
+
for p in regionprops(lbl_l):
|
|
60
|
+
if p.area < 5:
|
|
61
|
+
continue
|
|
62
|
+
circ = (4 * np.pi * p.area) / (p.perimeter ** 2 + 1e-9)
|
|
63
|
+
aspect = p.major_axis_length / (p.minor_axis_length + 1e-9)
|
|
64
|
+
lyso_records.append({
|
|
65
|
+
"Frame": f,
|
|
66
|
+
"Area": p.area,
|
|
67
|
+
"Eccentricity": p.eccentricity,
|
|
68
|
+
"Solidity": p.solidity,
|
|
69
|
+
"Circularity": circ,
|
|
70
|
+
"Aspect_Ratio": aspect,
|
|
71
|
+
"Orientation": p.orientation,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
mito_df = pd.DataFrame(mito_records)
|
|
75
|
+
lyso_df = pd.DataFrame(lyso_records)
|
|
76
|
+
|
|
77
|
+
mito_csv = Path(out_dir) / "Mito_ShapeMetrics.csv"
|
|
78
|
+
lyso_csv = Path(out_dir) / "Lyso_ShapeMetrics.csv"
|
|
79
|
+
mito_df.to_csv(mito_csv, index=False)
|
|
80
|
+
lyso_df.to_csv(lyso_csv, index=False)
|
|
81
|
+
print(f"Saved shape metrics for {len(mito_df)} mitochondria and {len(lyso_df)} lysosomes")
|
|
82
|
+
|
|
83
|
+
# ---------- KDE Distribution Plots ----------
|
|
84
|
+
fig, axes = plt.subplots(1, 2, figsize=(20, 8))
|
|
85
|
+
sns.kdeplot(mito_df["Circularity"], ax=axes[0], color="red", fill=True, alpha=0.5, label="Mito")
|
|
86
|
+
sns.kdeplot(lyso_df["Circularity"], ax=axes[0], color="green", fill=True, alpha=0.4, label="Lyso")
|
|
87
|
+
axes[0].set_title("Circularity Distribution")
|
|
88
|
+
axes[0].set_xlabel("Circularity (4πA/P²)")
|
|
89
|
+
axes[0].legend(frameon=False)
|
|
90
|
+
|
|
91
|
+
sns.kdeplot(mito_df["Solidity"], ax=axes[1], color="red", fill=True, alpha=0.5, label="Mito")
|
|
92
|
+
sns.kdeplot(lyso_df["Solidity"], ax=axes[1], color="green", fill=True, alpha=0.4, label="Lyso")
|
|
93
|
+
axes[1].set_title("Solidity Distribution")
|
|
94
|
+
axes[1].set_xlabel("Solidity")
|
|
95
|
+
axes[1].legend(frameon=False)
|
|
96
|
+
|
|
97
|
+
plt.tight_layout()
|
|
98
|
+
save_high_dpi(fig, Path(out_dir) / "Shape_Distributions.png")
|
|
99
|
+
|
|
100
|
+
print(f"Shape feature analysis complete — results saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Shape Profiling (Combined Organelle Metrics)
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import seaborn as sns
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from automorphotrack.utils import ensure_dir, save_high_dpi
|
|
10
|
+
|
|
11
|
+
def profile_shape_data(
|
|
12
|
+
mito_shape_path="Shape_Feature_Outputs/Mito_ShapeMetrics.csv",
|
|
13
|
+
lyso_shape_path="Shape_Feature_Outputs/Lyso_ShapeMetrics.csv",
|
|
14
|
+
out_dir="Shape_Profiling_Outputs"):
|
|
15
|
+
|
|
16
|
+
ensure_dir(out_dir)
|
|
17
|
+
|
|
18
|
+
# ---------- Load and label datasets ----------
|
|
19
|
+
mito_df = pd.read_csv(mito_shape_path).assign(Type="Mitochondria")
|
|
20
|
+
lyso_df = pd.read_csv(lyso_shape_path).assign(Type="Lysosomes")
|
|
21
|
+
combined_df = pd.concat([mito_df, lyso_df], ignore_index=True)
|
|
22
|
+
|
|
23
|
+
# ---------- Save combined CSV ----------
|
|
24
|
+
combined_csv_path = Path(out_dir) / "Combined_ShapeData.csv"
|
|
25
|
+
combined_df.to_csv(combined_csv_path, index=False)
|
|
26
|
+
print(f"Saved combined shape data CSV → {combined_csv_path}")
|
|
27
|
+
|
|
28
|
+
# ---------- Violin plots ----------
|
|
29
|
+
fig, axes = plt.subplots(1, 3, figsize=(30, 9))
|
|
30
|
+
metrics = ["Circularity", "Solidity", "Aspect_Ratio"]
|
|
31
|
+
|
|
32
|
+
for ax, metric in zip(axes, metrics):
|
|
33
|
+
sns.violinplot(
|
|
34
|
+
data=combined_df,
|
|
35
|
+
x="Type",
|
|
36
|
+
y=metric,
|
|
37
|
+
palette={"Mitochondria": "red", "Lysosomes": "green"},
|
|
38
|
+
cut=0,
|
|
39
|
+
inner="quartile",
|
|
40
|
+
ax=ax
|
|
41
|
+
)
|
|
42
|
+
ax.set_title(metric)
|
|
43
|
+
ax.set_xlabel("")
|
|
44
|
+
ax.set_ylabel(metric)
|
|
45
|
+
ax.grid(True, linestyle="--", alpha=0.5)
|
|
46
|
+
|
|
47
|
+
plt.tight_layout()
|
|
48
|
+
out_path = Path(out_dir) / "Shape_ViolinPlots.png"
|
|
49
|
+
save_high_dpi(fig, out_path)
|
|
50
|
+
|
|
51
|
+
print(f"Shape profiling complete — violin plots and combined CSV saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Integrated Summary and Correlation Analysis
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import pandas as pd, numpy as np, matplotlib.pyplot as plt, seaborn as sns
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from automorphotrack.utils import ensure_dir
|
|
8
|
+
|
|
9
|
+
def summarize_integrated_data(
|
|
10
|
+
shape_metrics_path="Shape_Feature_Outputs/Mito_ShapeMetrics.csv",
|
|
11
|
+
motility_path="Motility_Outputs/Motility_PerFrame.csv",
|
|
12
|
+
colocalization_path="Colocalization_Outputs/Colocalization.csv",
|
|
13
|
+
out_dir="Summary_Outputs"):
|
|
14
|
+
|
|
15
|
+
print("Starting integrated summary analysis...")
|
|
16
|
+
ensure_dir(out_dir)
|
|
17
|
+
|
|
18
|
+
# ---------- Load data ----------
|
|
19
|
+
try:
|
|
20
|
+
shape_df = pd.read_csv(shape_metrics_path)
|
|
21
|
+
print(f"Loaded shape metrics ({len(shape_df)} rows)")
|
|
22
|
+
mot_df = pd.read_csv(motility_path)
|
|
23
|
+
print(f"Loaded motility data ({len(mot_df)} rows)")
|
|
24
|
+
col_df = pd.read_csv(colocalization_path)
|
|
25
|
+
print(f"Loaded colocalization data ({len(col_df)} rows)")
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print("Error reading one of the input files:", e)
|
|
28
|
+
return None, None
|
|
29
|
+
|
|
30
|
+
# ---------- Aggregate by frame ----------
|
|
31
|
+
if "Frame" not in shape_df or "Frame" not in mot_df or "Frame" not in col_df:
|
|
32
|
+
print("Missing 'Frame' column in one or more input files.")
|
|
33
|
+
return None, None
|
|
34
|
+
|
|
35
|
+
shape_summary = shape_df.groupby("Frame").mean(numeric_only=True).reset_index()
|
|
36
|
+
print(f"Aggregated shape metrics into {len(shape_summary)} frames")
|
|
37
|
+
|
|
38
|
+
# ---------- Merge datasets ----------
|
|
39
|
+
merged = (
|
|
40
|
+
shape_summary
|
|
41
|
+
.merge(mot_df, on="Frame", how="inner")
|
|
42
|
+
.merge(col_df, on="Frame", how="inner")
|
|
43
|
+
.dropna()
|
|
44
|
+
)
|
|
45
|
+
print(f"Merged dataset dimensions: {merged.shape[0]} rows × {merged.shape[1]} columns")
|
|
46
|
+
|
|
47
|
+
# ---------- Correlation computation ----------
|
|
48
|
+
numeric_cols = merged.select_dtypes(include=[np.number])
|
|
49
|
+
corr = numeric_cols.corr().round(2)
|
|
50
|
+
print("Correlation matrix computed")
|
|
51
|
+
|
|
52
|
+
# ---------- Save results ----------
|
|
53
|
+
out_dir = Path(out_dir)
|
|
54
|
+
merged_csv = out_dir / "Integrated_Merged_Data.csv"
|
|
55
|
+
corr_csv = out_dir / "Integrated_CorrelationMatrix.csv"
|
|
56
|
+
merged.to_csv(merged_csv, index=False)
|
|
57
|
+
corr.to_csv(corr_csv)
|
|
58
|
+
print(f"Saved merged data → {merged_csv}")
|
|
59
|
+
print(f"Saved correlation matrix → {corr_csv}")
|
|
60
|
+
|
|
61
|
+
# ---------- Plot correlation heatmap ----------
|
|
62
|
+
fig, ax = plt.subplots(figsize=(28, 20))
|
|
63
|
+
sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", square=True)
|
|
64
|
+
ax.set_title("Integrated Correlation Matrix – Shape, Motility, Colocalization", fontsize=14)
|
|
65
|
+
plt.tight_layout()
|
|
66
|
+
heatmap_path = out_dir / "Integrated_CorrelationMatrix.png"
|
|
67
|
+
plt.savefig(heatmap_path, dpi=300)
|
|
68
|
+
plt.close(fig)
|
|
69
|
+
|
|
70
|
+
print(f"Saved heatmap → {heatmap_path}")
|
|
71
|
+
print("Integrated summary analysis complete.")
|
|
72
|
+
return merged, corr
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Organelle Tracking (Cumulative Paths)
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import numpy as np, cv2, pandas as pd, tifffile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from scipy.spatial import cKDTree
|
|
8
|
+
from skimage.measure import label, regionprops
|
|
9
|
+
from automorphotrack.utils import ensure_dir, write_video, upscale_frame
|
|
10
|
+
from automorphotrack.shape_features import detect_mask
|
|
11
|
+
|
|
12
|
+
def track_organelles(
|
|
13
|
+
tif_path="Composite.tif",
|
|
14
|
+
out_dir="Tracking_Outputs",
|
|
15
|
+
mito_channel=0,
|
|
16
|
+
lyso_channel=1,
|
|
17
|
+
fps=5,
|
|
18
|
+
max_dist=15):
|
|
19
|
+
|
|
20
|
+
ensure_dir(out_dir)
|
|
21
|
+
stack = tifffile.imread(tif_path)
|
|
22
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
23
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
24
|
+
n_frames = stack.shape[0]
|
|
25
|
+
print(f"Loaded {n_frames} frames for organelle tracking")
|
|
26
|
+
|
|
27
|
+
# ---------- Helper functions ----------
|
|
28
|
+
def get_centroids(mask):
|
|
29
|
+
lbl = label(mask)
|
|
30
|
+
return np.array([p.centroid for p in regionprops(lbl)]) if lbl.max() > 0 else np.empty((0, 2))
|
|
31
|
+
|
|
32
|
+
def nearest_neighbor(prev, curr, max_dist=15):
|
|
33
|
+
if len(prev) == 0 or len(curr) == 0:
|
|
34
|
+
return {}
|
|
35
|
+
tree = cKDTree(curr)
|
|
36
|
+
dists, idx = tree.query(prev, distance_upper_bound=max_dist)
|
|
37
|
+
return {i: j for i, j in enumerate(idx) if j < len(curr)}
|
|
38
|
+
|
|
39
|
+
mito_tracks, lyso_tracks = [], []
|
|
40
|
+
mito_data, lyso_data = [], []
|
|
41
|
+
|
|
42
|
+
# ---------- Collect centroids per frame ----------
|
|
43
|
+
for f in range(n_frames):
|
|
44
|
+
mito_mask, _ = detect_mask(stack[f], mito_channel, min_size=2, thr_factor=0.65)
|
|
45
|
+
lyso_mask, _ = detect_mask(stack[f], lyso_channel, min_size=2, thr_factor=0.85)
|
|
46
|
+
|
|
47
|
+
mito_curr = get_centroids(mito_mask)
|
|
48
|
+
lyso_curr = get_centroids(lyso_mask)
|
|
49
|
+
mito_tracks.append(mito_curr)
|
|
50
|
+
lyso_tracks.append(lyso_curr)
|
|
51
|
+
|
|
52
|
+
for i, (y, x) in enumerate(mito_curr):
|
|
53
|
+
mito_data.append([f, i, x, y])
|
|
54
|
+
for i, (y, x) in enumerate(lyso_curr):
|
|
55
|
+
lyso_data.append([f, i, x, y])
|
|
56
|
+
|
|
57
|
+
# ---------- Save centroid coordinates ----------
|
|
58
|
+
df_mito = pd.DataFrame(mito_data, columns=["Frame", "Organelle", "X", "Y"])
|
|
59
|
+
df_lyso = pd.DataFrame(lyso_data, columns=["Frame", "Organelle", "X", "Y"])
|
|
60
|
+
df_mito.to_csv(Path(out_dir) / "Mito_Tracks.csv", index=False)
|
|
61
|
+
df_lyso.to_csv(Path(out_dir) / "Lyso_Tracks.csv", index=False)
|
|
62
|
+
|
|
63
|
+
# ---------- Compute displacement and velocity ----------
|
|
64
|
+
def compute_disp_vel(df):
|
|
65
|
+
disps, vels = [], []
|
|
66
|
+
for oid in df["Organelle"].unique():
|
|
67
|
+
sub = df[df["Organelle"] == oid].sort_values("Frame")
|
|
68
|
+
if len(sub) > 1:
|
|
69
|
+
dx, dy = np.diff(sub["X"]), np.diff(sub["Y"])
|
|
70
|
+
disp = np.sqrt(dx ** 2 + dy ** 2)
|
|
71
|
+
disps.append([oid, disp.sum()])
|
|
72
|
+
vels.append([oid, disp.mean() * fps])
|
|
73
|
+
return (pd.DataFrame(disps, columns=["Organelle", "Total_Displacement"]),
|
|
74
|
+
pd.DataFrame(vels, columns=["Organelle", "Mean_Velocity"]))
|
|
75
|
+
|
|
76
|
+
mito_disp, mito_vel = compute_disp_vel(df_mito)
|
|
77
|
+
lyso_disp, lyso_vel = compute_disp_vel(df_lyso)
|
|
78
|
+
|
|
79
|
+
mito_disp.to_csv(Path(out_dir) / "Mito_Displacement.csv", index=False)
|
|
80
|
+
mito_vel.to_csv(Path(out_dir) / "Mito_Velocity.csv", index=False)
|
|
81
|
+
lyso_disp.to_csv(Path(out_dir) / "Lyso_Displacement.csv", index=False)
|
|
82
|
+
lyso_vel.to_csv(Path(out_dir) / "Lyso_Velocity.csv", index=False)
|
|
83
|
+
|
|
84
|
+
# ---------- Build cumulative frames ----------
|
|
85
|
+
mito_paths, lyso_paths = [], []
|
|
86
|
+
mito_frames, lyso_frames, composite_frames = [], [], []
|
|
87
|
+
|
|
88
|
+
for f in range(n_frames):
|
|
89
|
+
frame_rgb = np.zeros_like(stack[f][..., :3])
|
|
90
|
+
if f > 0:
|
|
91
|
+
if len(mito_tracks[f - 1]) and len(mito_tracks[f]):
|
|
92
|
+
for k, v in nearest_neighbor(mito_tracks[f - 1], mito_tracks[f], max_dist).items():
|
|
93
|
+
mito_paths.append((mito_tracks[f - 1][k], mito_tracks[f][v]))
|
|
94
|
+
if len(lyso_tracks[f - 1]) and len(lyso_tracks[f]):
|
|
95
|
+
for k, v in nearest_neighbor(lyso_tracks[f - 1], lyso_tracks[f], max_dist).items():
|
|
96
|
+
lyso_paths.append((lyso_tracks[f - 1][k], lyso_tracks[f][v]))
|
|
97
|
+
|
|
98
|
+
mito_frame, lyso_frame, comp_frame = frame_rgb.copy(), frame_rgb.copy(), frame_rgb.copy()
|
|
99
|
+
|
|
100
|
+
for p1, p2 in mito_paths:
|
|
101
|
+
cv2.line(mito_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])), (255, 0, 0), 1)
|
|
102
|
+
cv2.line(comp_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])), (255, 0, 0), 1)
|
|
103
|
+
for p1, p2 in lyso_paths:
|
|
104
|
+
cv2.line(lyso_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])), (0, 255, 0), 1)
|
|
105
|
+
cv2.line(comp_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])), (0, 255, 0), 1)
|
|
106
|
+
|
|
107
|
+
mito_frames.append(upscale_frame(mito_frame))
|
|
108
|
+
lyso_frames.append(upscale_frame(lyso_frame))
|
|
109
|
+
composite_frames.append(upscale_frame(comp_frame))
|
|
110
|
+
|
|
111
|
+
# ---------- Save videos ----------
|
|
112
|
+
write_video(mito_frames, Path(out_dir) / "Mito_CumulativeTracks.mp4", fps=fps)
|
|
113
|
+
write_video(lyso_frames, Path(out_dir) / "Lyso_CumulativeTracks.mp4", fps=fps)
|
|
114
|
+
write_video(composite_frames, Path(out_dir) / "Composite_CumulativeTracks.mp4", fps=fps)
|
|
115
|
+
|
|
116
|
+
# ---------- Save final stills ----------
|
|
117
|
+
cv2.imwrite(str(Path(out_dir) / "Cumulative_Mito.png"), cv2.cvtColor(mito_frames[-1], cv2.COLOR_RGB2BGR))
|
|
118
|
+
cv2.imwrite(str(Path(out_dir) / "Cumulative_Lyso.png"), cv2.cvtColor(lyso_frames[-1], cv2.COLOR_RGB2BGR))
|
|
119
|
+
cv2.imwrite(str(Path(out_dir) / "Cumulative_Composite.png"), cv2.cvtColor(composite_frames[-1], cv2.COLOR_RGB2BGR))
|
|
120
|
+
|
|
121
|
+
print(f"Tracking complete — outputs saved in {Path(out_dir).resolve()}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack – Organelle Tracking with Real-Intensity Overlays
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import numpy as np, cv2, tifffile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from skimage.measure import label, regionprops
|
|
8
|
+
from scipy.spatial import cKDTree
|
|
9
|
+
from automorphotrack.utils import ensure_dir
|
|
10
|
+
from automorphotrack.shape_features import detect_mask
|
|
11
|
+
|
|
12
|
+
def track_overlay(
|
|
13
|
+
tif_path="Composite.tif",
|
|
14
|
+
out_dir="TrackingOverlay_Outputs",
|
|
15
|
+
mito_channel=0,
|
|
16
|
+
lyso_channel=1,
|
|
17
|
+
fps=10,
|
|
18
|
+
upscale=2.0,
|
|
19
|
+
line_thickness=1):
|
|
20
|
+
|
|
21
|
+
ensure_dir(out_dir)
|
|
22
|
+
stack = tifffile.imread(tif_path)
|
|
23
|
+
if stack.ndim == 3:
|
|
24
|
+
stack = stack[..., np.newaxis]
|
|
25
|
+
if stack.shape[1] == 3 and stack.shape[-1] != 3:
|
|
26
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
27
|
+
n_frames = stack.shape[0]
|
|
28
|
+
print(f"Loaded {n_frames} frames for tracking overlays")
|
|
29
|
+
|
|
30
|
+
# ---------- Helper functions ----------
|
|
31
|
+
def get_centroids(mask):
|
|
32
|
+
lbl = label(mask)
|
|
33
|
+
return np.array([p.centroid for p in regionprops(lbl)]) if lbl.max() > 0 else np.empty((0, 2))
|
|
34
|
+
|
|
35
|
+
def nearest_neighbor(prev, curr, max_dist=15):
|
|
36
|
+
if len(prev) == 0 or len(curr) == 0:
|
|
37
|
+
return {}
|
|
38
|
+
tree = cKDTree(curr)
|
|
39
|
+
dists, idx = tree.query(prev, distance_upper_bound=max_dist)
|
|
40
|
+
return {i: j for i, j in enumerate(idx) if j < len(curr)}
|
|
41
|
+
|
|
42
|
+
def upscale_frame(img):
|
|
43
|
+
h, w = img.shape[:2]
|
|
44
|
+
return cv2.resize(img, (int(w * upscale), int(h * upscale)), interpolation=cv2.INTER_CUBIC)
|
|
45
|
+
|
|
46
|
+
def write_video(frames, out_path, fps):
|
|
47
|
+
h, w = frames[0].shape[:2]
|
|
48
|
+
out = cv2.VideoWriter(str(out_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
|
|
49
|
+
for fr in frames:
|
|
50
|
+
out.write(cv2.cvtColor(fr, cv2.COLOR_RGB2BGR))
|
|
51
|
+
out.release()
|
|
52
|
+
|
|
53
|
+
# ---------- Extract centroid tracks ----------
|
|
54
|
+
mito_tracks, lyso_tracks = [], []
|
|
55
|
+
for f in range(n_frames):
|
|
56
|
+
mito_mask, _ = detect_mask(stack[f], mito_channel, min_size=2, thr_factor=0.65)
|
|
57
|
+
lyso_mask, _ = detect_mask(stack[f], lyso_channel, min_size=2, thr_factor=0.85)
|
|
58
|
+
mito_tracks.append(get_centroids(mito_mask))
|
|
59
|
+
lyso_tracks.append(get_centroids(lyso_mask))
|
|
60
|
+
|
|
61
|
+
# ---------- Build cumulative trajectory frames ----------
|
|
62
|
+
mito_paths, lyso_paths = [], []
|
|
63
|
+
mito_frames, lyso_frames, composite_frames = [], [], []
|
|
64
|
+
|
|
65
|
+
for f in range(n_frames):
|
|
66
|
+
mito_mask, _ = detect_mask(stack[f], mito_channel, min_size=2, thr_factor=0.65)
|
|
67
|
+
lyso_mask, _ = detect_mask(stack[f], lyso_channel, min_size=2, thr_factor=0.85)
|
|
68
|
+
|
|
69
|
+
mito_img = stack[f, :, :, mito_channel]
|
|
70
|
+
lyso_img = stack[f, :, :, lyso_channel]
|
|
71
|
+
|
|
72
|
+
mito_norm = cv2.normalize(mito_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
|
73
|
+
lyso_norm = cv2.normalize(lyso_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
|
74
|
+
|
|
75
|
+
mito_base = cv2.cvtColor(mito_norm, cv2.COLOR_GRAY2RGB)
|
|
76
|
+
lyso_base = cv2.cvtColor(lyso_norm, cv2.COLOR_GRAY2RGB)
|
|
77
|
+
comp_merge = np.clip(mito_norm * 0.5 + lyso_norm * 0.5, 0, 255).astype(np.uint8)
|
|
78
|
+
comp_base = cv2.cvtColor(comp_merge, cv2.COLOR_GRAY2RGB)
|
|
79
|
+
|
|
80
|
+
if f > 0:
|
|
81
|
+
prev_m, curr_m = mito_tracks[f - 1], mito_tracks[f]
|
|
82
|
+
prev_l, curr_l = lyso_tracks[f - 1], lyso_tracks[f]
|
|
83
|
+
if len(prev_m) and len(curr_m):
|
|
84
|
+
nn = nearest_neighbor(prev_m, curr_m)
|
|
85
|
+
for k, v in nn.items():
|
|
86
|
+
mito_paths.append((prev_m[k], curr_m[v]))
|
|
87
|
+
if len(prev_l) and len(curr_l):
|
|
88
|
+
nn = nearest_neighbor(prev_l, curr_l)
|
|
89
|
+
for k, v in nn.items():
|
|
90
|
+
lyso_paths.append((prev_l[k], curr_l[v]))
|
|
91
|
+
|
|
92
|
+
mito_frame = mito_base.copy()
|
|
93
|
+
lyso_frame = lyso_base.copy()
|
|
94
|
+
comp_frame = comp_base.copy()
|
|
95
|
+
|
|
96
|
+
for p1, p2 in mito_paths:
|
|
97
|
+
cv2.line(mito_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])),
|
|
98
|
+
(255, 0, 0), line_thickness, cv2.LINE_AA)
|
|
99
|
+
cv2.line(comp_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])),
|
|
100
|
+
(255, 0, 0), line_thickness, cv2.LINE_AA)
|
|
101
|
+
for p1, p2 in lyso_paths:
|
|
102
|
+
cv2.line(lyso_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])),
|
|
103
|
+
(0, 255, 0), line_thickness, cv2.LINE_AA)
|
|
104
|
+
cv2.line(comp_frame, tuple(np.int32(p1[::-1])), tuple(np.int32(p2[::-1])),
|
|
105
|
+
(0, 255, 0), line_thickness, cv2.LINE_AA)
|
|
106
|
+
|
|
107
|
+
mito_frames.append(upscale_frame(mito_frame))
|
|
108
|
+
lyso_frames.append(upscale_frame(lyso_frame))
|
|
109
|
+
composite_frames.append(upscale_frame(comp_frame))
|
|
110
|
+
|
|
111
|
+
# ---------- Save outputs ----------
|
|
112
|
+
write_video(mito_frames, Path(out_dir) / "Mito_CumulativeTracks.mp4", fps=fps)
|
|
113
|
+
write_video(lyso_frames, Path(out_dir) / "Lyso_CumulativeTracks.mp4", fps=fps)
|
|
114
|
+
write_video(composite_frames, Path(out_dir) / "Composite_CumulativeTracks.mp4", fps=fps)
|
|
115
|
+
|
|
116
|
+
for name, frames in zip(["Mito", "Lyso", "Composite"],
|
|
117
|
+
[mito_frames, lyso_frames, composite_frames]):
|
|
118
|
+
out_path = Path(out_dir) / f"Cumulative_{name}.png"
|
|
119
|
+
cv2.imwrite(str(out_path),
|
|
120
|
+
cv2.cvtColor(frames[-1], cv2.COLOR_RGB2BGR),
|
|
121
|
+
[cv2.IMWRITE_PNG_COMPRESSION, 0])
|
|
122
|
+
|
|
123
|
+
print(f"Tracking overlay complete — results saved in {Path(out_dir).resolve()}")
|
automorphotrack/utils.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# AutoMorphoTrack Utilities
|
|
3
|
+
# ============================================================
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import cv2
|
|
8
|
+
import numpy as np
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
|
|
11
|
+
def ensure_dir(path):
|
|
12
|
+
"""Create directory if missing."""
|
|
13
|
+
Path(path).mkdir(parents=True, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
def save_high_dpi(fig, path, dpi=600):
|
|
16
|
+
"""Save Matplotlib figure with high resolution."""
|
|
17
|
+
fig.savefig(path, dpi=dpi, bbox_inches="tight")
|
|
18
|
+
plt.close(fig)
|
|
19
|
+
print(f"Saved figure: {path}")
|
|
20
|
+
|
|
21
|
+
def upscale_frame(img, scale=2):
|
|
22
|
+
"""Upscale an image for high-quality visualization."""
|
|
23
|
+
h, w = img.shape[:2]
|
|
24
|
+
return cv2.resize(img, (w*scale, h*scale), interpolation=cv2.INTER_CUBIC)
|
|
25
|
+
|
|
26
|
+
def write_video(frames, path, fps=5):
|
|
27
|
+
"""Write RGB frames into an MP4 video."""
|
|
28
|
+
if not frames:
|
|
29
|
+
print("No frames provided for video.")
|
|
30
|
+
return
|
|
31
|
+
h, w = frames[0].shape[:2]
|
|
32
|
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
33
|
+
out = cv2.VideoWriter(str(path), fourcc, fps, (w, h))
|
|
34
|
+
for fr in frames:
|
|
35
|
+
out.write(cv2.cvtColor(fr.astype(np.uint8), cv2.COLOR_RGB2BGR))
|
|
36
|
+
out.release()
|
|
37
|
+
print(f"Saved video: {path}")
|