diff --git a/requirements.txt b/requirements.txt index 8165f4a..4b44ec2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ cryptography>=41 rapidocr>=3.9 onnxruntime>=1.17 +opencv-python>=4.5 diff --git a/tool/qr.py b/tool/cv.py similarity index 55% rename from tool/qr.py rename to tool/cv.py index 90c36a3..3c3977e 100644 --- a/tool/qr.py +++ b/tool/cv.py @@ -1,33 +1,28 @@ -"""二维码工具。 - -基于 OpenCV 的 QRCodeDetector 完成二维码检测、裁剪与解码。 -""" +"""二维码检测与裁剪工具。""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from typing import Iterable import cv2 import numpy as np @dataclass -class QrCodeResult: - """单个二维码识别结果。""" +class QrRegion: + """单个二维码区域。""" - text: str points: tuple[tuple[int, int], ...] crop: np.ndarray | None = None @dataclass -class QrScanResult: - """二维码扫描结果。""" +class QrDetectResult: + """二维码检测结果。""" has_qr: bool - items: list[QrCodeResult] + items: list[QrRegion] def _load_image(image: bytes | np.ndarray | str | Path) -> np.ndarray: @@ -110,58 +105,35 @@ def _normalize_points(points: np.ndarray | None) -> list[tuple[tuple[int, int], return result -def _decode_with_multi_detector(img: np.ndarray) -> tuple[list[str], list[tuple[tuple[int, int], ...]], list[np.ndarray]]: +def _detect_points(img: np.ndarray) -> list[tuple[tuple[int, int], ...]]: detector = cv2.QRCodeDetector() - texts: list[str] = [] - points_list: list[tuple[tuple[int, int], ...]] = [] - crops: list[np.ndarray] = [] - - if hasattr(detector, "detectAndDecodeMulti"): - ok, decoded_info, points, _ = detector.detectAndDecodeMulti(img) - if ok and points is not None: + if hasattr(detector, "detectMulti"): + ok, points = detector.detectMulti(img) + if ok: normalized_points = _normalize_points(points) - if isinstance(decoded_info, Iterable) and not isinstance( - decoded_info, (str, bytes) - ): - decoded_iter = [item or "" for item in decoded_info] - else: - decoded_iter = [decoded_info or ""] + if normalized_points: + return normalized_points - for idx, quad in enumerate(normalized_points): - text = decoded_iter[idx] if idx < len(decoded_iter) else "" - crop = _warp_qr_image(img, np.array(quad, dtype=np.float32)) - if not text: - fallback_text, _, _ = detector.detectAndDecode(crop) - text = fallback_text or "" - texts.append(text) - points_list.append(quad) - crops.append(crop) + ok, points = detector.detect(img) + if ok: + normalized_points = _normalize_points(points) + if normalized_points: + return normalized_points - return texts, points_list, crops - - text, points, _ = detector.detectAndDecode(img) - normalized_points = _normalize_points(points) - if normalized_points: - crop = _warp_qr_image(img, np.array(normalized_points[0], dtype=np.float32)) - if not text: - fallback_text, _, _ = detector.detectAndDecode(crop) - text = fallback_text or "" - return [text or ""], [normalized_points[0]], [crop] - - return [], [], [] + return [] -def scan_qr(image: bytes | np.ndarray | str | Path) -> QrScanResult: - """扫描图片中的二维码,返回是否存在、位置和文本。""" +def scan_qr(image: bytes | np.ndarray | str | Path) -> QrDetectResult: + """扫描图片中是否存在二维码,并返回二维码区域。""" img = _load_image(image) - texts, points_list, crops = _decode_with_multi_detector(img) + points_list = _detect_points(img) items = [ - QrCodeResult(text=text, points=points, crop=crop) - for text, points, crop in zip(texts, points_list, crops) + QrRegion(points=points, crop=_warp_qr_image(img, np.array(points, dtype=np.float32))) + for points in points_list ] - return QrScanResult(has_qr=bool(items), items=items) + return QrDetectResult(has_qr=bool(items), items=items) def has_qr(image: bytes | np.ndarray | str | Path) -> bool: @@ -172,8 +144,3 @@ def has_qr(image: bytes | np.ndarray | str | Path) -> bool: 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] - - -def decode_qr(image: bytes | np.ndarray | str | Path) -> list[str]: - """识别图片中的二维码内容。""" - return [item.text for item in scan_qr(image).items if item.text] diff --git a/tool/qr_decode.py b/tool/qr_decode.py new file mode 100644 index 0000000..b9050a9 --- /dev/null +++ b/tool/qr_decode.py @@ -0,0 +1,54 @@ +"""二维码识别工具。""" + +from __future__ import annotations + +from pathlib import Path + +import cv2 +import numpy as np + +from .cv import _load_image, _normalize_points, _warp_qr_image + + +def _decode_with_detector(img: np.ndarray) -> list[str]: + detector = cv2.QRCodeDetector() + texts: list[str] = [] + + if hasattr(detector, "detectAndDecodeMulti"): + ok, decoded_info, points, _ = detector.detectAndDecodeMulti(img) + if ok and points is not None: + normalized_points = _normalize_points(points) + if isinstance(decoded_info, (list, tuple)): + decoded_iter = [item or "" for item in decoded_info] + else: + decoded_iter = [decoded_info or ""] + + for idx, points_item in enumerate(normalized_points): + text = decoded_iter[idx] if idx < len(decoded_iter) else "" + if not text: + crop = _warp_qr_image(img, np.array(points_item, dtype=np.float32)) + fallback_text, _, _ = detector.detectAndDecode(crop) + text = fallback_text or "" + if text: + texts.append(text) + if texts: + return texts + + text, points, _ = detector.detectAndDecode(img) + if text: + return [text] + + normalized_points = _normalize_points(points) + if normalized_points: + crop = _warp_qr_image(img, np.array(normalized_points[0], dtype=np.float32)) + fallback_text, _, _ = detector.detectAndDecode(crop) + if fallback_text: + return [fallback_text] + + return [] + + +def decode_qr(image: bytes | np.ndarray | str | Path) -> list[str]: + """识别图片中的二维码内容。""" + img = _load_image(image) + return _decode_with_detector(img)