如何处理国际象棋 OCR 中合法但识别错误的走法,避免棋盘状态错乱?
仅凭走法在当前局面中是否合法,无法判断 OCR 是否读对了棋谱。为减少错误传播,需要同时保留多个候选走法及其棋盘状态,结合后续记录比较整条路径,再决定是否接受。
如果 h4 和 d4 都合法,仅凭当前 OCR 输出 h4 和当前棋盘,无法确定棋手实际写了哪一步。即使结合后续记录,也可能有多条合法路径都能解释棋谱,这时需要重新识别原图或人工确认。
为什么当前代码会接受错误走法?
这行代码从合法走法中选出与 OCR 文本最相似的一步:
best_move = max(legal_moves, key=lambda m: jaro_winkler(m, ocr_move))
它无法确定哪个走法符合原始棋谱。当 OCR 输出为 h4,合法走法中也包含 h4 时,两个字符串完全相同,得到满分是正常的。这个分数衡量的是字符串相似程度,不能当作识别正确的概率。
随后调用 board.push_san(...),程序只保留这一步对应的局面。后续校验都基于这个局面,一次错误可能影响整份棋谱。
Jaro-Winkler 与 Levenshtein distance 是两种不同的字符串比较方法。更换算法可能改变候选排序,但无法消除两个走法都合法时的歧义。
将单步匹配改成多路径搜索
每个候选需要保存当前棋盘、已选择的走法序列和累计匹配分数。
处理下一条 OCR 记录时:
- 分别枚举每个候选棋盘上的合法走法,生成对应的 SAN。
- 计算 SAN 与 OCR 文本的匹配分数,保留符合候选条件的走法。
- 为每个分支复制棋盘,在副本上落子。
- 按累计分数排序,保留有限数量的候选路径。
- 继续读取后续记录,让后面的匹配情况影响前面歧义走法的选择。
可以用束搜索(beam search)实现这一过程。即使 h4 当前得分最高,d4 也有机会作为另一条路径继续参与比较。
如果每一步都允许把 OCR 文本任意改成任何合法走法,错误路径也可能一直延续。因此,仍需要限制候选走法,并在必要时重新识别或人工复核。
一个可用于改造现有流程的示例
下面的函数接收按实际落子顺序排列的 OCR 文本,例如白方第一步、黑方第一步、白方第二步。similarity 由调用方传入,约定返回 [0, 1] 范围内的相似度,数值越大越相似。
示例中的束宽和相似度阈值仅供演示,需要用实际棋谱样本调整。
import chess
def decode_moves(
raw_moves,
similarity,
beam_width=32,
min_similarity=0.6,
):
if beam_width < 1:
raise ValueError("beam_width must be positive")
# Each entry contains: cumulative score, board, SAN path.
beam = [(0.0, chess.Board(), [])]
processed = 0
def make_result(complete, failed_ply=None):
return {
"complete": complete,
"processed_plies": processed,
"failed_ply": failed_ply,
"candidates": [
{"score": score, "san": path}
for score, _, path in beam
],
}
for ply, raw in enumerate(raw_moves, start=1):
if not isinstance(raw, str) or not raw.strip():
raise ValueError(
f"Missing OCR text at ply {ply}; "
"restore the missing record before decoding"
)
token = raw.strip()
next_beam = []
for total_score, board, path in beam:
for move in board.legal_moves:
san = board.san(move)
score = float(similarity(san, token))
if not 0.0 <= score <= 1.0:
raise ValueError(
"similarity must return a value in [0, 1]"
)
if score < min_similarity:
continue
next_board = board.copy()
next_board.push(move)
next_beam.append((
total_score + score,
next_board,
path + [san],
))
if not next_beam:
return make_result(complete=False, failed_ply=ply)
next_beam.sort(key=lambda entry: entry[0], reverse=True)
beam = next_beam[:beam_width]
processed = ply
return make_result(complete=True)
如果现有的 jaro_winkler 函数符合上述返回值约定,可以这样调用:
result = decode_moves(
raw_moves,
similarity=jaro_winkler,
beam_width=32,
min_similarity=0.6,
)
代码始终使用候选棋盘生成 SAN,并通过 chess.Move 对象落子,保证每个分支上的走法与该分支的局面对应。
返回的 complete=True 只表示候选搜索覆盖了全部输入,不能说明 OCR 已经识别正确。score 是累计相似度,不是置信概率。同一批完整路径长度相同,可以用这个分数做初步排序。
如何让后续走法帮助判断前面的错误?
假设当前保留了 h4 和 d4 两个分支。处理后面的 Kg3 时,应分别检查两个局面的候选走法。
如果 Kg3 只在某个分支中合法,这条记录就为该分支提供了支持。但在另一个分支里,搜索仍可能把 Kg3 修正成别的合法走法,因此原样解析失败并不一定意味着整条路径会立即被淘汰。
如何判定取决于候选策略:
- 如果记录清晰,且只允许原样匹配,原样走法非法的分支就无法继续。
- 如果允许 OCR 修正,分支能否继续取决于替代走法是否满足候选条件,以及累计修正代价。
- 如果几条路径都能较好地解释后续记录,就应保留歧义,结合原图复核。
上面的代码采用第二种方式,可以比较多条路径,但不能证明哪条一定是真实对局。
相似度之外,还可以加入哪些证据?
如果 OCR 系统能提供多个识别候选或字符置信度,可以把这些信息纳入评分。例如,原图对 d 和 h 的支持程度,比两个 SAN 字符串是否接近更直接。
也可以根据实际样本建立字符混淆代价。只有在样本中观察到某些字符容易混淆时,才为对应替换设置较低代价。不要预先假定所有棋谱的笔迹、语言和识别错误都相同。
若使用概率评分,可以将 OCR 观测概率转换为负对数代价后累加,前提是这些值具有可解释的概率含义。直接把 Jaro-Winkler 分数当作概率,会使最终的“置信度”缺少依据。
不要把棋力评价作为强制纠错依据。真实对局可以包含弱着和失误,棋谱识别应优先还原记录。
接入现有 JSON 流程时要注意什么?
接入时,先将 white、black 按落子顺序展开,并保存对应的 move_no 和原字段,方便把候选结果写回原结构。某一格为空时,不要直接跳过该半回合:中间漏步会改变后续行棋方和局面,应先定位缺失记录。最后一行没有黑方走法,需要结合记录是否结束来处理。
比赛结果与走法要分开解析。原代码只检查白方字段中的 "1/2",覆盖范围较窄。可以根据输入格式识别 1-0、0-1、1/2-1/2、* 等结果标记,并结合字段位置确认记录结束。"1/2" 是否代表结果缩写,需要由你的 OCR 输入约定决定。
还应修改这段逻辑:
if not legal_moves:
return ocr_move
当前棋盘没有合法走法时,返回原始文本也不能让它成为合法走法。应检查对局是否已结束,或候选路径是否出现了问题,并停止该分支的落子处理。
输出时,保留原始文本、候选修正和待复核位置。最高分路径与次高分路径接近、候选全部消失,或者连续多步都需要修正时,应回看相关图片。束宽过小或相似度阈值过高,都可能提前剪掉真实路径;扩大搜索范围可以减少这类遗漏,但无法保证消除歧义。
备注:内容仅供参考。
