学习个人笔迹风格,生成带真实书写时序的笔画轨迹,导出 GRBL G 代码驱动写字机器人。
与现有写字软件的本质区别:传统工具只是描图片轮廓,没有笔顺、没有时序,写出来僵硬如机器描图。
本项目处理的是联机笔迹(笔尖坐标 + 笔序 + 时序 + 抬落笔 + 压感),复刻的是人的书写动作。
手写样本 ──▶ 风格模型 ──▶ 运动约束优化 ──▶ G 代码 ──▶ 写字机器人
(Python) (C++ core) (C++ core)
不只是写字:表格、思维导图、导入的 SVG 在管线里都只是 StrokeSet,
和「一个汉字」走同一条版面 → G 代码 → 机器链路。
handwriting compose table --rows 3 --cols 4 -o table.nc
handwriting compose mindmap --nodes "a:0.2,0.5,0.24,0.16;b:0.7,0.3,0.2,0.14" --edges "a,b"
handwriting compose svg --file drawing.svg --svg preview.svgPre-Alpha。 采集、字形 → G 代码、风格模型 v0、整行风格版面层、骨架对齐、
序列模型骨架均已跑通;真实个人风格训练待数位板行数据。
| 模块 | 状态 |
|---|---|
| 笔迹采集器(网页 / App 原生) | 可用 |
| 风格特征提取 | 可用 |
| 主动选字(覆盖度驱动) | 可用 |
| 字形骨架 → 轨迹 → G 代码 | 可用 |
| 横条本版面(字压横线) | 可用 |
| Qt 桌面预览工具 | 可用 |
| SVG 预览 | 可用 |
| 任意图形(表格 / 思维导图 / SVG 导入) | 可用 |
| 轨迹数据结构(C++ / Python) | 可用 |
| 数据集读取与质检 | 可用 |
| Qt 桌面 UI | 可用(常用面板 + 选项弹窗 + 横条本富文本编辑 + 实时预览 + 导出 + 串口直连书写 + 预览内点字编辑) |
| 项目保存/加载 + 设置持久化 | 可用(.hrproj、最近打开、未保存提示、QSettings) |
| 长文分页(按页预览 / 单页或全部页导出) | 可用 |
| 串口 / GRBL 通信(C++ 核心) | 可用(流控、状态机、安全检查,118 项断言覆盖) |
| 工作区标定(软限位) | 可用(4 步向导 + 示意图 + 越界保护) |
| 笔迹风格模型 v0(训练 PyTorch / 推理 C++,App 已接入) | 可用 |
| 整行风格 · 版面层(字大小 / 字距 / 基线) | 可用 |
| 骨架对齐(1.0 / 2.0 → 标准骨架) | 可用 |
| 序列模型骨架(GRU + MDN,Δ点/抬落笔/压感) | 可用(骨架) |
| 开发者模式(文章实时推理 + 排版 + 预览内编辑) | 可用 |
| 运动约束优化(论文核心) | 未开始 |
主界面只保留高频项:文本、常用版面、整行风格;低频设置收进「选项」弹窗。
| 选项弹窗 | 横条本文本编辑 |
|---|---|
![]() |
![]() |
「整行风格」开启后,字大小与基线会有统计波动,横条本仍压在横线上:
工具 → 采集与训练… 打开原生窗口。
| 采集 | 训练 |
|---|---|
![]() |
![]() |
- 采集:单字 / 整行,数位板或鼠标书写;「添加当前样本」累积多条,「导出 JSON」生成
1.0 / 2.0 采集文件。整行模式按笔画停顿自动切字(charRanges);
样本列表支持「删除选中 / 清空已采集」,写坏一条不用重录全部。 - 训练:选数据 → 勾选「对齐到标准骨架」→ 开始训练;日志/进度实时显示。
「对齐阈值」过滤匹配代价过高的笔画对;训练完自动export-weights并启用模型。 - 体检数据:训练前先点「体检数据」,日志会列出压感/采样率/缺字等问题;
发现问题时,点「开始训练」会二次确认。 - 训练仍由 Python/PyTorch 完成(ADR-0007),App 通过
QProcess调用;不再需要网页采集器。
- 文件 → 新建 / 打开 / 保存 / 另存为,
Ctrl+S保存;最近打开保留 8 个;关闭前提示未保存。 .hrproj保存文本、版面、整行风格、字形参数、风格模型开关/路径/强度。- 窗口几何、停靠布局、显示选项、最近文件用
QSettings持久化,重开自动恢复。 - 长文按「每行字数 × 每页行数」自动分页;工具栏「上一页 / 下一页」,状态栏显示「第 X / Y 页」。
- 导出 G 代码支持当前页或全部页;全部页自动生成
name-p1.nc、name-p2.nc…
默认输出是标准骨架,不是你本人的笔迹。风格模型 v0 是一个可训练的基线:
把每一笔拆成「中心线(结构)+ 残差(风格)」,用混合密度网络(MDN)
学习残差与笔压的分布,再作用到任意中心线上。
pip install -e ".[train]"
# 没有数位板时:用本地字形骨架 + 合成风格自测
handwriting train --chars "不八啼处字春晓永法眠觉闻鸟" --offline \
-o data/exports/style-model.pt
# 用模型生成(默认取混合均值;加 --sample 可逐点随机)
handwriting stylize "永" --model data/exports/style-model.pt --offline \
-o out.nc --svg out.svg
# 任意图形也能加风格
handwriting stylize --svg-file drawing.svg --model data/exports/style-model.pt --svg out.svgApp 里勾选左栏「使用风格模型」,在「选项 → 风格模型」里选权重 JSON、调强度;
预览和导出的 G 代码会走同一条 C++ 推理。加载失败会自动回退标准骨架,并在状态栏提示。
边界:v0 学的是「相对中心线的局部风格」(倾斜、提按、抖动),
不是完整的字形风格迁移;真实个人风格需要数位板数据。骨架对齐已实现
(train --align),真实数据训练待数位板。设计与限制见 docs/model.md。
语言分工:训练用 Python(PyTorch),推理在产品运行时用 C++
(core/src/style_model.cpp,零 Python 依赖)。训练完导出权重:
handwriting export-weights --model data/exports/style-model.pt \
-o data/exports/style-model.json跨语言对拍工具 hr-style-dump,当前最大误差 1.1e-07。规则见
ADR-0007。
开发者模式:粘贴文章,右侧实时推理 + 排版,并可直接点字编辑。
python scripts/serve_dev.py --model data/exports/style-model.pt \
--weights data/exports/style-model.json
# 打开 http://127.0.0.1:8765默认走 C++ hr-style-batch;找不到二进制时回退 Python 参考实现。
cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="C:/Qt/6.9.3/mingw_64"
cmake --build build -j
./build/app/handwriting-studio.exe左侧改参数、右侧实时出预览。预览与导出的 G 代码共用同一份版面数据,
所以看到的位置就是机器实际走的位置。
默认版面就是横条本(150×213 mm / 22 行,字压在横线上),
字宽默认等于行距、每行字数自动算 —— 正是作业本去掉页边距后的书写区。
底部「机器控制 · 串口直连」面板提供了与奎享雕刻类似的接口,软件内直接连 GRBL 控制板:
- 插上控制板(Arduino Uno + CNC Shield / ESP32 等跑 GRBL 的板子),系统会枚举出 COM 口
- 下拉选对应的串口,点 连接(默认 115200)
- 连上后自动读
$$拿行程与最大进给,做上机前安全检查 - 先用 回零
$H把笔归零,用 点动 把笔移到纸面左上角,再 设零点G10 L20 P1 - 左边调好文字与版面后,点 开始书写 —— 程序会带行号流控、按 GRBL 的 128 B 串口缓冲限速发送
手动指令(回零 / 点动 / 设零点 / 抬落笔)与书写程序严格分离:载入程序后没点「开始」之前,随手点回零不会把整个程序偷发下去——这是个真实的安全隐患,已有回归测试守着。
机器参数(本机实测):「机器」分组里抬笔 Z / 落笔 Z / 书写进给默认取自奎享雕刻
settings/默认Grbl机器/Grbl.json(抬笔 Z=0、落笔 Z=6、进给 8000)。注意这台机器
Z 越大越往下压,与「Z 向上为正」相反。另外它 Y+ 朝上(纸上向下走是 −Y),
需要勾选「Y 轴翻转」,否则整页会写到纸外。参数按自己机器实测为准。
步进机没有位置反馈,上电后手推机头不会更新 GRBL 坐标——所以通电后必须重新标定「设备能走到哪里」,否则 G 代码发下去机头可能冲到机械端点扯断皮带。
标定流程(底部「工作区标定(软限位)」分组):
- 断电手推机头到最左上角(物理起点,X 最小、Y 向上最大)
- 通电点「开始标定」,按向导走 4 步:
- ① 记录最左上角(若通电前已手推到位,直接点「记录此角」)
- ② 用点动把机头移到最右上角,点「记录此角」
- ③ 用点动把机头移到最右下角,点「记录此角」
- ④ 用点动把机头移到最左下角,点「记录此角」
- 点「保存边界」—— 写入
data/workspace-bounds.json,重启软件后自动加载 - 之后普通点动会被限制在边界内(越界自动贴边);发送 G 代码前会自动对照矩形做安全检查
- 示意图实时显示机头位置与边界线,机头越界立刻变红警告
记录顺序打乱也没关系——内部会自动取 min/max 归一化,所以无论你的坐标系方向如何都能得到正确矩形。
MinGW 构建注意:必须静态链接 libstdc++(
app/CMakeLists.txt已配置)。
Qt 的 mingw_64 包自带一个用较旧 MinGW 编译的libstdc++-6.dll,
运行时会优先加载它,构造std::ifstream这类触及std::locale的对象
就会在该 DLL 内崩溃,而且栈回溯只指向 libstdc++,完全看不出是自己的代码问题。
构建完成后,可执行文件位于:
build/app/handwriting-studio.exe
app/CMakeLists.txt 已在 POST_BUILD 阶段自动把运行所需的 DLL 与插件
复制到该目录,所以整个 build/app/ 文件夹拷到任意没有装 Qt 的 Windows 机器上,
双击 handwriting-studio.exe 就能跑,不会再报“找不到 xxx.dll”。
自动跟随 exe 一起部署的文件(来自对 PE 导入表的完整闭包分析):
| 类别 | 文件 |
|---|---|
| Qt 运行时 | Qt6Core.dll Qt6Gui.dll Qt6Widgets.dll |
| MinGW 运行时 | libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll |
| 平台插件 | platforms/qwindows.dll(普通 GUI 必选)、platforms/qoffscreen.dll(无头快照/演示必选) |
| 图像插件 | imageformats/qjpeg.dll qico.dll qgif.dll qsvg.dll(导出 PNG 等需要) |
分发要点:务必把
build/app/整个目录一起拷走,不要只拷handwriting-studio.exe——
上面这些 DLL 和platforms/、imageformats/子目录必须跟 exe 待在同一目录。
连机前驱动:控制板(CH340 / CP2102 等 USB 转串口芯片)首次插上,Windows 可能没枚举出
COM 口。请先安装对应厂家的 VCP 虚拟串口驱动,设备管理器里出现COMx后软件才能连上。
pip install -e ".[dev]"
# 预取字形(需要联网,之后可离线使用)
python -m handwriting.cli preload "春眠不觉晓"
# 按横条本排版(默认)
python -m handwriting.cli render "春眠不觉晓" -o chun.nc --svg chun.svg
# 自由版面,导到空白纸
python -m handwriting.cli render "春眠不觉晓" --mode grid --char-size 20
# 体检采集数据(自动识别单字 1.0 / 整行 2.0),打印质量与风格特征
python -m handwriting.cli inspect capture.json用浏览器打开 capture/index.html,按提示写 150 个汉字,导出 JSON。
零依赖、可离线,手机 / 平板 / 数位板 / 电脑都能用。
采集顺序不是字表原序,而是按「结构 × 复杂度」覆盖度排的。
前 8 个字是人工策展的冷启动集(永西叶好冬花国风),覆盖 8 个不同的结构-复杂度格子;
写完 40 个字,每一类都至少有 2 个样本。这样在同样的字数预算下,
风格覆盖比按使用频率顺序写好得多。
页面顶部实时显示压感是否支持与实际采样率,侧栏有质量检查面板——
采样率不足、压感缺失、频繁出格会在写的当下就提示,不必等采完 150 个字才发现整批报废。
推荐的输入设备(不需要自己造硬件):
| 方案 | 成本 | 采样率 | 压感 |
|---|---|---|---|
| 入门数位板(绘王 / 高漫 6×4 英寸) | 100–200 元 | 133–200 Hz | 有 |
| iPad + Apple Pencil | 已有则 0 | 120–240 Hz | 有 |
| 手机触屏 + 电容笔 | 20–50 元 | 60–120 Hz | 无 |
| 鼠标 | 0 | 60–125 Hz | 无(仅能验证链路) |
两种采集模式(页面左上角切换):
- 单字:一个字一格,坐标归一化到字格 —— 用于笔画骨架 / 单字形态,
按上面的覆盖度顺序写 150 字。 - 整行:按横线连续书写整行(可连笔),坐标归一化到整行方框、
strokeStartT以整行起笔为基准 ——
用于位置飘逸 / 字距 / 行气 / 连笔潦草,这些是单字采不到的。
导出handwriting-capture/2.0;加载时用LineSample.segment()
按「抬笔间隔 + 已知字数」自动分段,无需参考字形。
建议两条腿走:整行写句子(拿行级风格)+ 单字按字表补覆盖(拿笔画结构)。
整行若记录了分段,还能切出单字样本(split_chars()),一份数据两用。
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure只构建核心库与测试(不需要 Qt):
cmake -S . -B build -DHR_BUILD_APP=OFFQt 前端优先使用 Qt6;找不到时自动回退 Qt5.15,便于本地开发机快速构建。
pip install -e ".[dev]"
pytest # 262 个用例,联网用例默认跳过
pytest --run-network -m network # 验证真实字形数据
ruff check src testsHandwritingRobot/
├── core/ C++17 核心库(无 Qt、无 Python 依赖)
│ ├── include/handwriting/
│ ├── src/
│ └── tests/
├── app/ Qt 桌面前端(直接链接 core)
├── src/handwriting/ Python:字形、渲染、风格、数据集、G 代码、CLI
├── capture/index.html 笔迹采集器
├── benchmarks/ 性能基准(C++ 与 Python 同负载对比)
├── scripts/ 开发期工具(生成字表顺序等)
├── docs/
│ ├── architecture.md 系统架构与语言分工
│ ├── glyph-pipeline.md 字形 → 轨迹 → G 代码管线
│ ├── data-format.md 笔迹数据格式规范
│ └── adr/ 架构决策记录
├── data/ 本地采集数据与导出(**不入库**)
└── tests/ Python 测试
用相同合成负载(100 字 × 8 笔 × 60 点)实测:
| 环节 | Python | C++ |
|---|---|---|
| 轨迹归一化 | 65.15 ms | 0.13 ms |
| G 代码生成 | 65.67 ms | 37.42 ms |
| 串口传输 | 130.5 s | 130.5 s |
| 机床实际运行 | ~600 s | ~600 s |
| 计算占端到端 | 0.018% | 0.005% |
换语言把端到端从 730.8 秒降到 730.6 秒。 批处理吞吐不是选 C++ 的理由——
瓶颈是串口带宽与机械运动。真正的理由是交互延迟:拖动参数滑块要实时重算预览,
Python 的 65 ms 是 15 fps,C++ 的 0.13 ms 无感。加上发布版可彻底摆脱 Python 运行时
(安装包 2~3 GB → 几十 MB)。
模型训练仍留在 Python:PyTorch 的自动微分与 CUDA kernel 库只服务 Python,
用 C 重写训练代码等于放弃整个生态。
详见 docs/architecture.md 与 ADR-0003。
代码采用 Apache-2.0。
第三方数据与模型权重单独声明许可,不适用仓库根目录的 LICENSE。
详见 ADR-0004。
data/ 下的用户笔迹数据不会进入版本库——它是生物特征数据,隐私敏感。
- C++:CMake ≥ 3.21,支持 C++17 的编译器(MSVC 2019+ / GCC 11+ / Clang 14+)
- Qt:Qt 6.4+(推荐)或 Qt 5.15
- Python:≥ 3.10
- 本地推理显卡:任意支持 CUDA 的 NVIDIA 显卡。开发机为 RTX 5060 8GB(Blackwell,sm_120),
该架构必须使用 PyTorch ≥ 2.7 + CUDA 12.8,旧版 wheel 没有对应 kernel。
不要使用 TensorFlow 1.x 时代的代码,它在新卡上无法运行。







