2
This commit is contained in:
BIN
backend/txm/2dbb4e14b6b0df047806bef434338058.jpg
Normal file
BIN
backend/txm/2dbb4e14b6b0df047806bef434338058.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
23
backend/txm/README.md
Normal file
23
backend/txm/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
条形码识别端口(EAN-13)
|
||||
|
||||
本项目使用 Python 与 OpenCV 实现 EAN-13 条形码视觉识别,并提供 Tkinter 界面进行本地测试。配置项集中在 `config/config.yaml`,不做任何硬编码;程序仅在用户操作时执行识别,不会自动运行后台任务。
|
||||
|
||||
运行环境
|
||||
- Python 3.9+
|
||||
- Windows 10/11(其他平台需要替换字体路径等少量配置)
|
||||
|
||||
快速开始
|
||||
1. 安装依赖:参考 `requirements.txt`
|
||||
2. 运行 Tk 测试界面(不会自动识别,需手动选择图片):
|
||||
```bash
|
||||
python -m app.ui.tk_app
|
||||
```
|
||||
|
||||
目录结构
|
||||
- `app/` 核心源码
|
||||
- `config/` 配置文件(YAML)
|
||||
- `doc/` 文档(开放 API、数据库文档等)
|
||||
|
||||
许可证:MIT
|
||||
|
||||
|
||||
3
backend/txm/app/__init__.py
Normal file
3
backend/txm/app/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""EAN-13 条形码识别应用包初始化。"""
|
||||
|
||||
|
||||
BIN
backend/txm/app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/config_loader.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/config_loader.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/config_loader.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/config_loader.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/ean13_decoder.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/ean13_decoder.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/ean13_decoder.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/ean13_decoder.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/image_processing.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/image_processing.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/image_processing.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/image_processing.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/io_utils.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/io_utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/io_utils.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/io_utils.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/logging_utils.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/logging_utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/pipeline.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/pipeline.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/pipeline.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/pipeline.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/pyzbar_engine.cpython-311.pyc
Normal file
BIN
backend/txm/app/__pycache__/pyzbar_engine.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/__pycache__/pyzbar_engine.cpython-39.pyc
Normal file
BIN
backend/txm/app/__pycache__/pyzbar_engine.cpython-39.pyc
Normal file
Binary file not shown.
21
backend/txm/app/config_loader.py
Normal file
21
backend/txm/app/config_loader.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
import platform
|
||||
from typing import Any, Dict
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def load_config(config_path: str = "config/config.yaml") -> Dict[str, Any]:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
# 动态选择中文字体
|
||||
sys_name = platform.system().lower()
|
||||
if sys_name.startswith("win"):
|
||||
config.setdefault("font", {})["selected"] = config["font"].get("windows")
|
||||
elif sys_name.startswith("darwin") or sys_name.startswith("mac"):
|
||||
config.setdefault("font", {})["selected"] = config["font"].get("macos")
|
||||
else:
|
||||
config.setdefault("font", {})["selected"] = config["font"].get("linux")
|
||||
return config
|
||||
|
||||
|
||||
204
backend/txm/app/ean13_decoder.py
Normal file
204
backend/txm/app/ean13_decoder.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
# EAN-13 编码表(L/G/R 模式),每个数字对应 4 个模块(7 宽度)内的宽窄模式
|
||||
# 采用 7 位宽度单元,1 表示黑,0 表示白。此处用字符串仅做查表,不做模拟。
|
||||
L_CODES = {
|
||||
"0": "0001101",
|
||||
"1": "0011001",
|
||||
"2": "0010011",
|
||||
"3": "0111101",
|
||||
"4": "0100011",
|
||||
"5": "0110001",
|
||||
"6": "0101111",
|
||||
"7": "0111011",
|
||||
"8": "0110111",
|
||||
"9": "0001011",
|
||||
}
|
||||
|
||||
G_CODES = {
|
||||
"0": "0100111",
|
||||
"1": "0110011",
|
||||
"2": "0011011",
|
||||
"3": "0100001",
|
||||
"4": "0011101",
|
||||
"5": "0111001",
|
||||
"6": "0000101",
|
||||
"7": "0010001",
|
||||
"8": "0001001",
|
||||
"9": "0010111",
|
||||
}
|
||||
|
||||
R_CODES = {
|
||||
"0": "1110010",
|
||||
"1": "1100110",
|
||||
"2": "1101100",
|
||||
"3": "1000010",
|
||||
"4": "1011100",
|
||||
"5": "1001110",
|
||||
"6": "1010000",
|
||||
"7": "1000100",
|
||||
"8": "1001000",
|
||||
"9": "1110100",
|
||||
}
|
||||
|
||||
# 左侧 6 位的奇偶模式用来编码首位数字
|
||||
LEADING_PARITY_TO_FIRST = {
|
||||
"LLLLLL": "0",
|
||||
"LLGLGG": "1",
|
||||
"LLGGLG": "2",
|
||||
"LLGGGL": "3",
|
||||
"LGLLGG": "4",
|
||||
"LGGLLG": "5",
|
||||
"LGGGLL": "6",
|
||||
"LGLGLG": "7",
|
||||
"LGLGGL": "8",
|
||||
"LGGLGL": "9",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_run_lengths(line: np.ndarray, total_modules: int) -> Tuple[np.ndarray, List[int]]:
|
||||
# 将行强度阈值化为黑白,再统计 run-length,然后按照总模块数归一化为 95 个模块
|
||||
# 使用中位数作为阈值以抵抗亮度变化
|
||||
threshold = np.median(line)
|
||||
binary = (line < threshold).astype(np.uint8) # 黑为 1
|
||||
# run-length 编码
|
||||
values = binary.tolist()
|
||||
runs: List[int] = []
|
||||
last = values[0]
|
||||
length = 1
|
||||
for v in values[1:]:
|
||||
if v == last:
|
||||
length += 1
|
||||
else:
|
||||
runs.append(length)
|
||||
last = v
|
||||
length = 1
|
||||
runs.append(length)
|
||||
# 放缩为 total_modules 模块
|
||||
total_pixels = float(sum(runs))
|
||||
if total_pixels <= 0:
|
||||
return binary, runs
|
||||
scale = total_modules / total_pixels
|
||||
scaled = [max(1, int(round(r * scale))) for r in runs]
|
||||
# 对齐长度
|
||||
diff = total_modules - sum(scaled)
|
||||
if diff != 0:
|
||||
# 简单补偿到首个 run
|
||||
scaled[0] = max(1, scaled[0] + diff)
|
||||
# 展开为模块级二进制
|
||||
expanded = []
|
||||
color = binary[0] # 起始颜色
|
||||
for r in scaled:
|
||||
expanded.extend([color] * r)
|
||||
color = 1 - color
|
||||
return np.array(expanded[:total_modules], dtype=np.uint8), scaled
|
||||
|
||||
|
||||
def _find_guards(bits: np.ndarray, tol: float) -> Optional[Tuple[int, int, int, int]]:
|
||||
# 守卫位模式:左 101,中 01010,右 101
|
||||
# 以模块位寻找
|
||||
s = ''.join('1' if b else '0' for b in bits.tolist())
|
||||
# 直接匹配应对理想情况,否则滑窗匹配
|
||||
# 找左 101
|
||||
left_pos = s.find('101')
|
||||
if left_pos == -1:
|
||||
return None
|
||||
# 找中间 01010(需位于左与右之间)
|
||||
mid_pos = s.find('01010', left_pos + 3)
|
||||
if mid_pos == -1:
|
||||
return None
|
||||
# 找右 101
|
||||
right_pos = s.find('101', mid_pos + 5)
|
||||
if right_pos == -1:
|
||||
return None
|
||||
return left_pos, mid_pos, right_pos, right_pos + 3
|
||||
|
||||
|
||||
def _bits_to_digit(bits: np.ndarray, tables: List[Tuple[str, dict]]) -> Optional[Tuple[str, str]]:
|
||||
pattern = ''.join('1' if b else '0' for b in bits.tolist())
|
||||
for parity, table in tables:
|
||||
for d, code in table.items():
|
||||
if pattern == code:
|
||||
return d, parity
|
||||
return None
|
||||
|
||||
|
||||
def decode_ean13_from_row(bits_row: np.ndarray) -> Optional[str]:
|
||||
# 输入为 0/1 模块位数组,长度应为 95
|
||||
if bits_row.size != 95:
|
||||
return None
|
||||
guards = _find_guards(bits_row, tol=0.35)
|
||||
if not guards:
|
||||
return None
|
||||
left_start, mid_start, right_start, right_end = guards
|
||||
# 划分区域
|
||||
left_data = bits_row[left_start + 3 : mid_start]
|
||||
right_data = bits_row[mid_start + 5 : right_start]
|
||||
# 左右各 6 个数字,每个 7 位
|
||||
if left_data.size != 6 * 7 or right_data.size != 6 * 7:
|
||||
return None
|
||||
digits_left: List[str] = []
|
||||
parity_seq: List[str] = []
|
||||
for i in range(6):
|
||||
seg = left_data[i * 7 : (i + 1) * 7]
|
||||
ret = _bits_to_digit(seg, [("L", L_CODES), ("G", G_CODES)])
|
||||
if not ret:
|
||||
return None
|
||||
d, parity = ret
|
||||
digits_left.append(d)
|
||||
parity_seq.append(parity)
|
||||
parity_str = ''.join(parity_seq)
|
||||
first_digit = LEADING_PARITY_TO_FIRST.get(parity_str)
|
||||
if first_digit is None:
|
||||
return None
|
||||
digits_right: List[str] = []
|
||||
for i in range(6):
|
||||
seg = right_data[i * 7 : (i + 1) * 7]
|
||||
ret = _bits_to_digit(seg, [("R", R_CODES)])
|
||||
if not ret:
|
||||
return None
|
||||
d, _ = ret
|
||||
digits_right.append(d)
|
||||
code_12 = first_digit + ''.join(digits_left) + ''.join(digits_right[:-1])
|
||||
check_digit = int(digits_right[-1])
|
||||
# 校验位计算
|
||||
s = 0
|
||||
for idx, ch in enumerate(code_12):
|
||||
v = int(ch)
|
||||
if (idx + 1) % 2 == 0:
|
||||
s += v * 3
|
||||
else:
|
||||
s += v
|
||||
calc = (10 - (s % 10)) % 10
|
||||
if calc != check_digit:
|
||||
return None
|
||||
return code_12 + str(check_digit)
|
||||
|
||||
|
||||
def sample_and_decode(warped_gray: np.ndarray, sample_rows: List[float], total_modules: int) -> Optional[str]:
|
||||
h, w = warped_gray.shape[:2]
|
||||
results: List[str] = []
|
||||
for r in sample_rows:
|
||||
row_y = min(h - 1, max(0, int(round(h * r))))
|
||||
line = warped_gray[row_y, :].astype(np.float32)
|
||||
# 直方图均衡增强对比
|
||||
line_eq = line
|
||||
# 归一化为 0..255
|
||||
if line_eq.max() > line_eq.min():
|
||||
line_eq = (line_eq - line_eq.min()) / (line_eq.max() - line_eq.min()) * 255.0
|
||||
bits, _ = _normalize_run_lengths(line_eq, total_modules)
|
||||
if bits.size != total_modules:
|
||||
continue
|
||||
code = decode_ean13_from_row(bits)
|
||||
if code:
|
||||
results.append(code)
|
||||
if not results:
|
||||
return None
|
||||
# 取众数
|
||||
vals, counts = np.unique(np.array(results), return_counts=True)
|
||||
return vals[int(np.argmax(counts))]
|
||||
|
||||
|
||||
84
backend/txm/app/image_processing.py
Normal file
84
backend/txm/app/image_processing.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def resize_keep_aspect(image: np.ndarray, target_width: int) -> np.ndarray:
|
||||
if target_width <= 0:
|
||||
return image
|
||||
h, w = image.shape[:2]
|
||||
if w == target_width:
|
||||
return image
|
||||
scale = target_width / float(w)
|
||||
new_size = (target_width, int(round(h * scale)))
|
||||
return cv2.resize(image, new_size, interpolation=cv2.INTER_AREA)
|
||||
|
||||
|
||||
def enhance_barcode_stripes(gray: np.ndarray, gaussian_ksize: int, sobel_ksize: int) -> np.ndarray:
|
||||
g = gray
|
||||
if gaussian_ksize and gaussian_ksize > 1:
|
||||
g = cv2.GaussianBlur(g, (gaussian_ksize, gaussian_ksize), 0)
|
||||
# 使用水平 Sobel 捕捉垂直边缘(条纹)
|
||||
grad_x = cv2.Sobel(g, cv2.CV_32F, 1, 0, ksize=sobel_ksize)
|
||||
grad_x = cv2.convertScaleAbs(grad_x)
|
||||
return grad_x
|
||||
|
||||
|
||||
def binarize_image(img: np.ndarray, method: str = "otsu") -> np.ndarray:
|
||||
if method == "adaptive":
|
||||
return cv2.adaptiveThreshold(
|
||||
img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 31, 10
|
||||
)
|
||||
# 默认 OTSU
|
||||
_, th = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
|
||||
return th
|
||||
|
||||
|
||||
def morph_close(img: np.ndarray, kernel_size: int) -> np.ndarray:
|
||||
if kernel_size <= 1:
|
||||
return img
|
||||
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
|
||||
closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
|
||||
return closed
|
||||
|
||||
|
||||
def find_barcode_roi(binary: np.ndarray, original_shape: Tuple[int, int], min_area_ratio: float, min_wh_ratio: float) -> Optional[Tuple[np.ndarray, Tuple[int, int, int, int]]]:
|
||||
h, w = original_shape
|
||||
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
image_area = h * w
|
||||
candidates: List[Tuple[float, Tuple[int, int, int, int]]] = []
|
||||
for cnt in contours:
|
||||
x, y, cw, ch = cv2.boundingRect(cnt)
|
||||
area = cw * ch
|
||||
if area / image_area < min_area_ratio:
|
||||
continue
|
||||
wh_ratio = cw / float(ch + 1e-6)
|
||||
if wh_ratio < min_wh_ratio:
|
||||
continue
|
||||
candidates.append((area, (x, y, cw, ch)))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda t: t[0], reverse=True)
|
||||
_, bbox = candidates[0]
|
||||
x, y, cw, ch = bbox
|
||||
roi = binary[y : y + ch, x : x + cw]
|
||||
return roi, bbox
|
||||
|
||||
|
||||
def warp_barcode_region(gray: np.ndarray, bbox: Tuple[int, int, int, int], target_height: int, crop_bottom_ratio: float = 0.0) -> np.ndarray:
|
||||
x, y, cw, ch = bbox
|
||||
crop = gray[y : y + ch, x : x + cw]
|
||||
# 去除底部数字区域干扰
|
||||
if 0 < crop_bottom_ratio < 1:
|
||||
hb = int(round(ch * (1.0 - crop_bottom_ratio)))
|
||||
hb = max(10, min(ch, hb))
|
||||
crop = crop[:hb, :]
|
||||
if target_height <= 0:
|
||||
return crop
|
||||
scale = target_height / float(ch)
|
||||
target_width = int(round(cw * scale))
|
||||
warped = cv2.resize(crop, (target_width, target_height), interpolation=cv2.INTER_CUBIC)
|
||||
return warped
|
||||
|
||||
|
||||
39
backend/txm/app/io_utils.py
Normal file
39
backend/txm/app/io_utils.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
import logging
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
|
||||
def read_image_bgr(path: str) -> Optional[np.ndarray]:
|
||||
"""读取图片为 BGR(兼容中文/非 ASCII 路径)。
|
||||
|
||||
优先使用 np.fromfile + cv2.imdecode 规避 Windows 路径编码问题,
|
||||
若失败再回退到 cv2.imread。
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
if not path:
|
||||
logger.warning("read_image_bgr 收到空路径")
|
||||
return None
|
||||
# 优先使用 fromfile 方案,处理中文路径
|
||||
try:
|
||||
data = np.fromfile(path, dtype=np.uint8)
|
||||
if data.size > 0:
|
||||
img = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
||||
if img is not None:
|
||||
logger.debug("read_image_bgr 使用 fromfile 解码成功: %s", path)
|
||||
return img
|
||||
except Exception as e:
|
||||
logger.exception("read_image_bgr fromfile 失败: %s", e)
|
||||
# 回退到 imread
|
||||
try:
|
||||
img = cv2.imread(path, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
logger.warning("read_image_bgr imread 返回 None: %s", path)
|
||||
return img
|
||||
except Exception as e:
|
||||
logger.exception("read_image_bgr imread 异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
56
backend/txm/app/logging_utils.py
Normal file
56
backend/txm/app/logging_utils.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def setup_logging(config: Dict[str, Any]) -> None:
|
||||
"""根据配置初始化日志。
|
||||
|
||||
- 控制台输出
|
||||
- 可选文件轮转输出(默认启用)
|
||||
- 不重复添加 handler(幂等)
|
||||
"""
|
||||
debug_cfg = (config or {}).get("debug", {})
|
||||
log_level_name = str(debug_cfg.get("log_level", "INFO")).upper()
|
||||
log_to_file = bool(debug_cfg.get("log_to_file", True))
|
||||
out_dir = debug_cfg.get("out_dir", "debug_out")
|
||||
file_name = debug_cfg.get("file_name", "txm.log")
|
||||
max_bytes = int(debug_cfg.get("max_bytes", 10 * 1024 * 1024))
|
||||
backup_count = int(debug_cfg.get("backup_count", 5))
|
||||
|
||||
try:
|
||||
level = getattr(logging, log_level_name)
|
||||
except Exception:
|
||||
level = logging.INFO
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
if root_logger.handlers:
|
||||
# 已经初始化过
|
||||
root_logger.setLevel(level)
|
||||
return
|
||||
|
||||
fmt = (
|
||||
"%(asctime)s.%(msecs)03d | %(levelname)s | %(name)s | "
|
||||
"%(message)s"
|
||||
)
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
root_logger.setLevel(level)
|
||||
|
||||
console = logging.StreamHandler()
|
||||
console.setLevel(level)
|
||||
console.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt))
|
||||
root_logger.addHandler(console)
|
||||
|
||||
if log_to_file:
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
file_path = os.path.join(out_dir, file_name)
|
||||
file_handler = RotatingFileHandler(
|
||||
file_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
|
||||
)
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(logging.Formatter(fmt=fmt, datefmt=datefmt))
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
|
||||
139
backend/txm/app/pipeline.py
Normal file
139
backend/txm/app/pipeline.py
Normal file
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
|
||||
82
backend/txm/app/pyzbar_engine.py
Normal file
82
backend/txm/app/pyzbar_engine.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from pyzbar.pyzbar import decode, ZBarSymbol
|
||||
|
||||
|
||||
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] = []
|
||||
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")
|
||||
# 输入 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 = 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
|
||||
|
||||
|
||||
2
backend/txm/app/server/__init__.py
Normal file
2
backend/txm/app/server/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
BIN
backend/txm/app/server/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/txm/app/server/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/server/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
backend/txm/app/server/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/server/__pycache__/main.cpython-311.pyc
Normal file
BIN
backend/txm/app/server/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/server/__pycache__/main.cpython-39.pyc
Normal file
BIN
backend/txm/app/server/__pycache__/main.cpython-39.pyc
Normal file
Binary file not shown.
103
backend/txm/app/server/main.py
Normal file
103
backend/txm/app/server/main.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
import io
|
||||
import time
|
||||
import logging
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from ..config_loader import load_config
|
||||
from ..pipeline import EAN13Recognizer
|
||||
from ..logging_utils import setup_logging
|
||||
|
||||
|
||||
config = load_config()
|
||||
setup_logging(config)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="条形码识别端口 API", version="1.0.0")
|
||||
recognizer = EAN13Recognizer()
|
||||
|
||||
|
||||
@app.post("/recognize/ean13")
|
||||
async def recognize_ean13(file: UploadFile = File(...)):
|
||||
# 上传大小简易校验
|
||||
t0 = time.time()
|
||||
content = await file.read()
|
||||
max_bytes = int(config["app"]["server"]["max_upload_mb"]) * 1024 * 1024
|
||||
if len(content) > max_bytes:
|
||||
logger.warning("/recognize/ean13 上传超限: size=%d, limit=%d", len(content), max_bytes)
|
||||
raise HTTPException(status_code=413, detail="文件过大")
|
||||
# 读取为图像
|
||||
data = np.frombuffer(content, dtype=np.uint8)
|
||||
img = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
logger.error("/recognize/ean13 解码为图像失败, filename=%s, size=%d", getattr(file, 'filename', ''), len(content))
|
||||
raise HTTPException(status_code=400, detail="无法解析为图像")
|
||||
logger.debug("/recognize/ean13 收到图像: shape=%s, dtype=%s", getattr(img, 'shape', None), getattr(img, 'dtype', None))
|
||||
merged = recognizer.recognize_any_from_image(img)
|
||||
code = merged.get("ean13") or ""
|
||||
resp = {
|
||||
"code": code,
|
||||
"type": "EAN13" if code else "",
|
||||
"others": merged.get("others", []),
|
||||
"message": "ok" if code or merged.get("others") else "未识别",
|
||||
}
|
||||
logger.info("/recognize/ean13 识别完成: code=%s, others=%d, cost_ms=%.1f", code, len(merged.get("others", []) or []), (time.time()-t0)*1000)
|
||||
return JSONResponse(resp)
|
||||
|
||||
|
||||
@app.post("/api/barcode/scan")
|
||||
async def api_barcode_scan(file: UploadFile = File(...)):
|
||||
t0 = time.time()
|
||||
content = await file.read()
|
||||
max_bytes = int(config["app"]["server"]["max_upload_mb"]) * 1024 * 1024
|
||||
if len(content) > max_bytes:
|
||||
logger.warning("/api/barcode/scan 上传超限: size=%d, limit=%d", len(content), max_bytes)
|
||||
raise HTTPException(status_code=413, detail="文件过大")
|
||||
data = np.frombuffer(content, dtype=np.uint8)
|
||||
img = cv2.imdecode(data, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
logger.error("/api/barcode/scan 解码为图像失败, filename=%s, size=%d", getattr(file, 'filename', ''), len(content))
|
||||
raise HTTPException(status_code=400, detail="无法解析为图像")
|
||||
logger.debug("/api/barcode/scan 收到图像: shape=%s, dtype=%s", getattr(img, 'shape', None), getattr(img, 'dtype', None))
|
||||
merged = recognizer.recognize_any_from_image(img)
|
||||
ean13 = merged.get("ean13") or ""
|
||||
others = merged.get("others", []) or []
|
||||
# 优先返回 EAN-13;否则回退到任意码制的第一个结果
|
||||
if ean13:
|
||||
resp = {
|
||||
"success": True,
|
||||
"barcodeType": "EAN13",
|
||||
"barcode": ean13,
|
||||
"others": others,
|
||||
}
|
||||
logger.info("/api/barcode/scan 命中 EAN13: code=%s, others=%d, cost_ms=%.1f", ean13, len(others), (time.time()-t0)*1000)
|
||||
return JSONResponse(resp)
|
||||
if isinstance(others, list) and others:
|
||||
first = others[0] if isinstance(others[0], dict) else None
|
||||
if first and first.get("code"):
|
||||
resp = {
|
||||
"success": True,
|
||||
"barcodeType": first.get("type", ""),
|
||||
"barcode": first.get("code", ""),
|
||||
"others": others,
|
||||
}
|
||||
logger.info("/api/barcode/scan 命中非 EAN: type=%s, code=%s, cost_ms=%.1f", first.get("type", ""), first.get("code", ""), (time.time()-t0)*1000)
|
||||
return JSONResponse(resp)
|
||||
logger.warning("/api/barcode/scan 未识别, others=%d, cost_ms=%.1f", len(others), (time.time()-t0)*1000)
|
||||
return JSONResponse({"success": False, "message": "无法识别,请重新上传"}, status_code=400)
|
||||
|
||||
|
||||
def main():
|
||||
host = config["app"]["server"]["host"]
|
||||
port = int(config["app"]["server"]["port"])
|
||||
logger.info("启动 FastAPI 服务器: %s:%d", host, port)
|
||||
uvicorn.run("app.server.main:app", host=host, port=port, reload=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
2
backend/txm/app/ui/__init__.py
Normal file
2
backend/txm/app/ui/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
BIN
backend/txm/app/ui/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
backend/txm/app/ui/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/txm/app/ui/__pycache__/tk_app.cpython-39.pyc
Normal file
BIN
backend/txm/app/ui/__pycache__/tk_app.cpython-39.pyc
Normal file
Binary file not shown.
162
backend/txm/app/ui/tk_app.py
Normal file
162
backend/txm/app/ui/tk_app.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
from ..config_loader import load_config
|
||||
from ..io_utils import read_image_bgr
|
||||
from ..pipeline import EAN13Recognizer
|
||||
|
||||
|
||||
class TkEAN13App:
|
||||
def __init__(self) -> None:
|
||||
self.config = load_config()
|
||||
self.root = tk.Tk()
|
||||
self.root.title(self.config["app"]["ui"]["window_title"])
|
||||
|
||||
self.image_panel = tk.Label(self.root)
|
||||
self.image_panel.pack(side=tk.TOP, padx=10, pady=10)
|
||||
|
||||
btn_frame = tk.Frame(self.root)
|
||||
btn_frame.pack(side=tk.TOP, pady=5)
|
||||
|
||||
self.btn_open = tk.Button(btn_frame, text="选择图片", command=self.on_open)
|
||||
self.btn_open.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.btn_recognize = tk.Button(btn_frame, text="识别条码", command=self.on_recognize)
|
||||
self.btn_recognize.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.btn_camera = tk.Button(btn_frame, text="摄像头识别", command=self.on_camera)
|
||||
self.btn_camera.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.result_var = tk.StringVar(value="结果:-")
|
||||
self.result_label = tk.Label(self.root, textvariable=self.result_var)
|
||||
self.result_label.pack(side=tk.TOP, pady=5)
|
||||
|
||||
self.current_image_path: Optional[str] = None
|
||||
self.recognizer = EAN13Recognizer()
|
||||
self.cap = None
|
||||
self.cam_running = False
|
||||
|
||||
def on_open(self) -> None:
|
||||
path = filedialog.askopenfilename(
|
||||
title="选择条码图片",
|
||||
filetypes=[("Image Files", "*.png;*.jpg;*.jpeg;*.bmp;*.tif;*.tiff")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self.current_image_path = path
|
||||
img_bgr = read_image_bgr(path)
|
||||
if img_bgr is None:
|
||||
messagebox.showerror("错误", "无法读取图片")
|
||||
return
|
||||
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||
# 限制显示尺寸
|
||||
max_w = 800
|
||||
h, w = img_rgb.shape[:2]
|
||||
if w > max_w:
|
||||
scale = max_w / float(w)
|
||||
img_rgb = cv2.resize(img_rgb, (max_w, int(h * scale)), interpolation=cv2.INTER_AREA)
|
||||
im = Image.fromarray(img_rgb)
|
||||
self.tk_img = ImageTk.PhotoImage(im)
|
||||
self.image_panel.configure(image=self.tk_img)
|
||||
self.result_var.set("结果:-")
|
||||
|
||||
def on_recognize(self) -> None:
|
||||
if not self.current_image_path:
|
||||
messagebox.showinfo("提示", "请先选择图片")
|
||||
return
|
||||
result = self.recognizer.recognize_any_from_path(self.current_image_path)
|
||||
ean13 = result.get("ean13", "")
|
||||
if ean13:
|
||||
self.result_var.set(f"结果:EAN-13 {ean13}")
|
||||
return
|
||||
others = result.get("others", [])
|
||||
if others:
|
||||
first = others[0]
|
||||
self.result_var.set(f"结果:{first.get('type')} {first.get('code')}")
|
||||
return
|
||||
self.result_var.set("结果:未识别")
|
||||
|
||||
def on_camera(self) -> None:
|
||||
if self.cam_running:
|
||||
# 若已在运行,视为停止
|
||||
self.stop_camera()
|
||||
return
|
||||
cam_cfg = self.config.get("camera", {})
|
||||
index = int(cam_cfg.get("index", 0))
|
||||
self.cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
|
||||
if not self.cap.isOpened():
|
||||
messagebox.showerror("错误", f"无法打开摄像头 index={index}")
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
return
|
||||
# 设置分辨率(尽力设置)
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, float(cam_cfg.get("width", 1280)))
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, float(cam_cfg.get("height", 720)))
|
||||
self.cam_running = True
|
||||
self.result_var.set("结果:摄像头开启,正在识别...")
|
||||
self.btn_camera.configure(text="停止摄像头")
|
||||
self._camera_loop()
|
||||
|
||||
def stop_camera(self) -> None:
|
||||
self.cam_running = False
|
||||
try:
|
||||
if self.cap is not None:
|
||||
self.cap.release()
|
||||
finally:
|
||||
self.cap = None
|
||||
self.btn_camera.configure(text="摄像头识别")
|
||||
|
||||
def _camera_loop(self) -> None:
|
||||
if not self.cam_running or self.cap is None:
|
||||
return
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
# 读取失败,稍后重试
|
||||
self.root.after(int(self.config.get("camera", {}).get("interval_ms", 80)), self._camera_loop)
|
||||
return
|
||||
# 显示到面板
|
||||
show = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
max_w = 800
|
||||
h, w = show.shape[:2]
|
||||
if w > max_w:
|
||||
scale = max_w / float(w)
|
||||
show = cv2.resize(show, (max_w, int(h * scale)), interpolation=cv2.INTER_AREA)
|
||||
im = Image.fromarray(show)
|
||||
self.tk_img = ImageTk.PhotoImage(im)
|
||||
self.image_panel.configure(image=self.tk_img)
|
||||
|
||||
# 识别
|
||||
result = self.recognizer.recognize_any_from_image(frame)
|
||||
ean13 = result.get("ean13", "")
|
||||
if ean13:
|
||||
self.result_var.set(f"结果:EAN-13 {ean13}")
|
||||
self.stop_camera()
|
||||
return
|
||||
others = result.get("others", [])
|
||||
if others:
|
||||
first = others[0]
|
||||
self.result_var.set(f"结果:{first.get('type')} {first.get('code')}")
|
||||
self.stop_camera()
|
||||
return
|
||||
|
||||
# 未识别,继续下一帧
|
||||
self.root.after(int(self.config.get("camera", {}).get("interval_ms", 80)), self._camera_loop)
|
||||
|
||||
def run(self) -> None:
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = TkEAN13App()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
74
backend/txm/config/config.yaml
Normal file
74
backend/txm/config/config.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
# 全局配置(禁止硬编码)
|
||||
|
||||
app:
|
||||
language: zh_CN
|
||||
ui:
|
||||
window_title: "EAN-13 条形码识别测试"
|
||||
server:
|
||||
host: "127.0.0.1"
|
||||
port: 8000
|
||||
max_upload_mb: 8
|
||||
|
||||
preprocess:
|
||||
# 读取图像时是否等比缩放到此宽度(0 表示不缩放)
|
||||
resize_width: 1280
|
||||
# 高斯模糊核大小(奇数),0 表示不使用
|
||||
gaussian_blur_ksize: 3
|
||||
# 形态学顶帽/黑帽核大小(奇数)
|
||||
morphology_kernel: 17
|
||||
# Sobel 阈值用于增强条纹
|
||||
sobel_ksize: 3
|
||||
# 二值化方法:otsu | adaptive
|
||||
binarize: otsu
|
||||
# 形态学闭运算核大小(合并细条纹)
|
||||
close_kernel: 21
|
||||
|
||||
roi:
|
||||
# 轮廓面积下限(相对整图面积比例),用于过滤非条码区域
|
||||
min_area_ratio: 0.01
|
||||
# 宽高比下限(条码通常宽>高)
|
||||
min_wh_ratio: 2.0
|
||||
# 透视矫正时的目标高度(像素)
|
||||
warp_target_height: 120
|
||||
# 从底部裁掉的比例,去除数字区域影响(0-1)
|
||||
crop_bottom_ratio: 0.25
|
||||
|
||||
decoder:
|
||||
# EAN-13 采样线位置(相对高度 0-1),可多条线取众数
|
||||
sample_rows: [0.35, 0.5, 0.65]
|
||||
# 归一化后模块总数(EAN-13 固定 95)
|
||||
total_modules: 95
|
||||
# 守卫位宽容差(相对模块宽度)
|
||||
guard_tolerance: 0.35
|
||||
# 直方图峰谷检测阈值(相对振幅)
|
||||
peak_valley_rel_threshold: 0.2
|
||||
# 引擎优先级:pyzbar | ean13(自研)
|
||||
engine_order: ["pyzbar", "ean13"]
|
||||
# 是否对图像做反色尝试(黑白反转)
|
||||
try_invert: true
|
||||
# 旋转角度集合(度)用于鲁棒性提升
|
||||
rotations: [0, 90, 180, 270]
|
||||
|
||||
font:
|
||||
# Windows 常见中文字体路径(按需修改)
|
||||
windows: "C:/Windows/Fonts/msyh.ttc"
|
||||
macos: "/System/Library/Fonts/PingFang.ttc"
|
||||
linux: "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
|
||||
|
||||
debug:
|
||||
save_steps: false
|
||||
out_dir: "debug_out"
|
||||
log_level: "DEBUG"
|
||||
log_to_file: true
|
||||
file_name: "txm.log"
|
||||
max_bytes: 10485760
|
||||
backup_count: 5
|
||||
|
||||
camera:
|
||||
index: 0
|
||||
width: 1280
|
||||
height: 720
|
||||
# 采样间隔毫秒(UI 轮询帧率),过小会占用较多 CPU
|
||||
interval_ms: 80
|
||||
|
||||
|
||||
96
backend/txm/debug_out/txm.log
Normal file
96
backend/txm/debug_out/txm.log
Normal file
@@ -0,0 +1,96 @@
|
||||
2025-09-25 18:11:37.819 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:11:37.822 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:11:37.824 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:11:37.869 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:16.127 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:16.130 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:18:16.132 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:18:16.169 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:27.234 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:27.236 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:18:27.237 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:18:27.259 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:39.703 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:18:39.704 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:18:39.706 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:18:39.725 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:19:45.375 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:19:45.379 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:19:45.381 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:19:45.412 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:25:44.956 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:25:44.960 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:25:44.962 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:25:44.991 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:29:51.305 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:29:51.308 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:29:51.310 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:29:51.343 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:36:45.835 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:36:45.839 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:36:45.842 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:36:45.875 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:57:39.253 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 18:57:39.256 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 18:57:39.258 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 18:57:39.300 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:01:51.499 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:01:51.501 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 19:01:51.503 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 19:01:51.524 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:05:33.804 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:05:33.805 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 19:05:33.807 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 19:05:33.826 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:07:30.632 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:07:30.634 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 19:07:30.636 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 19:07:30.656 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:31:55.243 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:31:55.244 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 19:31:55.246 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 19:31:55.266 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:54:34.920 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 19:54:34.922 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 19:54:34.923 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 19:54:34.943 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 20:01:17.819 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 20:01:17.820 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 20:01:17.822 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 20:01:17.842 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 22:09:07.584 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 22:09:07.588 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 22:09:07.607 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 22:09:07.852 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 22:59:54.417 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-25 22:59:54.419 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-25 22:59:54.420 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-25 22:59:54.441 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 10:39:41.180 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 10:39:41.186 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 10:39:41.203 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 10:39:41.484 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 12:58:15.810 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 12:58:15.815 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 12:58:15.835 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 12:58:16.257 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 14:35:51.088 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 14:35:51.092 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 14:35:51.094 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 14:35:51.130 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 14:55:28.260 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 14:55:28.262 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 14:55:28.263 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 14:55:28.284 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 16:04:25.251 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 16:04:25.255 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 16:04:25.272 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 16:04:25.538 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 21:19:53.746 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 21:19:53.758 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 21:19:53.765 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 21:19:53.862 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 22:56:27.812 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
2025-09-27 22:56:27.814 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
|
||||
2025-09-27 22:56:27.815 | DEBUG | asyncio | Using proactor: IocpProactor
|
||||
2025-09-27 22:56:27.839 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
|
||||
71
backend/txm/doc/API.md
Normal file
71
backend/txm/doc/API.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 条形码识别 API 对接文档
|
||||
|
||||
本文档说明与 Java 后端对接的上传识别接口,返回 EAN‑13 结果;若无法识别返回明确错误提示。
|
||||
|
||||
## 基本信息
|
||||
- 接口地址:`/api/barcode/scan`
|
||||
- 请求方式:`POST multipart/form-data`
|
||||
- 参数:
|
||||
- `file`:图片文件(二进制)。
|
||||
- 成功响应(HTTP 200):
|
||||
```json
|
||||
{ "success": true, "barcodeType": "EAN13", "barcode": "6901234567892" }
|
||||
```
|
||||
- 失败响应(HTTP 400):
|
||||
```json
|
||||
{ "success": false, "message": "无法识别,请重新上传" }
|
||||
```
|
||||
|
||||
## 请求示例(PowerShell)
|
||||
```powershell
|
||||
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/barcode/scan -Method Post -Form @{ file = Get-Item .\sample.jpg }
|
||||
```
|
||||
|
||||
## 请求示例(curl)
|
||||
```bash
|
||||
curl -F "file=@sample.jpg" http://127.0.0.1:8000/api/barcode/scan
|
||||
```
|
||||
|
||||
## Java 示例(Spring WebClient)
|
||||
```java
|
||||
WebClient client = WebClient.create("http://127.0.0.1:8000");
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new FileSystemResource(new File("sample.jpg")));
|
||||
|
||||
Map res = client.post()
|
||||
.uri("/api/barcode/scan")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(BodyInserters.fromMultipartData(body))
|
||||
.retrieve()
|
||||
.onStatus(s -> s.value() == 400, r -> r.bodyToMono(String.class).map(RuntimeException::new))
|
||||
.bodyToMono(Map.class)
|
||||
.block();
|
||||
|
||||
System.out.println(res);
|
||||
```
|
||||
|
||||
## Java 示例(Spring RestTemplate)
|
||||
```java
|
||||
RestTemplate rest = new RestTemplate();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new FileSystemResource("sample.jpg"));
|
||||
HttpEntity<MultiValueMap<String, Object>> req = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<String> resp = rest.postForEntity("http://127.0.0.1:8000/api/barcode/scan", req, String.class);
|
||||
System.out.println(resp.getStatusCode());
|
||||
System.out.println(resp.getBody());
|
||||
```
|
||||
|
||||
## 返回字段说明
|
||||
- `success`:是否识别成功。
|
||||
- `barcodeType`:条码类型(当前为 `EAN13`)。
|
||||
- `barcode`:条码数字串。
|
||||
- `message`:失败时的人类可读说明。
|
||||
|
||||
## 错误码
|
||||
- 400:文件为空、图片解析失败、或未识别;返回 `{ success:false, message:"无法识别,请重新上传" }`。
|
||||
- 413:文件过大(受 `config/config.yaml` 中 `app.server.max_upload_mb` 限制)。
|
||||
|
||||
|
||||
51
backend/txm/doc/README.md
Normal file
51
backend/txm/doc/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
使用说明
|
||||
|
||||
1. 安装依赖:
|
||||
```powershell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
2. 启动 Tk 测试界面:
|
||||
```powershell
|
||||
python -m app.ui.tk_app
|
||||
```
|
||||
3. 在界面中点击“选择图片”,然后点击“识别 EAN-13”。
|
||||
|
||||
摄像头识别
|
||||
1. 在 Tk 界面点击“摄像头识别”,程序会打开默认摄像头(`config/config.yaml` 可配置 index、分辨率与轮询间隔)。
|
||||
2. 一旦识别到任意条码(优先 EAN‑13),会自动关闭摄像头并在界面显示结果。
|
||||
3. 再次点击“停止摄像头”可手动关闭。
|
||||
|
||||
HTTP 服务(上传识别)
|
||||
1. 启动服务:
|
||||
```powershell
|
||||
python -m app.server.main
|
||||
```
|
||||
2. PowerShell 上传示例:
|
||||
```powershell
|
||||
Invoke-RestMethod -Uri http://127.0.0.1:8000/recognize/ean13 -Method Post -Form @{ file = Get-Item .\sample.jpg }
|
||||
```
|
||||
3. 响应:
|
||||
```json
|
||||
{ "code": "6901234567892", "type": "EAN13", "others": [{ "type": "CODE128", "code": "..." }], "message": "ok" }
|
||||
```
|
||||
|
||||
配置说明
|
||||
- 编辑 `config/config.yaml` 可调整预处理、ROI 过滤、解码参数;字体路径已按系统自动选择。
|
||||
- `app.server` 中的 `host/port/max_upload_mb` 控制 HTTP 服务监听与上传大小限制。
|
||||
|
||||
注意事项
|
||||
- 该程序不会自动启动摄像头或后台任务,均需用户手动触发。
|
||||
- 若图片分辨率过低或条码倾斜严重,识别率会下降,可增大 `warp_target_height` 与 `sample_rows` 数量。
|
||||
|
||||
Pyzbar/ZBar 安装说明
|
||||
- Windows: 直接 `pip install pyzbar` 即可(已包含 zbar DLL)。
|
||||
- macOS: 安装 zbar 库后再安装 pyzbar:
|
||||
```bash
|
||||
brew install zbar; pip install pyzbar
|
||||
```
|
||||
- Linux (Debian/Ubuntu):
|
||||
```bash
|
||||
sudo apt-get update; sudo apt-get install -y libzbar0; pip install pyzbar
|
||||
```
|
||||
|
||||
|
||||
94
backend/txm/doc/openapi.yaml
Normal file
94
backend/txm/doc/openapi.yaml
Normal file
@@ -0,0 +1,94 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 条形码识别端口 API
|
||||
version: 1.0.0
|
||||
description: 本地视觉识别 EAN-13 的测试接口定义(示例)。
|
||||
servers:
|
||||
- url: http://localhost:8000
|
||||
paths:
|
||||
/recognize/ean13:
|
||||
post:
|
||||
summary: 识别 EAN-13(✅ 完全实现:Pyzbar+自研回退,UI 与服务端可用)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: EAN-13 数字串(未识别为空字符串)
|
||||
message:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
description: 命中的主类型;未识别为空
|
||||
others:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
"400":
|
||||
description: 参数错误
|
||||
"500":
|
||||
description: 服务器错误
|
||||
/api/barcode/scan:
|
||||
post:
|
||||
summary: 图片上传并识别 EAN-13(✅ 完全实现)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
barcodeType:
|
||||
type: string
|
||||
example: EAN13
|
||||
barcode:
|
||||
type: string
|
||||
example: 6901234567892
|
||||
"400":
|
||||
description: 无法识别或参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: 无法识别,请重新上传
|
||||
|
||||
|
||||
77
backend/txm/doc/同步文档.md
Normal file
77
backend/txm/doc/同步文档.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## 前后端数据库状态说明
|
||||
|
||||
**更新日期**: 2025-09-17
|
||||
|
||||
### 概要
|
||||
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com` 的 `partsinquiry` 库完成初始化(表结构与触发器已创建)。
|
||||
- 已生成根目录文档:`/doc/database_documentation.md` 已同步线上结构(字段、索引、外键、触发器)。
|
||||
- 后端代码仍未配置数据源依赖与连接,前端无本地结构化存储方案。
|
||||
|
||||
### 已建库与连接信息(用于部署/联调)
|
||||
- Address: `mysql.tonaspace.com`
|
||||
- Database: `partsinquiry`
|
||||
- User: `root`
|
||||
- 说明:所有结构变更均通过 MysqlMCP 执行并已落地到线上库。
|
||||
|
||||
### 角色与模拟数据策略(统一为店长)
|
||||
- 当前不进行角色划分,系统仅保留“店长”角色。
|
||||
- 已将所有用户记录统一为:`role='owner'`、`is_owner=1`。
|
||||
- 前端/后端权限逻辑暂未启用,后续若引入权限体系,再行扩展角色与边界。
|
||||
|
||||
### 小程序默认用户(可开关,默认关闭)
|
||||
- 目的:开发/演示阶段,便于免登录联调。
|
||||
- 机制:前端在请求头附加 `X-User-Id`(值为张老板 id=2),仅当开关开启时。
|
||||
- 开关:
|
||||
- 环境变量:`VITE_APP_ENABLE_DEFAULT_USER=true` 与 `VITE_APP_DEFAULT_USER_ID=2`
|
||||
- 或本地存储:`ENABLE_DEFAULT_USER=true` 与 `DEFAULT_USER_ID=2`
|
||||
- 关闭:不设置/置为 `false` 即可停用(生产环境默认关闭)。
|
||||
- 完全移除:删除 `frontend/common/config.js` 中默认用户配置与 `frontend/common/http.js` 中注入逻辑。
|
||||
|
||||
### 后端(Spring Boot)状态
|
||||
- 依赖:`pom.xml` 已包含 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j`。
|
||||
- 配置:`application.properties` 使用环境变量注入数据源,已补充 Hikari/JPA;新增附件占位图配置:
|
||||
- `attachments.placeholder.image-path`(env: `ATTACHMENTS_PLACEHOLDER_IMAGE`)
|
||||
- `attachments.placeholder.url-path`(env: `ATTACHMENTS_PLACEHOLDER_URL`,默认 `/api/attachments/placeholder`)
|
||||
- 接口:新增附件相关接口(占位方案):
|
||||
- POST `/api/attachments`:忽略内容,返回 `{ url: "/api/attachments/placeholder" }`
|
||||
- GET `/api/attachments/placeholder`:返回本地占位图二进制
|
||||
- 迁移:仍建议引入 Flyway/Liquibase;结构变更继续通过 MysqlMCP 并同步 `/doc/database_documentation.md`。
|
||||
|
||||
### 前端(uni-app)数据库状态
|
||||
- 数据持久化:未见 IndexedDB/WebSQL/SQLite/云数据库使用;页面数据为内置静态数据。
|
||||
- 本地存储:未见 `uni.setStorage`/`uni.getStorage` 的集中封装或结构化键空间设计。
|
||||
- 结论:前端当前不涉及本地数据库或结构化存储方案。
|
||||
|
||||
### 风险与影响
|
||||
- 后端未配置数据源与接口,应用无法读写远端库(虽已建表)。
|
||||
- 无接口契约,前后端仍无法联调涉及数据库的功能。
|
||||
|
||||
### 建议的后续行动(不自动执行)
|
||||
- 在后端引入依赖:`spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j`。
|
||||
- 配置数据源:使用环境变量注入 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD` 等,指向上述远程库。
|
||||
- 引入迁移工具(Flyway/Liquibase)管理 DDL;后续所有变更继续通过 MysqlMCP 执行,并同步 `/doc/database_documentation.md`。
|
||||
- 增加健康检查与基础 CRUD 接口;在 `/doc/openapi.yaml` 按规范登记并标注实现状态(❌/✅)。
|
||||
|
||||
### 前端默认连接策略
|
||||
- 默认后端地址:`http://127.0.0.1:8080`(可被环境变量/Storage 覆盖)
|
||||
- 多地址重试:按顺序尝试(去重处理):`[ENV, Storage, 127.0.0.1:8080, localhost:8080]`
|
||||
- 默认用户:开启(可被环境变量/Storage 关闭),请求自动附带 `X-User-Id`(默认 `2`)。
|
||||
- 如需关闭:在 Storage 或构建环境中设置 `ENABLE_DEFAULT_USER=false`。
|
||||
|
||||
|
||||
### 占位图策略(当前阶段)
|
||||
- 说明:所有图片上传与展示均统一使用占位图,实际文件存储暂不开发。
|
||||
- 本地占位图:`C:\Users\21826\Desktop\Wj\PartsInquiry\backend\picture\屏幕截图 2025-08-14 134657.png`
|
||||
- 配置方式:
|
||||
- PowerShell(当前用户持久化)
|
||||
```powershell
|
||||
setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
|
||||
setx ATTACHMENTS_PLACEHOLDER_URL "/api/attachments/placeholder"
|
||||
```
|
||||
- 应用重启后生效;也可在运行环境变量中注入。
|
||||
- 前端影响:
|
||||
- `components/ImageUploader.vue` 上传始终得到 `{ url: '/api/attachments/placeholder' }`
|
||||
- 商品列表/详情展示该占位图地址
|
||||
|
||||
|
||||
|
||||
BIN
backend/txm/image copy 2.png
Normal file
BIN
backend/txm/image copy 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
backend/txm/image copy.png
Normal file
BIN
backend/txm/image copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
backend/txm/image.png
Normal file
BIN
backend/txm/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
9
backend/txm/requirements.txt
Normal file
9
backend/txm/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
opencv-python>=4.9.0
|
||||
numpy>=1.26.0
|
||||
Pillow>=10.0.0
|
||||
PyYAML>=6.0.1
|
||||
fastapi>=0.111.0
|
||||
uvicorn[standard]>=0.30.0
|
||||
python-multipart>=0.0.9
|
||||
pyzbar>=0.1.9
|
||||
|
||||
Reference in New Issue
Block a user