wmr is a command-line tool that removes visible watermarks from images and videos made by Google Gemini, Veo, and NotebookLM, and can scrub the invisible SynthID watermark through lossy SDXL regeneration. Every release package is a single self-contained binary that runs on a clean system with nothing to install. macOS builds are Developer ID signed and Apple-notarized, so they pass Gatekeeper with no extra steps.
- What it does
- Quick start
- Usage reference
- Graphical mode
- How it works (for researchers)
- Building from source
- Platform support
- Privacy
- License
- Credits
| Watermark | Found on | How wmr handles it | Status |
|---|---|---|---|
| Gemini diamond logo | Gemini images | Exact reverse alpha-blend (mathematically inverts the overlay) | Exact removal |
| Veo video watermark | Veo videos | Per-frame reverse alpha-blend + edge cleanup | Exact removal |
| NotebookLM logo + wordmark | NotebookLM videos | Per-scene AI inpaint (MI-GAN on the Neural Engine or ONNX Runtime CPU) | Inpaint |
| SynthID (invisible) | Gemini images | Lossy SDXL img2img regeneration | Lossy; validated against Google's official manual verifier (see below) |
| C2PA / AI provenance metadata | AI-generated images | Lossless container-level strip (PNG, JPEG); default on remove/synthid | Lossless on pixels |
detect locates the visible watermark without modifying the file. wmr does not detect SynthID: Google's "Verify with SynthID" is a manual in-app tool with no API wmr can drive, and the spectral detector shipped before 1.16.0 had no discriminative power (it scored ROC AUC 0.20 on images labeled by that Google verifier, so it was removed). See docs/research/synthid-spectral-removal-record.md for the full evidence.
The SynthID removal is lossy and cannot be confirmed in-process (there is no verifier API). The default strength was validated against Google's manual verifier on a small, varied set including a double-watermarked image; see SynthID reality and the linked research docs for the honest scope.
What this does not do. Removing the visible mark, SynthID, and provenance metadata takes them out of the file. It does not erase any server-side record: the service that generated the image may still associate it with the account that created it. The metadata strip is container-level only (labels and manifests); it does not touch pixel-level watermarks, which is what the visible-mark and SynthID paths handle.
Grab a prebuilt binary from the Releases page. Every package is self-contained (it bundles the AI models and any runtime libraries it needs).
| Asset | Platform | Run |
|---|---|---|
wmr-macos-arm64.zip |
macOS 14+ (Apple Silicon) | unzip wmr-macos-arm64.zip && cd wmr-macos-arm64 && ./wmr |
wmr-macos-x86_64.zip |
macOS 14+ (Intel) | unzip wmr-macos-x86_64.zip && cd wmr-macos-x86_64 && ./wmr |
wmr-linux-x86_64.tar.gz |
Linux | tar xzf wmr-linux-x86_64.tar.gz && cd wmr-linux-x86_64 && ./wmr |
wmr-windows-x86_64.zip |
Windows | extract, then run wmr.exe |
- macOS ships native CoreML MI-GAN for NotebookLM (Neural Engine, about 28 ms/frame) and, on Apple Silicon, a native CoreML SDXL pipeline for SynthID regen (much faster than CPU).
- Linux and Windows ship ONNX Runtime MI-GAN (about 225 ms/frame, CPU) and run SynthID regen on the CPU (correct, but slow).
- macOS builds are Developer ID signed and notarized, so Gatekeeper allows them on first launch (a one-time online check). If Gatekeeper still blocks a build (for example on an offline machine), run
xattr -dr com.apple.quarantine <extracted-dir>. - Third-party licenses:
LICENSE-THIRD-PARTY.md.
# Gemini image (auto-detects + removes the sparkle logo)
wmr remove image.png -o clean.png
# Gemini / Veo video (auto-detects the watermark position + size)
wmr video video.mp4 -o clean.mp4
# Veo legacy text watermark
wmr video veo.mp4 --legacy -o clean.mp4
# NotebookLM video (auto-detects the logo + wordmark)
wmr video notebooklm.mp4 --notebooklm -o clean.mp4
# SynthID invisible watermark (lossy SDXL regen)
wmr synthid image.png -o clean.pngBatch a folder: wmr remove folder/ -o cleaned/ --recursive.
Which command? Image with a visible mark ->
remove. Gemini/Veo video ->video. NotebookLM video ->video --notebooklm. Invisible (SynthID) ->synthid. Just locate ->detect.
Gemini 3.6 and 3.8 Flash images: the visible diamond is auto-detected, so
wmr removejust works, including 3.8 2K outputs (around 1700px short side). If a mark is too faint to confirm (it blends with white text or a light background), force its position:wmr remove img.png --geo-preset gemini36-portrait -o clean.png(named presets per resolution class, e.g.gemini36-portrait,gemini36-large,gemini38-2k-portrait) orwmr remove img.png --rect x,y,w,h -o clean.png(exact box). Usewmr detect img.png -vto read the detected geometry.
Supported inputs: PNG, JPEG, WebP images; MP4 and other FFmpeg-supported video.
| Command | Does |
|---|---|
remove (default) |
Auto-detect + remove visible watermarks. Optional --synthid-attack regen also scrubs SynthID. |
synthid |
Scrub only the SynthID invisible watermark (lossy SDXL regen runs by default). |
video |
Remove watermarks from video (Gemini, Veo, NotebookLM). |
detect |
Detect the visible watermark without modifying. |
cache |
Manage wmr's local caches (--clear-coreml on macOS, --clear-cpu-models). |
Run wmr <subcommand> --help for the full flag list.
| Flag | Applies to | Description |
|---|---|---|
-o, --output |
most | Output path (required for single files; batch defaults to cleaned/) |
-f, --force |
remove, synthid, video | Skip detection, assume a watermark is present |
--no-progress |
all | Suppress progress output (errors + final summary only) |
--no-update-check |
all | Skip the update check (also WMR_NO_UPDATE_CHECK=1) |
-v, --verbose / -V, --version |
all | Debug verbosity / print version |
-r, --recursive |
remove | Process directories recursively |
wmr auto-detects the visible mark's corner and size, so most images need no flags. Override the position when a mark is too faint to confirm.
| Flag | Applies to | Description |
|---|---|---|
--rect x,y,w,h |
remove, detect | Force the watermark box; removal runs there even on a faint mark |
--geo-preset <name> |
remove, detect | Named geometry: gemini36-portrait (896x1200 class), gemini36-large (2400x1792 class), gemini38-2k-portrait (1696x2528 class) |
--no-auto-geometry |
remove, detect | Skip the content-based position search; use the model position |
--force-small / --force-large |
remove | Force 48x48 / 96x96 Gemini logo size |
--legacy |
remove, detect | Pin the legacy Gemini (pre-3.5) V1 profile |
--no-legacy |
remove, detect | Pin the current (Gemini 3.5+) V2 profile; disable auto fallback |
The visible mark is a diamond in the bottom-right corner. wmr reads its position and size from the image content rather than guessing from the resolution: Gemini 3.6 and 3.8 use a 48x48 diamond on small and mid-size outputs, Gemini 3.8 2K outputs (around 1700px short side) carry a 96x96 diamond, and large 3.6 exports such as 2400x1792 carry the 48px mark at a wider margin. The older Gemini 3.5 used 36x36 on small outputs and 96x96 on large ones. Multi-template detection covers all of these; a genuine older 96x96 mark still removes byte-for-byte. The 36x36 and 96x96 masks were recalibrated from real watermarked images in 1.16.12, which removed a faint outline the older 96x96 masks could leave behind on dark backgrounds.
wmr does not detect SynthID (see SynthID reality). The only SynthID operation is --synthid-attack regen: a lossy SDXL img2img scrub, opt-in on remove and the default on synthid.
wmr synthid image.png -o clean.png # regen runs by default
wmr remove image.png --synthid-attack regen -o clean.png # visible diamond first, then regen| Flag | Description |
|---|---|
--synthid-attack regen |
SynthID attack method (default and only value: regen). On remove, regen runs only when this is passed. |
--regen-strength |
img2img strength, 0.02 to 0.15 (default 0.10, the validated minimum) |
--regen-steps |
sampler steps (default 50) |
--regen-restore-detail |
force detail-restoration on (you accept the risk on dim content) |
--no-regen-restore-detail |
force full regen (guaranteed removal) |
--regen-restore-band-sigma |
band-split restore sigma (default 1.95; lower = safer, 0 = the previous Wiener + top-5% path) |
--regen-backend {auto|cpu|metal|vulkan|coreml} |
runtime backend (default auto) |
--regen-no-download |
refuse the first-run model fetch (errors if the model is absent) |
--regen-no-tile |
disable tiled img2img (whole-image; fails above about 1024px on most GPUs) |
--regen-model-path / --regen-vae-path |
point at a local model / VAE instead of the pinned download |
What you should expect. At the strength that reliably scrubs SynthID (0.10) the regen output is visibly smoothed, but on bright images the default detail-restoration (see below) transplants the original's fine detail back, so faces, text, and texture survive far better than a plain regen; dim images (mean luminance below 128) are left fully regenerated, because restoring them has been detected by Google's verifier in testing. Measured fidelity for the raw regen is about 29 to 41 dB PSNR versus the input; the restore recovers most of the fine detail on bright content (about twice the perceptual gain of the restore shipped before 1.17.1). Lowering the strength is not recommended: lighter strengths (0.04 to 0.08) cleared singly-watermarked images in testing but missed a double-watermarked image, so 0.10 is the default. Regen also leaves a forensic footprint (a regen output is itself a detectable diffusion output); this is an attack on the watermark, not an invisibility guarantee.
Model download. SynthID regen downloads its models on first use (SHA256-pinned, cached in ~/.cache/wmr/, fetched from huggingface.co/froggeric/wmr). What it fetches depends on the backend: about 4.7 GB of CoreML packages on an Apple Silicon Mac (the default there), or the 7.3 GB SDXL base checkpoint plus the fp16-fix VAE when the CPU backend is used (explicitly, or as the fallback when --regen-no-download is set or the CoreML setup fails). Pass --regen-no-download to refuse the fetch.
If an earlier version already downloaded the CPU models on your Mac and you are now on CoreML, wmr tells you on the next regen run that they are no longer needed and offers to delete them (about 7.3 GB back). wmr cache --clear-cpu-models does the same on demand; they re-download if you ever pass --regen-backend cpu.
Backend selection. All backends use the same SDXL base model and the same deterministic-Euler img2img schedule, so the validated strength (0.10 @ 50 steps = 5 actual denoise steps) applies uniformly.
| Backend | Platform | Performance | Notes |
|---|---|---|---|
auto (default) |
all | macOS Apple Silicon uses CoreML and downloads its models on first use; falls back to CPU if that fails or --regen-no-download is set |
Linux, Windows, and macOS Intel use CPU. |
coreml |
macOS Apple Silicon | about 20s per 1024-tile on M4, plus a one-time ~50s load | Native CoreML SDXL pipeline; much faster than CPU. |
cpu |
all | about 231s per 896x1200 tile | sdcpp via stable-diffusion.cpp. The default on Linux, Windows, and macOS Intel release binaries. |
metal |
macOS Apple Silicon (arm64 binary only) | not recommended | sdcpp Metal backend is unstable upstream on Apple Silicon; prefer auto or coreml. |
Set $WMR_COREML_SD_MODELS_DIR to place the CoreML models elsewhere (defaults to ~/.cache/wmr/coreml-sdxl/). Set $WMR_COREML_SD_COMPUTE_UNITS (all default, cpu_gpu, cpu_ane, cpu) to override the CoreML compute units.
On macOS Apple Silicon the CoreML UNet uses ORIGINAL attention and is GPU-bound. The Neural Engine is unused (SDXL's large fp16 attention matmuls are ANE-ineligible, so CoreML places the UNet on the GPU). On upgrade, wmr re-verifies each cached model against its pinned SHA and re-downloads only what changed, removing the old copy first, so the cache does not grow.
| Flag | Description |
|---|---|
--notebooklm |
Target the NotebookLM logo + wordmark |
--rect x,y,w,h |
Manual watermark rect (overrides auto-detect; Gemini, Veo, and NotebookLM) |
--notebooklm-method {auto|ns|migan} |
Inpaint method override (auto = platform default: MI-GAN everywhere on Apple Silicon, complexity-gated elsewhere) |
--complexity-threshold |
NS vs MI-GAN gate (default 15; consulted only on non-arm64 auto) |
--variant |
Force geometry: 720p-1, 720p-2, 1080p (otherwise auto-detected) |
--no-auto-geometry |
Skip the content-based geometry search; fall back to the resolution guess |
--no-edge-cleanup |
Pure reverse-blend; skip the Gemini diamond edge cleanup (cleanup is on by default) |
--legacy |
Use the Veo legacy text profile |
--scenes |
Split multi-scene videos into separate files |
--scene-threshold |
Scene-cut sensitivity 0.0 to 1.0 (default 0.3) |
--crf / --preset / --codec |
Encode settings (default CRF 14, slow, libx264) |
--inpaint-strength |
Inpaint strength 0.0 to 1.0 (default 0.85) |
Measuring a NotebookLM --rect: if auto-detection misses, grab a full-resolution frame and measure the mark's x,y,width,height in any image editor:
ffmpeg -ss 30 -i input.mp4 -frames:v 1 frame.png # then measure the mark in frame.pngLeave about a 1px border around the mark, and pick a frame where it is clearly visible.
The default visible removal is a pure reverse-alpha-blend (the exact mathematical inversion, no blur). Optionally clean up residual artifacts from an imperfect reversal with --denoise. The opt-in cleanup is residual-only: it only touches pixels where the reverse-blend left a real residual, so it never blurs a clean removal.
wmr remove in.png -o out.png # default: exact reverse-blend, no cleanup
wmr remove in.png --denoise ns -o out.png # residual-only Navier-Stokes
wmr remove in.png --denoise telea -o out.png # residual-only Telea
wmr remove in.png --denoise ai -o out.png # AI (FDnCNN, release builds only)
wmr remove in.png --strength 150 -o out.png # cleanup strength (0-300%)| Flag | Range | Default | Notes |
|---|---|---|---|
--denoise |
off|soft|ns|telea (release adds ai) |
off |
Residual cleanup method (off = exact reverse-blend) |
--sigma |
1 to 150 | 50 | FDnCNN noise level (AI only) |
--strength |
0 to 300 % | 120 | Cleanup strength |
--radius |
1 to 25 | 10 | Gaussian / TELEA / NS radius |
wmr cache manages wmr's local caches:
wmr cache --clear-coreml # clear the CoreML execution cache; CoreML recompiles on the next regen
wmr cache --clear-cpu-models # delete the cached CPU regen models (about 7.3 GB)--clear-coreml removes wmr's app-scoped compiled-Metal cache (~/Library/Caches/wmr/com.apple.e5rt.e5bundlecache/). It never touches the shared ~/Library/Caches/CoreML or the model .mlpackage files. It is a no-op on Linux and Windows. See CoreML cache management.
--clear-cpu-models deletes the sdcpp CPU regen models from the user cache. They are only needed for --regen-backend cpu; while the CoreML backend is in use they are dead weight, and they re-download on demand if you ever want the CPU path. wmr also offers this cleanup on its own the first time it notices the leftovers while CoreML is active.
wmr metadata reports and strips C2PA / AI-provenance metadata from PNG and JPEG images at the container level. It reads raw bytes and rewrites the container, dropping the C2PA manifest chunk/marker, the AI-specific tEXt / iTXt keys (ComfyUI, A1111, InvokeAI, Midjourney, Stable Diffusion parameters, prompt, workflow), and APP1 XMP / APP13 IPTC AI markers. It never decodes pixels: PNG IDAT bytes and JPEG entropy bytes are copied verbatim, so the image is bit-identical. This is a distinct concern from the visible diamond (reverse alpha-blend) and from SynthID (lossy regen).
wmr metadata image.png # report findings on image.png
wmr metadata image.png -o clean.png # report + lossless strip to clean.png
wmr metadata folder/ -o clean/ -r # batch a directory (recursive)
wmr metadata image.png --strip-all -o clean.png # also drop non-AI metadata| Flag | Description |
|---|---|
-o, --output |
Output path (file; for a directory input, defaults to <input>_clean/) |
--dry-run |
Report findings only; do not write |
--strip-all |
Also drop non-AI metadata (author, copyright, camera settings). Default keeps those. |
-r, --recursive |
Process directories recursively |
wmr remove and wmr synthid also strip provenance metadata from the output by default. Opt out with --keep-provenance. Honest note: on today's OpenCV output this strip is a no-op scan. cv::imwrite already drops all container metadata on write and injects nothing, so the post-write scan finds nothing to strip and leaves the file untouched. The pass exists so the output stays provenance-free independent of the encoder. The active, load-bearing path is the standalone wmr metadata, which losslessly strips input files that do carry metadata.
v1 scope: PNG and JPEG. WebP, AVIF, HEIF, JPEG-XL, and MP4/MOV are sniffed and reported as unsupported (bytes copied unchanged); they are later phases. On video, --keep-provenance is accepted for CLI symmetry and is a documented no-op (the FFmpeg re-encode already produces a fresh container with no carried-over input boxes).
Run wmr with no arguments (or wmr gui) to start the built-in graphical
mode: a small local server on 127.0.0.1 plus your default browser. No
installation, no separate app; the interface ships inside the binary.
| Results | Compare |
|---|---|
![]() |
![]() |
-
Drag and drop images (PNG, JPEG, WebP); they are processed in order and each result can be compared side by side and downloaded, with the mark's size and how its spot was chosen reported per file. When no watermark is found, "Retry with a hint" shows the original with the chosen diamond previewed at its standard position, then tries again.
-
The page talks only to the local server; it makes zero external requests.
-
Like the CLI, wmr itself checks GitHub once per 24 hours for a newer release and shows a small notice in the page when one exists. Opt out with
WMR_NO_UPDATE_CHECK=1(orCI/DO_NOT_TRACK=1), the same as the CLI. Builds without the update check (WMR_UPDATE_CHECK=OFF, the same flag as the CLI) show no notice. -
Session files live under
~/.cache/wmr/gui(permissions 0700) while wmr runs, and are deleted when wmr stops (Ctrl-C or the page's Quit button) or at its next start. -
--gui-port Npins a port (default: an ephemeral free port);--no-browserprints the URL instead of opening one. -
Running
wmrwith no arguments now starts this mode. Scripts that expect the help text: setWMR_NO_GUI=1(orCI), or runwmr --help. -
v1 covers still images only; video and SynthID regen remain CLI-only.
-
Builds compiled without the GUI (
WMR_BUILD_GUI=OFF) have no graphical mode:wmr guiprints a notice and exits 2, and no-args prints the help text.
wmr recognizes two watermark styles: the current watermark (internally V2), the diamond Gemini has placed on images since 3.5, and the older watermark (internally V1) used before 3.5. Positioning is one choice, not a stack: search, the usual spot, or a known spot. Pick one.
| Which watermark do my images have? | What you see | Size |
|---|---|---|
| Gemini 3.5 | small diamond, close to the corner | 36 x 36 px |
| Gemini 3.6 / 3.8 | small diamond, close to the corner | 48 × 48 px |
| Gemini 3.8 at 2K | large diamond, further from the corner | 96 x 96 px |
| before 3.5 | older mark, fixed spot | 48 or 96 px |
Example result line: "Watermark removed · small diamond (48 × 48 px) · found automatically".
This section is for technical readers who want the mechanism and the evidence. The detailed findings live under docs/research/; the links below are the entry points.
Visible Gemini and Veo watermarks are alpha-blended overlays: watermarked = a * logo + (1 - a) * original. With the logo and its alpha map known, removal inverts the blend exactly: original = (watermarked - a * logo) / (1 - a). This reverse-blend is the default and is mathematically exact. The optional --denoise cleanup is residual-only (it predicts clean content with cv::inpaint and blends it in only where the reversal left a real residual, never blurring a clean removal).
The pipeline has three stages, orchestrated by WatermarkEngine:
- Detect. A three-stage NCC detector (spatial template match, gradient match on Sobel magnitudes, variance analysis) fused as
spatial*0.50 + gradient*0.30 + variance*0.20, threshold 0.35. - Geometry auto-detection. A polarity-invariant NCC template match against native per-size alpha captures, anchored on the predicted position then widened to the corner. Still images run the search once at the CLI layer (anchored first, widened only if the hit is not trusted); video aggregates about 12 sampled frames so the static mark wins over transient content. Templates are captured per size and never resized, because resizing smears the anti-aliased edges the exact reversal depends on. Two refinements (1.16.12): saturated bright content is suppressed before matching (the diamond is a white overlay, so white content under it carries no signal but plenty of noise), and a second scoring pass weights each pixel by how much mark its local background can show, which recovers marks that sit mostly over white emblems or text. A recognized position is re-scored at the calibrated preset spot before it is trusted, so content that merely looks like the mark cannot be pinned to a known geometry.
- Remove. The reverse-blend per pixel (still) or per frame (video). Video adds shot-level detection, an occlusion gate, and a residual-gated edge cleanup that repairs the faint halo at the diamond's edge on compressed footage (a safe no-op when the mark sits on a uniform background). Audio is passed through untouched.
The 48px Gemini mark (Gemini 3.6 and 3.8), the 36px mark (Gemini 3.5), and the 96px marks (Gemini 3.5 large, Gemini 3.8 2K) are all supported through multi-template detection. The search runs on every image regardless of the resolution-derived size class, so the 48px mark is found on large exports too. The alpha masks are calibrated from real watermarked images: the 48px mask is the average of 10 distinct clean captures, and since 1.16.12 the 96px masks are measured from real images against exactly known backgrounds (one calibration set is the same watermark stamped by the generator onto eight solid colors, which pinned the overlay to pure white and settled the mask to within about 0.6/255). Video removes with the same clean per-size alpha used for stills.
NotebookLM marks are semi-transparent and color-adaptive (not a reversible alpha overlay; the alpha is approximately zero, so there is no clean inverse). They are removed by AI inpainting. MI-GAN (MIT, ICCV 2023) synthesizes the missing region; on Apple Silicon it runs on the Neural Engine (about 28 ms/frame), elsewhere on ONNX Runtime CPU (about 225 ms/frame), falling back to Navier-Stokes. Every scene is inpainted, with the method chosen per scene: MI-GAN everywhere on Apple Silicon; elsewhere a complexity gate picks MI-GAN for textured backgrounds and NS for uniform ones.
SynthID-Image (arXiv 2510.09263) is a content-conditional neural watermark Google attaches to Gemini image output (Google's SynthID page). The encoder maps f(image, payload) -> image per image, detected by a trained extractor via a conformal p-value. It is not a fixed carrier a frequency-domain subtractor can isolate.
wmr does not detect SynthID, for two reasons that are documented in the research record:
- There is no public verifier API. Google's "Verify with SynthID" is a manual in-app tool, account-gated and rate-limited (about 10 checks per day), emitting natural-language verdicts. There is no batchable ground-truth label source, and Google's only open SynthID code is text-only. Without labeled data there is no way to train or validate a detector. See
synthid-detection-feasibility.md. - The detector we shipped before 1.16.0 had no discriminative power. Scored for the first time against 8 images labeled by Google's official verifier, it scored ROC AUC 0.20 (worse than random; its score is a content-property constant at about 61%). See
synthid-detection-validation.md.
The spectral suppression path (codebook subtraction, noise-residual subtraction, an LAB a-channel attack, carrier-bin seeding) was tried over the whole SynthID effort and removed in 1.16.0 because it did not work: the carrier sits below the content noise floor on real images (10 to 30x weaker than the content residual), so a clean codebook is inert on content (0.16 to 0.38% attenuation over the phase-noise baseline). The canonical decision record, including every method tried and the conditions under which detection could be revisited, is synthid-spectral-removal-record.md. The carrier characterization is in synthid-carrier-characterization.md.
The only validated removal is low-strength SDXL img2img regeneration. --synthid-attack regen regenerates the whole image with the SDXL base model at a low starting noise. This is the single attack the published literature reports as validated against SynthID-Image, and the one wmr validated. The default strength 0.10 at 50 steps (5 actual denoise steps) cleared a varied 9-image set, including an image-to-image generation that carries SynthID twice, confirmed over two rounds of Google's manual verifier. This is a small sample (the verifier is rate-limited to about 10 checks/day), not a large-scale proof, but it spans posters, mockups, and AI art and includes the hardest known case. Lighter strengths (0.04 to 0.08) cleared singly-watermarked images but missed the double-watermark case, so 0.10 is the default. See synthid-regen-validation.md and synthid-light-reconstruction-attacks.md.
Because Google exposes no verifier API, success cannot be confirmed in-process. The removal is verifiable, but only by a human using the manual in-app tool.
Regen is lossy. A detail-restoration pass recovers much of that fidelity on the content where it is safe. Since 1.17.1 the default is a band-split restore: after regen produces R, the output is O - blur(O - R, sigma) with sigma 1.95, which keeps the low frequencies from the regeneration (where the watermark lived and was destroyed) and transplants the original's entire high-frequency band back (grain, pores, fabric weave, glyph edges). It recovers roughly twice the perceptual fidelity of the previous Wiener + top-5% restore, was preferred over it in 6/6 blind visual comparisons with zero observed artifacts, and cleared Google's verifier on bright images from ~1 MP up to 4.3 MP. --regen-restore-band-sigma tunes it (lower = safer; 0 selects the previous Wiener + top-5% path).
Two findings from synthid-diff-restoration-analysis.md and bandsplit-restore-evaluation.md drive the design:
- Restored watermark energy is what the detector keys on. Restored content re-imports its carrier; staying under the detector's aggregate threshold is a budget, not a guarantee of removal. The band-split spends that budget on the frequencies above the characterized carrier's dominant band (the carrier's magnitude envelope falls as r^-1.3 with its data concentrated at low-mid frequencies).
- Mean luminance predicts whether restoration stays clear. A 10-image clearance study found brightness cleanly separates clear from fail (the 4 dimmest images fail; the 6 brightest clear; threshold about 128, 10/10, monotone), because the carrier's signal-to-content ratio scales as roughly 1/luminance. Forcing restoration past the gate on dim images was verifier-detected at every tested band-split sigma on one test image (and cleared only at the weakest sigma on another), so the gate is mandatory, not a heuristic.
The shipped behavior is automatic and luminance-gated: bright images (mean luminance >= 128) get the band-split restoration; dim images (< 128) get full regen (no restoration). Override with --regen-restore-detail (force restoration on; you accept the documented risk) or --no-regen-restore-detail (force full regen, guaranteed removal).
It is best-effort, not a guarantee: there is no in-process verifier, and the luminance gate is empirical (a 10-image sample). Detail-restoration dilutes the watermark on the restored pixels (below the detector's aggregate threshold); it does not remove it. Use --no-regen-restore-detail when you need guaranteed removal.
Long operations (regen, video, batch) print perception-grounded progress: a stage frame ([1/4] ... [4/4]) so you always know what is happening and what is next; per-tile and per-frame progress with count, rate, backend, and an honest ETA (a rolling average, hidden until enough samples so it never gives a misleading early guess); one-time costs (the first-run download, the first CoreML compile) labeled as one-time so they do not read as stalls; and elapsed time, tile count, and backend on completion. Output is TTY-aware: a refreshing progress bar on an interactive terminal; clean append-only milestone lines when piped, so CI logs stay linear. Progress writes to stderr, so wmr ... > /dev/null still shows it and stdout stays clean. --no-progress suppresses it. See cli-progress-ux-design.md for the design principles and the per-operation spec.
On macOS, CoreML keeps an app-scoped compiled-Metal execution cache (~/Library/Caches/wmr/com.apple.e5rt.e5bundlecache/) with no eviction. Across model re-pins, wmr upgrades, and macOS upgrades it was observed growing to about 138 GB, and stale entries produced noisy Apple-framework warnings. Since 1.16.7 wmr auto-manages this cache: at CoreML init it clears the cache when stale (the wmr version, the model pin, or the macOS version changed since the last clear, tracked through a sidecar) or when it exceeds a size threshold, then lets CoreML recompile (a one-time ~30s). It only touches the app-scoped cache; the shared ~/Library/Caches/CoreML and the model .mlpackage files are never touched. Linux and Windows are unaffected. wmr cache --clear-coreml clears it on demand. See coreml-execution-cache-management.md.
The CoreML SD pipeline is a from-scratch Objective-C++ port (an Euler scheduler over Accelerate, VAE encode and decode, baked empty-prompt embeddings; text encoders are not shipped). The GPU/ANE placement investigation, including why the Neural Engine is unused for SDXL and why 6-bit weight palettization was tested and rejected (it is slower, not faster, on the GPU), is in coreml-sd-placement.md.
Requires CMake 3.21+, a C++20 compiler, and Ninja.
macOS (Homebrew), recommended: scripts/build.sh verifies the required formulas and self-heals a stale cache:
brew install cmake ninja opencv ffmpeg fmt spdlog cli11 catch2
scripts/build.sh # release + tests
RUN_TESTS=0 scripts/build.sh # build onlyvcpkg (all platforms):
cmake -B build -S . -GNinja
cmake --build buildTo match the release binaries (AI denoise, MI-GAN, SynthID regen, CoreML SD), set WMR_AI_MIGAN=1 WMR_AI_DENOISE=1 scripts/build.sh on macOS (also install vulkan-volk, vulkan-loader, vulkan-headers, molten-vk). Tests: ctest --test-dir build --output-on-failure.
See CLAUDE.md for the full build matrix, platform quirks, and the design notes behind each watermark path, and CHANGELOG.md for version history.
| Platform | Visible (image + video) | NotebookLM | SynthID regen | Update check |
|---|---|---|---|---|
| macOS 14+ Apple Silicon | exact reverse-blend | CoreML MI-GAN (Neural Engine) | CoreML SDXL (fast) | yes |
| macOS 14+ Intel | exact reverse-blend | CoreML MI-GAN | CPU (sdcpp, slow) | yes |
| Linux x86_64 | exact reverse-blend | ONNX Runtime MI-GAN (CPU) | CPU (sdcpp, slow) | yes |
| Windows x86_64 | exact reverse-blend | ONNX Runtime MI-GAN (CPU) | CPU (sdcpp, slow) | yes |
All packages ship the visible-removal alpha maps and the MI-GAN model. The SynthID regen models (about 4.7 GB CoreML on macOS, 7.3 GB CPU elsewhere) are downloaded on first use, not bundled. macOS packages are Developer ID signed and notarized.
--synthid-attack regen runs everywhere, but off Apple Silicon it is CPU-only and slow (minutes per image). The Linux and Windows release binaries ship CPU-only because the sdcpp (leejet/stable-diffusion.cpp + ggml) GPU backends have build gaps we could not CI-validate (Vulkan needs glslc and the loader on the Linux runner; sdcpp on MSVC was never green in CI). If you work with Vulkan or CUDA on ggml / stable-diffusion.cpp and can help wire and validate a GPU regen backend for Linux and Windows, that is the biggest speedup left. Build locally with WMR_BUILD_REGEN=ON (no WMR_REGEN_CPU_ONLY), point --regen-backend vulkan or cuda at a GPU, and report the timings and whether the output still clears Google's SynthID verifier. Open an issue to coordinate.
Other welcome contributions and open items:
- Gemini 3.5 calibration images. The 36x36 mask's strength was never verified against real 3.5 outputs (none could be located for testing; generations from that era are becoming rare). If you can still generate with a 3.5 image model, a handful of unedited PNGs with the mark near the bottom-right corner would let the mask be recalibrated the same way the others were. Open an issue.
- Marks that sit entirely over QR-code-like patterns or pure white areas can defeat template matching entirely (there is no usable signal).
--force --legacyhandles the known legacy cases at the standard position; a detection path that does not rely on correlation is an open idea. - New watermark profiles (still or video geometry and alpha captures), higher-fidelity or faster inpaint backends, and video SynthID.
- Safe detail restoration for dim images. The luminance gate routes dark images to full regen because forced restores there have been verifier-detected; the detection is per-image (a weak band-split restored one dim test image cleanly and was detected on another), and with no in-process verifier there is no safe way to pick the images that tolerate it today.
For a visible-only user, wmr makes zero network calls. The one network feature is the update check (notify-only, on by default since 1.16.4): once per day it fetches the latest release tag and prints a short notice to stderr after the command finishes. It never downloads or replaces the binary.
- Zero payload. The check is a single HTTPS GET of the latest release tag with a versionless
User-Agent: wmr. No version, OS, arch, or id is sent. - Opt out:
--no-update-check,WMR_NO_UPDATE_CHECK=1,CI, orDO_NOT_TRACK=1; it also auto-disables when stderr is not a terminal. - Throttle: at most once per 24h; override with
WMR_UPDATE_CHECK_INTERVAL=<seconds>. Cache at~/.cache/wmr/update-check.json. - Build without it:
cmake -DWMR_UPDATE_CHECK=OFF ....
The only other network call is the SynthID regen model download, which runs only when you use --synthid-attack regen (and you can refuse it with --regen-no-download).
MIT. Bundled third-party licenses (NCNN, volk, KAIR/FDnCNN, MI-GAN, ONNX Runtime, OpenCV) are in LICENSE-THIRD-PARTY.md.
Built on research and code from:
- GeminiWatermarkTool and VeoWatermarkRemover: reverse alpha-blend, NCC detection, inpainting, FDnCNN conversion (Allen Kuo)
- reverse-SynthID: SynthID spectral analysis
- Picsart-AI-Research/MI-GAN: MI-GAN inpainting (MIT, ICCV 2023)
- wiltodelta/remove-ai-watermarks: studied and reimplemented the byte-level container strip approach for C2PA / AI provenance (MIT; no verbatim code copied)
- KAIR/FDnCNN, Tencent/ncnn, zeux/volk, microsoft/onnxruntime: AI inference stack


