PAYATHON 2026

如何处理国际象棋 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 记录时:

  1. 分别枚举每个候选棋盘上的合法走法,生成对应的 SAN。
  2. 计算 SAN 与 OCR 文本的匹配分数,保留符合候选条件的走法。
  3. 为每个分支复制棋盘,在副本上落子。
  4. 按累计分数排序,保留有限数量的候选路径。
  5. 继续读取后续记录,让后面的匹配情况影响前面歧义走法的选择。

可以用束搜索(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

当前棋盘没有合法走法时,返回原始文本也不能让它成为合法走法。应检查对局是否已结束,或候选路径是否出现了问题,并停止该分支的落子处理。

输出时,保留原始文本、候选修正和待复核位置。最高分路径与次高分路径接近、候选全部消失,或者连续多步都需要修正时,应回看相关图片。束宽过小或相似度阈值过高,都可能提前剪掉真实路径;扩大搜索范围可以减少这类遗漏,但无法保证消除歧义。

备注:内容仅供参考。