isambalghari commited on
Commit
60bbdb2
Β·
1 Parent(s): 33a72a2

editing tracker

Browse files
dumy.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+
4
+ def dummy_fn(img):
5
+ return img
6
+
7
+ # Create transparent base (RGBA)
8
+ canvas = np.zeros((480, 640, 4), dtype=np.uint8)
9
+ canvas[..., 0] = 255 # R
10
+ canvas[..., 1] = 105 # G
11
+ canvas[..., 2] = 180 # B
12
+ canvas[..., 3] = 60 # alpha for semi-transparent pink background
13
+
14
+ iface = gr.Interface(
15
+ fn=dummy_fn,
16
+ inputs=gr.Image(
17
+ label="πŸ–ŒοΈ Paint ROI Mask",
18
+ tool="sketch",
19
+ type="numpy",
20
+ image_mode="RGBA",
21
+ value=canvas,
22
+ height=480,
23
+ ),
24
+ outputs="image"
25
+ )
26
+
27
+ iface.launch()
full_tracked_output.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a40a50c5a05f63a00cefd584874fa87e2346d89d6e5c40f5065c2000af0453f
3
+ size 1343470
mask_output.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7fa9d49423d0da4aad10d35e12f8b1f672d27607dbf2c9a9d64bf526b66afc12
3
+ size 363292
reversed_input.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1793dc36e250e315116923189df2e074f0f4620e3cc13b9c8a7d00c242e9e567
3
+ size 4484439
stabilized_mask_output.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36201117ba3818d6d3356312485bc62a64a0f7f709dd9fbdbb1f9e856465a48c
3
+ size 953497
track-pixels_gradio.py CHANGED
@@ -1,15 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
2
  import cv2
3
  import math
 
4
  import torch
5
  import numpy as np
 
6
  from tqdm import tqdm
7
  from pathlib import Path
8
- from torchvision import transforms
9
- import gradio as gr
10
  from argparse import Namespace
11
- import sys
12
- import time
13
 
14
  # === RAFT Setup ===
15
  sys.path.append("/app/preprocess/RAFT/core")
@@ -29,7 +450,6 @@ REVERSED_INPUT = "/app/reversed_input.mp4"
29
  # ==========================================================
30
 
31
  def reverse_video(input_path, output_path):
32
- """Reverse frames of input video and save as output."""
33
  cap = cv2.VideoCapture(input_path)
34
  if not cap.isOpened():
35
  raise FileNotFoundError(f"❌ Could not open video: {input_path}")
@@ -48,17 +468,13 @@ def reverse_video(input_path, output_path):
48
  frames.append(frame)
49
  cap.release()
50
 
51
- # Write reversed frames
52
  for frame in reversed(frames):
53
  out.write(frame)
54
  out.release()
55
-
56
  print(f"πŸ” Video reversed and saved: {output_path}")
57
  return output_path
58
 
59
-
60
  def reverse_video_file_inplace(path_in):
61
- """Reverse an existing video and overwrite it."""
62
  tmp_path = path_in.replace(".mp4", "_tmp.mp4")
63
  reverse_video(path_in, tmp_path)
64
  os.replace(tmp_path, path_in)
@@ -126,38 +542,50 @@ def save_mask(data):
126
  return mask_path, f"βœ… Saved mask ({np.count_nonzero(bin_mask)} painted pixels)"
127
 
128
  # ==========================================================
129
- # === CROP HELPERS =========================================
130
  # ==========================================================
131
 
132
- def get_mask_center(mask_path):
 
 
 
 
133
  mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
134
  if mask is None:
135
- raise FileNotFoundError("Mask not found: " + mask_path)
 
 
136
  ys, xs = np.where(mask > 0)
137
- h, w = mask.shape[:2]
 
138
  if len(xs) == 0:
139
- return w // 2, h // 2
140
- return int(np.mean(xs)), int(np.mean(ys))
141
-
142
- def clamp_crop(x0, y0, cw, ch, W, H):
143
- x0 = max(0, min(x0, W - 1))
144
- y0 = max(0, min(y0, H - 1))
145
- x1 = x0 + cw
146
- y1 = y0 + ch
147
- if x1 > W:
148
- x0 -= (x1 - W)
149
- x1 = W
150
- if y1 > H:
151
- y0 -= (y1 - H)
152
- y1 = H
153
- return x0, y0, x1, y1
154
-
155
- def compute_crop_box_from_mask(first_frame_bgr, mask_path, crop_w=400, crop_h=400):
156
- H, W = first_frame_bgr.shape[:2]
157
- cx, cy = get_mask_center(mask_path)
158
- x0 = cx - crop_w // 2
159
- y0 = cy - crop_h // 2
160
- return clamp_crop(x0, y0, crop_w, crop_h, W, H)
 
 
 
 
 
161
 
162
  def draw_crop_preview_on_frame(frame_rgb, crop_box, color=(0,255,0), thickness=2):
163
  x0, y0, x1, y1 = crop_box
@@ -170,7 +598,6 @@ def draw_crop_preview_on_frame(frame_rgb, crop_box, color=(0,255,0), thickness=2
170
  # ==========================================================
171
 
172
  def stabilize_black_regions(input_video):
173
- # === Define kernels ===
174
  kernel_fill = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
175
  kernel_edge = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
176
 
@@ -189,11 +616,9 @@ def stabilize_black_regions(input_video):
189
  if not ret:
190
  break
191
 
192
- # Convert to grayscale and threshold
193
  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
194
  _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
195
 
196
- # === Step 1: Fill black regions ===
197
  inv = cv2.bitwise_not(mask)
198
  flood = inv.copy()
199
  h, w = inv.shape
@@ -203,13 +628,9 @@ def stabilize_black_regions(input_video):
203
  filled = cv2.bitwise_or(inv, holes)
204
  filled = cv2.bitwise_not(filled)
205
 
206
- # === Step 2: Morphological stabilization ===
207
- # Fill small black holes and unify mask
208
  stable = cv2.morphologyEx(filled, cv2.MORPH_CLOSE, kernel_fill, iterations=1)
209
- # Smooth jagged edges
210
  stable = cv2.morphologyEx(stable, cv2.MORPH_OPEN, kernel_edge, iterations=1)
211
 
212
- # Write result
213
  out.write(cv2.cvtColor(stable, cv2.COLOR_GRAY2BGR))
214
 
215
  cap.release()
@@ -217,12 +638,14 @@ def stabilize_black_regions(input_video):
217
  print(f"βœ… Stabilized mask saved: {STABILIZED_MASK}")
218
  return STABILIZED_MASK
219
 
220
-
221
  # ==========================================================
222
  # === TRACKING =============================================
223
  # ==========================================================
224
 
225
- def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400, crop_h=400):
 
 
 
226
  reversed_path = reverse_video(video_path, REVERSED_INPUT)
227
  cap = cv2.VideoCapture(reversed_path)
228
  model = load_raft_model(MODEL_PATH)
@@ -233,7 +656,7 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400,
233
  return "❌ Could not read first frame.", None, None, None
234
  H, W = first_frame.shape[:2]
235
 
236
- x0, y0, x1, y1 = compute_crop_box_from_mask(first_frame, mask_path, crop_w, crop_h)
237
  cw, ch = x1 - x0, y1 - y0
238
 
239
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
@@ -248,7 +671,7 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400,
248
  ys, xs = np.where(crop_mask > 0)
249
  else:
250
  gray_first = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
251
- black_pixels = (gray_first[y0:y1, x0:x1] < 30)
252
  combined = (crop_mask > 0) & black_pixels
253
  ys, xs = np.where(combined)
254
 
@@ -256,14 +679,21 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400,
256
  prev_full_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
257
  prev_crop_rgb = prev_full_rgb[y0:y1, x0:x1]
258
 
 
 
 
 
259
  while True:
260
  ret, curr_frame = cap.read()
261
  if not ret:
262
  break
 
263
  curr_full_rgb = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2RGB)
264
  curr_crop_rgb = curr_full_rgb[y0:y1, x0:x1]
 
265
 
266
  flow_crop = compute_flow(model, prev_crop_rgb, curr_crop_rgb)
 
267
  vis_full = curr_full_rgb.copy()
268
  mask_full = np.full((H, W), 255, dtype=np.uint8)
269
 
@@ -276,28 +706,55 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400,
276
  nx = np.clip(nx, 0, cw-1)
277
  ny = np.clip(ny, 0, ch-1)
278
  new_points.append([nx, ny])
279
- fx, fy = int(nx + x0), int(ny + y0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  if 0 <= fx < W and 0 <= fy < H:
281
  cv2.circle(vis_full, (fx, fy), 1, (0,255,0), -1)
282
  mask_full[fy, fx] = 0
283
- tracked_points = np.array(new_points, dtype=np.float32)
284
  out_vis.write(cv2.cvtColor(vis_full, cv2.COLOR_RGB2BGR))
285
  out_mask.write(mask_full)
286
  prev_crop_rgb = curr_crop_rgb
287
 
 
 
 
288
  cap.release()
289
  out_vis.release()
290
  out_mask.release()
291
 
292
  stabilize_black_regions(OUTPUT_MASK_VIDEO)
293
 
294
- # Reverse outputs back to forward direction
295
  reverse_video_file_inplace(OUTPUT_VIDEO)
296
  reverse_video_file_inplace(OUTPUT_MASK_VIDEO)
297
  reverse_video_file_inplace(STABILIZED_MASK)
298
 
299
  return (
300
- f"βœ… Tracking complete ({selection_mode}).\nCrop {cw}x{ch} @ ({x0},{y0})\nSaved outputs reversed back to forward order.",
 
 
 
301
  OUTPUT_VIDEO,
302
  OUTPUT_MASK_VIDEO,
303
  STABILIZED_MASK
@@ -309,27 +766,28 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400,
309
 
310
  def build_app():
311
  with gr.Blocks() as demo:
312
- gr.Markdown("## 🎯 RAFT Pixel Tracker (Reversed Input Pipeline, Forward Outputs)")
 
313
  with gr.Row():
314
  video_in = gr.Video(label="🎞️ Upload Video")
315
  frame_num = gr.Number(value=0, visible=False)
 
316
  load_btn = gr.Button("πŸ“Έ Load Frame for Annotation")
317
- annot = gr.Image(label="πŸ–ŒοΈ Paint ROI Mask", tool="sketch", type="numpy", image_mode="RGBA", height=480)
318
  save_btn = gr.Button("πŸ’Ύ Save Mask")
319
  log = gr.Textbox(label="Logs", lines=8)
320
- pixel_mode = gr.Dropdown(["All Pixels", "Only Black Pixels"], value="All Pixels")
321
- crop_w = gr.Number(value=400, label="Crop Width")
322
- crop_h = gr.Number(value=400, label="Crop Height")
323
- preview_btn = gr.Button("πŸ”Ž Preview Crop")
324
- preview_frame = gr.Image(label="Preview Frame")
325
- preview_crop = gr.Image(label="Cropped Region")
326
  run_btn = gr.Button("πŸš€ Run Tracking")
327
  with gr.Row():
328
  result_video = gr.Video(label="🎬 Result (Forward)")
329
  mask_video = gr.Video(label="⬛ Mask (Forward)")
330
  stabilized_video = gr.Video(label="🧱 Stabilized (Forward)")
331
 
332
- # Load reversed frame for painting
333
  def load_reversed_frame(v, f):
334
  reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
335
  return extract_frame(reversed_path, int(f))
@@ -337,26 +795,27 @@ def build_app():
337
  load_btn.click(load_reversed_frame, [video_in, frame_num], annot)
338
  save_btn.click(save_mask, annot, [gr.State(), log])
339
 
340
- def preview_crop_fn(v, cw, ch):
341
  reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
342
  frame0 = extract_frame(reversed_path, 0)
343
  if frame0 is None or not os.path.exists("user_mask.png"):
344
  return None, None, "⚠️ Paint and Save Mask first."
345
- x0,y0,x1,y1 = compute_crop_box_from_mask(cv2.cvtColor(frame0, cv2.COLOR_RGB2BGR), "user_mask.png", int(cw), int(ch))
346
  frame_box = draw_crop_preview_on_frame(frame0, (x0,y0,x1,y1))
347
- return frame_box, frame0[y0:y1, x0:x1], f"Crop {cw}x{ch} at ({x0},{y0})"
348
 
349
- preview_btn.click(preview_crop_fn, [video_in, crop_w, crop_h], [preview_frame, preview_crop, log])
350
 
351
- def run_btn_fn(v, m, cw, ch):
352
  if not os.path.exists("user_mask.png"):
353
  return "⚠️ Save Mask first.", None, None, None
354
- return run_tracking(v.name if hasattr(v, "name") else v, "user_mask.png", m, int(cw), int(ch))
355
 
356
- run_btn.click(run_btn_fn, [video_in, pixel_mode, crop_w, crop_h],
357
  [log, result_video, mask_video, stabilized_video])
 
358
  return demo
359
 
360
  if __name__ == "__main__":
361
  app = build_app()
362
- app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
1
+ # import os
2
+ # import sys
3
+ # import cv2
4
+ # import math
5
+ # import time
6
+ # import torch
7
+ # import numpy as np
8
+ # import gradio as gr
9
+ # from tqdm import tqdm
10
+ # from pathlib import Path
11
+ # from collections import deque
12
+ # from argparse import Namespace
13
+ # from torchvision import transforms
14
+
15
+ # # === RAFT Setup ===
16
+ # sys.path.append("/app/preprocess/RAFT/core")
17
+ # from raft import RAFT
18
+ # from utils.utils import InputPadder
19
+
20
+ # # === CONFIG ===
21
+ # DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
22
+ # MODEL_PATH = "/app/RAFT/raft-things.pth"
23
+ # OUTPUT_VIDEO = "/app/full_tracked_output.mp4"
24
+ # OUTPUT_MASK_VIDEO = "/app/mask_output.mp4"
25
+ # STABILIZED_MASK = "/app/stabilized_mask_output.mp4"
26
+ # REVERSED_INPUT = "/app/reversed_input.mp4"
27
+
28
+ # # ==========================================================
29
+ # # === VIDEO UTILITIES =====================================
30
+ # # ==========================================================
31
+
32
+ # def reverse_video(input_path, output_path):
33
+ # """Reverse frames of input video and save as output."""
34
+ # cap = cv2.VideoCapture(input_path)
35
+ # if not cap.isOpened():
36
+ # raise FileNotFoundError(f"❌ Could not open video: {input_path}")
37
+
38
+ # fps = cap.get(cv2.CAP_PROP_FPS)
39
+ # width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
40
+ # height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
41
+ # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
42
+ # out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
43
+
44
+ # frames = []
45
+ # while True:
46
+ # ret, frame = cap.read()
47
+ # if not ret:
48
+ # break
49
+ # frames.append(frame)
50
+ # cap.release()
51
+
52
+ # # Write reversed frames
53
+ # for frame in reversed(frames):
54
+ # out.write(frame)
55
+ # out.release()
56
+
57
+ # print(f"πŸ” Video reversed and saved: {output_path}")
58
+ # return output_path
59
+
60
+
61
+ # def reverse_video_file_inplace(path_in):
62
+ # """Reverse an existing video and overwrite it."""
63
+ # tmp_path = path_in.replace(".mp4", "_tmp.mp4")
64
+ # reverse_video(path_in, tmp_path)
65
+ # os.replace(tmp_path, path_in)
66
+
67
+ # # ==========================================================
68
+ # # === RAFT LOADING =========================================
69
+ # # ==========================================================
70
+
71
+ # def load_raft_model(model_path):
72
+ # args = Namespace(
73
+ # small=False,
74
+ # mixed_precision=False,
75
+ # alternate_corr=False,
76
+ # dropout=0.0,
77
+ # max_depth=16,
78
+ # depth_network=False,
79
+ # depth_residual=False,
80
+ # depth_scale=1.0
81
+ # )
82
+ # model = torch.nn.DataParallel(RAFT(args))
83
+ # model.load_state_dict(torch.load(model_path, map_location=DEVICE))
84
+ # return model.module.to(DEVICE).eval()
85
+
86
+ # def to_tensor(image):
87
+ # return transforms.ToTensor()(image).unsqueeze(0).to(DEVICE)
88
+
89
+ # @torch.no_grad()
90
+ # def compute_flow(model, img1, img2):
91
+ # t1, t2 = to_tensor(img1), to_tensor(img2)
92
+ # padder = InputPadder(t1.shape)
93
+ # t1, t2 = padder.pad(t1, t2)
94
+ # _, flow = model(t1, t2, iters=30, test_mode=True)
95
+ # flow = padder.unpad(flow)[0]
96
+ # return flow.permute(1, 2, 0).cpu().numpy()
97
+
98
+ # # ==========================================================
99
+ # # === FRAME / MASK HELPERS ================================
100
+ # # ==========================================================
101
+
102
+ # def extract_frame(video_path, frame_number):
103
+ # cap = cv2.VideoCapture(video_path)
104
+ # cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
105
+ # ret, frame = cap.read()
106
+ # cap.release()
107
+ # if not ret:
108
+ # return None
109
+ # return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
110
+
111
+ # def save_mask(data):
112
+ # if data is None:
113
+ # return None, "⚠️ No mask data received!"
114
+ # if isinstance(data, dict):
115
+ # mask = data.get("mask")
116
+ # else:
117
+ # mask = data
118
+ # if mask is None:
119
+ # return None, "⚠️ Mask missing!"
120
+ # if mask.ndim == 3:
121
+ # mask_gray = cv2.cvtColor(mask, cv2.COLOR_RGBA2GRAY)
122
+ # else:
123
+ # mask_gray = mask
124
+ # _, bin_mask = cv2.threshold(mask_gray, 1, 255, cv2.THRESH_BINARY)
125
+ # mask_path = "user_mask.png"
126
+ # cv2.imwrite(mask_path, bin_mask)
127
+ # return mask_path, f"βœ… Saved mask ({np.count_nonzero(bin_mask)} painted pixels)"
128
+
129
+ # # ==========================================================
130
+ # # === CROP HELPERS =========================================
131
+ # # ==========================================================
132
+
133
+ # def get_mask_center(mask_path):
134
+ # mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
135
+ # if mask is None:
136
+ # raise FileNotFoundError("Mask not found: " + mask_path)
137
+ # ys, xs = np.where(mask > 0)
138
+ # h, w = mask.shape[:2]
139
+ # if len(xs) == 0:
140
+ # return w // 2, h // 2
141
+ # return int(np.mean(xs)), int(np.mean(ys))
142
+
143
+ # def clamp_crop(x0, y0, cw, ch, W, H):
144
+ # x0 = max(0, min(x0, W - 1))
145
+ # y0 = max(0, min(y0, H - 1))
146
+ # x1 = x0 + cw
147
+ # y1 = y0 + ch
148
+ # if x1 > W:
149
+ # x0 -= (x1 - W)
150
+ # x1 = W
151
+ # if y1 > H:
152
+ # y0 -= (y1 - H)
153
+ # y1 = H
154
+ # return x0, y0, x1, y1
155
+
156
+ # def compute_crop_box_from_mask(first_frame_bgr, mask_path, crop_w=400, crop_h=400):
157
+ # H, W = first_frame_bgr.shape[:2]
158
+ # cx, cy = get_mask_center(mask_path)
159
+ # x0 = cx - crop_w // 2
160
+ # y0 = cy - crop_h // 2
161
+ # return clamp_crop(x0, y0, crop_w, crop_h, W, H)
162
+
163
+
164
+ # def draw_crop_preview_on_frame(frame_rgb, crop_box, color=(0,255,0), thickness=2):
165
+ # x0, y0, x1, y1 = crop_box
166
+ # frame = frame_rgb.copy()
167
+ # cv2.rectangle(frame, (x0, y0), (x1, y1), color, thickness)
168
+ # return frame
169
+
170
+ # # ==========================================================
171
+ # # === STABILIZATION ========================================
172
+ # # ==========================================================
173
+
174
+ # def stabilize_black_regions(input_video):
175
+ # # === Define kernels ===
176
+ # kernel_fill = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
177
+ # kernel_edge = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
178
+
179
+ # cap = cv2.VideoCapture(input_video)
180
+ # if not cap.isOpened():
181
+ # raise FileNotFoundError(f"❌ Could not open video: {input_video}")
182
+
183
+ # fps = cap.get(cv2.CAP_PROP_FPS)
184
+ # width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
185
+ # height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
186
+ # fourcc = cv2.VideoWriter_fourcc(*"mp4v")
187
+ # out = cv2.VideoWriter(STABILIZED_MASK, fourcc, fps, (width, height))
188
+
189
+ # while True:
190
+ # ret, frame = cap.read()
191
+ # if not ret:
192
+ # break
193
+
194
+ # # Convert to grayscale and threshold
195
+ # gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
196
+ # _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
197
+
198
+ # # === Step 1: Fill black regions ===
199
+ # inv = cv2.bitwise_not(mask)
200
+ # flood = inv.copy()
201
+ # h, w = inv.shape
202
+ # flood_mask = np.zeros((h + 2, w + 2), np.uint8)
203
+ # cv2.floodFill(flood, flood_mask, (0, 0), 255)
204
+ # holes = cv2.bitwise_not(flood)
205
+ # filled = cv2.bitwise_or(inv, holes)
206
+ # filled = cv2.bitwise_not(filled)
207
+
208
+ # # === Step 2: Morphological stabilization ===
209
+ # # Fill small black holes and unify mask
210
+ # stable = cv2.morphologyEx(filled, cv2.MORPH_CLOSE, kernel_fill, iterations=1)
211
+ # # Smooth jagged edges
212
+ # stable = cv2.morphologyEx(stable, cv2.MORPH_OPEN, kernel_edge, iterations=1)
213
+
214
+ # # Write result
215
+ # out.write(cv2.cvtColor(stable, cv2.COLOR_GRAY2BGR))
216
+
217
+ # cap.release()
218
+ # out.release()
219
+ # print(f"βœ… Stabilized mask saved: {STABILIZED_MASK}")
220
+ # return STABILIZED_MASK
221
+
222
+
223
+ # # ==========================================================
224
+ # # === TRACKING =============================================
225
+ # # ==========================================================
226
+
227
+ # def run_tracking(video_path, mask_path, selection_mode="All Pixels", crop_w=400, crop_h=400):
228
+ # BLACK_THRESH = 1
229
+ # HISTORY_LEN = 5
230
+
231
+ # reversed_path = reverse_video(video_path, REVERSED_INPUT)
232
+ # cap = cv2.VideoCapture(reversed_path)
233
+ # model = load_raft_model(MODEL_PATH)
234
+
235
+ # fps = cap.get(cv2.CAP_PROP_FPS)
236
+ # ret, first_frame = cap.read()
237
+ # if not ret:
238
+ # return "❌ Could not read first frame.", None, None, None
239
+ # H, W = first_frame.shape[:2]
240
+
241
+ # x0, y0, x1, y1 = compute_crop_box_from_mask(first_frame, mask_path, crop_w, crop_h)
242
+ # cw, ch = x1 - x0, y1 - y0
243
+
244
+ # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
245
+ # out_vis = cv2.VideoWriter(OUTPUT_VIDEO, fourcc, fps, (W, H))
246
+ # out_mask = cv2.VideoWriter(OUTPUT_MASK_VIDEO, fourcc, fps, (W, H), isColor=False)
247
+
248
+ # full_mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
249
+ # full_mask = cv2.resize(full_mask, (W, H), interpolation=cv2.INTER_NEAREST)
250
+ # crop_mask = full_mask[y0:y1, x0:x1]
251
+
252
+ # if selection_mode == "All Pixels":
253
+ # ys, xs = np.where(crop_mask > 0)
254
+ # else:
255
+ # gray_first = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
256
+ # black_pixels = (gray_first[y0:y1, x0:x1] < BLACK_THRESH)
257
+ # combined = (crop_mask > 0) & black_pixels
258
+ # ys, xs = np.where(combined)
259
+
260
+ # tracked_points = np.vstack((xs, ys)).T.astype(np.float32)
261
+ # prev_full_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
262
+ # prev_crop_rgb = prev_full_rgb[y0:y1, x0:x1]
263
+
264
+ # # Frame-level history (deque of last 5 black-region detections)
265
+ # history = deque([True]*HISTORY_LEN, maxlen=HISTORY_LEN)
266
+ # stopped = False
267
+
268
+ # frame_idx = 0
269
+ # while True:
270
+ # ret, curr_frame = cap.read()
271
+ # if not ret:
272
+ # break
273
+ # frame_idx += 1
274
+ # curr_full_rgb = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2RGB)
275
+ # curr_crop_rgb = curr_full_rgb[y0:y1, x0:x1]
276
+ # gray_crop = cv2.cvtColor(curr_crop_rgb, cv2.COLOR_RGB2GRAY)
277
+
278
+ # # --- Compute optical flow ---
279
+ # flow_crop = compute_flow(model, prev_crop_rgb, curr_crop_rgb)
280
+
281
+ # vis_full = curr_full_rgb.copy()
282
+ # mask_full = np.full((H, W), 255, dtype=np.uint8)
283
+
284
+ # # --- Move points ---
285
+ # new_points = []
286
+ # for pt in tracked_points:
287
+ # px, py = int(pt[0]), int(pt[1])
288
+ # if 0 <= px < cw and 0 <= py < ch:
289
+ # dx, dy = flow_crop[py, px]
290
+ # nx, ny = pt[0] + dx, pt[1] + dy
291
+ # nx = np.clip(nx, 0, cw-1)
292
+ # ny = np.clip(ny, 0, ch-1)
293
+ # new_points.append([nx, ny])
294
+ # tracked_points = np.array(new_points, dtype=np.float32)
295
+
296
+ # # --- Check black presence ---
297
+ # black_mask = gray_crop < BLACK_THRESH
298
+ # black_indices = tracked_points.astype(int)
299
+ # has_black = False
300
+ # for (px, py) in black_indices:
301
+ # if 0 <= px < cw and 0 <= py < ch:
302
+ # if black_mask[py, px]:
303
+ # has_black = True
304
+ # break
305
+ # history.append(has_black)
306
+
307
+ # # --- Determine painting condition ---
308
+ # if stopped:
309
+ # paint = False
310
+ # elif has_black:
311
+ # paint = True
312
+ # elif not any(history): # last 5 all False
313
+ # stopped = True
314
+ # paint = False
315
+ # else:
316
+ # paint = True
317
+
318
+ # # --- Paint or skip ---
319
+ # if paint:
320
+ # for pt in tracked_points:
321
+ # fx, fy = int(pt[0] + x0), int(pt[1] + y0)
322
+ # if 0 <= fx < W and 0 <= fy < H:
323
+ # cv2.circle(vis_full, (fx, fy), 1, (0,255,0), -1)
324
+ # mask_full[fy, fx] = 0
325
+ # else:
326
+ # # no painting this frame
327
+ # pass
328
+
329
+ # out_vis.write(cv2.cvtColor(vis_full, cv2.COLOR_RGB2BGR))
330
+ # out_mask.write(mask_full)
331
+ # prev_crop_rgb = curr_crop_rgb
332
+
333
+ # # Optional: progress log
334
+ # if frame_idx % 10 == 0:
335
+ # print(f"Frame {frame_idx}: {'PAINT' if paint else 'NO-PAINT'} | has_black={has_black} | stopped={stopped}")
336
+
337
+ # cap.release()
338
+ # out_vis.release()
339
+ # out_mask.release()
340
+
341
+ # stabilize_black_regions(OUTPUT_MASK_VIDEO)
342
+
343
+ # reverse_video_file_inplace(OUTPUT_VIDEO)
344
+ # reverse_video_file_inplace(OUTPUT_MASK_VIDEO)
345
+ # reverse_video_file_inplace(STABILIZED_MASK)
346
+
347
+ # return (
348
+ # f"βœ… Tracking complete ({selection_mode}).\n"
349
+ # f"Crop {cw}x{ch} @ ({x0},{y0})\n"
350
+ # f"Painting stopped={'Yes' if stopped else 'No'} after {frame_idx} frames.\n"
351
+ # "Saved outputs reversed back to forward order.",
352
+ # OUTPUT_VIDEO,
353
+ # OUTPUT_MASK_VIDEO,
354
+ # STABILIZED_MASK
355
+ # )
356
+
357
+ # # ==========================================================
358
+ # # === GRADIO APP ===========================================
359
+ # # ==========================================================
360
+
361
+ # def build_app():
362
+ # with gr.Blocks() as demo:
363
+ # gr.Markdown("# 🎯 Pixel Tracker ")
364
+ # with gr.Row():
365
+ # video_in = gr.Video(label="🎞️ Upload Video")
366
+ # frame_num = gr.Number(value=0, visible=False)
367
+ # load_btn = gr.Button("πŸ“Έ Load Frame for Annotation")
368
+ # annot = gr.Image(label="πŸ–ŒοΈ Paint ROI Mask", tool="sketch", type="numpy", image_mode="RGBA", height=1000)
369
+ # save_btn = gr.Button("πŸ’Ύ Save Mask")
370
+ # log = gr.Textbox(label="Logs", lines=8)
371
+ # with gr.Row():
372
+ # pixel_mode = gr.Dropdown(["All Pixels", "Only Black Pixels"], value="All Pixels")
373
+ # crop_w = gr.Number(value=400, label="Crop Width")
374
+ # crop_h = gr.Number(value=400, label="Crop Height")
375
+
376
+ # preview_btn = gr.Button("πŸ”Ž Preview Crop")
377
+ # with gr.Row():
378
+ # preview_frame = gr.Image(label="Preview Frame")
379
+ # preview_crop = gr.Image(label="Cropped Region")
380
+
381
+ # run_btn = gr.Button("πŸš€ Run Tracking")
382
+ # with gr.Row():
383
+ # result_video = gr.Video(label="🎬 Result (Forward)")
384
+ # mask_video = gr.Video(label="⬛ Mask (Forward)")
385
+ # stabilized_video = gr.Video(label="🧱 Stabilized (Forward)")
386
+
387
+ # # Load reversed frame for painting
388
+ # def load_reversed_frame(v, f):
389
+ # reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
390
+ # return extract_frame(reversed_path, int(f))
391
+
392
+ # load_btn.click(load_reversed_frame, [video_in, frame_num], annot)
393
+ # save_btn.click(save_mask, annot, [gr.State(), log])
394
+
395
+ # def preview_crop_fn(v, cw, ch):
396
+ # reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
397
+ # frame0 = extract_frame(reversed_path, 0)
398
+ # if frame0 is None or not os.path.exists("user_mask.png"):
399
+ # return None, None, "⚠️ Paint and Save Mask first."
400
+ # x0,y0,x1,y1 = compute_crop_box_from_mask(cv2.cvtColor(frame0, cv2.COLOR_RGB2BGR), "user_mask.png", int(cw), int(ch))
401
+ # frame_box = draw_crop_preview_on_frame(frame0, (x0,y0,x1,y1))
402
+ # return frame_box, frame0[y0:y1, x0:x1], f"Crop {cw}x{ch} at ({x0},{y0})"
403
+
404
+ # preview_btn.click(preview_crop_fn, [video_in, crop_w, crop_h], [preview_frame, preview_crop, log])
405
+
406
+ # def run_btn_fn(v, m, cw, ch):
407
+ # if not os.path.exists("user_mask.png"):
408
+ # return "⚠️ Save Mask first.", None, None, None
409
+ # return run_tracking(v.name if hasattr(v, "name") else v, "user_mask.png", m, int(cw), int(ch))
410
+
411
+ # run_btn.click(run_btn_fn, [video_in, pixel_mode, crop_w, crop_h],
412
+ # [log, result_video, mask_video, stabilized_video])
413
+ # return demo
414
+
415
+ # if __name__ == "__main__":
416
+ # app = build_app()
417
+ # app.launch(server_name="0.0.0.0", server_port=7861, debug=True)
418
+
419
+
420
+
421
  import os
422
+ import sys
423
  import cv2
424
  import math
425
+ import time
426
  import torch
427
  import numpy as np
428
+ import gradio as gr
429
  from tqdm import tqdm
430
  from pathlib import Path
431
+ from collections import deque
 
432
  from argparse import Namespace
433
+ from torchvision import transforms
 
434
 
435
  # === RAFT Setup ===
436
  sys.path.append("/app/preprocess/RAFT/core")
 
450
  # ==========================================================
451
 
452
  def reverse_video(input_path, output_path):
 
453
  cap = cv2.VideoCapture(input_path)
454
  if not cap.isOpened():
455
  raise FileNotFoundError(f"❌ Could not open video: {input_path}")
 
468
  frames.append(frame)
469
  cap.release()
470
 
 
471
  for frame in reversed(frames):
472
  out.write(frame)
473
  out.release()
 
474
  print(f"πŸ” Video reversed and saved: {output_path}")
475
  return output_path
476
 
 
477
  def reverse_video_file_inplace(path_in):
 
478
  tmp_path = path_in.replace(".mp4", "_tmp.mp4")
479
  reverse_video(path_in, tmp_path)
480
  os.replace(tmp_path, path_in)
 
542
  return mask_path, f"βœ… Saved mask ({np.count_nonzero(bin_mask)} painted pixels)"
543
 
544
  # ==========================================================
545
+ # === UPDATED DYNAMIC CROP LOGIC ===========================
546
  # ==========================================================
547
 
548
+ def compute_crop_box_from_mask_dynamic(first_frame_bgr, mask_path, pad=200):
549
+ """
550
+ Compute a square crop region based on mask region + padding.
551
+ Ensures equal width & height.
552
+ """
553
  mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
554
  if mask is None:
555
+ raise FileNotFoundError(f"Mask not found: {mask_path}")
556
+
557
+ H, W = mask.shape[:2]
558
  ys, xs = np.where(mask > 0)
559
+
560
+ # Fallback to center crop if no mask
561
  if len(xs) == 0:
562
+ cx, cy = W // 2, H // 2
563
+ size = min(W, H) // 2
564
+ return cx - size // 2, cy - size // 2, cx + size // 2, cy + size // 2
565
+
566
+ x_min, x_max = np.min(xs), np.max(xs)
567
+ y_min, y_max = np.min(ys), np.max(ys)
568
+
569
+ # Add padding
570
+ x_min = max(0, x_min - pad)
571
+ y_min = max(0, y_min - pad)
572
+ x_max = min(W, x_max + pad)
573
+ y_max = min(H, y_max + pad)
574
+
575
+ # Make it square
576
+ width = x_max - x_min
577
+ height = y_max - y_min
578
+ side = max(width, height)
579
+
580
+ cx = (x_min + x_max) // 2
581
+ cy = (y_min + y_max) // 2
582
+
583
+ x_min = max(0, cx - side // 2)
584
+ y_min = max(0, cy - side // 2)
585
+ x_max = min(W, x_min + side)
586
+ y_max = min(H, y_min + side)
587
+
588
+ return int(x_min), int(y_min), int(x_max), int(y_max)
589
 
590
  def draw_crop_preview_on_frame(frame_rgb, crop_box, color=(0,255,0), thickness=2):
591
  x0, y0, x1, y1 = crop_box
 
598
  # ==========================================================
599
 
600
  def stabilize_black_regions(input_video):
 
601
  kernel_fill = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
602
  kernel_edge = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
603
 
 
616
  if not ret:
617
  break
618
 
 
619
  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
620
  _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
621
 
 
622
  inv = cv2.bitwise_not(mask)
623
  flood = inv.copy()
624
  h, w = inv.shape
 
628
  filled = cv2.bitwise_or(inv, holes)
629
  filled = cv2.bitwise_not(filled)
630
 
 
 
631
  stable = cv2.morphologyEx(filled, cv2.MORPH_CLOSE, kernel_fill, iterations=1)
 
632
  stable = cv2.morphologyEx(stable, cv2.MORPH_OPEN, kernel_edge, iterations=1)
633
 
 
634
  out.write(cv2.cvtColor(stable, cv2.COLOR_GRAY2BGR))
635
 
636
  cap.release()
 
638
  print(f"βœ… Stabilized mask saved: {STABILIZED_MASK}")
639
  return STABILIZED_MASK
640
 
 
641
  # ==========================================================
642
  # === TRACKING =============================================
643
  # ==========================================================
644
 
645
+ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
646
+ BLACK_THRESH = 1
647
+ HISTORY_LEN = 5
648
+
649
  reversed_path = reverse_video(video_path, REVERSED_INPUT)
650
  cap = cv2.VideoCapture(reversed_path)
651
  model = load_raft_model(MODEL_PATH)
 
656
  return "❌ Could not read first frame.", None, None, None
657
  H, W = first_frame.shape[:2]
658
 
659
+ x0, y0, x1, y1 = compute_crop_box_from_mask_dynamic(first_frame, mask_path, pad=200)
660
  cw, ch = x1 - x0, y1 - y0
661
 
662
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
 
671
  ys, xs = np.where(crop_mask > 0)
672
  else:
673
  gray_first = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
674
+ black_pixels = (gray_first[y0:y1, x0:x1] < BLACK_THRESH)
675
  combined = (crop_mask > 0) & black_pixels
676
  ys, xs = np.where(combined)
677
 
 
679
  prev_full_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
680
  prev_crop_rgb = prev_full_rgb[y0:y1, x0:x1]
681
 
682
+ history = deque([True]*HISTORY_LEN, maxlen=HISTORY_LEN)
683
+ stopped = False
684
+
685
+ frame_idx = 0
686
  while True:
687
  ret, curr_frame = cap.read()
688
  if not ret:
689
  break
690
+ frame_idx += 1
691
  curr_full_rgb = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2RGB)
692
  curr_crop_rgb = curr_full_rgb[y0:y1, x0:x1]
693
+ gray_crop = cv2.cvtColor(curr_crop_rgb, cv2.COLOR_RGB2GRAY)
694
 
695
  flow_crop = compute_flow(model, prev_crop_rgb, curr_crop_rgb)
696
+
697
  vis_full = curr_full_rgb.copy()
698
  mask_full = np.full((H, W), 255, dtype=np.uint8)
699
 
 
706
  nx = np.clip(nx, 0, cw-1)
707
  ny = np.clip(ny, 0, ch-1)
708
  new_points.append([nx, ny])
709
+ tracked_points = np.array(new_points, dtype=np.float32)
710
+
711
+ black_mask = gray_crop < BLACK_THRESH
712
+ black_indices = tracked_points.astype(int)
713
+ has_black = any(
714
+ 0 <= px < cw and 0 <= py < ch and black_mask[py, px]
715
+ for px, py in black_indices
716
+ )
717
+ history.append(has_black)
718
+
719
+ if stopped:
720
+ paint = False
721
+ elif has_black:
722
+ paint = True
723
+ elif not any(history):
724
+ stopped = True
725
+ paint = False
726
+ else:
727
+ paint = True
728
+
729
+ if paint:
730
+ for pt in tracked_points:
731
+ fx, fy = int(pt[0] + x0), int(pt[1] + y0)
732
  if 0 <= fx < W and 0 <= fy < H:
733
  cv2.circle(vis_full, (fx, fy), 1, (0,255,0), -1)
734
  mask_full[fy, fx] = 0
735
+
736
  out_vis.write(cv2.cvtColor(vis_full, cv2.COLOR_RGB2BGR))
737
  out_mask.write(mask_full)
738
  prev_crop_rgb = curr_crop_rgb
739
 
740
+ if frame_idx % 10 == 0:
741
+ print(f"Frame {frame_idx}: {'PAINT' if paint else 'NO-PAINT'} | has_black={has_black} | stopped={stopped}")
742
+
743
  cap.release()
744
  out_vis.release()
745
  out_mask.release()
746
 
747
  stabilize_black_regions(OUTPUT_MASK_VIDEO)
748
 
 
749
  reverse_video_file_inplace(OUTPUT_VIDEO)
750
  reverse_video_file_inplace(OUTPUT_MASK_VIDEO)
751
  reverse_video_file_inplace(STABILIZED_MASK)
752
 
753
  return (
754
+ f"βœ… Tracking complete ({selection_mode}).\n"
755
+ f"Square Crop {cw}x{ch} @ ({x0},{y0}) with padding=100\n"
756
+ f"Painting stopped={'Yes' if stopped else 'No'} after {frame_idx} frames.\n"
757
+ "Saved outputs reversed back to forward order.",
758
  OUTPUT_VIDEO,
759
  OUTPUT_MASK_VIDEO,
760
  STABILIZED_MASK
 
766
 
767
  def build_app():
768
  with gr.Blocks() as demo:
769
+ gr.Markdown("# 🎯 Pixel Tracker (Dynamic Square Crop)")
770
+
771
  with gr.Row():
772
  video_in = gr.Video(label="🎞️ Upload Video")
773
  frame_num = gr.Number(value=0, visible=False)
774
+
775
  load_btn = gr.Button("πŸ“Έ Load Frame for Annotation")
776
+ annot = gr.Image(label="πŸ–ŒοΈ Paint ROI Mask", tool="sketch", type="numpy", image_mode="RGBA", height=1000)
777
  save_btn = gr.Button("πŸ’Ύ Save Mask")
778
  log = gr.Textbox(label="Logs", lines=8)
779
+
780
+ preview_btn = gr.Button("πŸ”Ž Preview Crop", visible=False)
781
+ with gr.Row():
782
+ preview_frame = gr.Image(label="Preview Frame", visible=False)
783
+ preview_crop = gr.Image(label="Cropped Region", visible=False)
784
+
785
  run_btn = gr.Button("πŸš€ Run Tracking")
786
  with gr.Row():
787
  result_video = gr.Video(label="🎬 Result (Forward)")
788
  mask_video = gr.Video(label="⬛ Mask (Forward)")
789
  stabilized_video = gr.Video(label="🧱 Stabilized (Forward)")
790
 
 
791
  def load_reversed_frame(v, f):
792
  reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
793
  return extract_frame(reversed_path, int(f))
 
795
  load_btn.click(load_reversed_frame, [video_in, frame_num], annot)
796
  save_btn.click(save_mask, annot, [gr.State(), log])
797
 
798
+ def preview_crop_fn(v):
799
  reversed_path = reverse_video(v.name if hasattr(v, "name") else v, REVERSED_INPUT)
800
  frame0 = extract_frame(reversed_path, 0)
801
  if frame0 is None or not os.path.exists("user_mask.png"):
802
  return None, None, "⚠️ Paint and Save Mask first."
803
+ x0,y0,x1,y1 = compute_crop_box_from_mask_dynamic(cv2.cvtColor(frame0, cv2.COLOR_RGB2BGR), "user_mask.png", pad=200)
804
  frame_box = draw_crop_preview_on_frame(frame0, (x0,y0,x1,y1))
805
+ return frame_box, frame0[y0:y1, x0:x1], f"Square crop {x1-x0}x{y1-y0} at ({x0},{y0})"
806
 
807
+ preview_btn.click(preview_crop_fn, video_in, [preview_frame, preview_crop, log])
808
 
809
+ def run_btn_fn(v, m):
810
  if not os.path.exists("user_mask.png"):
811
  return "⚠️ Save Mask first.", None, None, None
812
+ return run_tracking(v.name if hasattr(v, "name") else v, "user_mask.png", m)
813
 
814
+ run_btn.click(run_btn_fn, [video_in, gr.Dropdown(["All Pixels", "Only Black Pixels"], value="All Pixels")],
815
  [log, result_video, mask_video, stabilized_video])
816
+
817
  return demo
818
 
819
  if __name__ == "__main__":
820
  app = build_app()
821
+ app.launch(server_name="0.0.0.0", server_port=7861, debug=True)
user_mask.png CHANGED