"""二维码检测与裁剪工具。""" 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]