BitCheck Codex commited on
Commit
662bc65
·
1 Parent(s): af292f7

feat: add watermark template matching

Browse files
app/services/watermark_template_matcher.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import cv2
7
+ import numpy as np
8
+ from PIL import Image, ImageOps
9
+
10
+ from app.config import settings
11
+
12
+
13
+ TEMPLATE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
14
+
15
+
16
+ def match_watermark_templates(path: Path, verification_id: str, output_dir: Path | None = None) -> dict[str, Any]:
17
+ templates = [p for p in settings.watermark_templates_dir.iterdir() if p.suffix.lower() in TEMPLATE_EXTENSIONS]
18
+ if not templates:
19
+ return {
20
+ "checked": True,
21
+ "template_count": 0,
22
+ "found": False,
23
+ "detected_template": None,
24
+ "confidence": 0.0,
25
+ "location": None,
26
+ "annotated_image_url": None,
27
+ "risk_score": 0.0,
28
+ "flags": [],
29
+ }
30
+
31
+ try:
32
+ with Image.open(path) as img:
33
+ rgb = np.array(ImageOps.exif_transpose(img).convert("RGB"))
34
+ gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
35
+ regions = _corner_regions(gray)
36
+ best: dict[str, Any] = {"confidence": 0.0, "template": None, "box": None, "location": None}
37
+
38
+ for template_path in templates:
39
+ tmpl = cv2.imread(str(template_path), cv2.IMREAD_GRAYSCALE)
40
+ if tmpl is None or tmpl.size == 0:
41
+ continue
42
+ for location, roi, offset in regions:
43
+ score, box = _match_one(roi, tmpl, offset)
44
+ if score > best["confidence"]:
45
+ best = {"confidence": score, "template": template_path.name, "box": box, "location": location}
46
+
47
+ found = best["confidence"] >= 0.72
48
+ annotated_url = None
49
+ if found and best["box"] and output_dir:
50
+ annotated_url = _save_annotation(rgb, best["box"], verification_id, output_dir)
51
+
52
+ flags = [f"Visible watermark template matched: {best['template']}."] if found else []
53
+ return {
54
+ "checked": True,
55
+ "template_count": len(templates),
56
+ "found": found,
57
+ "detected_template": best["template"] if found else None,
58
+ "confidence": round(float(best["confidence"]), 3),
59
+ "location": best["location"] if found else None,
60
+ "annotated_image_url": annotated_url,
61
+ "risk_score": 0.85 if found else 0.0,
62
+ "flags": flags,
63
+ }
64
+ except Exception as exc:
65
+ return {
66
+ "checked": True,
67
+ "template_count": len(templates),
68
+ "found": False,
69
+ "detected_template": None,
70
+ "confidence": 0.0,
71
+ "location": None,
72
+ "annotated_image_url": None,
73
+ "risk_score": 0.0,
74
+ "flags": ["Watermark template matching failed."],
75
+ "error": str(exc),
76
+ }
77
+
78
+
79
+ def _corner_regions(gray: np.ndarray) -> list[tuple[str, np.ndarray, tuple[int, int]]]:
80
+ h, w = gray.shape
81
+ rw = max(80, int(w * 0.4))
82
+ rh = max(60, int(h * 0.28))
83
+ return [
84
+ ("bottom_right", gray[h - rh : h, w - rw : w], (w - rw, h - rh)),
85
+ ("bottom_left", gray[h - rh : h, 0:rw], (0, h - rh)),
86
+ ("top_right", gray[0:rh, w - rw : w], (w - rw, 0)),
87
+ ("top_left", gray[0:rh, 0:rw], (0, 0)),
88
+ ]
89
+
90
+
91
+ def _match_one(roi: np.ndarray, template: np.ndarray, offset: tuple[int, int]) -> tuple[float, tuple[int, int, int, int] | None]:
92
+ if template.shape[0] > roi.shape[0] or template.shape[1] > roi.shape[1]:
93
+ scale = min(roi.shape[0] / template.shape[0], roi.shape[1] / template.shape[1], 1.0)
94
+ if scale <= 0:
95
+ return 0.0, None
96
+ template = cv2.resize(template, (max(1, int(template.shape[1] * scale)), max(1, int(template.shape[0] * scale))))
97
+ result = cv2.matchTemplate(roi, template, cv2.TM_CCOEFF_NORMED)
98
+ _, max_val, _, max_loc = cv2.minMaxLoc(result)
99
+ x = offset[0] + max_loc[0]
100
+ y = offset[1] + max_loc[1]
101
+ return float(max_val), (x, y, template.shape[1], template.shape[0])
102
+
103
+
104
+ def _save_annotation(rgb: np.ndarray, box: tuple[int, int, int, int], verification_id: str, output_dir: Path) -> str:
105
+ x, y, w, h = box
106
+ annotated = rgb.copy()
107
+ cv2.rectangle(annotated, (x, y), (x + w, y + h), (255, 80, 40), max(2, annotated.shape[1] // 300))
108
+ path = output_dir / f"{verification_id}_watermark_template.jpg"
109
+ Image.fromarray(annotated).save(path, quality=92)
110
+ return f"/outputs/{path.name}"