demark 0.0.1__py3-none-any.whl
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/__init__.py +45 -0
- demark/__main__.py +4 -0
- demark/__version__.py +1 -0
- demark/api.py +227 -0
- demark/cli.py +188 -0
- demark/core.py +1830 -0
- demark/harmonic.py +278 -0
- demark/tools.py +119 -0
- demark-0.0.1.dist-info/METADATA +195 -0
- demark-0.0.1.dist-info/RECORD +13 -0
- demark-0.0.1.dist-info/WHEEL +5 -0
- demark-0.0.1.dist-info/entry_points.txt +2 -0
- demark-0.0.1.dist-info/top_level.txt +1 -0
demark/__init__.py
ADDED
|
@@ -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}")
|
demark/__main__.py
ADDED
demark/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
demark/api.py
ADDED
|
@@ -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__})
|
demark/cli.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for DeMark with unified flags and remove/detect subcommands.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .__version__ import __version__
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("demark")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="demark",
|
|
19
|
+
description="Automatic image watermark detection and removal toolkit.",
|
|
20
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
21
|
+
epilog="""
|
|
22
|
+
Examples:
|
|
23
|
+
demark remove watermarked.jpg clean.jpg
|
|
24
|
+
demark remove watermarked.jpg clean.jpg --method fft_notch
|
|
25
|
+
demark remove watermarked.jpg clean.jpg --method template --template logo.png
|
|
26
|
+
demark remove watermarked.jpg clean.jpg --method alpha_invert --watermark wm.png --alpha 0.4
|
|
27
|
+
demark detect watermarked.jpg mask.png
|
|
28
|
+
demark --json remove watermarked.jpg clean.jpg
|
|
29
|
+
""",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("-V", "--version", action="version", version=f"demark {__version__}")
|
|
32
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
33
|
+
parser.add_argument("-o", "--output", type=str, default=None, help="Output path (for JSON results)")
|
|
34
|
+
parser.add_argument("--json", action="store_true", dest="json_output", help="Output results as JSON")
|
|
35
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress non-essential output")
|
|
36
|
+
|
|
37
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
38
|
+
|
|
39
|
+
rm_p = subparsers.add_parser("remove", help="Remove watermark from image")
|
|
40
|
+
rm_p.add_argument("input", help="Input image path")
|
|
41
|
+
rm_p.add_argument("output", help="Output image path")
|
|
42
|
+
rm_p.add_argument("--method", type=str, default="auto",
|
|
43
|
+
choices=[m.value for m in __import__("demark.core", fromlist=["Methods"]).Methods],
|
|
44
|
+
help="Removal method (default: auto)")
|
|
45
|
+
rm_p.add_argument("--mask", type=str, default=None, help="Pre-made mask image (white=watermark)")
|
|
46
|
+
rm_p.add_argument("--template", type=str, default=None, help="Watermark template image (for method=template)")
|
|
47
|
+
rm_p.add_argument("--watermark", type=str, default=None, help="Watermark overlay image (for method=alpha_invert)")
|
|
48
|
+
rm_p.add_argument("--region", type=str, default="bottom-right",
|
|
49
|
+
choices=["bottom-right", "bottom-left", "top-right", "top-left", "bottom", "top", "full"],
|
|
50
|
+
help="Region to detect watermark in (default: bottom-right)")
|
|
51
|
+
rm_p.add_argument("--ratio", type=float, default=0.3, help="Region size fraction (default 0.3)")
|
|
52
|
+
rm_p.add_argument("--bbox", type=int, nargs=4, default=None,
|
|
53
|
+
metavar=("X1","Y1","X2","Y2"),
|
|
54
|
+
help="Precise pixel bbox for --method precise (e.g. 1056 1249 1304 1305)")
|
|
55
|
+
rm_p.add_argument("--watermark-text", type=str, default="千问AI生成",
|
|
56
|
+
help="Watermark text content (default: 千问AI生成)")
|
|
57
|
+
rm_p.add_argument("--char-widths", type=int, nargs='+', default=None,
|
|
58
|
+
metavar="W",
|
|
59
|
+
help="Width in pixels of each character, e.g. --char-widths 50 48 35 12 48 48")
|
|
60
|
+
rm_p.add_argument("--alpha", type=float, default=None, help="Alpha transparency 0-1 (for method=alpha_invert)")
|
|
61
|
+
rm_p.add_argument("--radius", type=int, default=5, help="Inpaint radius in pixels (default 5)")
|
|
62
|
+
rm_p.add_argument("--scale-levels", type=int, default=3,
|
|
63
|
+
help="Pyramid levels for method=harmonic (default 3; 1 disables multi-scale)")
|
|
64
|
+
rm_p.add_argument("--no-harmonic-auto", action="store_true",
|
|
65
|
+
help="Disable auto-optimisation for method=harmonic (uses v57 defaults)")
|
|
66
|
+
|
|
67
|
+
det_p = subparsers.add_parser("detect", help="Detect watermark mask only")
|
|
68
|
+
det_p.add_argument("input", help="Input image path")
|
|
69
|
+
det_p.add_argument("output", help="Output mask path")
|
|
70
|
+
det_p.add_argument("--method", type=str, default="auto",
|
|
71
|
+
choices=["auto", "edge", "color", "otsu"],
|
|
72
|
+
help="Detection method (default: auto)")
|
|
73
|
+
|
|
74
|
+
subparsers.add_parser("list-methods", help="List all available removal methods")
|
|
75
|
+
|
|
76
|
+
return parser
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def setup_logging(verbose: bool, quiet: bool):
|
|
80
|
+
level = logging.WARNING if quiet else (logging.DEBUG if verbose else logging.INFO)
|
|
81
|
+
logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
parser = build_parser()
|
|
86
|
+
args = parser.parse_args()
|
|
87
|
+
setup_logging(args.verbose, args.quiet)
|
|
88
|
+
|
|
89
|
+
from . import api
|
|
90
|
+
from .core import Methods
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if args.command == "remove":
|
|
94
|
+
# Auto-build char_bboxes from --watermark-text + --char-widths (if precise)
|
|
95
|
+
char_bboxes = None
|
|
96
|
+
if args.method in ("precise", "lama_precise") and args.bbox and args.watermark_text:
|
|
97
|
+
bx1, by1, bx2, by2 = args.bbox
|
|
98
|
+
if args.char_widths and len(args.char_widths) == len(args.watermark_text):
|
|
99
|
+
xs = bx1
|
|
100
|
+
char_bboxes = []
|
|
101
|
+
for w in args.char_widths:
|
|
102
|
+
char_bboxes.append((xs, by1, xs + w, by2))
|
|
103
|
+
xs += w
|
|
104
|
+
else:
|
|
105
|
+
defaults = {"千问AI生成": [50, 48, 35, 12, 48, 48]}
|
|
106
|
+
if args.watermark_text in defaults:
|
|
107
|
+
xs = bx1
|
|
108
|
+
char_bboxes = []
|
|
109
|
+
for w in defaults[args.watermark_text]:
|
|
110
|
+
char_bboxes.append((xs, by1, xs + w, by2))
|
|
111
|
+
xs += w
|
|
112
|
+
result = api.remove_watermark(
|
|
113
|
+
input_path=args.input,
|
|
114
|
+
output_path=args.output,
|
|
115
|
+
method=args.method,
|
|
116
|
+
mask_path=args.mask,
|
|
117
|
+
template_path=args.template,
|
|
118
|
+
watermark_path=args.watermark,
|
|
119
|
+
watermark_text=args.watermark_text,
|
|
120
|
+
region=args.region,
|
|
121
|
+
ratio=args.ratio,
|
|
122
|
+
alpha=args.alpha,
|
|
123
|
+
inpaint_radius=args.radius,
|
|
124
|
+
bbox=tuple(args.bbox) if args.bbox else None,
|
|
125
|
+
char_bboxes=char_bboxes,
|
|
126
|
+
scale_levels=args.scale_levels,
|
|
127
|
+
harmonic_auto=not args.no_harmonic_auto,
|
|
128
|
+
)
|
|
129
|
+
elif args.command == "detect":
|
|
130
|
+
result = api.detect_watermark(
|
|
131
|
+
input_path=args.input,
|
|
132
|
+
output_path=args.output,
|
|
133
|
+
method=args.method,
|
|
134
|
+
)
|
|
135
|
+
elif args.command == "list-methods":
|
|
136
|
+
methods_info = []
|
|
137
|
+
for m in Methods:
|
|
138
|
+
desc = {
|
|
139
|
+
"auto": "Auto-detect + best method selection",
|
|
140
|
+
"cv_telea": "OpenCV Telea fast-marching inpaint [6]",
|
|
141
|
+
"cv_ns": "OpenCV Navier-Stokes inpaint [7]",
|
|
142
|
+
"fft_notch": "FFT notch filtering for repeating watermarks",
|
|
143
|
+
"color_mask": "HSV color segmentation + inpaint",
|
|
144
|
+
"edge_inpaint": "Canny edge + morphological dilation + inpaint [5]",
|
|
145
|
+
"template": "Template matching for known watermark + inpaint",
|
|
146
|
+
"alpha_invert": "Alpha matting inversion (lossless if W+alpha known) [8]",
|
|
147
|
+
"lama": "LaMa deep learning inpainting [1]",
|
|
148
|
+
"sd_inpaint": "Stable Diffusion inpainting (generative fill)",
|
|
149
|
+
"fuse": "Multi-method fusion + iterative optimization (best quality)",
|
|
150
|
+
"precise": "Column-interp background + alpha-inversion (needs --bbox)",
|
|
151
|
+
"harmonic": "Scale-adaptive multi-scale + reference-free auto-optimisation (needs --bbox)",
|
|
152
|
+
}
|
|
153
|
+
methods_info.append(f" {m.value:15s} — {desc.get(m.value, '')}")
|
|
154
|
+
print("\n".join(methods_info))
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
else:
|
|
157
|
+
parser.print_help()
|
|
158
|
+
sys.exit(2)
|
|
159
|
+
|
|
160
|
+
if args.json_output:
|
|
161
|
+
print(json.dumps(result.to_dict(), ensure_ascii=False, indent=2))
|
|
162
|
+
elif result.success:
|
|
163
|
+
if args.command == "remove":
|
|
164
|
+
d = result.data
|
|
165
|
+
print(f"Removed watermark ({d['method']}) → {d['output']}")
|
|
166
|
+
print(f" Mask: {d['mask']}")
|
|
167
|
+
print(f" Time: {d['elapsed']}s")
|
|
168
|
+
elif args.command == "detect":
|
|
169
|
+
d = result.data
|
|
170
|
+
print(f"Detected mask → {d['output']}")
|
|
171
|
+
print(f" Coverage: {d['coverage']*100:.1f}%")
|
|
172
|
+
else:
|
|
173
|
+
print(f"Error: {result.error}", file=sys.stderr)
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
if args.output and result.success and args.json_output:
|
|
177
|
+
with open(args.output, "w") as f:
|
|
178
|
+
json.dump(result.to_dict(), f, ensure_ascii=False, indent=2)
|
|
179
|
+
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
except KeyboardInterrupt:
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
if args.json_output:
|
|
185
|
+
print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False))
|
|
186
|
+
else:
|
|
187
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
188
|
+
sys.exit(1)
|