Video Logo / Subtitle Removal (video-watermark-remover)
Uses ProPainter to inpaint fixed-position logos, corner bugs, and subtitles (burned into the video). It crops only the ROI around the element for inpainting, then feathers it back into the original video, leaving pixels outside the ROI untouched.
⚠ ProPainter's license is S-Lab License 1.0: non-commercial use only. Contact the original authors for commercial use. Only use on content you have permission to process.
0. Environment (First Use)
Scripts are in scripts/. The working directory must be this skill's directory (where SKILL.md resides).
python scripts/setup_env.py # Check ffmpeg / PyTorch (CUDA), clone a fixed version of ProPainter, generate config.json
python scripts/selftest.py # Synthesize a test video, run the full pipeline, compare with ground truth, output PASS/FAIL
- If
config.jsondoes not exist,setup_env.pywill prompt what is missing and how to install it. PyTorch requires the user to install the CUDA version specific to their GPU. - Do not proceed if self-test fails; fix the environment based on prompts.
- The
pythonbelow refers to the interpreter recorded in config bysetup_env.py(i.e., the environment used when running setup).
What the User Needs to Provide
| Item | Required | Description |
|---|---|---|
| Video file | ✅ | One or more |
| What to remove | ✅ | Natural language: "top-left corner bug", "center large logo", "bottom subtitle" |
| Position hint | Optional | Only needed when there are multiple instances of the same element |
| Time range | Optional | Default is automatic detection; only needed if you want to keep part of it |
| Subsequent plans | Recommend asking | Whether a new logo will be overlaid, subtitles re-inserted, or new subtitles have a background bar. If there is a bar, just blur it – no need to inpaint |
If mandatory items are missing, ask the user; do not ask for optional ones.
Capability Boundaries (Check Before Starting; Inform User If Exceeded)
- ✅ Fixed-position logos, corner bugs, station IDs, including semi-transparent, fade-in/out, or those stretched in scene transitions (e.g., intro/outro).
- ✅ Fixed-position, horizontally centered, single-line subtitles (burned in), white or dark, with or without shadows.
- ⚠️ Moving or scaling logos, scrolling subtitles, multi-line subtitles, non-centered subtitles: the script cannot auto-track them; the user must specify time ranges and manually handle with
--blockor--grow-from, with reduced effectiveness. - ⚠️ Preexisting text in the frame (signs, on-screen text) that overlaps with the element to be removed will be blurred too. Flag this in the identification list.
- Requires an NVIDIA GPU with at least 4GB free VRAM. Can run on CPU only but is very slow.
Workflow (8 Steps, In Order)
Intermediate files go to a temporary working directory $W. The final output is placed next to the original video; QC images go to <video directory>/qc/.
1. Probe
python scripts/probe.py IN1.mp4 [IN2.mp4] --out-dir $W
Examine the stitched frames to determine:
- Where the element is;
- Whether it's a corner bug or a centered large logo;
- Whether there are fades (record the stable frame range of the logo), black frames, scaling or blur transitions;
- Whether there are subtitles.
2. Locate (Static Logo)
python scripts/locate_static.py --video IN1.mp4@stable_start_frame:stable_end_frame [--video IN2.mp4@a:b] --out-dir $W
- Avoid fade-in, black frames, and transitions for start/end frames.
- When the same logo appears in multiple clips, pass them all together. The script only takes pixels that are stationary across all clips, excluding accidentally stationary background blocks.
- View
static_rel.png(darker = more stationary). Define the rough logo bounding box--boxand threshold: solid logos use 0.3–0.5, semi-transparent logos use 0.6–0.75.
3. Create Mask and Must Get User Confirmation
Static logo:
python scripts/mask_logo.py --rel $W/static_rel.npy --box x0,y0,x1,y1 --thresh 0.5 \
[--block rectangle] [--clear region --block region] --preview IN1.mp4@frame [--preview IN2.mp4@frame] --out-dir $W
- Thin elements (text, rays, lines) are traced by strokes, leaving the real background between strokes as reference for the model.
- Only use
--blockrectangle in two cases: solid large blocks, or thin text so small it can't be traced completely (e.g., a line of tiny English text). - ❌ Do not fully cover thin elements with a block. As the camera slowly pushes in, the blocked area has almost no real background exposed, forcing the model to generate from scratch, resulting in "scaly texture."
- Check the preview image for missing edges and tails of the element. Missed parts will carry over as colored trails during inpainting.
Subtitles:
python scripts/mask_subtitle.py IN.mp4 --out-dir $W [--rows y0,y1] [--dark] [--shadow 0,0]
- Without
--rows, automatically finds subtitle rows in the lower 40% of the frame; if wrong, specify manually. - Watch for warnings about "close to frame edge" in the output.
User confirmation: Show the user mask_preview.png or subtitle_mask_preview.png, along with an identification list. Use coordinates printed by mask_logo.py, for example:
① Top-left station logo: repair box (21,15)–(124,50), appears throughout ② Bottom subtitle: y485–524, total 47 lines ③ At 1:34, a sign in the frame falls inside the repair area and will be blurred together
Proceed to step 5 only after user confirmation.
4. Segment
python scripts/shots.py IN.mp4 --out $W/bounds.json [--extra 178]
Hard cuts and fade transitions are split. If not segmented, static shots will produce "pill-shaped color patches" of adjacent shot colors.
5+6. Inpaint + Composite
For long videos, run in background, periodically check logs and proactively report progress.
python scripts/inpaint.py --video IN.mp4 (--mask $W/mask.npy | --masks $W/masks.npz) --bounds $W/bounds.json \
--workdir $W/run --out OUT.mp4 [--mask-dilation 2] [--grow-from 178:14] [--workers 2]
--mask-dilation: default 2; use 4 for semi-transparent or glowing logos.--grow-from frame:pixels: expand the mask outward by a number of pixels starting from a given frame, to cover logos stretched in scaling or blur transitions.- For the same video needing both logo and subtitle removal, run twice: the second
--videouses the first output; do not merge the two ROIs into one large box. - If interrupted, rerun the same command – completed segments will be skipped.
- Name output as
original-nologo.mp4ororiginal-nologo-nosub.mp4. The script refuses to overwrite the original. - Time reference (RTX 3060, 2 workers): 960×540, 126-second video: ~17 min for top-left logo, ~21 min for subtitles; 5-second centered large logo: ~1 min.
7. QC
python scripts/qc.py --orig IN.mp4 --out OUT.mp4 (--mask .. | --masks ..) --bounds $W/bounds.json --workdir $W/run \
[--residue blue] [--times 3,12,20] --out-dir <video_dir>/qc/xxx
Numerical metrics:
- Residual: variance of output should approximately equal variance of outer region.
- Flicker: median ratio around 1 or smaller, very few spike frames.
- Seam: ratio approximately 1.
- Out-of-region error: less than 2.
- Residual color: near 0.
- All-black frames: 0.
⚠ Numerical metrics may pass while local issues exist – visually inspect every `sheet_XX.png (color patches, scaly texture, residual text, colored lines). If problems are found, return to the corresponding step:
- Color patches: add cut points via
shots.py --extra. - Scaly texture: switch to stroke-based masking.
- Residual lines: add
--block.
After fixing, rerun only the affected segments, then QC again.
8. Delivery
- Provide the absolute paths of the output video and comparison images, and show
before_after.pngto the user. - If multiple clips need concatenation, use
python scripts/concat.py A.mp4 B.mp4 C.mp4 --out FULL.mp4; the script will verify total frames equal sum of parts. - Write a coordinates document using the template at
references/positions-template.md. For each element, document three nested boxes: the original element range, the actual repair range, and the outermost feathering range (i.e., the recommended coverage for a new logo). For subtitles, useunion_boxfromsubtitle_info.json. - List known defects with timestamps clearly; do not hide them.
Hard Rules (Learned from Practice)
- ROI width and height must both be ≥160. At height 96, ProPainter outputs all black in the mask region, even with fp32.
inpaint.pyensures this automatically when selecting the ROI. - Transitions must be split. Splitting only at hard cuts misses fades, causing ProPainter to borrow pixels across shots, producing color patches. This cannot be fixed by enlarging the mask or using fp32.
- Thin elements: trace by strokes, do not fully cover with a block.
- Subtitles: crop symmetrically and use the same mask for the same subtitle line. Without symmetric cropping, background bright streaks are cropped in; changing masks for the same line causes flicker, and fades also leak.
- Numerical metrics can miss problems – visually inspect every contact sheet page.
- Get user confirmation for the mask before inpainting. Do not overwrite the original; do not delete user files.
- Background subprocesses must not pop up windows (already built into scripts). Run long tasks in the background and proactively report progress.