Install
openclaw skills install @c0s1n3/scan-watermark-removalDeterministically remove light translucent text watermarks from scanned paper pages (lecture notes, handouts, exam papers, workbooks) as images or PDF, plus printed coloured header logos. Use when the user asks to 去水印 / remove watermark / clean watermarks from 扫描笔记、讲义、试卷、资料图片、PDF 讲义, especially batches of course-note scans with pale diagonal tiled text (WeChat id, brand name, URL) or a brand logo in the page header. Not for photo watermarks, dark or photographic logos, or cases where the watermark colour is indistinguishable from content — those need generative inpainting instead, which redraws the page and can destroy handwriting, so warn the user and require per-page review.
openclaw skills install @c0s1n3/scan-watermark-removalRemove two different things that both get called "watermark" on scanned course material:
| Family | Looks like | Method |
|---|---|---|
| Translucent text | pale pink/purple diagonal tiled text over the whole page | colour-cone pixel isolation |
| Printed header logo | solid coloured brand text (e.g. a teacher's name) in the page header | colour wedge + shape test, header band only |
Both are deterministic: no model, no generative redraw, no network. Red pen, black print, blue table rules, grey figures and colour illustrations survive 100% — verified by pixel accounting on every page.
Open 2–3 pages at full resolution. Identify which families are present:
Pick a patch of paper that has watermark but no content, read the RGB. Typical measured values:
| Watermark | Sample colour | Signal |
|---|---|---|
| pink | (244,191,191) | r-g ≈ 53, ` |
| purple | (200,190,215) | b-g ≈ 20, ` |
| header logo | (255,192,0) | r-g = 63, b = 0 |
If the sampled colour is far from these, adjust the constants at the top of
scripts/wmcore.py (PINK, PURPLE, FAINT) — see
references/watermark-signature.md for how to derive thresholds.
# image folder or single image
python scripts/remove_watermark.py <folder_or_image>
# PDF
python scripts/remove_watermark.py <file.pdf> --output <out.pdf>
Defaults: output <input>/去水印输出/ for images, <name>_去水印.pdf for PDF;
suffix _去水印; both colour cones on; header logo cleared. Originals are never
modified.
Useful flags: --near (ink-protection window, default 5), --no-logo,
--no-pink, --no-purple, --all (reprocess already-suffixed files).
Every page prints:
erased px → must be 0. Pixels that were unambiguously content (dark
min < 120, saturated red, saturated blue) and are now near-white. This is the
real safety metric.residue px plus visible in parentheses → watermark pixels on clean paper,
and how many are dark enough to actually be seen. Visible must be ~0; the
raw count is often in the thousands on a JPEG page and means nothing.red / dark / blue retention % → coverage reference only, expected ≥ 90.Why erased rather than a retention percentage: JPEG re-encoding shifts the
palest antialiased edges by a few units, which pushes a percent or two of them
below the "is this content" threshold. Retention can read 91% on a page where
zero real pixels were lost. Percentage thresholds therefore either fail correct
output or invite loosening until they hide real damage. Counting documented
content that disappeared has neither problem.
To audit the old way anyway, print the original min channel of every lost pixel:
deficits from min ∈ [120,150] are edge jitter, min < 100 is real loss.
Exit code 0 means all pages passed. Non-zero lists the specific pages.
python scripts/verify_batch.py <original folder>
python scripts/verify_batch.py <original.pdf> --output <out.pdf>
It recomputes retention and residue from scratch using the output naming rule, so it catches what self-checks miss: wrong output filenames, silently lost content, and header logos that survived. Always run it before telling the user a large batch is done.
Confirm headings, page numbers, handwritten formulas and diagrams are intact, and that no diagonal ghost lines remain on near-blank pages (back covers and chapter dividers expose residue most readily).
The watermark is a uniform colour at uniform opacity over paper white:
observed = α · watermark_colour + (1 - α) · paper
so every watermark pixel lies on a ray from the white point in chroma space. Real content does not lie on that ray, which makes the split exact:
r-g ∈ [6,62] and |b-g| ≤ 10; purple:
b-g ∈ [4,32] and |r-g| ≤ 10. Cyan table rules (r-g ≈ -20) and grey
figures (luma < 180) fall outside naturally.r-g > 55, r > 110). Where watermark
crosses a stroke only a barely visible tint remains.Not translucent at all — it is flat opaque colour, so colour alone is unsafe (body illustrations can contain the same hue). Constraints matter:
r ≥ 240, g ∈ [150,235],
b ≤ 60, r-b ≥ 150. A blue frame at (79,129,189) fails on b, so the
decorative frame survives.r ≥ 200, r-b ≥ 8) within 4px of the
glyphs are whitened too. Cool colours (b > r) are untouched, so the frame
and rule line stay.Because the logo is opaque colour, it also counts as "red" and "dark" content. The reported retention therefore excludes the logo region (dilated 5px); without that exclusion a blank page would report 0% red after a correct removal.
PDF watermarks of this kind are not PDF objects — the page has no text layer
(get_text() returns empty) and the watermark is baked into the scan bitmap.
There is no object to delete, so the pipeline rasterises:
PDF ──render 300dpi──> bitmap ──pixel cleanup──> bitmap ──repackage──> PDF
Two gotchas worth remembering:
get_pixmap(). Do not grab the largest embedded
image to save time — some pages are collages of several smaller images, and
the largest one may be a figure rather than the page.Output PDF is rasterised, so text is no longer selectable. For born-digital PDFs prefer deleting the watermark object with a PDF editor instead.
| Symptom | Fix |
|---|---|
| Content touched, strokes thinned | raise --near to 7 (more conservative) |
| Watermark remains in patches | lower --near to 3–4, or widen the cone in wmcore.py |
| Residue on near-blank pages | FAINT window too narrow — widen lo/hi |
| Header logo untouched | colour differs from (255,192,0) — recalibrate LOGO_CORE in wm_pdf.py |
| Body colour illustration erased | header band too tall, or shape test too loose — tighten both |
remove_watermark.py — single entry point for images and PDF, with metrics.wmcore.py — colour-cone watermarks: masks, ink protection, batch driver,
retention/residue metrics. Pure numpy + pillow, no scipy, no network.wm_pdf.py — PDF rasterise/repack plus header-logo detection and removal.
logo_only() also cleans logos on plain images without pymupdf installed.verify_batch.py — independent verifier for images and PDF.watermark-signature.md — how to sample the colour, derive thresholds, and
read the anomaly table (ghost lines, clipped content, pink residue on strokes).pdf-and-logo.md — PDF pipeline details and header-logo calibration
worksheet.