299 lines
10 KiB
Python
299 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""二维码处理核心类 - 基于 OpenCV QRCodeDetector
|
||
|
||
本模块使用 OpenCV 的 QRCodeDetector 进行二维码识别,
|
||
配合多种图像预处理策略,确保高识别率和跨平台兼容性。
|
||
"""
|
||
import base64
|
||
import io
|
||
from typing import Optional, Callable, Tuple
|
||
|
||
import cv2
|
||
import httpx
|
||
import numpy as np
|
||
import qrcode
|
||
from PIL import Image
|
||
|
||
|
||
class QRCodeProcessor:
|
||
"""二维码处理器 - 负责二维码的生成、解析和图像预处理"""
|
||
|
||
# 预处理策略映射
|
||
PREPROCESSING_STRATEGIES = {
|
||
"original": ("原图", lambda img, gray: img),
|
||
"grayscale": ("灰度图", lambda img, gray: cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)),
|
||
"clahe": ("CLAHE增强", lambda img, gray: cv2.cvtColor(
|
||
cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray),
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"adaptive_threshold": ("自适应二值化", lambda img, gray: cv2.cvtColor(
|
||
cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2),
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"otsu": ("Otsu二值化", lambda img, gray: cv2.cvtColor(
|
||
cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1],
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"denoise": ("去噪+二值化", lambda img, gray: cv2.cvtColor(
|
||
cv2.threshold(
|
||
cv2.fastNlMeansDenoising(gray, None, 10, 7, 21),
|
||
0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
|
||
)[1],
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"sharpen": ("锐化", lambda img, gray: cv2.cvtColor(
|
||
cv2.filter2D(gray, -1, np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])),
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"morphology": ("形态学处理", lambda img, gray: cv2.cvtColor(
|
||
cv2.morphologyEx(
|
||
cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2),
|
||
cv2.MORPH_CLOSE,
|
||
cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
|
||
),
|
||
cv2.COLOR_GRAY2BGR
|
||
)),
|
||
"scale_0.5": ("0.5x缩放", lambda img, gray: cv2.resize(
|
||
img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA
|
||
)),
|
||
"scale_1.5": ("1.5x缩放", lambda img, gray: cv2.resize(
|
||
img, None, fx=1.5, fy=1.5, interpolation=cv2.INTER_CUBIC
|
||
)),
|
||
"scale_2.0": ("2.0x缩放", lambda img, gray: cv2.resize(
|
||
img, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC
|
||
)),
|
||
}
|
||
|
||
# 策略组合映射
|
||
STRATEGY_MAP = {
|
||
"basic": ["original", "grayscale"],
|
||
"enhanced": ["original", "grayscale", "clahe", "adaptive_threshold", "otsu", "denoise", "sharpen", "morphology"],
|
||
"all": ["original", "grayscale", "clahe", "adaptive_threshold", "otsu", "denoise", "sharpen", "morphology", "scale_0.5", "scale_1.5", "scale_2.0"],
|
||
}
|
||
|
||
@staticmethod
|
||
def generate(
|
||
content: str,
|
||
size: int = 300,
|
||
error_correction: str = "H",
|
||
box_size: int = 10,
|
||
border: int = 4
|
||
) -> str:
|
||
"""
|
||
生成二维码
|
||
|
||
Args:
|
||
content: 二维码内容
|
||
size: 图片大小(像素)
|
||
error_correction: 纠错级别 (L/M/Q/H)
|
||
box_size: 每个格子的像素大小
|
||
border: 边框大小(格子数)
|
||
|
||
Returns:
|
||
base64编码的图片数据 (data:image/png;base64,...)
|
||
|
||
Raises:
|
||
ValueError: 参数错误时抛出异常
|
||
"""
|
||
# 验证纠错级别
|
||
error_levels = {
|
||
"L": qrcode.constants.ERROR_CORRECT_L, # 7% 容错
|
||
"M": qrcode.constants.ERROR_CORRECT_M, # 15% 容错
|
||
"Q": qrcode.constants.ERROR_CORRECT_Q, # 25% 容错
|
||
"H": qrcode.constants.ERROR_CORRECT_H, # 30% 容错
|
||
}
|
||
|
||
if error_correction not in error_levels:
|
||
raise ValueError(f"无效的纠错级别: {error_correction},支持: L/M/Q/H")
|
||
|
||
# 创建二维码对象
|
||
qr = qrcode.QRCode(
|
||
version=None, # 自动确定版本
|
||
error_correction=error_levels[error_correction],
|
||
box_size=box_size,
|
||
border=border,
|
||
)
|
||
|
||
# 添加数据并生成
|
||
qr.add_data(content)
|
||
qr.make(fit=True)
|
||
|
||
# 生成图片
|
||
img = qr.make_image(fill_color="black", back_color="white")
|
||
|
||
# 调整到指定大小
|
||
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
||
|
||
# 转换为base64
|
||
buffer = io.BytesIO()
|
||
img.save(buffer, format="PNG")
|
||
img_base64 = base64.b64encode(buffer.getvalue()).decode()
|
||
|
||
return f"data:image/png;base64,{img_base64}"
|
||
|
||
@staticmethod
|
||
async def load_image(image_source: str) -> np.ndarray:
|
||
"""
|
||
加载图片(支持URL、base64、本地路径)
|
||
|
||
Args:
|
||
image_source: 图片来源
|
||
- URL: http://... 或 https://...
|
||
- base64: data:image/...;base64,...
|
||
- 本地路径: /path/to/image.png
|
||
|
||
Returns:
|
||
OpenCV图片对象 (BGR格式)
|
||
|
||
Raises:
|
||
ValueError: 图片加载失败时抛出异常
|
||
"""
|
||
try:
|
||
# 检查是否为base64
|
||
if image_source.startswith("data:image"):
|
||
# 提取base64数据
|
||
base64_data = image_source.split(",")[1]
|
||
img_data = base64.b64decode(base64_data)
|
||
img_array = np.frombuffer(img_data, np.uint8)
|
||
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
|
||
|
||
elif image_source.startswith("http://") or image_source.startswith("https://"):
|
||
# 下载图片
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
response = await client.get(image_source)
|
||
response.raise_for_status()
|
||
img_array = np.frombuffer(response.content, np.uint8)
|
||
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
|
||
|
||
else:
|
||
# 本地文件
|
||
img = cv2.imread(image_source)
|
||
|
||
if img is None:
|
||
raise ValueError("无法解析图片数据")
|
||
|
||
return img
|
||
|
||
except Exception as e:
|
||
raise ValueError(f"图片加载失败: {str(e)}")
|
||
|
||
@staticmethod
|
||
async def search_qrcode(img: np.ndarray)-> list[np.ndarray]:
|
||
"""
|
||
搜索二维码,只搜索不解析
|
||
|
||
Args:
|
||
img: OpenCV图像对象
|
||
|
||
Returns:
|
||
二维码列表
|
||
"""
|
||
detector = cv2.QRCodeDetector()
|
||
imgs = detector.detect(img)
|
||
|
||
@staticmethod
|
||
def decode(img: np.ndarray) -> Optional[str]:
|
||
"""
|
||
使用 OpenCV QRCodeDetector 解码二维码
|
||
|
||
Args:
|
||
img: OpenCV图像对象
|
||
|
||
Returns:
|
||
二维码内容,如果没有检测到返回None
|
||
"""
|
||
detector = cv2.QRCodeDetector()
|
||
data, bbox, _ = detector.detectAndDecode(img)
|
||
|
||
if data:
|
||
return data
|
||
|
||
return None
|
||
|
||
@staticmethod
|
||
async def parse(
|
||
image_source: str,
|
||
strategy: str = "auto"
|
||
) -> dict:
|
||
"""
|
||
解析二维码(使用 OpenCV + 按需预处理策略)
|
||
|
||
解码策略:
|
||
1. 根据 strategy 参数选择预处理步骤列表
|
||
2. 按需应用每种预处理算法并立即尝试解码
|
||
3. 一旦成功立即返回,避免不必要的计算
|
||
|
||
Args:
|
||
image_source: 图片来源(URL/base64/本地路径)
|
||
strategy: 预处理策略
|
||
- basic: 原图 + 灰度图(2种)
|
||
- enhanced: basic + 6种增强算法(8种)
|
||
- all: auto + 多尺度(11种)
|
||
|
||
Returns:
|
||
解析结果字典:
|
||
{
|
||
"success": bool,
|
||
"content": str or None,
|
||
"strategy_used": str, # 使用的预处理策略名称
|
||
"preprocessing_index": int, # 预处理索引
|
||
"total_attempts": int,
|
||
"message": str # 仅失败时有
|
||
}
|
||
"""
|
||
# 验证策略参数
|
||
if strategy not in QRCodeProcessor.STRATEGY_MAP:
|
||
raise ValueError(f"无效的策略: {strategy},支持: {list(QRCodeProcessor.STRATEGY_MAP.keys())}")
|
||
|
||
# 加载原始图片
|
||
img = await QRCodeProcessor.load_image(image_source)
|
||
|
||
# 预先生成灰度图(很多预处理都需要)
|
||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||
|
||
# 获取该策略对应的预处理步骤列表
|
||
preprocessing_steps = QRCodeProcessor.STRATEGY_MAP[strategy]
|
||
|
||
# 依次应用每种预处理并尝试解码
|
||
for idx, step_key in enumerate(preprocessing_steps):
|
||
strategy_name, preprocess_func = QRCodeProcessor.PREPROCESSING_STRATEGIES[step_key]
|
||
|
||
try:
|
||
# 按需处理图像
|
||
processed_img = preprocess_func(img, gray)
|
||
|
||
# 立即尝试解码
|
||
result = QRCodeProcessor.decode(processed_img)
|
||
|
||
if result:
|
||
# 解码成功,立即返回
|
||
return {
|
||
"success": True,
|
||
"content": result,
|
||
"strategy_used": f"opencv_{strategy_name}",
|
||
"preprocessing_index": idx,
|
||
"total_attempts": idx + 1
|
||
}
|
||
|
||
except Exception as e:
|
||
# 某个预处理步骤失败,继续尝试下一个
|
||
continue
|
||
|
||
# 所有预处理方法都失败
|
||
return {
|
||
"success": False,
|
||
"content": None,
|
||
"message": f"未检测到二维码或二维码损坏(已尝试 {len(preprocessing_steps)} 种预处理)",
|
||
"total_attempts": len(preprocessing_steps)
|
||
}
|
||
|
||
if __name__ == "__main__":
|
||
import asyncio
|
||
|
||
async def main():
|
||
# 示例用法
|
||
result = await QRCodeProcessor.parse("F:/Project/urbanLifeline/docs/qrcode.png", "enhanced")
|
||
print(result)
|
||
|
||
asyncio.run(main())
|