static-ghost 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- static_ghost-0.4.0/LICENSE +21 -0
- static_ghost-0.4.0/PKG-INFO +308 -0
- static_ghost-0.4.0/README.md +283 -0
- static_ghost-0.4.0/pyproject.toml +40 -0
- static_ghost-0.4.0/setup.cfg +4 -0
- static_ghost-0.4.0/static_ghost/__init__.py +1 -0
- static_ghost-0.4.0/static_ghost/cli.py +261 -0
- static_ghost-0.4.0/static_ghost/detector.py +115 -0
- static_ghost-0.4.0/static_ghost/fast_inpaint.py +354 -0
- static_ghost-0.4.0/static_ghost/inpainter.py +33 -0
- static_ghost-0.4.0/static_ghost/mask_generator.py +34 -0
- static_ghost-0.4.0/static_ghost/picker.py +118 -0
- static_ghost-0.4.0/static_ghost/video_engine.py +123 -0
- static_ghost-0.4.0/static_ghost.egg-info/PKG-INFO +308 -0
- static_ghost-0.4.0/static_ghost.egg-info/SOURCES.txt +22 -0
- static_ghost-0.4.0/static_ghost.egg-info/dependency_links.txt +1 -0
- static_ghost-0.4.0/static_ghost.egg-info/entry_points.txt +2 -0
- static_ghost-0.4.0/static_ghost.egg-info/requires.txt +6 -0
- static_ghost-0.4.0/static_ghost.egg-info/top_level.txt +1 -0
- static_ghost-0.4.0/tests/test_cli.py +80 -0
- static_ghost-0.4.0/tests/test_detector.py +51 -0
- static_ghost-0.4.0/tests/test_inpainter.py +28 -0
- static_ghost-0.4.0/tests/test_mask_generator.py +37 -0
- static_ghost-0.4.0/tests/test_video_engine.py +43 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: static-ghost
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Remove static watermarks from videos using LaMa inpainting
|
|
5
|
+
Author: redredchen01
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/redredchen01/static-ghost
|
|
8
|
+
Project-URL: Repository, https://github.com/redredchen01/static-ghost
|
|
9
|
+
Project-URL: Issues, https://github.com/redredchen01/static-ghost/issues
|
|
10
|
+
Keywords: video,watermark,removal,inpainting,lama,ffmpeg,iopaint
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Multimedia :: Video
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: opencv-python-headless>=4.8
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: Pillow>=10.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Static Ghost
|
|
27
|
+
|
|
28
|
+
Remove static watermarks from videos using LaMa inpainting.
|
|
29
|
+
|
|
30
|
+
Takes a video with a fixed-position watermark (TV logo, site branding, corner text), detects or lets you specify the watermark location, and removes it frame-by-frame using the [LaMa](https://github.com/advimman/lama) inpainting model via [IOPaint](https://github.com/Sanster/IOPaint).
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Input video → Extract frames → Crop watermark region → LaMa inpaint → Paste back → Reassemble video
|
|
36
|
+
(FFmpeg) (multiprocess) (IOPaint) (multiprocess) (FFmpeg)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The **crop-and-paste optimization** only processes the small watermark region instead of the full frame — typically 10-15x fewer pixels, making a 10-minute 1080p video processable in ~2 hours on CPU instead of 20+.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
**Prerequisites:**
|
|
44
|
+
```bash
|
|
45
|
+
brew install ffmpeg # or your package manager
|
|
46
|
+
pip install iopaint # LaMa inpainting engine
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Install static-ghost:**
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/redredchen01/static-ghost.git
|
|
52
|
+
cd static-ghost
|
|
53
|
+
pip install -e ".[dev]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> **macOS note:** If `iopaint` is not in PATH after install:
|
|
57
|
+
> ```bash
|
|
58
|
+
> export PATH="$HOME/Library/Python/3.9/bin:$PATH"
|
|
59
|
+
> ```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Quick start — draw the watermark region
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
static-ghost pick video.mp4 --dilation 15 --device mps -o video_clean.mp4
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Opens your browser with a frame from the video. Draw a rectangle around the watermark, click confirm, and it runs the full removal pipeline.
|
|
70
|
+
|
|
71
|
+
### Specify coordinates directly
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
static-ghost remove video.mp4 --region 1400,920,520,160 --dilation 15 --device mps
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Coordinates are `x,y,width,height` from the top-left corner. Multiple watermarks:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
static-ghost remove video.mp4 \
|
|
81
|
+
--region 1400,920,520,160 \
|
|
82
|
+
--region 20,15,200,60 \
|
|
83
|
+
--dilation 15
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Auto-detect watermark
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
static-ghost detect video.mp4
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Uses multi-frame differencing to find regions that stay static across the video. Works best on opaque, high-contrast watermarks. For semi-transparent watermarks, raise the threshold:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
static-ghost detect video.mp4 --threshold 35
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Full auto pipeline
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
static-ghost remove video.mp4 --device mps
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Auto-detects → shows preview → asks for confirmation → removes.
|
|
105
|
+
|
|
106
|
+
## Options
|
|
107
|
+
|
|
108
|
+
| Flag | Default | Description |
|
|
109
|
+
|------|---------|-------------|
|
|
110
|
+
| `--region x,y,w,h` | — | Watermark bounding box (repeatable) |
|
|
111
|
+
| `--pick` | — | Open browser to draw region |
|
|
112
|
+
| `--dilation N` | 5 | Expand mask by N pixels (use 10-15 for logos) |
|
|
113
|
+
| `--device cpu\|mps` | cpu | `mps` = Apple Metal GPU, ~2x faster |
|
|
114
|
+
| `--threshold N` | 15 | Detection sensitivity (higher = more permissive) |
|
|
115
|
+
| `--keep-temp` | — | Keep extracted frames for debugging |
|
|
116
|
+
| `-o PATH` | `{name}_clean.mp4` | Output path |
|
|
117
|
+
|
|
118
|
+
## Performance
|
|
119
|
+
|
|
120
|
+
Crop-and-paste mode (default). Times for 1080p 30fps video:
|
|
121
|
+
|
|
122
|
+
| Video length | Frames | CPU | MPS (Apple Metal) |
|
|
123
|
+
|-------------|--------|-----|-------------------|
|
|
124
|
+
| 30 sec | 900 | ~8 min | ~4 min |
|
|
125
|
+
| 10 min | 18,000 | ~2.5 hr | ~1.5 hr |
|
|
126
|
+
| 30 min | 54,000 | ~7.5 hr | ~4 hr |
|
|
127
|
+
|
|
128
|
+
Actual times depend on watermark size and system load. Use `--device mps` on macOS for best performance.
|
|
129
|
+
|
|
130
|
+
## Tips
|
|
131
|
+
|
|
132
|
+
- **Always test on 30 seconds first.** Extract a clip with `ffmpeg -i input.mp4 -t 30 -c copy test.mp4` and verify the result before committing to a full video.
|
|
133
|
+
- **Be generous with region size.** It's better to over-cover the watermark area than to leave edges visible. Add 50-100px padding beyond the visible watermark.
|
|
134
|
+
- **Dilation matters.** Default is 5px. For large logos with solid color blocks, use 15-20.
|
|
135
|
+
- **Semi-transparent watermarks** are harder. Auto-detect may fail — use `--pick` or `--region` to specify manually.
|
|
136
|
+
|
|
137
|
+
## Project structure
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
static_ghost/
|
|
141
|
+
├── cli.py # CLI entry point and flow orchestration
|
|
142
|
+
├── video_engine.py # FFmpeg: probe, extract frames, merge
|
|
143
|
+
├── detector.py # Multi-frame differencing watermark detection
|
|
144
|
+
├── mask_generator.py # Coordinates → binary mask PNG
|
|
145
|
+
├── fast_inpaint.py # Crop-inpaint-paste with multiprocessing
|
|
146
|
+
├── inpainter.py # IOPaint CLI wrapper
|
|
147
|
+
└── picker.py # Browser-based interactive region selector
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Tests
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
pytest tests/ -v
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
20 tests covering all modules. Tests mock IOPaint so they run without it installed.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
# Agent Integration Guide
|
|
162
|
+
|
|
163
|
+
This section is for AI coding agents (Claude Code, Cursor, Copilot, etc.) that need to use static-ghost programmatically.
|
|
164
|
+
|
|
165
|
+
## When to use this tool
|
|
166
|
+
|
|
167
|
+
- User wants to remove a watermark/logo from a **video** (not an image)
|
|
168
|
+
- The watermark is **static** — fixed position across all frames
|
|
169
|
+
- Examples: TV station logos, website branding, "SAMPLE" overlays
|
|
170
|
+
|
|
171
|
+
**Do NOT use for:** single images, moving/animated watermarks, subtitles (use subtitle extraction instead).
|
|
172
|
+
|
|
173
|
+
## Python API
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from static_ghost.video_engine import probe, extract_sample_frames, extract_all_frames, merge
|
|
177
|
+
from static_ghost.detector import Region, detect_static_regions, save_preview
|
|
178
|
+
from static_ghost.mask_generator import create_mask
|
|
179
|
+
from static_ghost.fast_inpaint import fast_remove
|
|
180
|
+
from static_ghost.inpainter import check_iopaint
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Step 1: Probe
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
meta = probe(video_path)
|
|
187
|
+
# Returns: {"width": 1920, "height": 1080, "fps": 30.0, "duration": 637.1, "codec": "h264", "audio_codec": "aac"}
|
|
188
|
+
total_frames = int(meta["fps"] * meta["duration"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Step 2: Get watermark coordinates
|
|
192
|
+
|
|
193
|
+
**Option A — User provides coordinates:**
|
|
194
|
+
```python
|
|
195
|
+
regions = [Region(x=1400, y=920, w=520, h=160, confidence=1.0)]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Option B — Auto-detect:**
|
|
199
|
+
```python
|
|
200
|
+
import tempfile
|
|
201
|
+
tmp = tempfile.mkdtemp()
|
|
202
|
+
sample_paths = extract_sample_frames(video_path, n=30, output_dir=tmp)
|
|
203
|
+
regions = detect_static_regions(sample_paths, threshold=15)
|
|
204
|
+
# If empty, try threshold=25, 35, 50
|
|
205
|
+
# If still empty, fall back to visual inspection or ask user
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Option C — Visual inspection (when agent can see images):**
|
|
209
|
+
```python
|
|
210
|
+
# Extract sample frames
|
|
211
|
+
paths = extract_sample_frames(video_path, n=5, output_dir=tmp)
|
|
212
|
+
# Read frames with vision tool, inspect corners for watermarks
|
|
213
|
+
# Crop suspected area to verify:
|
|
214
|
+
import cv2
|
|
215
|
+
img = cv2.imread(paths[0])
|
|
216
|
+
crop = img[h-150:h, w-500:w] # bottom-right corner
|
|
217
|
+
cv2.imwrite("/tmp/corner.png", crop)
|
|
218
|
+
# Estimate coordinates from visual inspection
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Step 3: Test on 30-second clip
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
import subprocess
|
|
225
|
+
subprocess.run(["ffmpeg", "-y", "-i", video_path, "-t", "30", "-c", "copy", "/tmp/test_30s.mp4"], capture_output=True, check=True)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Run removal on clip, verify output visually, then proceed to full video.
|
|
229
|
+
|
|
230
|
+
### Step 4: Run removal
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from static_ghost.cli import parse_args, cmd_remove
|
|
234
|
+
|
|
235
|
+
args = parse_args([
|
|
236
|
+
"remove", video_path,
|
|
237
|
+
"--region", "1400,920,520,160",
|
|
238
|
+
"--dilation", "15",
|
|
239
|
+
"--device", "mps", # "cpu" if no Metal GPU
|
|
240
|
+
"-o", output_path,
|
|
241
|
+
])
|
|
242
|
+
cmd_remove(args)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**For long videos, run in background** (if your environment supports it) and check progress:
|
|
246
|
+
```python
|
|
247
|
+
import os
|
|
248
|
+
# Count output frames in temp dir
|
|
249
|
+
tmp_dirs = [d for d in os.listdir("/var/folders/...") if d.startswith("static_ghost_")]
|
|
250
|
+
# Compare against total_frames for progress
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Step 5: Verify
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
orig_meta = probe(video_path)
|
|
257
|
+
clean_meta = probe(output_path)
|
|
258
|
+
assert orig_meta["width"] == clean_meta["width"]
|
|
259
|
+
assert orig_meta["height"] == clean_meta["height"]
|
|
260
|
+
assert abs(orig_meta["duration"] - clean_meta["duration"]) < 1.0
|
|
261
|
+
assert clean_meta["audio_codec"] is not None
|
|
262
|
+
# Visually verify sample frames from output
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Decision tree for agents
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
User wants watermark removed from video
|
|
269
|
+
│
|
|
270
|
+
├─ User provided coordinates? → Use them directly
|
|
271
|
+
├─ Auto-detect finds regions? → Show to user for confirmation
|
|
272
|
+
├─ Auto-detect fails?
|
|
273
|
+
│ ├─ Agent has vision? → Extract frames, inspect corners, estimate coords
|
|
274
|
+
│ └─ Agent has no vision? → Ask user for coordinates or use --pick
|
|
275
|
+
│
|
|
276
|
+
├─ Test on 30s clip
|
|
277
|
+
│ ├─ Watermark gone? → Run full video
|
|
278
|
+
│ ├─ Partially visible? → Increase region size / dilation, re-test
|
|
279
|
+
│ └─ Artifacts? → Reduce dilation, re-test
|
|
280
|
+
│
|
|
281
|
+
└─ Run full video (background for >5 min videos)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Time estimation
|
|
285
|
+
|
|
286
|
+
Before running the full video, benchmark 5 frames to estimate total time:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
import time
|
|
290
|
+
test_paths = extract_sample_frames(video_path, n=5, output_dir="/tmp/bench_in")
|
|
291
|
+
os.makedirs("/tmp/bench_out", exist_ok=True)
|
|
292
|
+
start = time.time()
|
|
293
|
+
fast_remove("/tmp/bench_in", "/tmp/bench_out", regions, dilation=15, device="mps")
|
|
294
|
+
per_frame = (time.time() - start) / 5
|
|
295
|
+
est_minutes = per_frame * total_frames / 60
|
|
296
|
+
print(f"Estimated: {est_minutes:.0f} minutes")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Common pitfalls
|
|
300
|
+
|
|
301
|
+
| Mistake | Fix |
|
|
302
|
+
|---------|-----|
|
|
303
|
+
| Region too small | Add 50-100px padding beyond visible watermark edges |
|
|
304
|
+
| Dilation too low for large logos | Use `--dilation 15` for logos with solid color blocks |
|
|
305
|
+
| Running full video without testing | Always test on 30s clip first |
|
|
306
|
+
| Forgetting `--device mps` on macOS | 2x speed improvement for free |
|
|
307
|
+
| Auto-detect on semi-transparent watermarks | Will likely fail — use manual coordinates |
|
|
308
|
+
| Not checking disk space | 1080p 10min ≈ 40-80GB temp space |
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Static Ghost
|
|
2
|
+
|
|
3
|
+
Remove static watermarks from videos using LaMa inpainting.
|
|
4
|
+
|
|
5
|
+
Takes a video with a fixed-position watermark (TV logo, site branding, corner text), detects or lets you specify the watermark location, and removes it frame-by-frame using the [LaMa](https://github.com/advimman/lama) inpainting model via [IOPaint](https://github.com/Sanster/IOPaint).
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Input video → Extract frames → Crop watermark region → LaMa inpaint → Paste back → Reassemble video
|
|
11
|
+
(FFmpeg) (multiprocess) (IOPaint) (multiprocess) (FFmpeg)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The **crop-and-paste optimization** only processes the small watermark region instead of the full frame — typically 10-15x fewer pixels, making a 10-minute 1080p video processable in ~2 hours on CPU instead of 20+.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
**Prerequisites:**
|
|
19
|
+
```bash
|
|
20
|
+
brew install ffmpeg # or your package manager
|
|
21
|
+
pip install iopaint # LaMa inpainting engine
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Install static-ghost:**
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/redredchen01/static-ghost.git
|
|
27
|
+
cd static-ghost
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **macOS note:** If `iopaint` is not in PATH after install:
|
|
32
|
+
> ```bash
|
|
33
|
+
> export PATH="$HOME/Library/Python/3.9/bin:$PATH"
|
|
34
|
+
> ```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Quick start — draw the watermark region
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
static-ghost pick video.mp4 --dilation 15 --device mps -o video_clean.mp4
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Opens your browser with a frame from the video. Draw a rectangle around the watermark, click confirm, and it runs the full removal pipeline.
|
|
45
|
+
|
|
46
|
+
### Specify coordinates directly
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
static-ghost remove video.mp4 --region 1400,920,520,160 --dilation 15 --device mps
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Coordinates are `x,y,width,height` from the top-left corner. Multiple watermarks:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
static-ghost remove video.mp4 \
|
|
56
|
+
--region 1400,920,520,160 \
|
|
57
|
+
--region 20,15,200,60 \
|
|
58
|
+
--dilation 15
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Auto-detect watermark
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
static-ghost detect video.mp4
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Uses multi-frame differencing to find regions that stay static across the video. Works best on opaque, high-contrast watermarks. For semi-transparent watermarks, raise the threshold:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
static-ghost detect video.mp4 --threshold 35
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Full auto pipeline
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
static-ghost remove video.mp4 --device mps
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Auto-detects → shows preview → asks for confirmation → removes.
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
| Flag | Default | Description |
|
|
84
|
+
|------|---------|-------------|
|
|
85
|
+
| `--region x,y,w,h` | — | Watermark bounding box (repeatable) |
|
|
86
|
+
| `--pick` | — | Open browser to draw region |
|
|
87
|
+
| `--dilation N` | 5 | Expand mask by N pixels (use 10-15 for logos) |
|
|
88
|
+
| `--device cpu\|mps` | cpu | `mps` = Apple Metal GPU, ~2x faster |
|
|
89
|
+
| `--threshold N` | 15 | Detection sensitivity (higher = more permissive) |
|
|
90
|
+
| `--keep-temp` | — | Keep extracted frames for debugging |
|
|
91
|
+
| `-o PATH` | `{name}_clean.mp4` | Output path |
|
|
92
|
+
|
|
93
|
+
## Performance
|
|
94
|
+
|
|
95
|
+
Crop-and-paste mode (default). Times for 1080p 30fps video:
|
|
96
|
+
|
|
97
|
+
| Video length | Frames | CPU | MPS (Apple Metal) |
|
|
98
|
+
|-------------|--------|-----|-------------------|
|
|
99
|
+
| 30 sec | 900 | ~8 min | ~4 min |
|
|
100
|
+
| 10 min | 18,000 | ~2.5 hr | ~1.5 hr |
|
|
101
|
+
| 30 min | 54,000 | ~7.5 hr | ~4 hr |
|
|
102
|
+
|
|
103
|
+
Actual times depend on watermark size and system load. Use `--device mps` on macOS for best performance.
|
|
104
|
+
|
|
105
|
+
## Tips
|
|
106
|
+
|
|
107
|
+
- **Always test on 30 seconds first.** Extract a clip with `ffmpeg -i input.mp4 -t 30 -c copy test.mp4` and verify the result before committing to a full video.
|
|
108
|
+
- **Be generous with region size.** It's better to over-cover the watermark area than to leave edges visible. Add 50-100px padding beyond the visible watermark.
|
|
109
|
+
- **Dilation matters.** Default is 5px. For large logos with solid color blocks, use 15-20.
|
|
110
|
+
- **Semi-transparent watermarks** are harder. Auto-detect may fail — use `--pick` or `--region` to specify manually.
|
|
111
|
+
|
|
112
|
+
## Project structure
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
static_ghost/
|
|
116
|
+
├── cli.py # CLI entry point and flow orchestration
|
|
117
|
+
├── video_engine.py # FFmpeg: probe, extract frames, merge
|
|
118
|
+
├── detector.py # Multi-frame differencing watermark detection
|
|
119
|
+
├── mask_generator.py # Coordinates → binary mask PNG
|
|
120
|
+
├── fast_inpaint.py # Crop-inpaint-paste with multiprocessing
|
|
121
|
+
├── inpainter.py # IOPaint CLI wrapper
|
|
122
|
+
└── picker.py # Browser-based interactive region selector
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Tests
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install -e ".[dev]"
|
|
129
|
+
pytest tests/ -v
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
20 tests covering all modules. Tests mock IOPaint so they run without it installed.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# Agent Integration Guide
|
|
137
|
+
|
|
138
|
+
This section is for AI coding agents (Claude Code, Cursor, Copilot, etc.) that need to use static-ghost programmatically.
|
|
139
|
+
|
|
140
|
+
## When to use this tool
|
|
141
|
+
|
|
142
|
+
- User wants to remove a watermark/logo from a **video** (not an image)
|
|
143
|
+
- The watermark is **static** — fixed position across all frames
|
|
144
|
+
- Examples: TV station logos, website branding, "SAMPLE" overlays
|
|
145
|
+
|
|
146
|
+
**Do NOT use for:** single images, moving/animated watermarks, subtitles (use subtitle extraction instead).
|
|
147
|
+
|
|
148
|
+
## Python API
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from static_ghost.video_engine import probe, extract_sample_frames, extract_all_frames, merge
|
|
152
|
+
from static_ghost.detector import Region, detect_static_regions, save_preview
|
|
153
|
+
from static_ghost.mask_generator import create_mask
|
|
154
|
+
from static_ghost.fast_inpaint import fast_remove
|
|
155
|
+
from static_ghost.inpainter import check_iopaint
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Step 1: Probe
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
meta = probe(video_path)
|
|
162
|
+
# Returns: {"width": 1920, "height": 1080, "fps": 30.0, "duration": 637.1, "codec": "h264", "audio_codec": "aac"}
|
|
163
|
+
total_frames = int(meta["fps"] * meta["duration"])
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Step 2: Get watermark coordinates
|
|
167
|
+
|
|
168
|
+
**Option A — User provides coordinates:**
|
|
169
|
+
```python
|
|
170
|
+
regions = [Region(x=1400, y=920, w=520, h=160, confidence=1.0)]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Option B — Auto-detect:**
|
|
174
|
+
```python
|
|
175
|
+
import tempfile
|
|
176
|
+
tmp = tempfile.mkdtemp()
|
|
177
|
+
sample_paths = extract_sample_frames(video_path, n=30, output_dir=tmp)
|
|
178
|
+
regions = detect_static_regions(sample_paths, threshold=15)
|
|
179
|
+
# If empty, try threshold=25, 35, 50
|
|
180
|
+
# If still empty, fall back to visual inspection or ask user
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Option C — Visual inspection (when agent can see images):**
|
|
184
|
+
```python
|
|
185
|
+
# Extract sample frames
|
|
186
|
+
paths = extract_sample_frames(video_path, n=5, output_dir=tmp)
|
|
187
|
+
# Read frames with vision tool, inspect corners for watermarks
|
|
188
|
+
# Crop suspected area to verify:
|
|
189
|
+
import cv2
|
|
190
|
+
img = cv2.imread(paths[0])
|
|
191
|
+
crop = img[h-150:h, w-500:w] # bottom-right corner
|
|
192
|
+
cv2.imwrite("/tmp/corner.png", crop)
|
|
193
|
+
# Estimate coordinates from visual inspection
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Step 3: Test on 30-second clip
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import subprocess
|
|
200
|
+
subprocess.run(["ffmpeg", "-y", "-i", video_path, "-t", "30", "-c", "copy", "/tmp/test_30s.mp4"], capture_output=True, check=True)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Run removal on clip, verify output visually, then proceed to full video.
|
|
204
|
+
|
|
205
|
+
### Step 4: Run removal
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from static_ghost.cli import parse_args, cmd_remove
|
|
209
|
+
|
|
210
|
+
args = parse_args([
|
|
211
|
+
"remove", video_path,
|
|
212
|
+
"--region", "1400,920,520,160",
|
|
213
|
+
"--dilation", "15",
|
|
214
|
+
"--device", "mps", # "cpu" if no Metal GPU
|
|
215
|
+
"-o", output_path,
|
|
216
|
+
])
|
|
217
|
+
cmd_remove(args)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**For long videos, run in background** (if your environment supports it) and check progress:
|
|
221
|
+
```python
|
|
222
|
+
import os
|
|
223
|
+
# Count output frames in temp dir
|
|
224
|
+
tmp_dirs = [d for d in os.listdir("/var/folders/...") if d.startswith("static_ghost_")]
|
|
225
|
+
# Compare against total_frames for progress
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Step 5: Verify
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
orig_meta = probe(video_path)
|
|
232
|
+
clean_meta = probe(output_path)
|
|
233
|
+
assert orig_meta["width"] == clean_meta["width"]
|
|
234
|
+
assert orig_meta["height"] == clean_meta["height"]
|
|
235
|
+
assert abs(orig_meta["duration"] - clean_meta["duration"]) < 1.0
|
|
236
|
+
assert clean_meta["audio_codec"] is not None
|
|
237
|
+
# Visually verify sample frames from output
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Decision tree for agents
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
User wants watermark removed from video
|
|
244
|
+
│
|
|
245
|
+
├─ User provided coordinates? → Use them directly
|
|
246
|
+
├─ Auto-detect finds regions? → Show to user for confirmation
|
|
247
|
+
├─ Auto-detect fails?
|
|
248
|
+
│ ├─ Agent has vision? → Extract frames, inspect corners, estimate coords
|
|
249
|
+
│ └─ Agent has no vision? → Ask user for coordinates or use --pick
|
|
250
|
+
│
|
|
251
|
+
├─ Test on 30s clip
|
|
252
|
+
│ ├─ Watermark gone? → Run full video
|
|
253
|
+
│ ├─ Partially visible? → Increase region size / dilation, re-test
|
|
254
|
+
│ └─ Artifacts? → Reduce dilation, re-test
|
|
255
|
+
│
|
|
256
|
+
└─ Run full video (background for >5 min videos)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Time estimation
|
|
260
|
+
|
|
261
|
+
Before running the full video, benchmark 5 frames to estimate total time:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
import time
|
|
265
|
+
test_paths = extract_sample_frames(video_path, n=5, output_dir="/tmp/bench_in")
|
|
266
|
+
os.makedirs("/tmp/bench_out", exist_ok=True)
|
|
267
|
+
start = time.time()
|
|
268
|
+
fast_remove("/tmp/bench_in", "/tmp/bench_out", regions, dilation=15, device="mps")
|
|
269
|
+
per_frame = (time.time() - start) / 5
|
|
270
|
+
est_minutes = per_frame * total_frames / 60
|
|
271
|
+
print(f"Estimated: {est_minutes:.0f} minutes")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Common pitfalls
|
|
275
|
+
|
|
276
|
+
| Mistake | Fix |
|
|
277
|
+
|---------|-----|
|
|
278
|
+
| Region too small | Add 50-100px padding beyond visible watermark edges |
|
|
279
|
+
| Dilation too low for large logos | Use `--dilation 15` for logos with solid color blocks |
|
|
280
|
+
| Running full video without testing | Always test on 30s clip first |
|
|
281
|
+
| Forgetting `--device mps` on macOS | 2x speed improvement for free |
|
|
282
|
+
| Auto-detect on semi-transparent watermarks | Will likely fail — use manual coordinates |
|
|
283
|
+
| Not checking disk space | 1080p 10min ≈ 40-80GB temp space |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "static-ghost"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Remove static watermarks from videos using LaMa inpainting"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "redredchen01"},
|
|
10
|
+
]
|
|
11
|
+
keywords = ["video", "watermark", "removal", "inpainting", "lama", "ffmpeg", "iopaint"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Multimedia :: Video",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"opencv-python-headless>=4.8",
|
|
21
|
+
"numpy>=1.26",
|
|
22
|
+
"Pillow>=10.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/redredchen01/static-ghost"
|
|
27
|
+
Repository = "https://github.com/redredchen01/static-ghost"
|
|
28
|
+
Issues = "https://github.com/redredchen01/static-ghost/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
static-ghost = "static_ghost.cli:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["setuptools>=68.0"]
|
|
40
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|