Add OpenArt visible watermark detection and removal

OpenArt.ai stamps free-plan generations with a semi-transparent white
"OpenArt" wordmark (bowtie/infinity icon + brand name) placed near the
CENTER of the frame rather than in a corner. Confirmed from a customer-
reported raiw-app production case where the mark went completely
undetected and unremoved, with no C2PA/EXIF/TC260 signal for identify
to fall back on.

- Add a new "cc" (frame-center) corner mode to the shared
  TextMarkConfig/TextMarkEngine base, alongside the existing
  corner/edge anchors.
- Add openart_engine.py, following the existing text-mark engine
  pattern. No real capture exists (OpenArt requires an account, out of
  scope to create), so the bundled alpha asset is a procedural
  reconstruction of the public logo and the NCC threshold is an
  explicitly-labeled provisional guess, not a measured calibration.
- Register the mark in watermark_registry.py, which auto-derives
  identify's platform attribution.
- Add tests/test_openart_engine.py (synthetic composites only) plus
  registry/identify test coverage, and document the mark and its
  calibration caveats in README.md, docs/supported-signals.md,
  docs/module-internals.md, and the skill's commands.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Victor KuznetsovandClaude Sonnet 5 committed 2026-09-21 21:51:46 -07:00
1 parent 7a6491fdc1
commit 9d29d2c777
12 files changed
+547 -7

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
"""Build the PROVISIONAL synthetic OpenArt alpha asset.
Unlike ``visible_alpha_solve.py``, this does NOT recover an alpha map from a
controlled capture: OpenArt (openart.ai) requires an account to generate
images (no anonymous or API-key access, confirmed 2026-09-21 via its own
help center and MCP docs), and creating an account is outside what this
project's tooling is allowed to do on its own. No committed capture exists
under ``data/calibration/openart/``.
This script instead PROCEDURALLY DRAWS a reconstruction of OpenArt's own
brand mark (the bowtie/infinity icon followed by the "OpenArt" wordmark, as
shown on openart.ai's own marketing pages) as a semi-transparent white
overlay, matching the general rendering style (near-white, low-saturation,
anti-aliased) that every other bundled text-mark asset in this module was
solved from. It is a reconstruction from the public logo, not a recovered
watermark alpha map, and ``openart_engine.py`` documents that provenance
gap explicitly. Replace this asset (and re-run the engine's calibration)
the moment a real captured OpenArt export is available under
``data/calibration/openart/``.
Usage::
uv run python scripts/build_openart_alpha.py
"""
# cv2/numpy boundary: third-party libs ship no usable element types; relax the
# unknown-type rules for this file only (mirrors visible_alpha_solve.py).
# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingTypeArgument=false, reportMissingTypeStubs=false, reportMissingImports=false, reportArgumentType=false, reportAssignmentType=false, reportReturnType=false, reportCallIssue=false, reportIndexIssue=false, reportOperatorIssue=false
from __future__ import annotations
import sys
from pathlib import Path
import cv2
import numpy as np
sys.path.insert(0, str(Path(__file__).parent.parent))
_ROOT = Path(__file__).resolve().parents[1]
_ASSET = _ROOT / "src" / "remove_ai_watermarks" / "assets" / "openart_alpha.png"
# Canvas sized to comfortably fit "OpenArt" set bold at this height plus the
# icon and inter-glyph gap, at a resolution close to the other bundled
# assets (doubao_alpha.png is 335x83).
_W, _H = 360, 90
_ALPHA_PEAK = 173 # matches doubao_alpha.png's measured peak (~68% opacity)
def _draw_infinity_icon(canvas: np.ndarray, cx: int, cy: int, size: int) -> None:
"""Draw the bowtie/infinity glyph left of the wordmark (two mirrored lobes)."""
half = size // 2
# Two overlapping filled triangles meeting at the center, echoing the
# angular "infinity" mark on openart.ai's own site header.
left_lobe = np.array(
[[cx - size, cy - half], [cx - size, cy + half], [cx, cy]],
dtype=np.int32,
)
right_lobe = np.array(
[[cx + size, cy - half], [cx + size, cy + half], [cx, cy]],
dtype=np.int32,
)
cv2.fillConvexPoly(canvas, left_lobe, (_ALPHA_PEAK,), lineType=cv2.LINE_AA)
cv2.fillConvexPoly(canvas, right_lobe, (_ALPHA_PEAK,), lineType=cv2.LINE_AA)
def build() -> np.ndarray:
canvas = np.zeros((_H, _W), np.uint8)
icon_cx, icon_cy, icon_size = 34, _H // 2, 22
_draw_infinity_icon(canvas, icon_cx, icon_cy, icon_size)
text_x = icon_cx + icon_size + 18
cv2.putText(
canvas,
"OpenArt",
(text_x, _H // 2 + 16),
cv2.FONT_HERSHEY_DUPLEX,
1.3,
(_ALPHA_PEAK,),
thickness=2,
lineType=cv2.LINE_AA,
)
return canvas
def main() -> None:
canvas = build()
_ASSET.parent.mkdir(parents=True, exist_ok=True)
cv2.imwrite(str(_ASSET), canvas)
print(f"Wrote {_ASSET} ({canvas.shape[1]}x{canvas.shape[0]}, peak={int(canvas.max())})")
if __name__ == "__main__":
main()