Spaces:
Running
Running
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}"
|