Python国际象棋OCR走法匹配错误:Levenshtein相似度缺陷求解
稳健的国际象棋OCR走法修正策略
问题根源分析
当前方案仅依赖Levenshtein文本相似度匹配,完全忽略了国际象棋记谱的语义特征和OCR手写识别的常见错误模式,导致出现合法但不符合实际的匹配结果,且单状态推进一旦出错就无法挽回。
具体修正策略
1. 基于记谱特征的加权相似度计算
国际象棋SAN记谱的不同部分优先级差异极大:棋子类型(B/N/R/Q/K)> 目标位置 > 来源位置(可选)。可以给不同部分设置权重,比如棋子类型匹配占60%权重,位置匹配占40%,避免因长度差异导致错误匹配。
示例修改后的score函数:
def score(self, ocr_move, legal_move): ocr = self.normalize(ocr_move) legal = self.normalize(legal_move) # 拆分记谱元素:棋子类型+位置 def parse_san(s): piece = s[0] if s[0] in 'bnrqk' else '' pos = s[len(piece):] return piece, pos ocr_piece, ocr_pos = parse_san(ocr) legal_piece, legal_pos = parse_san(legal) # 棋子类型匹配权重0.6,位置匹配权重0.4 piece_score = 1.0 if ocr_piece == legal_piece else 0.0 pos_dist = Levenshtein.distance(ocr_pos, legal_pos) pos_max_len = max(len(ocr_pos), len(legal_pos), 1) pos_score = 1 - pos_dist / pos_max_len return 0.6 * piece_score + 0.4 * pos_score
这个修改能直接解决示例2的问题:Bf4(棋子类型b)和b4(棋子类型空)的棋子类型得分是0,最终总得分会远低于合法的Bf4(如果存在)。
2. 引入手写OCR错误映射表
统计手写国际象棋记谱中高频认错的字符对,比如:
- 字母形近:d↔f, b↔h, n↔u, r↔k
- 大小写混淆:B↔b, N↔n
在计算距离时,把这些配对的字符距离设为0.5而非1,更贴合实际错误场景:
# 预定义OCR常见错误映射 OCR_ERROR_MAP = { 'd': {'f': 0.5}, 'f': {'d': 0.5}, 'b': {'h': 0.5, 'B': 0.0}, 'B': {'b': 0.0}, 'n': {'u': 0.5, 'N': 0.0}, 'N': {'n': 0.0} } def char_distance(c1, c2): if c1 == c2: return 0.0 # 检查错误映射 if c1 in OCR_ERROR_MAP and c2 in OCR_ERROR_MAP[c1]: return OCR_ERROR_MAP[c1][c2] return 1.0 # 修改Levenshtein距离计算为自定义加权距离 def weighted_levenshtein(s1, s2): m, n = len(s1), len(s2) dp = [[0.0]*(n+1) for _ in range(m+1)] for i in range(m+1): dp[i][0] = i for j in range(n+1): dp[0][j] = j for i in range(1, m+1): for j in range(1, n+1): cost = char_distance(s1[i-1], s2[j-1]) dp[i][j] = min( dp[i-1][j] + 1, # 删除 dp[i][j-1] + 1, # 插入 dp[i-1][j-1] + cost # 替换 ) return dp[m][n]
用这个加权距离替代原Levenshtein距离,能让实际手写错误的匹配得分更合理。
3. 多候选棋局回溯机制
放弃单状态推进,维护多个候选棋局状态(比如Top 3得分的走法对应的棋局),每一步OCR结果都在所有候选状态中尝试匹配,当某个候选状态无法找到有效匹配时,就丢弃该状态,保留可行的状态继续推进。
示例简化实现:
class OcrChessEngine: def __init__(self): # 维护多个候选状态:(board_state, move_history, confidence) self.candidates = [(chess.Board(), [], 1.0)] def apply_ocr_move(self, ocr_move, top_n=3): new_candidates = [] for board, history, conf in self.candidates: legal_moves = list(board.legal_moves) # 计算所有合法走法的得分并排序 scored_moves = [] for move in legal_moves: san = board.san(move) s = self.score(ocr_move, san) scored_moves.append((s, move, san, board.copy())) # 取Top N候选 scored_moves.sort(reverse=True, key=lambda x: x[0]) for s, move, san, new_board in scored_moves[:top_n]: new_board.push(move) new_history = history + [(san, s)] new_conf = conf * s new_candidates.append((new_board, new_history, new_conf)) # 按总置信度排序,保留Top N候选 new_candidates.sort(reverse=True, key=lambda x: x[2]) self.candidates = new_candidates[:top_n] # 返回置信度最高的结果 return self.candidates[0][1][-1] if self.candidates else (None, 0)
这个机制能避免一步错误导致全局崩坏,后续走法可以验证前面的候选是否合理。
4. 结合棋局合理性过滤
利用国际象棋的常识或引擎评估过滤不合理的走法:
- 开局阶段:给e4、d4、Nf3等常见开局走法额外加0.1-0.2的得分权重;
- 中局/残局:用python-chess的评估函数(比如
board.evaluate())给更优的走法加权重,过滤掉明显送子、自绝后路的走法。
示例开局权重加成:
OPENING_MOVES = {'e4', 'd4', 'Nf3', 'Nc3', 'f4', 'b4'} def score(self, ocr_move, legal_move): # 原有得分计算 base_score = ... # 开局阶段加成 if len(self.board.move_stack) < 10 and legal_move in OPENING_MOVES: base_score += 0.15 return base_score
这能解决示例1的问题:即使OCR输出f4,如果实际是d4,在开局阶段d4的得分会高于f4,从而被优先选择。
内容的提问来源于stack exchange,提问作者TacerTV




