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))]