【免费下载链接】Xime
我的自用安卓输入法,基于Rime 构建,部分功能由本人设计的本地AI模型驱动,支持五笔/拼音/自定义方案。
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模型驱动,支持五笔/拼音/自定义方案。
相关推荐
moonlight-vplus 串流手写笔与本地输入法手写识别隔离:StreamView 动态输入连接设计与 API 33+ 自动手写关闭方案
moonlight vplus 串流手写笔与本地输入法手写识别隔离:StreamView 动态输入连接设计与 API 33+ 自动手写关闭方案 导读 本文基于
移动开发音视频如何用ML Kit实现终极移动手写识别:从入门到精通的完整指南
如何用ML Kit实现终极移动手写识别:从入门到精通的完整指南 ML Kit数字墨水识别是Google提供的强大移动AI工具,能够在Android和iOS设备上
示例工程移动开发Eidos手写识别:笔迹数据数字化
Eidos手写识别:笔迹数据数字化 在数字化时代,手写内容的价值依然不可替代。无论是会议笔记、创意草图还是个人日记,手写记录承载着独特的个人风格和思维过程。Ei
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考