demark 0.0.1__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.
demark-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: demark
3
+ Version: 0.0.1
4
+ Summary: Automatic image watermark detection and removal toolkit (CV + DL methods)
5
+ Author-email: akino <6130092+cycleuser@users.noreply.github.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/cycleuser/demark
8
+ Project-URL: Repository, https://github.com/cycleuser/demark
9
+ Project-URL: Documentation, https://github.com/cycleuser/demark#readme
10
+ Project-URL: Changelog, https://github.com/cycleuser/demark/releases
11
+ Keywords: watermark,watermark-removal,inpainting,image-processing,opencv,lama,alpha-matting,line-art
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Multimedia :: Graphics :: Editors
22
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: numpy>=1.24
26
+ Requires-Dist: opencv-python>=4.8
27
+ Requires-Dist: scipy>=1.10
28
+ Requires-Dist: scikit-image>=0.20
29
+ Requires-Dist: Pillow>=10.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Requires-Dist: mypy; extra == "dev"
34
+ Provides-Extra: deep
35
+ Requires-Dist: torch>=2.0; extra == "deep"
36
+ Requires-Dist: simple-lama-inpainting; extra == "deep"
37
+ Provides-Extra: sd
38
+ Requires-Dist: torch>=2.0; extra == "sd"
39
+ Requires-Dist: diffusers>=0.20; extra == "sd"
40
+ Requires-Dist: transformers>=4.30; extra == "sd"
41
+
42
+ # DeMark
43
+
44
+ Automatic image watermark detection and removal toolkit. Multiple methods from classic CV to deep learning, pick what fits your watermark type.
45
+
46
+ ## What it does
47
+
48
+ Detects watermark regions in images and removes them while preserving underlying content. Ten methods are implemented:
49
+
50
+ | Method | Type | Best for | Lossless? |
51
+ |--------|------|----------|-----------|
52
+ | `auto` | — | Auto-selects best method | — |
53
+ | `cv_telea` | CV inpaint [6] | Small/thin watermarks | No |
54
+ | `cv_ns` | CV inpaint [7] | Smooth gradients around watermark | No |
55
+ | `fft_notch` | Frequency | Repeating/tiled periodic watermarks | Partial |
56
+ | `color_mask` | CV segmentation | Solid-color text (white on photo) | No |
57
+ | `edge_inpaint` | Edge + inpaint [5] | Text/logos with sharp edges | No |
58
+ | `template` | Template match | Known watermark logo | No |
59
+ | `alpha_invert` | Alpha matting [8] | Known watermark + transparency | **Yes** |
60
+ | `lama` | Deep learning [1] | Large masked regions | No |
61
+ | `sd_inpaint` | Diffusion [2] | Generative content fill | No |
62
+ | `precise` | Alpha inversion + line protection | Semi-transparent text on line art | Partial |
63
+ | `harmonic` | Multi-scale + auto-optimisation | Any scale, self-tuning | Partial |
64
+
65
+ ## What it does NOT do
66
+
67
+ - No batch GUI — CLI and Python API only. For a GUI, use IOPaint (23k stars) which wraps LaMa.
68
+ - No video watermark removal — for video use ProPainter (sczhou/ProPainter).
69
+ - No invisible/digital watermark removal — those require diffusion-based attacks (see [10]).
70
+ - `alpha_invert` is the only truly lossless method, and it requires the watermark pattern and alpha to be known.
71
+
72
+ ## Requirements
73
+
74
+ - Python 3.9+
75
+ - OpenCV, NumPy, SciPy, scikit-image, Pillow
76
+ - For deep learning methods: `pip install demark[deep]` (LaMa) or `pip install demark[sd]` (Stable Diffusion)
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ # From PyPI
82
+ pip install demark
83
+
84
+ # Optional deep learning extras (LaMa / Stable Diffusion)
85
+ pip install "demark[deep]" # LaMa inpainting
86
+ pip install "demark[sd]" # Stable Diffusion inpainting
87
+
88
+ # From source
89
+ git clone https://github.com/cycleuser/demark.git
90
+ cd demark
91
+ pip install -e .
92
+ ```
93
+
94
+ ## Quick Start
95
+
96
+ ```bash
97
+ # Auto-detect and remove
98
+ demark remove watermarked.jpg clean.jpg
99
+
100
+ # Use specific method
101
+ demark remove watermarked.jpg clean.jpg --method fft_notch
102
+
103
+ # Known watermark logo — template matching
104
+ demark remove watermarked.jpg clean.jpg --method template --template logo.png
105
+
106
+ # Known watermark overlay + alpha — lossless removal
107
+ demark remove watermarked.jpg clean.jpg --method alpha_invert --watermark wm_overlay.png --alpha 0.4
108
+
109
+ # Scale-adaptive multi-scale + auto-optimisation (works at any scale)
110
+ demark remove watermarked.jpg clean.jpg --method harmonic --bbox 1056 1249 1304 1305
111
+
112
+ # Just detect the mask
113
+ demark detect watermarked.jpg mask.png
114
+
115
+ # JSON output
116
+ demark --json remove watermarked.jpg clean.jpg
117
+ ```
118
+
119
+ ## Usage
120
+
121
+ ### CLI
122
+
123
+ ```
124
+ demark [-V] [-v] [-o OUTPUT] [--json] [-q] <command> ...
125
+
126
+ Commands:
127
+ remove <input> <output> [--method M] [--mask M.png] [--template T.png]
128
+ [--watermark W.png] [--alpha 0.4] [--radius 5]
129
+ detect <input> <output> [--method auto|edge|color|otsu]
130
+ list-methods
131
+ ```
132
+
133
+ ### Python API
134
+
135
+ ```python
136
+ from demark import remove_watermark, detect_watermark
137
+
138
+ # Auto remove
139
+ r = remove_watermark(input_path="wm.jpg", output_path="clean.jpg")
140
+ print(r.success) # True
141
+ print(r.data["method"]) # "edge_inpaint"
142
+ print(r.data["elapsed"])# 0.123
143
+
144
+ # Specific method
145
+ r = remove_watermark(
146
+ input_path="wm.jpg",
147
+ output_path="clean.jpg",
148
+ method="fft_notch"
149
+ )
150
+
151
+ # Detect only
152
+ r = detect_watermark(input_path="wm.jpg", output_path="mask.png")
153
+ print(r.data["coverage"]) # 0.0523
154
+ ```
155
+
156
+ ### Agent Integration (OpenAI Function Calling)
157
+
158
+ ```python
159
+ from demark.tools import TOOLS, dispatch
160
+
161
+ # Pass TOOLS to your LLM's function-calling API
162
+ result = dispatch("demark_remove_watermark", {
163
+ "input_path": "/data/wm.jpg",
164
+ "output_path": "/data/clean.jpg",
165
+ "method": "auto"
166
+ })
167
+ print(result["success"]) # True
168
+ ```
169
+
170
+ ## References
171
+
172
+ | # | Paper | Year | Venue |
173
+ |---|-------|------|-------|
174
+ | [1] | Suvorov et al., "Resolution-robust Large Mask Inpainting with Fourier Convolutions" | 2021 | WACV 2022 |
175
+ | [2] | Rombach et al., "High-Resolution Image Synthesis with Latent Diffusion Models" | 2022 | CVPR |
176
+ | [3] | Liu et al., "WDNet: Watermark-Decomposition Network for Visible Watermark Removal" | 2020 | WACV 2021 |
177
+ | [4] | Cun & Pun, "Split then Refine: Stacked Attention-guided ResUNets for Blind Watermark Removal" | 2020 | AAAI 2021 |
178
+ | [5] | Robinette & Johnson, "Blind Visible Watermark Removal with Morphological Dilation" | 2025 | arXiv:2502.02676 |
179
+ | [6] | Telea, "An Image Inpainting Technique Based on the Fast Marching Method" | 2004 | J. Graphics Tools |
180
+ | [7] | Bertalmio et al., "Navier-Stokes, Fluid Dynamics, and Image and Video Inpainting" | 2001 | CVPR |
181
+ | [8] | Levin et al., "A Closed-Form Solution to Natural Image Matting" | 2006 | IEEE TPAMI |
182
+ | [9] | Huo et al., "WMFormer++: Nested Transformer for Visible Watermark Removal" | 2023 | arXiv:2308.10195 |
183
+ | [10] | "Vanishing Watermarks: Diffusion-Based Image Editing Undermines Robust Invisible Watermarking" | 2026 | arXiv:2602.20680 |
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ pip install -e ".[dev]"
189
+ pytest tests/test_unified_api.py -v
190
+ ruff check demark/
191
+ ```
192
+
193
+ ## License
194
+
195
+ GPL-3.0
demark-0.0.1/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # DeMark
2
+
3
+ Automatic image watermark detection and removal toolkit. Multiple methods from classic CV to deep learning, pick what fits your watermark type.
4
+
5
+ ## What it does
6
+
7
+ Detects watermark regions in images and removes them while preserving underlying content. Ten methods are implemented:
8
+
9
+ | Method | Type | Best for | Lossless? |
10
+ |--------|------|----------|-----------|
11
+ | `auto` | — | Auto-selects best method | — |
12
+ | `cv_telea` | CV inpaint [6] | Small/thin watermarks | No |
13
+ | `cv_ns` | CV inpaint [7] | Smooth gradients around watermark | No |
14
+ | `fft_notch` | Frequency | Repeating/tiled periodic watermarks | Partial |
15
+ | `color_mask` | CV segmentation | Solid-color text (white on photo) | No |
16
+ | `edge_inpaint` | Edge + inpaint [5] | Text/logos with sharp edges | No |
17
+ | `template` | Template match | Known watermark logo | No |
18
+ | `alpha_invert` | Alpha matting [8] | Known watermark + transparency | **Yes** |
19
+ | `lama` | Deep learning [1] | Large masked regions | No |
20
+ | `sd_inpaint` | Diffusion [2] | Generative content fill | No |
21
+ | `precise` | Alpha inversion + line protection | Semi-transparent text on line art | Partial |
22
+ | `harmonic` | Multi-scale + auto-optimisation | Any scale, self-tuning | Partial |
23
+
24
+ ## What it does NOT do
25
+
26
+ - No batch GUI — CLI and Python API only. For a GUI, use IOPaint (23k stars) which wraps LaMa.
27
+ - No video watermark removal — for video use ProPainter (sczhou/ProPainter).
28
+ - No invisible/digital watermark removal — those require diffusion-based attacks (see [10]).
29
+ - `alpha_invert` is the only truly lossless method, and it requires the watermark pattern and alpha to be known.
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.9+
34
+ - OpenCV, NumPy, SciPy, scikit-image, Pillow
35
+ - For deep learning methods: `pip install demark[deep]` (LaMa) or `pip install demark[sd]` (Stable Diffusion)
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ # From PyPI
41
+ pip install demark
42
+
43
+ # Optional deep learning extras (LaMa / Stable Diffusion)
44
+ pip install "demark[deep]" # LaMa inpainting
45
+ pip install "demark[sd]" # Stable Diffusion inpainting
46
+
47
+ # From source
48
+ git clone https://github.com/cycleuser/demark.git
49
+ cd demark
50
+ pip install -e .
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```bash
56
+ # Auto-detect and remove
57
+ demark remove watermarked.jpg clean.jpg
58
+
59
+ # Use specific method
60
+ demark remove watermarked.jpg clean.jpg --method fft_notch
61
+
62
+ # Known watermark logo — template matching
63
+ demark remove watermarked.jpg clean.jpg --method template --template logo.png
64
+
65
+ # Known watermark overlay + alpha — lossless removal
66
+ demark remove watermarked.jpg clean.jpg --method alpha_invert --watermark wm_overlay.png --alpha 0.4
67
+
68
+ # Scale-adaptive multi-scale + auto-optimisation (works at any scale)
69
+ demark remove watermarked.jpg clean.jpg --method harmonic --bbox 1056 1249 1304 1305
70
+
71
+ # Just detect the mask
72
+ demark detect watermarked.jpg mask.png
73
+
74
+ # JSON output
75
+ demark --json remove watermarked.jpg clean.jpg
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ### CLI
81
+
82
+ ```
83
+ demark [-V] [-v] [-o OUTPUT] [--json] [-q] <command> ...
84
+
85
+ Commands:
86
+ remove <input> <output> [--method M] [--mask M.png] [--template T.png]
87
+ [--watermark W.png] [--alpha 0.4] [--radius 5]
88
+ detect <input> <output> [--method auto|edge|color|otsu]
89
+ list-methods
90
+ ```
91
+
92
+ ### Python API
93
+
94
+ ```python
95
+ from demark import remove_watermark, detect_watermark
96
+
97
+ # Auto remove
98
+ r = remove_watermark(input_path="wm.jpg", output_path="clean.jpg")
99
+ print(r.success) # True
100
+ print(r.data["method"]) # "edge_inpaint"
101
+ print(r.data["elapsed"])# 0.123
102
+
103
+ # Specific method
104
+ r = remove_watermark(
105
+ input_path="wm.jpg",
106
+ output_path="clean.jpg",
107
+ method="fft_notch"
108
+ )
109
+
110
+ # Detect only
111
+ r = detect_watermark(input_path="wm.jpg", output_path="mask.png")
112
+ print(r.data["coverage"]) # 0.0523
113
+ ```
114
+
115
+ ### Agent Integration (OpenAI Function Calling)
116
+
117
+ ```python
118
+ from demark.tools import TOOLS, dispatch
119
+
120
+ # Pass TOOLS to your LLM's function-calling API
121
+ result = dispatch("demark_remove_watermark", {
122
+ "input_path": "/data/wm.jpg",
123
+ "output_path": "/data/clean.jpg",
124
+ "method": "auto"
125
+ })
126
+ print(result["success"]) # True
127
+ ```
128
+
129
+ ## References
130
+
131
+ | # | Paper | Year | Venue |
132
+ |---|-------|------|-------|
133
+ | [1] | Suvorov et al., "Resolution-robust Large Mask Inpainting with Fourier Convolutions" | 2021 | WACV 2022 |
134
+ | [2] | Rombach et al., "High-Resolution Image Synthesis with Latent Diffusion Models" | 2022 | CVPR |
135
+ | [3] | Liu et al., "WDNet: Watermark-Decomposition Network for Visible Watermark Removal" | 2020 | WACV 2021 |
136
+ | [4] | Cun & Pun, "Split then Refine: Stacked Attention-guided ResUNets for Blind Watermark Removal" | 2020 | AAAI 2021 |
137
+ | [5] | Robinette & Johnson, "Blind Visible Watermark Removal with Morphological Dilation" | 2025 | arXiv:2502.02676 |
138
+ | [6] | Telea, "An Image Inpainting Technique Based on the Fast Marching Method" | 2004 | J. Graphics Tools |
139
+ | [7] | Bertalmio et al., "Navier-Stokes, Fluid Dynamics, and Image and Video Inpainting" | 2001 | CVPR |
140
+ | [8] | Levin et al., "A Closed-Form Solution to Natural Image Matting" | 2006 | IEEE TPAMI |
141
+ | [9] | Huo et al., "WMFormer++: Nested Transformer for Visible Watermark Removal" | 2023 | arXiv:2308.10195 |
142
+ | [10] | "Vanishing Watermarks: Diffusion-Based Image Editing Undermines Robust Invisible Watermarking" | 2026 | arXiv:2602.20680 |
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ pip install -e ".[dev]"
148
+ pytest tests/test_unified_api.py -v
149
+ ruff check demark/
150
+ ```
151
+
152
+ ## License
153
+
154
+ GPL-3.0
@@ -0,0 +1,45 @@
1
+ """
2
+ DeMark — Automatic image watermark detection and removal toolkit.
3
+
4
+ Implements multiple methods from classic CV to deep learning, so the user
5
+ can pick whichever suits the watermark type (semi-transparent logo, tiled
6
+ repeating pattern, solid-color text, known template, etc.).
7
+
8
+ Public API:
9
+ remove_watermark — auto-detect + remove (method auto or specified)
10
+ detect_watermark — detect watermark mask only
11
+ Methods enum — all available removal strategies
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from .__version__ import __version__
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "ToolResult",
20
+ "remove_watermark",
21
+ "detect_watermark",
22
+ "Methods",
23
+ "harmonic_loss",
24
+ "coordinate_descent",
25
+ "grid_search",
26
+ ]
27
+
28
+
29
+ def __getattr__(name: str):
30
+ if name == "ToolResult":
31
+ from .api import ToolResult
32
+ return ToolResult
33
+ if name == "remove_watermark":
34
+ from .api import remove_watermark
35
+ return remove_watermark
36
+ if name == "detect_watermark":
37
+ from .api import detect_watermark
38
+ return detect_watermark
39
+ if name == "Methods":
40
+ from .core import Methods
41
+ return Methods
42
+ if name in ("harmonic_loss", "coordinate_descent", "grid_search"):
43
+ from . import harmonic
44
+ return getattr(harmonic, name)
45
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,227 @@
1
+ """
2
+ Unified Python API for DeMark.
3
+
4
+ Every function returns ToolResult so callers never need try/except.
5
+ Path inputs accept both str and pathlib.Path.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Optional
14
+
15
+ from .__version__ import __version__
16
+
17
+
18
+ @dataclass
19
+ class ToolResult:
20
+ success: bool
21
+ data: Any = None
22
+ error: Optional[str] = None
23
+ metadata: dict = field(default_factory=dict)
24
+
25
+ def to_dict(self) -> dict:
26
+ return {
27
+ "success": self.success,
28
+ "data": self.data,
29
+ "error": self.error,
30
+ "metadata": self.metadata,
31
+ }
32
+
33
+
34
+ def remove_watermark(
35
+ *,
36
+ input_path: str,
37
+ output_path: str,
38
+ method: str = "auto",
39
+ mask_path: Optional[str] = None,
40
+ template_path: Optional[str] = None,
41
+ watermark_path: Optional[str] = None,
42
+ watermark_text: Optional[str] = None,
43
+ region: str = "bottom-right",
44
+ ratio: float = 0.3,
45
+ alpha: Optional[float] = None,
46
+ inpaint_radius: int = 5,
47
+ bbox: Optional[tuple] = None,
48
+ char_bboxes: Optional[list] = None,
49
+ scale_levels: int = 3,
50
+ harmonic_auto: bool = True,
51
+ ) -> ToolResult:
52
+ """Remove watermark from an image and save the result.
53
+
54
+ Args:
55
+ input_path: Path to input image (jpg/png/bmp/webp).
56
+ output_path: Path to save the de-watermarked image.
57
+ method: Removal strategy — one of: auto, cv_telea, cv_ns,
58
+ fft_notch, color_mask, edge_inpaint, template,
59
+ alpha_invert, lama, sd_inpaint, fuse, background_aware,
60
+ precise. ``precise`` requires ``bbox`` (x1,y1,x2,y2).
61
+ mask_path: Optional pre-made mask image (white=watermark).
62
+ If None, DeMark auto-detects.
63
+ template_path: Path to watermark template image (for method=template).
64
+ watermark_path: Path to watermark overlay image (for method=alpha_invert).
65
+ watermark_text: Watermark text content (e.g. "千问AI生成") for text-based
66
+ detection. DeMark renders the text and template-matches it.
67
+ region: Region hint for detection — "bottom-right", "full", "bottom", etc.
68
+ ratio: Region size fraction (0.3 = bottom 30%).
69
+ alpha: Alpha transparency value 0-1 (for method=alpha_invert).
70
+ If None, auto-estimated.
71
+ inpaint_radius: Inpaint radius in pixels (for CV methods).
72
+
73
+ Returns:
74
+ ToolResult with data={"method": str, "output": str, "elapsed": float}
75
+ and metadata={"version": str}.
76
+
77
+ >>> from demark import remove_watermark
78
+ >>> r = remove_watermark(input_path="watermarked.jpg", output_path="clean.jpg")
79
+ >>> r.success
80
+ True
81
+ >>> r.data["method"]
82
+ 'edge_inpaint'
83
+ """
84
+ try:
85
+ import cv2
86
+ import numpy as np
87
+ from .core import Methods, remove, detect_watermark_smart, detect_auto
88
+
89
+ inp = str(Path(input_path).resolve())
90
+ outp = str(Path(output_path).resolve())
91
+
92
+ if not os.path.exists(inp):
93
+ return ToolResult(success=False, error=f"Input not found: {inp}",
94
+ metadata={"version": __version__})
95
+
96
+ img = cv2.imread(inp)
97
+ if img is None:
98
+ return ToolResult(success=False, error=f"Cannot read image: {inp}",
99
+ metadata={"version": __version__})
100
+
101
+ mask = None
102
+ if mask_path and os.path.exists(mask_path):
103
+ mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
104
+ elif method == "auto" or method in ("cv_telea", "cv_ns", "edge_inpaint",
105
+ "color_mask", "fft_notch"):
106
+ # smart detection: text-based if watermark_text given, else region
107
+ mask = detect_watermark_smart(img, watermark_text, region, ratio)
108
+
109
+ # build kwargs for the specific method
110
+ kwargs = {}
111
+ if method == "template" and template_path:
112
+ kwargs["template_path"] = template_path
113
+ if method == "alpha_invert":
114
+ if watermark_path:
115
+ kwargs["watermark_img"] = watermark_path
116
+ if alpha is not None:
117
+ kwargs["alpha"] = alpha
118
+ if method in ("cv_telea", "cv_ns", "color_mask", "edge_inpaint", "template"):
119
+ kwargs["inpaint_radius"] = inpaint_radius
120
+ if method == "precise":
121
+ if bbox is None:
122
+ return ToolResult(success=False,
123
+ error="precise method requires bbox=(x1,y1,x2,y2) pixel coords",
124
+ metadata={"version": __version__})
125
+ kwargs["bbox"] = bbox
126
+ if char_bboxes is not None:
127
+ kwargs["char_bboxes"] = char_bboxes
128
+ if method == "lama_precise":
129
+ if bbox is None:
130
+ return ToolResult(success=False,
131
+ error="lama_precise method requires bbox=(x1,y1,x2,y2) pixel coords",
132
+ metadata={"version": __version__})
133
+ kwargs["bbox"] = bbox
134
+ if method == "harmonic":
135
+ if bbox is None:
136
+ return ToolResult(success=False,
137
+ error="harmonic method requires bbox=(x1,y1,x2,y2) pixel coords",
138
+ metadata={"version": __version__})
139
+ kwargs["bbox"] = bbox
140
+ kwargs["scale_levels"] = scale_levels
141
+ kwargs["auto"] = harmonic_auto
142
+
143
+ try:
144
+ method_enum = Methods(method)
145
+ except ValueError:
146
+ return ToolResult(success=False,
147
+ error=f"Unknown method: {method}. Valid: {[m.value for m in Methods]}",
148
+ metadata={"version": __version__})
149
+
150
+ result = remove(img, method_enum, mask=mask, **kwargs)
151
+
152
+ os.makedirs(os.path.dirname(outp) or ".", exist_ok=True)
153
+ cv2.imwrite(outp, result.image)
154
+
155
+ # also save the mask alongside output for inspection
156
+ mask_out = outp.rsplit(".", 1)[0] + "_mask.png"
157
+ cv2.imwrite(mask_out, result.mask)
158
+
159
+ return ToolResult(
160
+ success=True,
161
+ data={
162
+ "method": result.method,
163
+ "output": outp,
164
+ "mask": mask_out,
165
+ "elapsed": round(result.elapsed, 3),
166
+ },
167
+ metadata={"version": __version__, "image_size": img.shape[:2]},
168
+ )
169
+ except Exception as e:
170
+ return ToolResult(success=False, error=str(e),
171
+ metadata={"version": __version__})
172
+
173
+
174
+ def detect_watermark(
175
+ *,
176
+ input_path: str,
177
+ output_path: str,
178
+ method: str = "auto",
179
+ ) -> ToolResult:
180
+ """Detect watermark mask and save it as a binary image.
181
+
182
+ Args:
183
+ input_path: Path to input image.
184
+ output_path: Path to save the mask (white=watermark, black=clean).
185
+ method: Detection method — auto, edge, color, or otsu.
186
+
187
+ Returns:
188
+ ToolResult with data={"output": str, "coverage": float}.
189
+
190
+ >>> from demark import detect_watermark
191
+ >>> r = detect_watermark(input_path="img.jpg", output_path="mask.png")
192
+ >>> r.success
193
+ True
194
+ >>> r.data["coverage"] < 1.0
195
+ True
196
+ """
197
+ try:
198
+ import cv2
199
+ import numpy as np
200
+ from .core import detect_only
201
+
202
+ inp = str(Path(input_path).resolve())
203
+ outp = str(Path(output_path).resolve())
204
+
205
+ if not os.path.exists(inp):
206
+ return ToolResult(success=False, error=f"Input not found: {inp}",
207
+ metadata={"version": __version__})
208
+
209
+ img = cv2.imread(inp)
210
+ if img is None:
211
+ return ToolResult(success=False, error=f"Cannot read image: {inp}",
212
+ metadata={"version": __version__})
213
+
214
+ mask = detect_only(img, method)
215
+ os.makedirs(os.path.dirname(outp) or ".", exist_ok=True)
216
+ cv2.imwrite(outp, mask)
217
+
218
+ coverage = float(np.count_nonzero(mask)) / mask.size
219
+
220
+ return ToolResult(
221
+ success=True,
222
+ data={"output": outp, "coverage": round(coverage, 4)},
223
+ metadata={"version": __version__, "image_size": img.shape[:2]},
224
+ )
225
+ except Exception as e:
226
+ return ToolResult(success=False, error=str(e),
227
+ metadata={"version": __version__})