99 lines
3.6 KiB
Python
99 lines
3.6 KiB
Python
from typing import Dict, List, Tuple
|
||
|
||
import cv2
|
||
import numpy as np
|
||
import logging
|
||
|
||
# 尝试延迟引入 pyzbar;在未安装 zbar 运行库的环境下,保证服务可启动,
|
||
# 后续仅使用自研 EAN-13 解码或返回空结果。
|
||
try:
|
||
from pyzbar.pyzbar import decode as _pyzbar_decode, ZBarSymbol as _ZBarSymbol
|
||
_PYZBAR_AVAILABLE = True
|
||
except Exception as _e: # ImportError 或 zbar 动态库缺失
|
||
_PYZBAR_AVAILABLE = False
|
||
_PYZBAR_IMPORT_ERR = _e
|
||
_pyzbar_decode = None
|
||
_ZBarSymbol = None
|
||
|
||
|
||
def _prepare_images(gray: np.ndarray, try_invert: bool, rotations: List[int]) -> List[Tuple[np.ndarray, int, bool]]:
|
||
# 返回 (图像, 旋转角度, 是否反色)
|
||
images: List[Tuple[np.ndarray, int, bool]] = []
|
||
for ang in rotations:
|
||
if ang % 360 == 0:
|
||
rot = gray
|
||
elif ang % 360 == 90:
|
||
rot = cv2.rotate(gray, cv2.ROTATE_90_CLOCKWISE)
|
||
elif ang % 360 == 180:
|
||
rot = cv2.rotate(gray, cv2.ROTATE_180)
|
||
elif ang % 360 == 270:
|
||
rot = cv2.rotate(gray, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
||
else:
|
||
# 任意角度插值旋转
|
||
h, w = gray.shape[:2]
|
||
M = cv2.getRotationMatrix2D((w / 2, h / 2), ang, 1.0)
|
||
rot = cv2.warpAffine(gray, M, (w, h), flags=cv2.INTER_LINEAR)
|
||
images.append((rot, ang, False))
|
||
if try_invert:
|
||
images.append((cv2.bitwise_not(rot), ang, True))
|
||
return images
|
||
|
||
|
||
def _collect_supported_symbols() -> List["_ZBarSymbol"]:
|
||
names = [
|
||
"EAN13",
|
||
"EAN8",
|
||
"UPCA",
|
||
"UPCE",
|
||
"CODE128",
|
||
"CODE39",
|
||
"QRCODE",
|
||
# 兼容不同版本:有的叫 ITF,有的叫 I25
|
||
"ITF",
|
||
"I25",
|
||
]
|
||
symbols: List["_ZBarSymbol"] = []
|
||
if not _PYZBAR_AVAILABLE:
|
||
return symbols
|
||
for n in names:
|
||
if hasattr(_ZBarSymbol, n):
|
||
symbols.append(getattr(_ZBarSymbol, n))
|
||
# 若列表为空,退回由 zbar 自行决定的默认集合
|
||
return symbols
|
||
|
||
|
||
def decode_with_pyzbar(image_bgr: np.ndarray, try_invert: bool, rotations: List[int]) -> List[Dict[str, str]]:
|
||
logger = logging.getLogger("pyzbar_engine")
|
||
if not _PYZBAR_AVAILABLE or _pyzbar_decode is None:
|
||
# 缺少 pyzbar/zbar,返回空集合并提示
|
||
logger.warning("pyzbar 或 zbar 未就绪,已跳过 pyzbar 解码: %s", str(locals().get('_PYZBAR_IMPORT_ERR', ''))) # type: ignore
|
||
return []
|
||
# 输入 BGR,转灰度
|
||
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
|
||
results: List[Dict[str, str]] = []
|
||
symbols = _collect_supported_symbols()
|
||
logger.debug("调用 pyzbar: symbols=%d, rotations=%s, try_invert=%s", len(symbols) if symbols else 0, rotations, try_invert)
|
||
for img, ang, inv in _prepare_images(gray, try_invert=try_invert, rotations=rotations):
|
||
# pyzbar 要求 8bit 图像
|
||
decoded = _pyzbar_decode(img, symbols=symbols or None)
|
||
for obj in decoded:
|
||
data = obj.data.decode("utf-8", errors="ignore")
|
||
typ = obj.type
|
||
results.append({"type": typ, "code": data})
|
||
if results:
|
||
# 若当前设置已识别出内容,继续下一个旋转场景以收集更多,但不必反复
|
||
# 这里不提前返回,以便尽量收集多结果
|
||
pass
|
||
# 去重(按 type+code)
|
||
uniq = []
|
||
seen = set()
|
||
for r in results:
|
||
key = (r["type"], r["code"]) if isinstance(r, dict) else (None, None)
|
||
if key not in seen:
|
||
seen.add(key)
|
||
uniq.append(r)
|
||
logger.debug("pyzbar 返回结果数: %d", len(uniq))
|
||
return uniq
|
||
|
||
|