isambalghari commited on
Commit
ac5932f
Β·
1 Parent(s): bc4df63

updating morphological operations

Browse files
full_tracked_output.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d2279854fb8576c214c8c149b62a085ae6fd17407d51cde7be57d8ae6a016b9d
3
- size 1421621
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf24b765d6b476d81ebcca98e1ff600918d6a17f03eec10b3d4358f0b479e49c
3
+ size 1343525
mask_output.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d606f04cd29d4d71677020403774296c6cfbaf6b9151423eef8f982eecf2bf6f
3
- size 432686
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02921d8587510296ae109ba7dbfd4b27ff2db3bf5b6ee594b39ac6f194f65035
3
+ size 492518
requirements.txt CHANGED
@@ -9,4 +9,6 @@ imageio==2.37.0
9
  decord==0.6.0
10
  scipy==1.15.3
11
  gradio==3.50.2
12
- matplotlib
 
 
 
9
  decord==0.6.0
10
  scipy==1.15.3
11
  gradio==3.50.2
12
+ matplotlib
13
+ opencv-python-headless
14
+ imageio-ffmpeg
reversed_input.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1793dc36e250e315116923189df2e074f0f4620e3cc13b9c8a7d00c242e9e567
3
- size 4484439
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1d147eaccf834a6e6190ff84075d7fce74e3fdd251548711c430bf188680b420
3
+ size 4945014
stabilized_mask.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7d8cf1a8201b0335b11e0eb2dd68ce4ead163a2eeb93f19bac84f8df6b2fc90
3
+ size 650111
stabilized_mask_output.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9d6392f0c333a98e46aa72e81651cc9785bb0360db1b55c25d7485f34bb542a8
3
- size 1009136
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0969cdfc18e05ecc07499c7441cb24d86bcea071c3cc40b2c272b7d5a75513ef
3
+ size 374321
stabilized_morph_intelligent.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:23a8db84a20395c09436f77da5c1081ce929b06ed5e6b61dc460170e2922d8a6
3
+ size 1251727
track-pixels_gradio.py CHANGED
@@ -1,423 +1,3 @@
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
@@ -448,37 +28,6 @@ REVERSED_INPUT = "/app/reversed_input.mp4"
448
  # ==========================================================
449
  # === VIDEO UTILITIES =====================================
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}")
456
-
457
- # fps = cap.get(cv2.CAP_PROP_FPS)
458
- # width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
459
- # height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
460
- # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
461
- # out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
462
-
463
- # frames = []
464
- # while True:
465
- # ret, frame = cap.read()
466
- # if not ret:
467
- # break
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)
481
-
482
  def reverse_video(input_path, output_path):
483
  """
484
  Reverse frames robustly β€” preserves all readable frames
@@ -641,170 +190,118 @@ def draw_crop_preview_on_frame(frame_rgb, crop_box, color=(0,255,0), thickness=2
641
  # === STABILIZATION ========================================
642
  # ==========================================================
643
 
644
- def stabilize_black_regions(input_video):
645
- kernel_fill = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
646
- kernel_edge = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  cap = cv2.VideoCapture(input_video)
649
  if not cap.isOpened():
650
- raise FileNotFoundError(f"❌ Could not open video: {input_video}")
651
 
652
  fps = cap.get(cv2.CAP_PROP_FPS)
653
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
654
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
655
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
656
- out = cv2.VideoWriter(STABILIZED_MASK, fourcc, fps, (width, height))
657
 
658
- while True:
 
 
 
659
  ret, frame = cap.read()
660
  if not ret:
661
  break
662
-
663
  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
664
- _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
 
 
 
 
665
 
666
- inv = cv2.bitwise_not(mask)
667
- flood = inv.copy()
668
- h, w = inv.shape
669
- flood_mask = np.zeros((h + 2, w + 2), np.uint8)
670
- cv2.floodFill(flood, flood_mask, (0, 0), 255)
671
- holes = cv2.bitwise_not(flood)
672
- filled = cv2.bitwise_or(inv, holes)
673
- filled = cv2.bitwise_not(filled)
674
 
675
- stable = cv2.morphologyEx(filled, cv2.MORPH_CLOSE, kernel_fill, iterations=1)
676
- stable = cv2.morphologyEx(stable, cv2.MORPH_OPEN, kernel_edge, iterations=1)
677
 
678
- out.write(cv2.cvtColor(stable, cv2.COLOR_GRAY2BGR))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
  cap.release()
681
  out.release()
682
- print(f"βœ… Stabilized mask saved: {STABILIZED_MASK}")
683
- return STABILIZED_MASK
684
 
685
  # ==========================================================
686
  # === TRACKING =============================================
687
  # ==========================================================
688
 
689
- # def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
690
- # BLACK_THRESH = 1
691
- # HISTORY_LEN = 5
692
-
693
- # reversed_path = reverse_video(video_path, REVERSED_INPUT)
694
- # cap = cv2.VideoCapture(reversed_path)
695
- # model = load_raft_model(MODEL_PATH)
696
-
697
- # fps = cap.get(cv2.CAP_PROP_FPS)
698
- # ret, first_frame = cap.read()
699
- # if not ret:
700
- # return "❌ Could not read first frame.", None, None, None
701
- # H, W = first_frame.shape[:2]
702
-
703
- # x0, y0, x1, y1 = compute_crop_box_from_mask_dynamic(first_frame, mask_path, pad=200)
704
- # cw, ch = x1 - x0, y1 - y0
705
-
706
- # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
707
- # out_vis = cv2.VideoWriter(OUTPUT_VIDEO, fourcc, fps, (W, H))
708
- # out_mask = cv2.VideoWriter(OUTPUT_MASK_VIDEO, fourcc, fps, (W, H), isColor=False)
709
-
710
- # full_mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
711
- # full_mask = cv2.resize(full_mask, (W, H), interpolation=cv2.INTER_NEAREST)
712
- # crop_mask = full_mask[y0:y1, x0:x1]
713
-
714
- # if selection_mode == "All Pixels":
715
- # ys, xs = np.where(crop_mask > 0)
716
- # else:
717
- # gray_first = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
718
- # black_pixels = (gray_first[y0:y1, x0:x1] < BLACK_THRESH)
719
- # combined = (crop_mask > 0) & black_pixels
720
- # ys, xs = np.where(combined)
721
-
722
- # tracked_points = np.vstack((xs, ys)).T.astype(np.float32)
723
- # prev_full_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
724
- # prev_crop_rgb = prev_full_rgb[y0:y1, x0:x1]
725
-
726
- # history = deque([True]*HISTORY_LEN, maxlen=HISTORY_LEN)
727
- # stopped = False
728
-
729
- # frame_idx = 0
730
- # while True:
731
- # ret, curr_frame = cap.read()
732
- # if not ret:
733
- # break
734
- # frame_idx += 1
735
- # curr_full_rgb = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2RGB)
736
- # curr_crop_rgb = curr_full_rgb[y0:y1, x0:x1]
737
- # gray_crop = cv2.cvtColor(curr_crop_rgb, cv2.COLOR_RGB2GRAY)
738
-
739
- # flow_crop = compute_flow(model, prev_crop_rgb, curr_crop_rgb)
740
-
741
- # vis_full = curr_full_rgb.copy()
742
- # mask_full = np.full((H, W), 255, dtype=np.uint8)
743
-
744
- # new_points = []
745
- # for pt in tracked_points:
746
- # px, py = int(pt[0]), int(pt[1])
747
- # if 0 <= px < cw and 0 <= py < ch:
748
- # dx, dy = flow_crop[py, px]
749
- # nx, ny = pt[0] + dx, pt[1] + dy
750
- # nx = np.clip(nx, 0, cw-1)
751
- # ny = np.clip(ny, 0, ch-1)
752
- # new_points.append([nx, ny])
753
- # tracked_points = np.array(new_points, dtype=np.float32)
754
-
755
- # black_mask = gray_crop < BLACK_THRESH
756
- # black_indices = tracked_points.astype(int)
757
- # has_black = any(
758
- # 0 <= px < cw and 0 <= py < ch and black_mask[py, px]
759
- # for px, py in black_indices
760
- # )
761
- # history.append(has_black)
762
-
763
- # if stopped:
764
- # paint = False
765
- # elif has_black:
766
- # paint = True
767
- # elif not any(history):
768
- # stopped = True
769
- # paint = False
770
- # else:
771
- # paint = True
772
-
773
- # if paint:
774
- # for pt in tracked_points:
775
- # fx, fy = int(pt[0] + x0), int(pt[1] + y0)
776
- # if 0 <= fx < W and 0 <= fy < H:
777
- # cv2.circle(vis_full, (fx, fy), 1, (0,255,0), -1)
778
- # mask_full[fy, fx] = 0
779
-
780
- # out_vis.write(cv2.cvtColor(vis_full, cv2.COLOR_RGB2BGR))
781
- # out_mask.write(mask_full)
782
- # prev_crop_rgb = curr_crop_rgb
783
-
784
- # if frame_idx % 10 == 0:
785
- # print(f"Frame {frame_idx}: {'PAINT' if paint else 'NO-PAINT'} | has_black={has_black} | stopped={stopped}")
786
-
787
- # cap.release()
788
- # out_vis.release()
789
- # out_mask.release()
790
-
791
- # stabilize_black_regions(OUTPUT_MASK_VIDEO)
792
-
793
- # reverse_video_file_inplace(OUTPUT_VIDEO)
794
- # reverse_video_file_inplace(OUTPUT_MASK_VIDEO)
795
- # reverse_video_file_inplace(STABILIZED_MASK)
796
-
797
- # return (
798
- # f"βœ… Tracking complete ({selection_mode}).\n"
799
- # f"Square Crop {cw}x{ch} @ ({x0},{y0}) with padding=100\n"
800
- # f"Painting stopped={'Yes' if stopped else 'No'} after {frame_idx} frames.\n"
801
- # "Saved outputs reversed back to forward order.",
802
- # OUTPUT_VIDEO,
803
- # OUTPUT_MASK_VIDEO,
804
- # STABILIZED_MASK
805
- # )
806
-
807
-
808
 
809
  def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
810
  BLACK_THRESH = 1
@@ -969,10 +466,10 @@ def build_app():
969
  save_btn = gr.Button("πŸ’Ύ Save Mask")
970
  log = gr.Textbox(label="Logs", lines=8)
971
 
972
- preview_btn = gr.Button("πŸ”Ž Preview Crop", visible=False)
973
  with gr.Row():
974
  preview_frame = gr.Image(label="Preview Frame", visible=False)
975
- preview_crop = gr.Image(label="Cropped Region", visible=False)
976
 
977
  run_btn = gr.Button("πŸš€ Run Tracking")
978
  with gr.Row():
@@ -1010,4 +507,4 @@ def build_app():
1010
 
1011
  if __name__ == "__main__":
1012
  app = build_app()
1013
- app.launch(server_name="0.0.0.0", server_port=7861, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
  import cv2
 
28
  # ==========================================================
29
  # === VIDEO UTILITIES =====================================
30
  # ==========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def reverse_video(input_path, output_path):
32
  """
33
  Reverse frames robustly β€” preserves all readable frames
 
190
  # === STABILIZATION ========================================
191
  # ==========================================================
192
 
 
 
 
193
 
194
+ def stabilize_black_regions(input_video, output_path=STABILIZED_MASK, blend=0.3, sample_frames=10):
195
+ """
196
+ Visually consistent black region stabilizer:
197
+ - Repairs broken, thick edges and fills missing gaps.
198
+ - Maintains consistent thickness and stable edges across frames.
199
+ - Smooth temporal blending removes flicker and breathing effects.
200
+
201
+ Args:
202
+ input_video (str): Path to input mask video (black/white).
203
+ output_path (str): Path to save stabilized video.
204
+ blend (float): Temporal smoothing factor (0.0–1.0).
205
+ sample_frames (int): Number of initial frames to sample for parameter estimation.
206
+ """
207
  cap = cv2.VideoCapture(input_video)
208
  if not cap.isOpened():
209
+ raise FileNotFoundError(f"Could not open video: {input_video}")
210
 
211
  fps = cap.get(cv2.CAP_PROP_FPS)
212
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
213
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
214
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
215
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
216
 
217
+ # === Step 1: Estimate global morphology parameters from first N frames ===
218
+ thickness_samples = []
219
+ count = 0
220
+ while count < sample_frames:
221
  ret, frame = cap.read()
222
  if not ret:
223
  break
 
224
  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
225
+ _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
226
+ dist = cv2.distanceTransform(mask, cv2.DIST_L2, 3)
227
+ if np.any(mask > 0):
228
+ thickness_samples.append(np.mean(dist[mask > 0]))
229
+ count += 1
230
 
231
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # rewind
232
+ avg_thickness = np.median(thickness_samples) if thickness_samples else 5
233
+ k = int(np.clip(avg_thickness / 2.0, 3, 9))
234
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
235
+ min_area = (width * height) * 0.0005
 
 
 
236
 
237
+ print(f"🧠 Fixed morphology parameters β€” kernel={k} | min_area={min_area:.1f}")
 
238
 
239
+ prev_mask = None
240
+
241
+ # === Step 2: Process all frames ===
242
+ while True:
243
+ ret, frame = cap.read()
244
+ if not ret:
245
+ break
246
+
247
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
248
+ _, mask = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
249
+
250
+ # --- (A) Connectivity repair: bridge gaps & fill ---
251
+ bridge_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
252
+ repaired = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, bridge_kernel, iterations=2)
253
+ filled = cv2.morphologyEx(repaired, cv2.MORPH_CLOSE,
254
+ cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=2)
255
+ filled = cv2.morphologyEx(filled, cv2.MORPH_OPEN,
256
+ cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=1)
257
+
258
+ # --- (B) Edge thickness normalization ---
259
+ dist = cv2.distanceTransform(cv2.bitwise_not(filled), cv2.DIST_L2, 3)
260
+ normalized = (dist < avg_thickness * 1.2).astype(np.uint8) * 255
261
+ base_clean = cv2.bitwise_not(normalized)
262
+
263
+ # --- (C) Morphological cleanup (fixed parameters) ---
264
+ base_clean = cv2.morphologyEx(base_clean, cv2.MORPH_CLOSE, kernel, iterations=2)
265
+
266
+ num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(base_clean, connectivity=8)
267
+ filtered_mask = np.zeros_like(base_clean)
268
+
269
+ for i in range(1, num_labels):
270
+ area = stats[i, cv2.CC_STAT_AREA]
271
+ component_mask = (labels == i).astype(np.uint8) * 255
272
+ if area >= min_area:
273
+ filtered_mask = cv2.bitwise_or(filtered_mask, component_mask)
274
+ else:
275
+ # Merge small blobs softly
276
+ merge_mask = cv2.dilate(component_mask, kernel, iterations=2)
277
+ filtered_mask = cv2.bitwise_or(filtered_mask, merge_mask)
278
+
279
+ # --- (D) Edge reinforcement ---
280
+ edges = cv2.morphologyEx(filtered_mask, cv2.MORPH_GRADIENT,
281
+ cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
282
+ reinforced = cv2.bitwise_or(filtered_mask, edges)
283
+ reinforced = cv2.morphologyEx(reinforced, cv2.MORPH_CLOSE, kernel, iterations=2)
284
+ reinforced = cv2.medianBlur(reinforced, 3)
285
+
286
+ # --- (E) Temporal stabilization ---
287
+ if prev_mask is not None:
288
+ reinforced = cv2.addWeighted(reinforced, 1 - blend, prev_mask, blend, 0)
289
+ reinforced = (reinforced > 127).astype(np.uint8) * 255 # re-binarize
290
+ prev_mask = reinforced.copy()
291
+
292
+ # Invert back to black region mask
293
+ # clean = cv2.bitwise_not(reinforced)
294
+ out.write(cv2.cvtColor(reinforced, cv2.COLOR_GRAY2BGR))
295
 
296
  cap.release()
297
  out.release()
298
+ print(f"βœ… Visually stable and connected mask saved: {output_path}")
299
+ return output_path
300
 
301
  # ==========================================================
302
  # === TRACKING =============================================
303
  # ==========================================================
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
  def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
307
  BLACK_THRESH = 1
 
466
  save_btn = gr.Button("πŸ’Ύ Save Mask")
467
  log = gr.Textbox(label="Logs", lines=8)
468
 
469
+ preview_btn = gr.Button("πŸ”Ž Preview Crop", visible=True)
470
  with gr.Row():
471
  preview_frame = gr.Image(label="Preview Frame", visible=False)
472
+ preview_crop = gr.Image(label="Cropped Region", visible=True)
473
 
474
  run_btn = gr.Button("πŸš€ Run Tracking")
475
  with gr.Row():
 
507
 
508
  if __name__ == "__main__":
509
  app = build_app()
510
+ app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
user_mask.png CHANGED