| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- # -*- coding: utf-8 -*-
- """card_no rec 后处理:单次宽容政策 + 合法格式判定。
- 规则见 data_all/卡牌区域/卡牌区域OCR_rec宽容政策.md
- 合法格式见 data_all/卡牌区域/卡牌区域OCR内容指南.md
- """
- from __future__ import annotations
- import re
- # 稀有度后缀,按长度从长到短匹配
- RARITY_SUFFIXES = ["RRR", "SSR", "SAR", "CHR", "CSR", "UR", "HR", "SR", "RR", "AR",
- "MR", "MA", "PR", "TR"]
- # 末尾单符号(可带前导空格)
- TAIL_CHARS = ["*", "★", "$", "C"]
- # 纯数字固定位置:长度 → 0-based 索引
- DIGIT_SLASH_INDEX = {7: 3, 6: 2, 5: 2, 4: 1}
- RE_DIGIT_SPACE_SLASH = re.compile(r"^[\d /]+$")
- RE_DIGIT_DOT = re.compile(r"^\d+\.\d+$")
- RE_NO_MO = re.compile(r"^[NnmMm][AaOo]\.?\s*(\d{1,4})$")
- # 合法格式(老五种 + 补全:斜杠1~5、促销更长、中点·、长码≥3)
- RE_VALID_SLASH = re.compile(
- r"^[A-Za-z0-9]{1,5}/"
- r"(?:"
- r"[A-Za-z0-9]{1,5}" # 普通两侧 1~5
- r"|[A-Za-z0-9]{1,6}-[A-Za-z0-9]{1,3}" # 连字符促销 如 SM-P / 30th-P / S-P
- r"|[A-Za-z0-9]{1,6}·[A-Za-z0-9]{1,3}" # 中点促销 如 SM·P
- r")$"
- )
- RE_VALID_NO = re.compile(r"^No\.\d{1,4}$")
- RE_VALID_LONG = re.compile(r"^[A-Z][A-Z0-9]{2,}$") # 总长≥3,字母开头,含数字在 is_valid 再查
- RE_VALID_SHORT_NUM = re.compile(r"^\d{1,3}$")
- RE_VALID_HASH = re.compile(r"^[A-Za-z]{3,4}#\d+$")
- CARD_NO_BLACKLIST = {"999", "000"}
- # 空结果原因(与“无检测框”区分)
- EMPTY_NO_DETECTION = "no_detection" # yolo26s conf<0.3,卡面本身无编号
- EMPTY_BLUR_UNREADABLE = "blur_unreadable" # 有检测框,但 rec 低分且不合规(图模糊)
- EMPTY_REC_EMPTY = "rec_empty" # 有检测框,rec 直接空文本
- def apply_leniency(text: str) -> str:
- """对 rec 识别文本应用宽容政策,返回最终 card_no。仅执行一次,不递归。"""
- if text is None:
- return ""
- text = str(text).strip()
- if not text:
- return ""
- # 1. 末尾稀有度后缀 / 单符号剥离(允许前导空格)
- for suffix in RARITY_SUFFIXES:
- if text.endswith(" " + suffix):
- return text[: -(len(suffix) + 1)].rstrip()
- if text.endswith(suffix):
- return text[: -len(suffix)].rstrip()
- for ch in TAIL_CHARS:
- if text.endswith(" " + ch):
- return text[:-2].rstrip()
- if text.endswith(ch):
- return text[:-1].rstrip()
- # 2. 末尾 '-' 补 'P'(如 001/SM- → 001/SM-P)
- if text.endswith("-"):
- return text + "P"
- # 3. 纯数字固定位置 1/7 → /
- if text.isdigit() and len(text) in DIGIT_SLASH_INDEX:
- idx = DIGIT_SLASH_INDEX[len(text)]
- if text[idx] in "17":
- return text[:idx] + "/" + text[idx + 1 :]
- # 4. 纯数字或数字+/ 中间空格去除
- if RE_DIGIT_SPACE_SLASH.fullmatch(text) and " " in text:
- return text.replace(" ", "")
- # 5. 纯数字中间点号 → /
- if RE_DIGIT_DOT.fullmatch(text):
- return text.replace(".", "/")
- # 6. No./mo./Na./ma. 等 → No.数字
- m = RE_NO_MO.fullmatch(text)
- if m:
- return f"No.{m.group(1)}"
- return text
- def is_valid_card_no_format(text: str) -> bool:
- """是否命中合法 card_no 格式(指南老格式 + 四种补全形态)。"""
- if text is None:
- return False
- t = str(text).strip()
- if not t:
- return False
- if t in CARD_NO_BLACKLIST:
- return False
- if RE_VALID_SLASH.fullmatch(t):
- return True
- if RE_VALID_NO.fullmatch(t):
- return True
- if RE_VALID_LONG.fullmatch(t) and re.search(r"\d", t) and re.search(r"[A-Z]", t):
- return True
- if RE_VALID_SHORT_NUM.fullmatch(t):
- return True
- if RE_VALID_HASH.fullmatch(t):
- return True
- return False
- def finalize_card_no(rec_text: str, rec_score: float, score_threshold: float = 0.9) -> dict:
- """宽容 + 低分不合规置空。
- 返回:
- card_no, leniency_applied, empty_reason
- empty_reason:
- None — 有有效编号
- rec_empty — rec 原文为空
- blur_unreadable — rec_score≤阈值 且宽容后仍不合规(图模糊)
- 注意:无检测框的 no_detection 由上层在缺检测记录时写入,不在此函数判定。
- """
- rec_text = (rec_text or "").strip()
- if not rec_text:
- return {
- "card_no": "",
- "leniency_applied": False,
- "empty_reason": EMPTY_REC_EMPTY,
- }
- final = apply_leniency(rec_text)
- applied = final != rec_text
- score = float(rec_score or 0.0)
- # rec_score > 0.9:人眼已确认基本正确,保留(即使偶发形态偏怪)
- if score > score_threshold:
- return {
- "card_no": final,
- "leniency_applied": applied,
- "empty_reason": None,
- }
- # rec_score ≤ 0.9:必须命中合法格式,否则视为模糊不可读 → 置空
- if is_valid_card_no_format(final):
- return {
- "card_no": final,
- "leniency_applied": applied,
- "empty_reason": None,
- }
- return {
- "card_no": "",
- "leniency_applied": applied,
- "empty_reason": EMPTY_BLUR_UNREADABLE,
- }
|