添加Ai 能力,重构项目结构

This commit is contained in:
zk
2026-07-24 17:55:27 +08:00
parent 39e162ae0f
commit d7f1ba4812
13 changed files with 127 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
"""二维码检测与裁剪工具。"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import cv2
import numpy as np
@dataclass
class QrRegion:
"""单个二维码区域。"""
points: tuple[tuple[int, int], ...]
crop: np.ndarray | None = None
@dataclass
class QrDetectResult:
"""二维码检测结果。"""
has_qr: bool
items: list[QrRegion]
def _load_image(image: bytes | np.ndarray | str | Path) -> np.ndarray:
"""把输入转成 BGR 图像。"""
if isinstance(image, np.ndarray):
if image.ndim == 2:
return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
return image.copy()
if isinstance(image, (str, Path)):
data = Path(image).read_bytes()
else:
data = image
arr = np.frombuffer(data, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if img is None:
raise ValueError("Failed to decode image data.")
return img
def _order_points(points: np.ndarray) -> np.ndarray:
"""把四个角点整理成左上、右上、右下、左下。"""
pts = np.asarray(points, dtype=np.float32).reshape(4, 2)
rect = np.zeros((4, 2), dtype=np.float32)
s = pts.sum(axis=1)
diff = np.diff(pts, axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
def _warp_qr_image(img: np.ndarray, points: np.ndarray) -> np.ndarray:
"""按四边形点做透视矫正,截取二维码区域。"""
rect = _order_points(points)
(tl, tr, br, bl) = rect
width_a = np.linalg.norm(br - bl)
width_b = np.linalg.norm(tr - tl)
height_a = np.linalg.norm(tr - br)
height_b = np.linalg.norm(tl - bl)
width = max(int(round(max(width_a, width_b))), 1)
height = max(int(round(max(height_a, height_b))), 1)
dst = np.array(
[
[0, 0],
[width - 1, 0],
[width - 1, height - 1],
[0, height - 1],
],
dtype=np.float32,
)
matrix = cv2.getPerspectiveTransform(rect, dst)
return cv2.warpPerspective(img, matrix, (width, height))
def _normalize_points(points: np.ndarray | None) -> list[tuple[tuple[int, int], ...]]:
if points is None:
return []
arr = np.asarray(points)
if arr.ndim == 2 and arr.shape == (4, 2):
arr = arr[None, ...]
elif arr.ndim == 3 and arr.shape[-2:] == (4, 2):
pass
else:
return []
result: list[tuple[tuple[int, int], ...]] = []
for item in arr:
quad = tuple((int(round(x)), int(round(y))) for x, y in item)
result.append(quad)
return result
def _detect_points(img: np.ndarray) -> list[tuple[tuple[int, int], ...]]:
detector = cv2.QRCodeDetector()
if hasattr(detector, "detectMulti"):
ok, points = detector.detectMulti(img)
if ok:
normalized_points = _normalize_points(points)
if normalized_points:
return normalized_points
ok, points = detector.detect(img)
if ok:
normalized_points = _normalize_points(points)
if normalized_points:
return normalized_points
return []
def scan_qr(image: bytes | np.ndarray | str | Path) -> QrDetectResult:
"""扫描图片中是否存在二维码,并返回二维码区域。"""
img = _load_image(image)
points_list = _detect_points(img)
items = [
QrRegion(points=points, crop=_warp_qr_image(img, np.array(points, dtype=np.float32)))
for points in points_list
]
return QrDetectResult(has_qr=bool(items), items=items)
def has_qr(image: bytes | np.ndarray | str | Path) -> bool:
"""判断图片里有没有二维码。"""
return scan_qr(image).has_qr
def crop_qr(image: bytes | np.ndarray | str | Path) -> list[np.ndarray]:
"""裁剪出图片中的二维码区域。"""
return [item.crop for item in scan_qr(image).items if item.crop is not None]