Spaces:
Sleeping
Sleeping
Commit
Β·
bc4df63
1
Parent(s):
4f42968
editing tracker
Browse files- full_tracked_output.mp4 +2 -2
- mask_output.mp4 +2 -2
- stabilized_mask_output.mp4 +2 -2
- track-pixels_gradio.py +202 -10
- user_mask.png +0 -0
full_tracked_output.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d2279854fb8576c214c8c149b62a085ae6fd17407d51cde7be57d8ae6a016b9d
|
| 3 |
+
size 1421621
|
mask_output.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d606f04cd29d4d71677020403774296c6cfbaf6b9151423eef8f982eecf2bf6f
|
| 3 |
+
size 432686
|
stabilized_mask_output.mp4
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d6392f0c333a98e46aa72e81651cc9785bb0360db1b55c25d7485f34bb542a8
|
| 3 |
+
size 1009136
|
track-pixels_gradio.py
CHANGED
|
@@ -449,16 +449,49 @@ REVERSED_INPUT = "/app/reversed_input.mp4"
|
|
| 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:
|
|
@@ -468,16 +501,27 @@ def reverse_video(input_path, output_path):
|
|
| 468 |
frames.append(frame)
|
| 469 |
cap.release()
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
for frame in reversed(frames):
|
| 472 |
out.write(frame)
|
| 473 |
out.release()
|
| 474 |
-
|
|
|
|
|
|
|
| 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 |
# ==========================================================
|
| 483 |
# === RAFT LOADING =========================================
|
|
@@ -642,20 +686,145 @@ def stabilize_black_regions(input_video):
|
|
| 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)
|
| 652 |
|
| 653 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
|
|
|
|
|
|
|
|
| 654 |
ret, first_frame = cap.read()
|
| 655 |
if not ret:
|
| 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 |
|
|
@@ -681,8 +850,10 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
|
|
| 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:
|
|
@@ -692,11 +863,13 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
|
|
| 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 |
|
|
|
|
| 700 |
new_points = []
|
| 701 |
for pt in tracked_points:
|
| 702 |
px, py = int(pt[0]), int(pt[1])
|
|
@@ -708,6 +881,7 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
|
|
| 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(
|
|
@@ -716,16 +890,18 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
|
|
| 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)
|
|
@@ -740,21 +916,37 @@ def run_tracking(video_path, mask_path, selection_mode="All Pixels"):
|
|
| 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=
|
| 756 |
-
f"Painting stopped={'Yes' if stopped else 'No'} after {frame_idx} frames.\n"
|
| 757 |
-
"
|
| 758 |
OUTPUT_VIDEO,
|
| 759 |
OUTPUT_MASK_VIDEO,
|
| 760 |
STABILIZED_MASK
|
|
@@ -818,4 +1010,4 @@ def build_app():
|
|
| 818 |
|
| 819 |
if __name__ == "__main__":
|
| 820 |
app = build_app()
|
| 821 |
-
app.launch(server_name="0.0.0.0", server_port=
|
|
|
|
| 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
|
| 485 |
+
even if OpenCV metadata is off by one.
|
| 486 |
+
"""
|
| 487 |
cap = cv2.VideoCapture(input_path)
|
| 488 |
if not cap.isOpened():
|
| 489 |
raise FileNotFoundError(f"β Could not open video: {input_path}")
|
| 490 |
+
|
| 491 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 492 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 493 |
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 494 |
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
|
|
| 495 |
|
| 496 |
frames = []
|
| 497 |
while True:
|
|
|
|
| 501 |
frames.append(frame)
|
| 502 |
cap.release()
|
| 503 |
|
| 504 |
+
if len(frames) == 0:
|
| 505 |
+
raise ValueError("No frames read from video!")
|
| 506 |
+
|
| 507 |
+
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
| 508 |
for frame in reversed(frames):
|
| 509 |
out.write(frame)
|
| 510 |
out.release()
|
| 511 |
+
cv2.destroyAllWindows()
|
| 512 |
+
|
| 513 |
+
print(f"β
Reversed {len(frames)} frames β {output_path}")
|
| 514 |
return output_path
|
| 515 |
|
| 516 |
+
|
| 517 |
def reverse_video_file_inplace(path_in):
|
| 518 |
+
"""
|
| 519 |
+
Reverse a video in-place without losing frames.
|
| 520 |
+
"""
|
| 521 |
tmp_path = path_in.replace(".mp4", "_tmp.mp4")
|
| 522 |
reverse_video(path_in, tmp_path)
|
| 523 |
os.replace(tmp_path, path_in)
|
| 524 |
+
print(f"π Overwrote {path_in} with reversed version (same frame count).")
|
| 525 |
|
| 526 |
# ==========================================================
|
| 527 |
# === RAFT LOADING =========================================
|
|
|
|
| 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
|
| 811 |
HISTORY_LEN = 5
|
| 812 |
|
| 813 |
+
# --- Reverse input for backward tracking ---
|
| 814 |
reversed_path = reverse_video(video_path, REVERSED_INPUT)
|
| 815 |
cap = cv2.VideoCapture(reversed_path)
|
| 816 |
model = load_raft_model(MODEL_PATH)
|
| 817 |
|
| 818 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 819 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 820 |
+
print(f"ποΈ Input video: {total_frames} frames at {fps:.2f} FPS")
|
| 821 |
+
|
| 822 |
ret, first_frame = cap.read()
|
| 823 |
if not ret:
|
| 824 |
return "β Could not read first frame.", None, None, None
|
| 825 |
H, W = first_frame.shape[:2]
|
| 826 |
|
| 827 |
+
# --- Compute dynamic square crop from mask ---
|
| 828 |
x0, y0, x1, y1 = compute_crop_box_from_mask_dynamic(first_frame, mask_path, pad=200)
|
| 829 |
cw, ch = x1 - x0, y1 - y0
|
| 830 |
|
|
|
|
| 850 |
|
| 851 |
history = deque([True]*HISTORY_LEN, maxlen=HISTORY_LEN)
|
| 852 |
stopped = False
|
|
|
|
| 853 |
frame_idx = 0
|
| 854 |
+
curr_full_rgb = None
|
| 855 |
+
|
| 856 |
+
# === Main tracking loop ===
|
| 857 |
while True:
|
| 858 |
ret, curr_frame = cap.read()
|
| 859 |
if not ret:
|
|
|
|
| 863 |
curr_crop_rgb = curr_full_rgb[y0:y1, x0:x1]
|
| 864 |
gray_crop = cv2.cvtColor(curr_crop_rgb, cv2.COLOR_RGB2GRAY)
|
| 865 |
|
| 866 |
+
# --- Optical flow between prev and curr ---
|
| 867 |
flow_crop = compute_flow(model, prev_crop_rgb, curr_crop_rgb)
|
| 868 |
|
| 869 |
vis_full = curr_full_rgb.copy()
|
| 870 |
mask_full = np.full((H, W), 255, dtype=np.uint8)
|
| 871 |
|
| 872 |
+
# --- Move tracked points ---
|
| 873 |
new_points = []
|
| 874 |
for pt in tracked_points:
|
| 875 |
px, py = int(pt[0]), int(pt[1])
|
|
|
|
| 881 |
new_points.append([nx, ny])
|
| 882 |
tracked_points = np.array(new_points, dtype=np.float32)
|
| 883 |
|
| 884 |
+
# --- Detect black pixels ---
|
| 885 |
black_mask = gray_crop < BLACK_THRESH
|
| 886 |
black_indices = tracked_points.astype(int)
|
| 887 |
has_black = any(
|
|
|
|
| 890 |
)
|
| 891 |
history.append(has_black)
|
| 892 |
|
| 893 |
+
# --- Painting logic ---
|
| 894 |
if stopped:
|
| 895 |
paint = False
|
| 896 |
elif has_black:
|
| 897 |
paint = True
|
| 898 |
+
elif not any(history): # last N all False
|
| 899 |
stopped = True
|
| 900 |
paint = False
|
| 901 |
else:
|
| 902 |
paint = True
|
| 903 |
|
| 904 |
+
# --- Paint or skip ---
|
| 905 |
if paint:
|
| 906 |
for pt in tracked_points:
|
| 907 |
fx, fy = int(pt[0] + x0), int(pt[1] + y0)
|
|
|
|
| 916 |
if frame_idx % 10 == 0:
|
| 917 |
print(f"Frame {frame_idx}: {'PAINT' if paint else 'NO-PAINT'} | has_black={has_black} | stopped={stopped}")
|
| 918 |
|
| 919 |
+
# === Add final static frame to preserve frame count ===
|
| 920 |
+
try:
|
| 921 |
+
if curr_full_rgb is not None:
|
| 922 |
+
out_vis.write(cv2.cvtColor(curr_full_rgb, cv2.COLOR_RGB2BGR))
|
| 923 |
+
out_mask.write(mask_full)
|
| 924 |
+
print("π§© Added final frame to preserve total frame count.")
|
| 925 |
+
except Exception as e:
|
| 926 |
+
print(f"β οΈ Could not add final frame: {e}")
|
| 927 |
+
|
| 928 |
cap.release()
|
| 929 |
out_vis.release()
|
| 930 |
out_mask.release()
|
| 931 |
|
| 932 |
+
# === Post-process: stabilization + reversal ===
|
| 933 |
stabilize_black_regions(OUTPUT_MASK_VIDEO)
|
|
|
|
| 934 |
reverse_video_file_inplace(OUTPUT_VIDEO)
|
| 935 |
reverse_video_file_inplace(OUTPUT_MASK_VIDEO)
|
| 936 |
reverse_video_file_inplace(STABILIZED_MASK)
|
| 937 |
|
| 938 |
+
# === Verify output frame counts ===
|
| 939 |
+
for path in [OUTPUT_VIDEO, OUTPUT_MASK_VIDEO, STABILIZED_MASK]:
|
| 940 |
+
cap_test = cv2.VideoCapture(path)
|
| 941 |
+
n = int(cap_test.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 942 |
+
cap_test.release()
|
| 943 |
+
print(f"β
Verified {os.path.basename(path)} β {n} frames")
|
| 944 |
+
|
| 945 |
return (
|
| 946 |
f"β
Tracking complete ({selection_mode}).\n"
|
| 947 |
+
f"Square Crop {cw}x{ch} @ ({x0},{y0}) with padding=200\n"
|
| 948 |
+
f"Painting stopped={'Yes' if stopped else 'No'} after {frame_idx} processed frames.\n"
|
| 949 |
+
f"All outputs now match input frame count ({total_frames}).",
|
| 950 |
OUTPUT_VIDEO,
|
| 951 |
OUTPUT_MASK_VIDEO,
|
| 952 |
STABILIZED_MASK
|
|
|
|
| 1010 |
|
| 1011 |
if __name__ == "__main__":
|
| 1012 |
app = build_app()
|
| 1013 |
+
app.launch(server_name="0.0.0.0", server_port=7861, debug=True)
|
user_mask.png
CHANGED
|
|
|
|