140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
from typing import Optional, List, Dict
|
|
|
|
import cv2
|
|
import logging
|
|
|
|
from .config_loader import load_config
|
|
from .image_processing import (
|
|
binarize_image,
|
|
enhance_barcode_stripes,
|
|
find_barcode_roi,
|
|
morph_close,
|
|
resize_keep_aspect,
|
|
warp_barcode_region,
|
|
)
|
|
from .ean13_decoder import sample_and_decode
|
|
from .io_utils import read_image_bgr
|
|
from .pyzbar_engine import decode_with_pyzbar
|
|
|
|
|
|
class EAN13Recognizer:
|
|
def __init__(self, config_path: str = "config/config.yaml") -> None:
|
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
self.config = load_config(config_path)
|
|
self.logger.debug("配置加载完成: preprocess=%s, roi=%s, decoder=%s", self.config.get("preprocess"), self.config.get("roi"), self.config.get("decoder"))
|
|
|
|
def recognize_from_path(self, image_path: str) -> Optional[str]:
|
|
image = read_image_bgr(image_path)
|
|
if image is None:
|
|
self.logger.warning("读取图像失败: path=%s", image_path)
|
|
return None
|
|
return self.recognize_from_image(image)
|
|
|
|
def _recognize_with_pyzbar(self, img) -> Optional[str]:
|
|
decoder_cfg = self.config["decoder"]
|
|
pyz_res = decode_with_pyzbar(
|
|
img,
|
|
try_invert=decoder_cfg.get("try_invert", True),
|
|
rotations=decoder_cfg.get("rotations", [0, 90, 180, 270]),
|
|
)
|
|
if pyz_res:
|
|
self.logger.debug("pyzbar 识别到 %d 条结果", len(pyz_res))
|
|
for r in pyz_res:
|
|
if r.get("type") in ("EAN13", "EAN-13") and r.get("code"):
|
|
self.logger.info("pyzbar 命中 EAN13: %s", r.get("code"))
|
|
return r.get("code")
|
|
return None
|
|
|
|
def recognize_from_image(self, image) -> Optional[str]:
|
|
cfg = self.config
|
|
preprocess_cfg = cfg["preprocess"]
|
|
roi_cfg = cfg["roi"]
|
|
decoder_cfg = cfg["decoder"]
|
|
|
|
img = resize_keep_aspect(image, preprocess_cfg.get("resize_width", 0))
|
|
self.logger.debug("输入尺寸=%s, 预处理后尺寸=%s", getattr(image, 'shape', None), getattr(img, 'shape', None))
|
|
|
|
# 先按引擎优先级尝试 Pyzbar
|
|
engine_order: List[str] = decoder_cfg.get("engine_order", ["pyzbar", "ean13"])
|
|
if "pyzbar" in engine_order:
|
|
code = self._recognize_with_pyzbar(img)
|
|
if code:
|
|
return code
|
|
if "ean13" not in engine_order:
|
|
return None
|
|
|
|
# 回退到自研 EAN-13 解码
|
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
grad = enhance_barcode_stripes(
|
|
gray,
|
|
gaussian_ksize=preprocess_cfg.get("gaussian_blur_ksize", 3),
|
|
sobel_ksize=preprocess_cfg.get("sobel_ksize", 3),
|
|
)
|
|
bin_img = binarize_image(grad, preprocess_cfg.get("binarize", "otsu"))
|
|
closed = morph_close(bin_img, preprocess_cfg.get("close_kernel", 21))
|
|
|
|
roi_result = find_barcode_roi(
|
|
closed,
|
|
original_shape=gray.shape[:2],
|
|
min_area_ratio=roi_cfg.get("min_area_ratio", 0.01),
|
|
min_wh_ratio=roi_cfg.get("min_wh_ratio", 2.0),
|
|
)
|
|
if not roi_result:
|
|
self.logger.debug("未找到条码 ROI")
|
|
return None
|
|
_, bbox = roi_result
|
|
self.logger.debug("ROI bbox=%s", bbox)
|
|
warped = warp_barcode_region(
|
|
gray,
|
|
bbox,
|
|
roi_cfg.get("warp_target_height", 120),
|
|
crop_bottom_ratio=roi_cfg.get("crop_bottom_ratio", 0.0),
|
|
)
|
|
self.logger.debug("透视矫正后尺寸=%s", getattr(warped, 'shape', None))
|
|
|
|
code = sample_and_decode(
|
|
warped,
|
|
decoder_cfg.get("sample_rows", [0.5]),
|
|
decoder_cfg.get("total_modules", 95),
|
|
)
|
|
if code:
|
|
self.logger.info("自研 EAN13 解码成功: %s", code)
|
|
else:
|
|
self.logger.debug("自研 EAN13 解码失败")
|
|
return code
|
|
|
|
def recognize_any_from_image(self, image) -> Dict[str, object]:
|
|
cfg = self.config
|
|
decoder_cfg = cfg["decoder"]
|
|
img = resize_keep_aspect(image, cfg["preprocess"].get("resize_width", 0))
|
|
|
|
# Pyzbar 全量识别
|
|
pyz_res = decode_with_pyzbar(
|
|
img,
|
|
try_invert=decoder_cfg.get("try_invert", True),
|
|
rotations=decoder_cfg.get("rotations", [0, 90, 180, 270]),
|
|
)
|
|
self.logger.debug("pyzbar 返回 %d 条结果", len(pyz_res) if isinstance(pyz_res, list) else 0)
|
|
ean13 = ""
|
|
for r in pyz_res:
|
|
if r.get("type") in ("EAN13", "EAN-13") and r.get("code"):
|
|
ean13 = r.get("code")
|
|
break
|
|
others = [r for r in pyz_res if r.get("type") not in ("EAN13", "EAN-13")]
|
|
if not ean13:
|
|
e = self.recognize_from_image(img)
|
|
if e:
|
|
ean13 = e
|
|
if ean13:
|
|
self.logger.info("recognize_any 命中 EAN13: %s, others=%d", ean13, len(others))
|
|
else:
|
|
self.logger.debug("recognize_any 未命中 EAN13, others=%d", len(others))
|
|
return {"ean13": ean13, "others": others}
|
|
|
|
def recognize_any_from_path(self, image_path: str) -> Dict[str, object]:
|
|
image = read_image_bgr(image_path)
|
|
if image is None:
|
|
return {"ean13": "", "others": []}
|
|
return self.recognize_any_from_image(image)
|
|
|