bened commited on
Commit
b8124e3
·
verified ·
1 Parent(s): eb7f2ce

Upload 2 files

Browse files
Files changed (1) hide show
  1. 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
- work = Path(tempfile.mkdtemp(prefix="wm_video_v3_"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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("[Video] Phase 1: extracting frames and detecting watermarks...")
 
 
 
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] Frame {frame_index}: "
250
- f"{len(boxes)} bounding box(es)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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] Detection samples: {len(detections)}")
268
 
269
  # ========================================================
270
- # Phase 2: build temporal detection track
271
  # ========================================================
272
  print("[Video] Phase 2: building per-frame watermark tracks...")
273
- per_frame_boxes = _build_per_frame_detections(
274
- detections,
275
- frame_index,
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,