Upload 2 files
Browse files- watermark/video.py +169 -18
watermark/video.py
CHANGED
|
@@ -205,7 +205,26 @@ def process_video(
|
|
| 205 |
skip = max(1, int(detection_skip))
|
| 206 |
print(f"[Video] Detection interval: every {skip} frame(s)")
|
| 207 |
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
input_dir = work / "input"
|
| 210 |
output_dir = work / "output"
|
| 211 |
input_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -214,11 +233,30 @@ def process_video(
|
|
| 214 |
detections = {}
|
| 215 |
cap = None
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
try:
|
| 218 |
# ========================================================
|
| 219 |
-
# Phase 1: extract frames + Florence detection
|
| 220 |
# ========================================================
|
| 221 |
-
print(
|
|
|
|
|
|
|
|
|
|
| 222 |
cap = cv2.VideoCapture(video_path)
|
| 223 |
if not cap.isOpened():
|
| 224 |
raise ValueError("Unable to reopen uploaded video.")
|
|
@@ -238,19 +276,74 @@ def process_video(
|
|
| 238 |
if not saved:
|
| 239 |
raise RuntimeError(f"Failed to save frame {frame_index}.")
|
| 240 |
|
|
|
|
| 241 |
if frame_index % skip == 0:
|
| 242 |
-
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 243 |
-
image = Image.fromarray(rgb)
|
| 244 |
-
boxes = _valid_boxes(
|
| 245 |
-
_boxes(image, detection_prompt)
|
| 246 |
-
)
|
| 247 |
-
detections[frame_index] = boxes
|
| 248 |
print(
|
| 249 |
-
f"[Florence]
|
| 250 |
-
f"{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
)
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
frame_index += 1
|
|
|
|
| 254 |
_update_progress(
|
| 255 |
progress,
|
| 256 |
0.35 * frame_index / max(1, total),
|
|
@@ -264,16 +357,75 @@ def process_video(
|
|
| 264 |
raise ValueError("The uploaded video contains no readable frames.")
|
| 265 |
|
| 266 |
print(f"[Video] Extracted {frame_index} frames.")
|
| 267 |
-
print(f"[Video]
|
| 268 |
|
| 269 |
# ========================================================
|
| 270 |
-
# Phase 2: build
|
| 271 |
# ========================================================
|
| 272 |
print("[Video] Phase 2: building per-frame watermark tracks...")
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# ========================================================
|
| 279 |
# Phase 3: LaMa inpainting
|
|
@@ -294,7 +446,6 @@ def process_video(
|
|
| 294 |
max_bbox_percent,
|
| 295 |
)
|
| 296 |
|
| 297 |
-
# Avoid calling LaMa when the filtered mask is empty.
|
| 298 |
if np.asarray(mask, dtype=np.uint8).any():
|
| 299 |
result = _lama(
|
| 300 |
image,
|
|
|
|
| 205 |
skip = max(1, int(detection_skip))
|
| 206 |
print(f"[Video] Detection interval: every {skip} frame(s)")
|
| 207 |
|
| 208 |
+
# ------------------------------------------------------------
|
| 209 |
+
# V4 detection strategy
|
| 210 |
+
# ------------------------------------------------------------
|
| 211 |
+
# We keep the cheap periodic Florence scan, but when a watermark is
|
| 212 |
+
# detected for the first time in a sampling interval, we immediately
|
| 213 |
+
# scan the preceding frames in that interval. This recovers the actual
|
| 214 |
+
# appearance frame instead of waiting until the next sampled frame.
|
| 215 |
+
#
|
| 216 |
+
# Example with skip=5:
|
| 217 |
+
# sampled: 0,5,10,15...
|
| 218 |
+
# watermark appears at 7
|
| 219 |
+
# frame 10 is detected -> retrospectively scan 6,7,8,9
|
| 220 |
+
# first positive frame becomes 7
|
| 221 |
+
#
|
| 222 |
+
# This is much cheaper than running Florence on every video frame and
|
| 223 |
+
# fixes the user's "watermark exists at the beginning, then disappears"
|
| 224 |
+
# symptom without creating a watermark before it actually appears.
|
| 225 |
+
# ------------------------------------------------------------
|
| 226 |
+
|
| 227 |
+
work = Path(tempfile.mkdtemp(prefix="wm_video_v4_"))
|
| 228 |
input_dir = work / "input"
|
| 229 |
output_dir = work / "output"
|
| 230 |
input_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 233 |
detections = {}
|
| 234 |
cap = None
|
| 235 |
|
| 236 |
+
def detect_frame(frame_index, frame):
|
| 237 |
+
"""Run Florence on one BGR frame and cache the result."""
|
| 238 |
+
if frame_index in detections:
|
| 239 |
+
return detections[frame_index]
|
| 240 |
+
|
| 241 |
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 242 |
+
image = Image.fromarray(rgb)
|
| 243 |
+
boxes = _valid_boxes(_boxes(image, detection_prompt))
|
| 244 |
+
detections[frame_index] = boxes
|
| 245 |
+
|
| 246 |
+
print(
|
| 247 |
+
f"[Florence] Frame {frame_index}: "
|
| 248 |
+
f"{len(boxes)} bounding box(es)"
|
| 249 |
+
)
|
| 250 |
+
return boxes
|
| 251 |
+
|
| 252 |
try:
|
| 253 |
# ========================================================
|
| 254 |
+
# Phase 1: extract frames + periodic Florence detection
|
| 255 |
# ========================================================
|
| 256 |
+
print(
|
| 257 |
+
"[Video] Phase 1: extracting frames and detecting watermarks..."
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
cap = cv2.VideoCapture(video_path)
|
| 261 |
if not cap.isOpened():
|
| 262 |
raise ValueError("Unable to reopen uploaded video.")
|
|
|
|
| 276 |
if not saved:
|
| 277 |
raise RuntimeError(f"Failed to save frame {frame_index}.")
|
| 278 |
|
| 279 |
+
# Periodic detection.
|
| 280 |
if frame_index % skip == 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
print(
|
| 282 |
+
f"[Florence] Detecting sampled frame "
|
| 283 |
+
f"{frame_index}/{total}"
|
| 284 |
+
)
|
| 285 |
+
sampled_boxes = detect_frame(frame_index, frame)
|
| 286 |
+
|
| 287 |
+
# ------------------------------------------------
|
| 288 |
+
# BACKWARD LOOKUP
|
| 289 |
+
# ------------------------------------------------
|
| 290 |
+
# Only back-scan when the previous sampled frame was
|
| 291 |
+
# negative. If the previous sample was already positive,
|
| 292 |
+
# the watermark is known to be present and another full
|
| 293 |
+
# interval scan would waste ZeroGPU time.
|
| 294 |
+
# ------------------------------------------------
|
| 295 |
+
previous_sample = frame_index - skip
|
| 296 |
+
previous_boxes = (
|
| 297 |
+
detections.get(previous_sample, [])
|
| 298 |
+
if previous_sample >= 0
|
| 299 |
+
else []
|
| 300 |
)
|
| 301 |
|
| 302 |
+
if (
|
| 303 |
+
sampled_boxes
|
| 304 |
+
and skip > 1
|
| 305 |
+
and not previous_boxes
|
| 306 |
+
):
|
| 307 |
+
start = max(0, frame_index - skip)
|
| 308 |
+
|
| 309 |
+
print(
|
| 310 |
+
f"[Video] Positive detection at frame {frame_index}; "
|
| 311 |
+
f"back-scanning frames {start}-{frame_index - 1} "
|
| 312 |
+
"to locate watermark onset..."
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# Read only the preceding frames from disk. We have
|
| 316 |
+
# already written every frame above, so no second video
|
| 317 |
+
# decode pass is required.
|
| 318 |
+
first_positive = None
|
| 319 |
+
for candidate in range(start, frame_index):
|
| 320 |
+
candidate_path = (
|
| 321 |
+
input_dir / f"{candidate:08d}.png"
|
| 322 |
+
)
|
| 323 |
+
candidate_bgr = cv2.imread(
|
| 324 |
+
str(candidate_path),
|
| 325 |
+
cv2.IMREAD_COLOR,
|
| 326 |
+
)
|
| 327 |
+
if candidate_bgr is None:
|
| 328 |
+
continue
|
| 329 |
+
|
| 330 |
+
candidate_boxes = detect_frame(
|
| 331 |
+
candidate,
|
| 332 |
+
candidate_bgr,
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
if candidate_boxes:
|
| 336 |
+
first_positive = candidate
|
| 337 |
+
break
|
| 338 |
+
|
| 339 |
+
if first_positive is not None:
|
| 340 |
+
print(
|
| 341 |
+
f"[Video] Watermark onset located at frame "
|
| 342 |
+
f"{first_positive} ({first_positive / max(fps, 1):.3f}s)."
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
frame_index += 1
|
| 346 |
+
|
| 347 |
_update_progress(
|
| 348 |
progress,
|
| 349 |
0.35 * frame_index / max(1, total),
|
|
|
|
| 357 |
raise ValueError("The uploaded video contains no readable frames.")
|
| 358 |
|
| 359 |
print(f"[Video] Extracted {frame_index} frames.")
|
| 360 |
+
print(f"[Video] Florence detections cached: {len(detections)}")
|
| 361 |
|
| 362 |
# ========================================================
|
| 363 |
+
# Phase 2: build exact per-frame detection track
|
| 364 |
# ========================================================
|
| 365 |
print("[Video] Phase 2: building per-frame watermark tracks...")
|
| 366 |
+
|
| 367 |
+
detection_keys = sorted(detections.keys())
|
| 368 |
+
per_frame_boxes = {}
|
| 369 |
+
|
| 370 |
+
# The detection dictionary now contains extra frames around the
|
| 371 |
+
# first appearance of each sampled positive interval. We still use
|
| 372 |
+
# the conservative V3 temporal rules so a negative interval does not
|
| 373 |
+
# leak a previous watermark backward into frames where it was absent.
|
| 374 |
+
for i in range(frame_index):
|
| 375 |
+
if i in detections:
|
| 376 |
+
per_frame_boxes[i] = _valid_boxes(detections[i])
|
| 377 |
+
continue
|
| 378 |
+
|
| 379 |
+
prev_key = None
|
| 380 |
+
next_key = None
|
| 381 |
+
|
| 382 |
+
# Find nearest known samples on each side.
|
| 383 |
+
for key in detection_keys:
|
| 384 |
+
if key < i:
|
| 385 |
+
prev_key = key
|
| 386 |
+
elif key > i:
|
| 387 |
+
next_key = key
|
| 388 |
+
break
|
| 389 |
+
|
| 390 |
+
# Nothing detected yet -> keep original frame.
|
| 391 |
+
if prev_key is None:
|
| 392 |
+
per_frame_boxes[i] = []
|
| 393 |
+
continue
|
| 394 |
+
|
| 395 |
+
prev_boxes = _valid_boxes(detections.get(prev_key, []))
|
| 396 |
+
|
| 397 |
+
# No future sample -> keep the last known state.
|
| 398 |
+
if next_key is None:
|
| 399 |
+
per_frame_boxes[i] = prev_boxes
|
| 400 |
+
continue
|
| 401 |
+
|
| 402 |
+
next_boxes = _valid_boxes(detections.get(next_key, []))
|
| 403 |
+
|
| 404 |
+
# Positive -> negative transition: only carry the old box to
|
| 405 |
+
# the midpoint. This prevents a stale watermark from persisting
|
| 406 |
+
# long after it disappeared.
|
| 407 |
+
if prev_boxes and not next_boxes:
|
| 408 |
+
midpoint = (prev_key + next_key) / 2.0
|
| 409 |
+
per_frame_boxes[i] = (
|
| 410 |
+
prev_boxes if i <= midpoint else []
|
| 411 |
+
)
|
| 412 |
+
continue
|
| 413 |
+
|
| 414 |
+
# Negative -> positive transition: do not remove anything before
|
| 415 |
+
# the first known positive frame. This is the key fix for the
|
| 416 |
+
# user's original problem.
|
| 417 |
+
if not prev_boxes and next_boxes:
|
| 418 |
+
per_frame_boxes[i] = []
|
| 419 |
+
continue
|
| 420 |
+
|
| 421 |
+
# Both sides positive -> interpolate box movement.
|
| 422 |
+
per_frame_boxes[i] = _interpolate_boxes(
|
| 423 |
+
prev_boxes,
|
| 424 |
+
prev_key,
|
| 425 |
+
next_boxes,
|
| 426 |
+
next_key,
|
| 427 |
+
i,
|
| 428 |
+
)
|
| 429 |
|
| 430 |
# ========================================================
|
| 431 |
# Phase 3: LaMa inpainting
|
|
|
|
| 446 |
max_bbox_percent,
|
| 447 |
)
|
| 448 |
|
|
|
|
| 449 |
if np.asarray(mask, dtype=np.uint8).any():
|
| 450 |
result = _lama(
|
| 451 |
image,
|