riponazad commited on
Commit
21494a2
·
1 Parent(s): e086603

deploy 1.0

Browse files
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ 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
31
+ -----END CERTIFICATE-----
README.md CHANGED
@@ -11,4 +11,138 @@ license: mit
11
  short_description: To run EchoTracker instantly on a custom or given videos.
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  short_description: To run EchoTracker instantly on a custom or given videos.
12
  ---
13
 
14
+ # 🫀 EchoTracker
15
+
16
+ **Advancing Myocardial Point Tracking in Echocardiography**
17
+
18
+ [![MICCAI 2024](https://img.shields.io/badge/MICCAI-2024-blue)](https://link.springer.com/chapter/10.1007/978-3-031-72083-3_60)
19
+ [![arXiv](https://img.shields.io/badge/arXiv-2405.08587-red)](https://arxiv.org/abs/2405.08587)
20
+ [![GitHub](https://img.shields.io/badge/GitHub-riponazad%2Fechotracker-black)](https://github.com/riponazad/echotracker)
21
+ [![Project Page](https://img.shields.io/badge/Project-Page-purple)](https://riponazad.github.io/echotracker/)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
23
+
24
+ EchoTracker is an interactive demo for tracking user-selected points on cardiac tissue across echocardiography video sequences. It was presented at **MICCAI 2024** and demonstrates strong generalisation across cardiac views and scanner types — including out-of-distribution settings not seen during training.
25
+
26
+ ---
27
+
28
+ ## Demo
29
+
30
+ Try the live demo on Hugging Face Spaces: [EchoTracker Space](https://huggingface.co/spaces/riponazad/echotracker)
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Interactive point selection** — click directly on a video frame to place up to 100 tracking points on cardiac structures (e.g. LV/RV walls, myocardium)
37
+ - **Frame navigation** — scrub through frames with a slider to choose the optimal query frame (end-diastolic recommended)
38
+ - **Multi-view support** — handles A4C (apical 4-chamber), RV (right ventricle), and PSAX (parasternal short-axis) views
39
+ - **Out-of-distribution (OOD) generalisation** — tested on scanner types and views not seen during training
40
+ - **Faded trajectory visualisation** — output video overlays colour-coded tracks with fade-trail rendering
41
+ - **Built-in examples** — four bundled clips (A4C, A4C OOD, RV OOD, PSAX OOD) for instant testing
42
+
43
+ ---
44
+
45
+ ## How to Use
46
+
47
+ 1. **Load a video** — upload your own echocardiography clip or click one of the provided example thumbnails.
48
+ 2. **Navigate to the query frame** — use the frame slider to find the desired starting frame. The end-diastolic frame is recommended for best results.
49
+ 3. **Place tracking points** — click anywhere on the frame image to add a point. Up to **100 points** are supported per run.
50
+ 4. **Adjust selection** — use **Undo** to remove the last point or **Clear All** to start over.
51
+ 5. **Run the tracker** — press **▶ Run EchoTracker** to generate trajectories for all selected points.
52
+ 6. **View output** — the annotated video with colour-coded tracks appears in the output player.
53
+
54
+ > **Tip:** Points are stored as `(x, y)` pixel coordinates on the original frame and are automatically rescaled to the model's 256 × 256 input resolution.
55
+
56
+ ---
57
+
58
+ ## Running Locally
59
+
60
+ ### Prerequisites
61
+
62
+ - Python 3.10+
63
+ - A CUDA-capable GPU (optional but recommended; CPU inference is supported)
64
+
65
+ ### Installation
66
+
67
+ ```bash
68
+ git clone https://github.com/riponazad/echotracker.git
69
+ cd echotracker
70
+ pip install gradio torch opencv-python-headless numpy Pillow mediapy scikit-image
71
+ ```
72
+
73
+ ### Launch
74
+
75
+ ```bash
76
+ python app.py
77
+ ```
78
+
79
+ The Gradio interface will be available at `http://localhost:7860`.
80
+
81
+ ### Model Weights
82
+
83
+ The pre-trained TorchScript model (`echotracker_cvamd_ts.pt`) must be present in the project root. It is included in this repository/Space and loaded automatically at startup.
84
+
85
+ ---
86
+
87
+ ## Repository Structure
88
+
89
+ ```
90
+ echotracker/
91
+ ├── app.py # Gradio application and UI
92
+ ├── utils.py # Point-to-tensor conversion and tracking visualisation
93
+ ├── echotracker_cvamd_ts.pt # Pre-trained TorchScript model weights
94
+ ├── example_samples/ # Bundled example echocardiography clips
95
+ │ ├── input1.mp4 # A4C view
96
+ │ ├── input2.mp4 # A4C view (OOD)
97
+ │ ├── input3_RV.mp4 # RV view (OOD)
98
+ │ └── psax_video_crop.mp4 # PSAX view (OOD)
99
+ └── outputs/ # Saved tracking output videos
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Technical Details
105
+
106
+ | Property | Value |
107
+ |---|---|
108
+ | Model format | TorchScript (`.pt`) |
109
+ | Input resolution | 256 × 256 (grayscale) |
110
+ | Max tracking points | 100 |
111
+ | Output video FPS | 25 |
112
+ | Supported views | A4C, RV, PSAX |
113
+ | Device | CUDA (auto) or CPU |
114
+
115
+ The tracker receives a batch of grayscale frames of shape `[B, T, 1, H, W]` and a set of query points `[B, N, 3]` (frame index, x, y). It returns per-point trajectories that are denormalised and overlaid on the original-resolution frames.
116
+
117
+ ---
118
+
119
+ ## Citation
120
+
121
+ If you use EchoTracker in your research, please cite:
122
+
123
+ ```bibtex
124
+ @InProceedings{azad2024echotracker,
125
+ author = {Azad, Md Abulkalam and Chernyshov, Artem and Nyberg, John
126
+ and Tveten, Ingrid and Lovstakken, Lasse and Dalen, H{\aa}vard
127
+ and Grenne, Bj{\o}rnar and {\O}stvik, Andreas},
128
+ title = {EchoTracker: Advancing Myocardial Point Tracking in Echocardiography},
129
+ booktitle = {Medical Image Computing and Computer Assisted Intervention -- MICCAI 2024},
130
+ year = {2024},
131
+ publisher = {Springer Nature Switzerland},
132
+ doi = {10.1007/978-3-031-72083-3_60}
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Authors
139
+
140
+ Md Abulkalam Azad, Artem Chernyshov, John Nyberg, Ingrid Tveten, Lasse Lovstakken, Håvard Dalen, Bjørnar Grenne, Andreas Østvik
141
+
142
+ ---
143
+
144
+ ## License
145
+
146
+ This project is licensed under the [MIT License](LICENSE).
147
+
148
+ > **Note:** The bundled example echocardiography clips are provided for demonstration purposes only and should not be downloaded, reproduced, or used outside this demo.
__pycache__/utils.cpython-311.pyc ADDED
Binary file (6.26 kB). View file
 
app.py ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import torch
4
+ import cv2
5
+ import numpy as np
6
+ import random
7
+ from PIL import Image
8
+ from utils import points_to_tensor
9
+ from utils import visualize_tracking
10
+ import mediapy as media
11
+
12
+ # ── Colormap (matches your viz_utils.get_colors logic) ───────────────────────
13
+ def get_colors(n):
14
+ """Generate n random but unique colors in RGB 0-255."""
15
+ random.seed(42) # remove this line if you want different colors each run
16
+
17
+ # Spread hues evenly across 0-179 (HSV in OpenCV), then shuffle
18
+ hues = list(range(0, 180, max(1, 180 // n)))[:n]
19
+ random.shuffle(hues)
20
+
21
+ colors = []
22
+ for hue in hues:
23
+ # Randomize saturation and value slightly for more visual variety
24
+ sat = random.randint(180, 255)
25
+ val = random.randint(180, 255)
26
+ hsv = np.uint8([[[hue, sat, val]]])
27
+ rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)[0][0]
28
+ colors.append(tuple(int(c) for c in rgb))
29
+
30
+ return colors
31
+
32
+ N_POINTS = 100
33
+ COLORMAP = get_colors(N_POINTS)
34
+ select_points = [] # will hold np.array([x, y]) entries
35
+
36
+ # ── Video helpers ─────────────────────────────────────────────────────────────
37
+ def get_frame(video_path: str, frame_idx: int) -> np.ndarray:
38
+ """Extract a single frame from video by index."""
39
+ cap = cv2.VideoCapture(video_path)
40
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
41
+ ret, frame = cap.read()
42
+ cap.release()
43
+ if not ret:
44
+ raise ValueError(f"Could not read frame {frame_idx}")
45
+ return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
46
+
47
+ def get_total_frames(video_path: str) -> int:
48
+ cap = cv2.VideoCapture(video_path)
49
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
50
+ cap.release()
51
+ return total
52
+
53
+ # ── Draw points on frame ──────────────────────────────────────────────────────
54
+ def draw_points(frame: np.ndarray, points: list) -> np.ndarray:
55
+ """Draw colored circle markers on frame for each selected point."""
56
+ out = frame.copy()
57
+ for i, pt in enumerate(points):
58
+ color = COLORMAP[i % N_POINTS] # RGB tuple
59
+ bgr = (color[2], color[1], color[0]) # cv2 uses BGR
60
+ cv2.circle(out, (pt[0], pt[1]), radius=6,
61
+ color=bgr, thickness=-1)
62
+ cv2.circle(out, (pt[0], pt[1]), radius=6,
63
+ color=(255, 255, 255), thickness=2) # white border
64
+ cv2.putText(out, str(i + 1), (pt[0] + 10, pt[1] - 6),
65
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
66
+ return out
67
+
68
+ _SAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_samples")
69
+
70
+ # JS injected into gr.Blocks — controls download availability on video players
71
+ _DOWNLOAD_CTRL_JS = """
72
+ (function () {
73
+ const EXAMPLE_IDS = ['video_upload_player', 'out_video_player'];
74
+ const USER_IDS = ['out_video_player'];
75
+
76
+ function applyNoDownload(ids) {
77
+ ids.forEach(function (id) {
78
+ var el = document.getElementById(id);
79
+ if (!el) return;
80
+ el.querySelectorAll('video').forEach(function (v) {
81
+ v.setAttribute('controlsList', 'nodownload');
82
+ v.oncontextmenu = function (e) { e.preventDefault(); };
83
+ });
84
+ el.querySelectorAll('a').forEach(function (a) {
85
+ a.style.cssText = 'display:none!important;pointer-events:none!important';
86
+ });
87
+ el.querySelectorAll('button').forEach(function (btn) {
88
+ var lbl = (btn.getAttribute('aria-label') || btn.getAttribute('title') || '').toLowerCase();
89
+ if (lbl.includes('download') || lbl.includes('save')) {
90
+ btn.style.cssText = 'display:none!important;pointer-events:none!important';
91
+ }
92
+ });
93
+ });
94
+ }
95
+
96
+ function clearNoDownload(ids) {
97
+ ids.forEach(function (id) {
98
+ var el = document.getElementById(id);
99
+ if (!el) return;
100
+ el.querySelectorAll('video').forEach(function (v) {
101
+ v.removeAttribute('controlsList');
102
+ v.oncontextmenu = null;
103
+ });
104
+ el.querySelectorAll('a').forEach(function (a) { a.style.cssText = ''; });
105
+ el.querySelectorAll('button').forEach(function (btn) { btn.style.cssText = ''; });
106
+ });
107
+ }
108
+
109
+ window._isExampleMode = false;
110
+
111
+ function applyCurrentMode() {
112
+ if (window._isExampleMode) applyNoDownload(EXAMPLE_IDS);
113
+ else clearNoDownload(USER_IDS);
114
+ }
115
+
116
+ /* Watch both containers for DOM changes (e.g. when video src updates) */
117
+ EXAMPLE_IDS.concat(['out_video_player']).forEach(function (id) {
118
+ (function tryObserve() {
119
+ var el = document.getElementById(id);
120
+ if (!el) { setTimeout(tryObserve, 400); return; }
121
+ new MutationObserver(applyCurrentMode)
122
+ .observe(el, { childList: true, subtree: true });
123
+ })();
124
+ });
125
+
126
+ /* Intercept value setter on hidden textbox to receive mode signal from Python */
127
+ function hookTrigger() {
128
+ var container = document.querySelector('#download_ctrl textarea');
129
+ if (!container) { setTimeout(hookTrigger, 300); return; }
130
+ var desc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
131
+ Object.defineProperty(container, 'value', {
132
+ get: function () { return desc.get.call(this); },
133
+ set: function (v) {
134
+ desc.set.call(this, v);
135
+ window._isExampleMode = (v === '1');
136
+ applyCurrentMode();
137
+ },
138
+ configurable: true,
139
+ });
140
+ }
141
+
142
+ setTimeout(hookTrigger, 500);
143
+ })();
144
+ """
145
+ # label → (path, is_ood)
146
+ EXAMPLE_VIDEOS = {
147
+ "A4C": (os.path.join(_SAMPLES_DIR, "input1.mp4"), False),
148
+ "A4C (OOD)": (os.path.join(_SAMPLES_DIR, "input2.mp4"), True),
149
+ "RV (OOD)": (os.path.join(_SAMPLES_DIR, "input3_RV.mp4"), True),
150
+ "PSAX (OOD)": (os.path.join(_SAMPLES_DIR, "psax_video_crop.mp4"), True),
151
+ }
152
+
153
+ def _get_thumbnail(video_path: str) -> np.ndarray | None:
154
+ """Extract a single frame near the middle of the video for use as a thumbnail."""
155
+ try:
156
+ cap = cv2.VideoCapture(video_path)
157
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
158
+ cap.set(cv2.CAP_PROP_POS_FRAMES, max(0, int(total * 0.4)))
159
+ ret, frame = cap.read()
160
+ cap.release()
161
+ if ret:
162
+ return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
163
+ except Exception:
164
+ pass
165
+ return None
166
+
167
+ THUMBNAILS = {label: _get_thumbnail(path) for label, (path, _) in EXAMPLE_VIDEOS.items()}
168
+
169
+ # ── Gradio event handlers ─────────────────────────────────────────────────────
170
+ def on_video_upload(video_path):
171
+ """Called when video is uploaded — jump to 72% frame."""
172
+ if video_path is None:
173
+ # return None, gr.update(value=0, maximum=0, interactive=False), "No video loaded.", []
174
+ return None
175
+
176
+ total = get_total_frames(video_path)
177
+ idx_72 = int(total * 0.72)
178
+
179
+ frame = get_frame(video_path, idx_72)
180
+ #drawn = draw_points(frame, select_points)
181
+
182
+ frame_display_update = gr.update(
183
+ value=frame,
184
+ interactive=True, # enables click events via gr.SelectData
185
+ )
186
+
187
+ slider_update = gr.update(
188
+ value=idx_72,
189
+ minimum=0,
190
+ maximum=total - 1,
191
+ step=1,
192
+ interactive=True,
193
+ label=f"Frame selector (total: {total} frames)"
194
+ )
195
+
196
+ select_points.clear() # clear any existing points when new video is loaded
197
+
198
+ status = f"📹 Loaded — {total} frames | 🎞️ Showing frame {idx_72} (72%)"
199
+ # last value resets the download-control style (user upload → downloads allowed)
200
+ return frame_display_update, slider_update, status, video_path, ""
201
+
202
+ def load_example(video_path):
203
+ """Load an example video, reset all output/selection fields, and hide downloads."""
204
+ frame_upd, slider_upd, status, state, _ = on_video_upload(video_path)
205
+ return (
206
+ gr.update(value=video_path), # video_upload
207
+ frame_upd, # frame_display
208
+ slider_upd, # frame_slider
209
+ status, # status_text
210
+ state, # video_state
211
+ gr.update(value=None), # out_video — clear previous result
212
+ gr.update(value="No points selected yet."), # points_display
213
+ "1", # download_ctrl — disable downloads
214
+ )
215
+
216
+ def on_slider_release(frame_idx, video_path, points_display):
217
+ """Called when slider is released — show new frame, keep existing points."""
218
+ if video_path is None:
219
+ return None, "No video loaded.", points_display
220
+ frame = get_frame(video_path, int(frame_idx))
221
+ select_points.clear() # clear any existing points when new video is loaded
222
+ #print(f"Selected point: {select_points}")
223
+ points_display = gr.update(
224
+ value="No points selected yet.",
225
+ label="📋 Selected Points",
226
+ lines=5,
227
+ interactive=False,
228
+ )
229
+ #drawn = draw_points(frame, select_points)
230
+ status = f"🎞️ Showing Frame {int(frame_idx)} ({int(frame_idx) / get_total_frames(video_path) * 100:.1f}%) | {len(select_points)} point(s) selected"
231
+ return frame, status, points_display
232
+
233
+ def on_point_select(frame_idx, video_path, evt: gr.SelectData):
234
+ """Called when user clicks on the image — add point, redraw."""
235
+ if video_path is None:
236
+ return None, "Upload a video first.", format_points()
237
+
238
+ if len(select_points) >= N_POINTS:
239
+ status = f"⚠️ Max {N_POINTS} points reached."
240
+ frame = get_frame(video_path, int(frame_idx))
241
+ return draw_points(frame, select_points), status, format_points()
242
+
243
+ x, y = int(evt.index[0]), int(evt.index[1])
244
+ select_points.append(np.array([x, y]))
245
+
246
+ #print(f"Selected point: {select_points}")
247
+
248
+ frame = get_frame(video_path, int(frame_idx))
249
+ drawn = draw_points(frame, select_points)
250
+ status = f"✅ Point {len(select_points)} added at ({x}, {y}) | Frame {int(frame_idx)}"
251
+ return drawn, status, format_points()
252
+
253
+ def on_clear_points(frame_idx, video_path):
254
+ """Clear all selected points."""
255
+ select_points.clear()
256
+ if video_path is None:
257
+ return None, "Points cleared.", format_points()
258
+ frame = get_frame(video_path, int(frame_idx))
259
+ return draw_points(frame, select_points), "🗑️ All points cleared.", format_points()
260
+
261
+ def on_undo_point(frame_idx, video_path):
262
+ """Remove last selected point."""
263
+ if select_points:
264
+ removed = select_points.pop()
265
+ msg = f"↩️ Removed point at ({removed[0]}, {removed[1]})"
266
+ else:
267
+ msg = "No points to undo."
268
+ if video_path is None:
269
+ return None, msg, format_points()
270
+ frame = get_frame(video_path, int(frame_idx))
271
+ return draw_points(frame, select_points), msg, format_points()
272
+
273
+ def format_points():
274
+ """Format select_points for display in the textbox."""
275
+ if not select_points:
276
+ return "No points selected yet."
277
+ lines = [f" [{i+1}] x={p[0]}, y={p[1]}" for i, p in enumerate(select_points)]
278
+ return "select_points:\n" + "\n".join(lines)
279
+
280
+ def track(video_path, frame_idx, out_video, target_size=(256, 256)):
281
+ """Placeholder for tracking function — replace with your actual tracking logic."""
282
+ if video_path is None:
283
+ status = f"⚠️ No video loaded. Cannot run the tracker."
284
+ return status
285
+ if len(select_points) < 1:
286
+ status = f"⚠️ No points selected. Please select at least one point to track."
287
+ return status
288
+
289
+ tracker, device = load_model("echotracker_cvamd_ts.pt")
290
+
291
+ cap = cv2.VideoCapture(video_path)
292
+ W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
293
+ H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
294
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
295
+
296
+ frames = []
297
+ paint_frames = []
298
+ while cap.isOpened():
299
+ ret, frame = cap.read()
300
+ if not ret:
301
+ break
302
+ paint_frames.append(frame)
303
+ frame = cv2.resize(frame, target_size)
304
+ frames.append(Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)))
305
+ cap.release()
306
+ paint_frames = np.array(paint_frames)
307
+ frames = torch.from_numpy(np.array(frames)).unsqueeze(0).unsqueeze(2).float().to(device) # shape: [B, T, H, W]
308
+ q_points = points_to_tensor(select_points, frame_idx, H, W, 256).to(device) # shape: [1, N, 3]
309
+ #print(f"✅ Loaded video frames: {frames.shape} {paint_frames.shape}")
310
+ # print(f"Selected points: {q_points.shape}")
311
+
312
+ with torch.no_grad():
313
+ output = tracker(frames, q_points)
314
+ trajs_e = output[-1].cpu().permute(0, 2, 1, 3)
315
+
316
+ q_points[...,1] /= 256 - 1
317
+ q_points[...,2] /= 256 - 1
318
+ trajs_e[...,0] /= 256 - 1
319
+ trajs_e[...,1] /= 256 - 1
320
+ #print(f"Tracker output trajectories: {trajs_e.shape}")
321
+ paint_frames = visualize_tracking(
322
+ frames=paint_frames, points=trajs_e.squeeze().cpu().numpy(),
323
+ vis_color='random',
324
+ thickness=5,
325
+ track_length=30,
326
+ )
327
+ # Save or display paint_frames as needed (e.g., save as video or show in Gradio)
328
+ out_vid = "outputs/output.mp4"
329
+ os.makedirs("outputs", exist_ok=True)
330
+ media.write_video(out_vid, paint_frames, fps=25)
331
+ status = f"✅ Tracking completed! The output is visualized below."
332
+ out_video = gr.update(value=out_vid, autoplay=True, loop=True)
333
+ return out_video, status
334
+
335
+
336
+ def load_model(model_path: str, device: str = "cuda" if torch.cuda.is_available() else "cpu"):
337
+ """Load a torchscript model
338
+
339
+ Args:
340
+ model_path (str): path to the torchscript weights
341
+ device (str, optional): Defaults to "cuda" if torch.cuda.is_available() else "cpu".
342
+
343
+ Returns:
344
+ model: the loaded torchscript model
345
+ """
346
+ model = torch.jit.load(model_path, map_location=device).eval()
347
+ #print(f"✅ TorchScript model loaded on {device}")
348
+ return model, device
349
+
350
+
351
+ # ── Gradio UI ─────────────────────────────────────────────────────────────────
352
+ HEADER = """
353
+ <div style="text-align:center; padding: 20px 0 8px;">
354
+ <h1 style="font-size:2.2rem; font-weight:700; margin-bottom:4px;">🫀 EchoTracker</h1>
355
+ <p style="font-size:1.05rem; color:var(--echo-muted); margin:4px 0 0;">
356
+ Advancing Myocardial Point Tracking in Echocardiography
357
+ </p>
358
+ <p style="font-size:0.9rem; color:var(--echo-subtle); margin:2px 0 0;">
359
+ MICCAI 2024 &nbsp;·&nbsp;
360
+ Azad, Chernyshov, Nyberg, Tveten, Lovstakken, Dalen, Grenne, Østvik
361
+ </p>
362
+ <div style="margin-top:12px; display:flex; justify-content:center; gap:10px; flex-wrap:wrap;">
363
+ <a href="https://link.springer.com/chapter/10.1007/978-3-031-72083-3_60"
364
+ target="_blank"
365
+ style="display:inline-flex;align-items:center;gap:5px;padding:5px 14px;border-radius:6px;
366
+ background:#2563eb;color:white;font-size:0.85rem;text-decoration:none;font-weight:500;">
367
+ 📄 Paper (MICCAI 2024)
368
+ </a>
369
+ <a href="https://arxiv.org/abs/2405.08587" target="_blank"
370
+ style="display:inline-flex;align-items:center;gap:5px;padding:5px 14px;border-radius:6px;
371
+ background:#dc2626;color:white;font-size:0.85rem;text-decoration:none;font-weight:500;">
372
+ 📝 ArXiv
373
+ </a>
374
+ <a href="https://github.com/riponazad/echotracker" target="_blank"
375
+ style="display:inline-flex;align-items:center;gap:5px;padding:5px 14px;border-radius:6px;
376
+ background:#1f2937;color:white;font-size:0.85rem;text-decoration:none;font-weight:500;">
377
+ 💻 GitHub
378
+ </a>
379
+ <a href="https://riponazad.github.io/echotracker/" target="_blank"
380
+ style="display:inline-flex;align-items:center;gap:5px;padding:5px 14px;border-radius:6px;
381
+ background:#7c3aed;color:white;font-size:0.85rem;text-decoration:none;font-weight:500;">
382
+ 🌐 Project Page
383
+ </a>
384
+ </div>
385
+ </div>
386
+ """
387
+
388
+ CITATION_MD = """
389
+ If you use EchoTracker in your research, please cite:
390
+
391
+ ```bibtex
392
+ @InProceedings{azad2024echotracker,
393
+ author = {Azad, Md Abulkalam and Chernyshov, Artem and Nyberg, John
394
+ and Tveten, Ingrid and Lovstakken, Lasse and Dalen, H{\\aa}vard
395
+ and Grenne, Bj{\\o}rnar and {\\O}stvik, Andreas},
396
+ title = {EchoTracker: Advancing Myocardial Point Tracking in Echocardiography},
397
+ booktitle = {Medical Image Computing and Computer Assisted Intervention -- MICCAI 2024},
398
+ year = {2024},
399
+ publisher = {Springer Nature Switzerland},
400
+ doi = {10.1007/978-3-031-72083-3_60}
401
+ }
402
+ ```
403
+ """
404
+
405
+ with gr.Blocks(title="EchoTracker", theme=gr.themes.Soft(),
406
+ css="""
407
+ .gr-button { font-weight: 600; }
408
+ :root { --echo-muted: #444; --echo-subtle: #666; }
409
+ .dark { --echo-muted: #c0c0c0; --echo-subtle: #a8a8a8; }
410
+ """,
411
+ js=_DOWNLOAD_CTRL_JS) as demo:
412
+
413
+ gr.HTML(HEADER)
414
+ gr.Markdown("---")
415
+
416
+ # ── Instructions ──────────────────────────────────────────────────────────
417
+ with gr.Accordion("ℹ️ How to use", open=False):
418
+ gr.Markdown("""
419
+ 1. **Load a video** — upload your own echocardiography clip, or click one of the provided example videos below the panel.
420
+ 2. **Navigate** to the desired query frame using the frame slider.
421
+ 3. **Click** on the frame image to place tracking points on cardiac tissue surfaces (e.g. LV/RV walls, myocardium).
422
+ 4. Use **Undo** or **Clear All** to adjust your selection.
423
+ 5. Press **▶ Run EchoTracker** to generate tracked trajectories for all selected points.
424
+
425
+ > **Tip:** Select points at the *end-diastolic* frame for best results. Up to 100 points are supported.
426
+ > Example clips cover apical 4-chamber (A4C), right-ventricle (RV), and parasternal short-axis (PSAX) views.
427
+ > Clips marked **OOD** (🔶) are out-of-distribution — different scanner or view not seen during training, showcasing EchoTracker's generalisation ability.
428
+ """)
429
+
430
+ # hidden state
431
+ video_state = gr.State(value=None)
432
+ # injects/removes CSS that hides download buttons on example videos
433
+ download_ctrl = gr.Textbox(value="0", visible=False, elem_id="download_ctrl")
434
+
435
+ gr.Markdown("### Step 1 — Upload & Select Query Points")
436
+ gr.Markdown(
437
+ "Upload your own echocardiography video, or click one of the **example clips** below to get started."
438
+ )
439
+
440
+ with gr.Row(equal_height=False):
441
+ # ── Left column: input + points ───────────────────────────────────────
442
+ with gr.Column(scale=1, min_width=300):
443
+ video_upload = gr.Video(
444
+ label="Echocardiography Video — upload yours or use an example below",
445
+ sources="upload",
446
+ include_audio=False,
447
+ autoplay=True,
448
+ loop=True,
449
+ elem_id="video_upload_player",
450
+ )
451
+ points_display = gr.Textbox(
452
+ value="No points selected yet.",
453
+ label="📋 Selected Query Points",
454
+ lines=5,
455
+ max_lines=5,
456
+ interactive=False,
457
+ )
458
+ gr.Markdown(
459
+ "<small style='color:var(--echo-subtle)'>Coordinates are stored as "
460
+ "<code>np.array([x, y])</code> and passed to the tracker.</small>"
461
+ )
462
+
463
+ # ── Right column: frame viewer + controls ─────────────────────────────
464
+ with gr.Column(scale=2, min_width=400):
465
+ frame_display = gr.Image(
466
+ label="Query Frame — click to place tracking points",
467
+ interactive=True,
468
+ type="numpy",
469
+ sources=[],
470
+ )
471
+ frame_slider = gr.Slider(
472
+ minimum=0, maximum=100, value=0, step=1,
473
+ label="Frame",
474
+ interactive=False,
475
+ )
476
+ status_text = gr.Textbox(
477
+ label="Status", lines=1, interactive=False, show_label=False,
478
+ placeholder="Status messages will appear here…",
479
+ )
480
+ with gr.Row():
481
+ undo_btn = gr.Button("↩ Undo", scale=1)
482
+ clear_btn = gr.Button("🗑 Clear All", variant="stop", scale=1)
483
+
484
+ gr.Markdown("---")
485
+ gr.Markdown("### Step 2 — Run Tracker & View Output")
486
+ with gr.Row():
487
+ with gr.Column(scale=1):
488
+ run_btn = gr.Button("▶ Run EchoTracker", variant="primary", size="lg")
489
+ with gr.Column(scale=2):
490
+ out_video = gr.Video(
491
+ label="Tracking Output",
492
+ sources=[],
493
+ include_audio=False,
494
+ interactive=False,
495
+ autoplay=True,
496
+ loop=True,
497
+ elem_id="out_video_player",
498
+ )
499
+
500
+ gr.Markdown("---")
501
+
502
+ gr.Markdown(
503
+ "**Or try an example clip** "
504
+ "<small style='color:var(--echo-subtle)'>— OOD = out-of-distribution (different scanner / view not seen during training)</small>"
505
+ )
506
+ gr.Markdown(
507
+ "> ⚠️ **Example videos are provided for demonstration purposes only. "
508
+ "They should not be downloaded, reproduced, or used for any purpose outside this demo.**"
509
+ )
510
+ ex_btns = []
511
+ with gr.Row(equal_height=True):
512
+ for label, (path, is_ood) in EXAMPLE_VIDEOS.items():
513
+ with gr.Column(min_width=120):
514
+ gr.Image(
515
+ value=THUMBNAILS[label],
516
+ show_label=False,
517
+ interactive=False,
518
+ height=110,
519
+ container=False,
520
+ )
521
+ btn_label = f"{label} 🔶" if is_ood else label
522
+ ex_btns.append(gr.Button(btn_label, size="sm"))
523
+
524
+ # ── Citation ──────────────────────────────────────────────────────────────
525
+ with gr.Accordion("📝 Citation", open=False):
526
+ gr.Markdown(CITATION_MD)
527
+
528
+ # ── Wire events ───────────────────────────────────────────────────────────
529
+ video_upload.upload(
530
+ fn=on_video_upload,
531
+ inputs=[video_upload],
532
+ outputs=[frame_display, frame_slider, status_text, video_state, download_ctrl]
533
+ )
534
+
535
+ frame_slider.release(
536
+ fn=on_slider_release,
537
+ inputs=[frame_slider, video_state, points_display],
538
+ outputs=[frame_display, status_text, points_display]
539
+ )
540
+
541
+ frame_display.select(
542
+ fn=on_point_select,
543
+ inputs=[frame_slider, video_state],
544
+ outputs=[frame_display, status_text, points_display]
545
+ )
546
+
547
+ undo_btn.click(
548
+ fn=on_undo_point,
549
+ inputs=[frame_slider, video_state],
550
+ outputs=[frame_display, status_text, points_display]
551
+ )
552
+
553
+ clear_btn.click(
554
+ fn=on_clear_points,
555
+ inputs=[frame_slider, video_state],
556
+ outputs=[frame_display, status_text, points_display]
557
+ )
558
+
559
+ for btn, (path, _) in zip(ex_btns, EXAMPLE_VIDEOS.values()):
560
+ btn.click(
561
+ fn=load_example,
562
+ inputs=gr.State(path),
563
+ outputs=[video_upload, frame_display, frame_slider, status_text, video_state,
564
+ out_video, points_display, download_ctrl]
565
+ )
566
+
567
+ run_btn.click(
568
+ fn=track,
569
+ inputs=[video_state, frame_slider, out_video],
570
+ outputs=[out_video, status_text]
571
+ )
572
+
573
+ demo.launch(share=False)
echotracker_cvamd_ts.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:596e5357d25cc6fc246bb0f32f0ab12c1dabb521d9577c6207f07a7ccdc03281
3
+ size 40905188
example_samples/input1.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2bff8916f610b91c34165983d780590d556b627643f58c0733f59093e608f98
3
+ size 878926
example_samples/input2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:431f6920f0e88de2ed8dec17b52f51c0ceed358885fcb43ef27f2fc462b0b7c7
3
+ size 386306
example_samples/input3_RV.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ed04601df34c55e8aa98fc8872ec4665557a2ba9a65beda915c7f1ab3139b9a
3
+ size 1364528
example_samples/psax_video_crop.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed6195d6aed88725dd1fa41ffee5d8d2bbb6db8770a33d65b56657cf1d60ae81
3
+ size 1583236
outputs/output.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2ad3045a6d841562f52736a6fa49ce57050d2b1682897417511225599152bdb6
3
+ size 630372
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ numpy>=1.24.0
3
+ opencv-python-headless>=4.8.0
4
+ Pillow>=9.5.0
5
+ mediapy>=1.2.0
6
+ scikit-image>=0.21.0
7
+ gradio==6.12.0
utils.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import cv2
3
+ import numpy as np
4
+ from skimage.color import gray2rgb
5
+
6
+
7
+ def points_to_tensor(points: list, qt: int, orig_H: int, orig_W: int, target: int = 256) -> torch.Tensor:
8
+ """
9
+ Convert [(x1,y1), ..., (xn,yn)] to tensor of shape [1, n, 3]
10
+ where last dim is (qt, x, y), with x/y scaled to target resolution.
11
+
12
+ Args:
13
+ points : list of (x, y) tuples or np.array([x, y])
14
+ qt : single int, same for all points
15
+ orig_H : original frame height
16
+ orig_W : original frame width
17
+ target : target resolution (default 256)
18
+
19
+ Returns:
20
+ tensor of shape [1, n, 3], dtype float32
21
+ """
22
+ scale_x = target / orig_W
23
+ scale_y = target / orig_H
24
+
25
+ arr = np.array(
26
+ [[qt, p[0] * scale_x, p[1] * scale_y] for p in points],
27
+ dtype=np.float32
28
+ ) # (n, 3)
29
+
30
+ return torch.tensor(arr).unsqueeze(0) # (1, n, 3)
31
+
32
+
33
+
34
+ def visualize_tracking(
35
+ frames: np.ndarray,
36
+ points: np.ndarray,
37
+ tracking_quality: np.ndarray = None,
38
+ vis_color='random',
39
+ color_map: np.ndarray = None,
40
+ gray: bool = False,
41
+ alpha: float = 1.0,
42
+ track_length: int = 0,
43
+ thickness: int = 2,
44
+ ) -> np.ndarray:
45
+
46
+ num_points, num_frames = points.shape[:2]
47
+ height, width = frames.shape[1:3]
48
+
49
+ if gray and frames.shape[-1] != 3:
50
+ frames = gray2rgb(frames.squeeze())
51
+
52
+ radius = max(6, int(0.006 * min(height, width)))
53
+
54
+ quality_colors = {
55
+ 0: np.array([255, 0, 0]),
56
+ 1: np.array([255, 255, 0]),
57
+ 2: np.array([0, 255, 0]),
58
+ }
59
+
60
+ video = frames.copy()
61
+
62
+ # Stable random colors
63
+ if vis_color == 'random' and tracking_quality is None and color_map is None:
64
+ rand_colors = np.random.randint(0, 256, size=(num_points, 3))
65
+
66
+ for t in range(num_frames):
67
+ overlay = np.zeros_like(video[t], dtype=np.uint8)
68
+ t_start = max(1, t - track_length)
69
+
70
+ for i in range(num_points):
71
+
72
+ # -------------------------------------------------
73
+ # Resolve color ONCE (fixes UnboundLocalError)
74
+ # -------------------------------------------------
75
+ if tracking_quality is not None:
76
+ color = quality_colors.get(
77
+ int(tracking_quality[i, t]),
78
+ np.array([255, 255, 255])
79
+ )
80
+
81
+ elif color_map is not None:
82
+ color = np.asarray(color_map[i])
83
+
84
+ elif isinstance(vis_color, (list, tuple, np.ndarray)):
85
+ color = np.asarray(vis_color)
86
+
87
+ else:
88
+ if vis_color == 'random':
89
+ color = rand_colors[i]
90
+ elif vis_color == 'red':
91
+ color = quality_colors[0]
92
+ elif vis_color == 'yellow':
93
+ color = quality_colors[1]
94
+ elif vis_color == 'green':
95
+ color = quality_colors[2]
96
+ else:
97
+ raise ValueError(f"Unknown vis_color: {vis_color}")
98
+
99
+ color = color.astype(np.uint8)
100
+
101
+ # -------------------------------------------------
102
+ # Draw track lines
103
+ # -------------------------------------------------
104
+ for tt in range(t_start, t):
105
+ fade = (tt - t_start + 1) / max(1, (t - t_start))
106
+
107
+ x0n, y0n = points[i, tt - 1]
108
+ x1n, y1n = points[i, tt]
109
+
110
+ x0 = int(np.clip(x0n * width, 0, width - 1))
111
+ y0 = int(np.clip(y0n * height, 0, height - 1))
112
+ x1 = int(np.clip(x1n * width, 0, width - 1))
113
+ y1 = int(np.clip(y1n * height, 0, height - 1))
114
+
115
+ faded_color = (color * fade).astype(np.uint8)
116
+
117
+ cv2.line(
118
+ overlay,
119
+ (x0, y0),
120
+ (x1, y1),
121
+ faded_color.tolist(),
122
+ thickness=thickness,
123
+ lineType=cv2.LINE_AA
124
+ )
125
+
126
+ # -------------------------------------------------
127
+ # Draw dot (current position)
128
+ # -------------------------------------------------
129
+ xc = int(points[i, t, 0] * width)
130
+ yc = int(points[i, t, 1] * height)
131
+
132
+ cv2.circle(
133
+ overlay,
134
+ (xc, yc),
135
+ radius=radius,
136
+ color=color.tolist(),
137
+ thickness=-1
138
+ )
139
+
140
+ video[t] = cv2.addWeighted(video[t], 1.0, overlay, alpha, 0)
141
+
142
+ return video