☰
深入 Xime 手写叠写识别:过分割 + 动态规划,如何在连笔中切出多个字
2026/10/14 18:00:38 网站建设 项目流程

【免费下载链接】Xime

我的自用安卓输入法,基于Rime 构建,部分功能由本人设计的本地AI模型驱动,支持五笔/拼音/自定义方案。

项目地址:https://gitcode.com/gh_mirrors/xi/Xime
点击查看免费下载

Xime 是一款基于 Rime 构建的开源安卓输入法,部分功能由作者自研的本地 AI 模型驱动。它的手写叠写识别模块有一个很有代表性的难题:用户连笔写"你好"时,一整串笔画没有天然的字边界,输入法必须自己决定"从哪几笔切成一个字"。本文深入拆解 Xime 用**过分割 + 动态规划(DP)**解决连笔切字的完整思路,以及它如何防止把"张"错切成"弓 + 长"。

一、从"单字"到"连写":一个绕不开的陷阱

Xime 的手写引擎底层是单字识别模型:给它一段笔画,它返回最像的汉字。这在逐字书写时工作良好——但在手写输入场景中,用户往往一口气连写两三个字,笔画流是一整条"长龙"。

真正棘手的是一个经典陷阱:部件成字。比如"张"= 弓 + 长,而"弓"和"长"本身都是合法汉字。当你写到第 3 笔时,"弓"恰好成段,模型会非常自信地输出"弓";等你写完剩下 4 笔,正确结果应该是"张",算法却可能已经固化为"弓 + 长"两个字。

Xime 的答案不是让模型"更聪明",而是在模型之上加一层切分决策引擎:核心类 OverlappedHandwritingRecognizer.kt 只有 230 行,却包含了整套算法的灵魂。

二、过分割 + 动态规划:把切分变成搜索问题

过分割:枚举"所有可能的切法"

传统思路是"先检测边界、再逐段识别",但手写笔画里边界信号很弱。Xime 反其道而行——不过早决策,先把所有可能的切分段都试一遍:

  • 笔画序列有 n 笔,任何一个位置都可以是切分点;
  • 每两个相邻切分点之间就是一个候选段,段内笔画数不超过 20(DEFAULT_MAX_STROKES_PER_SEGMENT);
  • 每个候选段都送单字 ONNX 模型识别一次,取 Top 1 分数作为该段的"识别分"。

这一步是"过分割":它宁可多算,把问题从"猜边界"转化为"在所有切法中挑最优"。

动态规划:在指数级切法里找全局最优

n 笔画的切法数量随笔画数指数增长,逐个枚举不可行。DP 把它压到多项式复杂度,核心是一张二维表best[k][i](见 recognize() 主循环):

best[k][i] = 前 i 笔切成 k 段时的最大识别分总和

转移时,枚举第 k 段的起点 j,用best[k-1][j] + 该段 Top1 分 + 时间偏置更新表格;同时记录back[k][i] = j作为回溯指针。填完表后从back[bestK][n]一路回溯,就还原出全局最优的切分序列。

复杂度上有个关键护栏:同时最多叠写 5 个字(DEFAULT_MAX_SEGMENTS = 5),所以 DP 表只有 5 层,每追加一笔的增量推理被段缓存压到个位数次 ONNX 调用,后台线程即可跑完。

三、两把"筛子":决定哪些切法能胜出

单纯比"分段分数总和"会踩两个坑,Xime 用两个机制分别堵上。

段均分 + 切分惩罚:防止"多一刀白赚分数"

两段低分字的总和可能虚高过单段高分字(0.9 + 0.6 > 0.95),所以段间比较不用总和,而是用(比较逻辑):

归一化分 = 总分 ÷ 段数 − 0.02 × (段数 − 1)

均分让"平均每个字像不像字"成为比较基准;每多切一刀再扣 0.02 的惩罚(SEGMENT_PENALTY),让"合并"在打平时占优——这正是部件成字能自愈的原因:写完"张"后,合并段 0.95/1 高于[弓][卜]的 (0.9+0.6)/2 − 0.02,DP 自动切回单段,已上屏内容随之替换纠正。

时间偏置:停顿是最强的分字信号

🕐 笔间时间间隔是手写数据里几乎免费的强特征。gaps[j]记录第 j 笔起笔与上一笔收笔的间隔,DP 转移时叠加偏置(gapBias):

笔间间隔判定偏置
≥ 500ms明显停顿,倾向在此切分+0.15
≤ 150ms连笔,压制切分−0.08
其余无信号0

于是 2 笔笔画、间隔 600ms 会被切成两段,间隔 80ms 则保持单段——这个"停顿分裂 / 连笔不分裂"的行为被单元测试逐一钉死(见 测试用例)。

四、"一个字"先验:为什么"在"不会被拆成 [一][丿][丨]

DP 再强也敌不过一个现实:汉字中途的任意几笔,往往都能被识别成某个简单字。连笔写"在"到第 2 笔时,[一][一]的均分(0.88)甚至能打过合并段。如果照单全收,输入框会先蹦出两个"一"再变回"在",体验崩坏。

Xime 加了一道换字停顿先验(hasPauseBoundary),用相对阈值而非绝对阈值:

  • 窗口内笔间间隔的中位数 ×2(下限 250ms)才构成"显著停顿";
  • 有显著停顿 → 信任 DP 的多段切分(真换字了);
  • 无显著停顿 → 回退把整个窗口当一个字识别(合并段),只要合并段分数 ≥ 0.35(MERGED_CHAR_MIN_SCORE),就强制按单字处理,输入框始终只显示一个字。

相对阈值的设计很巧妙:慢写者字内间隔本身偏高,绝对阈值会把匀速书写的复杂字(如 17 笔的"赢")拆碎;快写者字间间隔虽小,但相对中位数依然突出,照样能被检出。两个方向的误切都被这条先验挡住,对应测试用例慢写复杂字匀速笔迹不拆分和连笔单字中途不被拆分。

五、工程侧:缓存、滑窗与固化

算法要丝滑,靠的是三层工程配合:

  • 段缓存segCache:笔画只允许尾部追加(前缀不变),所以[j,i)段的识别结果跨笔画复用;每追加一笔,真正新增的 ONNX 推理只有含新笔画的尾部少数段。窗口头部裁剪时,onStrokesTrimmed把缓存 key 整体平移,被裁笔画之外的段零重推理。
  • 滑窗固化:UI 层(scheduleRecognition)维护一个识别窗口,超过 25 笔(HW_RECOGNIZE_WINDOW_LIMIT)就把 DP 选出的最早一段固化上屏,循环至不超限——正常分字仍交给停顿,窗口只是兜底。
  • 推理隔离:ONNX 模型(ochwpro.onnx)与笔画预处理全部跑在独立的:inference进程里(HandwritingInference.kt),键盘主进程只通过 IPC 传原始笔画、收回候选(HandwritingEngine.kt),进程被系统回收后还能按需自愈重载。

视觉细节也服务于这个机制:完成字的笔画只是淡出而非消失,因为识别窗口全程保留——单字中途写完部件后的停顿(如"中"写完"口")不会清窗,后续笔画落下时全窗口重识别,自动把"口"纠正为"中"。

六、小结与延伸阅读

Xime 的叠写识别没有追求花哨的端到端模型,而是把工程问题拆干净:过分割负责"不遗漏",动态规划负责"选最优",段均分 + 惩罚负责"防虚高",时间偏置 + 单字先验负责"防误切",段缓存负责"不卡顿"。每层都小到可以单元测试,15 个用例覆盖了从"两字连写切两段"到"慢写复杂字不拆分"的关键路径。

想动手验证,从这几个入口读起:

模块路径
叠写识别核心(过分割 + DP)OverlappedHandwritingRecognizer.kt
单字模型 IPC 客户端HandwritingEngine.kt
推理进程管线HandwritingInference.kt
手写键盘与固化调度HandwritingKeyboardLayout.kt
算法单测(15 个用例)OverlappedHandwritingRecognizerTest.kt

如果对手写输入中的切分与识别感兴趣,这套"先过分割、再全局打分"的框架也很值得借鉴到语音、OCR 等一切"连续流切单元"的问题上。

【免费下载链接】Xime

我的自用安卓输入法,基于Rime 构建,部分功能由本人设计的本地AI模型驱动,支持五笔/拼音/自定义方案。

项目地址:https://gitcode.com/gh_mirrors/xi/Xime
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询