Files
urbanLifeline/difyPlugin/app/services/workcase/qrcode/QrCode.py
2025-12-30 18:37:07 +08:00

299 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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())