[控场AI]
· 8 分钟阅读· 4,424 字

从0到74%准确率:用图像识别字体的浏览器插件实战复盘

从0到74%准确率:用图像识别字体的浏览器插件实战复盘

开发者用ArcFace微调与海量合成数据,将图片字体识别从0分做到12张测试图top-5全对。

一位运营字体网站的开发者尝试从像素直接识别字体,历经一整个夏天完成了从惨败到可用的迭代。V1方案沿用ImageNet预训练特征加最近邻索引,自测93%准确率却在真实照片上top-5命中率为零,暴露了训练与测试数据分布一致导致的"虚假高分"问题。V2转用ArcFace度量学习头对骨干网络做字体身份微调,配合家族级标签、随机词表渲染和丰富的真实场景数据增强,最终在未见过的文字上取得74% top-1、86% top-5,26.2万款字体索引无需重训即可扩展。项目复盘了豆腐块误匹配、深色背景反相、过度裁剪虚高分数等大量工程踩坑,在12张图的横向对比中top-5满分,诚实指出手写体仍是当前弱点。

传统的字体识别工具(如 WhatFont)本质上是读取网页 CSS,一旦文字变成 JPEG、logo、海报照片或截图,它们就彻底无能为力。一位运营字体网站的开发者决定换一条路:直接从像素识别字体。他有一个包含几十万款字体的数据库,一块 16GB 的消费级 GPU,原本以为是周末项目,结果做了整整一个夏天。这篇复盘记录了真正奏效的方法、踩过的坑,以及最后冷冰冰的测试数据。

字体识别浏览器插件项目

V1:预训练特征 + 最近邻索引,自测93%却全军覆没

第一版用的是最直觉的方案:把目录里每款字体渲染成几张样本图,过一遍 ImageNet 预训练的 EfficientNet-B2,存下 embedding;查询时把用户图片也 embed 一下,返回最近邻结果。

自测结果 top-1 准确率高达 93%,开发者兴奋了整整一周。

直到他用 137 张已知字体的真实照片(招牌、书封、包装)搭建基准测试集重新跑了一遍——top-5 准确率是 0/137。不是很低,而是零。一张手工裁剪、二值化、干净得完美的 Montserrat 照片,返回的全是垃圾。

问题出在自测本身:它用图库自己的渲染图当查询输入,相同的文字、相同的渲染风格,衡量的其实是"近似重复检索",而不是"字体身份"。ImageNet 骨干网络对"这是什么词""这是什么纹理"的编码能力,远远强于"这些字形长什么样"。这里的教训很直白:测试字体识别器,必须用模型从未见过的文字、以及你没有亲手渲染的照片。

这里暴露的问题在机器学习中称为数据泄漏(data leakage)或分布外泛化失败(out-of-distribution failure)。当训练集和测试集共享了某种"捷径特征"时,模型会学会利用这条捷径而非真正的目标特征。在 V1 的自测中,查询图片与索引图片均来自同一套渲染流程、相同的文字内容,模型实际上在识别"这段文字的渲染外观是否相似",而非"这两张图的字体是否相同"。EfficientNet 是 Google 在 2019 年提出的一族高效卷积网络,通过复合缩放方法同时调整深度、宽度和分辨率,在 ImageNet 上取得了当时最优的精度/效率比。然而 ImageNet 预训练让骨干网络对"物体类别""材质纹理"高度敏感,对"字形结构"的感知却十分薄弱——这是域外迁移失败的根本原因,而不是网络本身的缺陷。

V2:教会模型什么才是"字体"

修正方案是用 ArcFace 头对骨干网络做字体身份的微调,训练数据是可以无限量生成的合成数据:

  • 约 35,000 个字体家族作为类别,采用家族级标签(同一家族的所有字重归为一类),因为 Montserrat Bold 的照片绝不能和 Montserrat Regular 被判成不同字体;
  • 从 6000 词的词表随机抽取真实单词,防止模型记忆样本文字;
  • 沿可变字体轴采样字重,叠加人工加粗与减细;
  • 模拟用户真实上传场景的数据增强:透视变形、不均匀光照、低分辨率、JPEG 压缩痕迹、带纹理背景;
  • 后续又为每个家族增加 12 种"效果"渲染:描边、3D 挤出、发光、纹理填充、浮雕、像素化、遮挡、宽字距、多行排版。

每一轮都从上一个检查点热启动。一个 3900 款字体的小规模试验耗时 23 分钟,就把未见过文字的 top-1 从 1% 拉到 61%。完整训练在家用 GPU 上跑了约 5 小时,最终落在未见文字约 74% top-1、86% top-5。

开发者最在意的指标是对训练中从未出现过字体的泛化能力:约 59% top-1,相比见过的字体 65%,只差 6 个点。这意味着新增一款字体只需"渲染 12 张图、embed、追加索引",完全不用重训。目前索引收录了 26.2 万款字体、约 250 万条渲染 embedding,而且它只是 CPU 上一个普通张量,不是什么向量数据库。打分逻辑是 max(家族原型, 最佳单张渲染),8 线程 CPU 下中位查询延迟约半秒——GPU 要和其他任务共享,所以推理干脆放在 CPU 上跑。

ArcFace 是一种用于人脸识别的度量学习损失函数,由 Deng et al. 于 2019 年提出。它在 softmax 损失的基础上,在角度空间中加入固定的间隔惩罚(additive angular margin),迫使同类别的 embedding 在超球面上聚得更紧、不同类别之间的边界更清晰。这一特性使它特别适合"开放集"识别问题——即测试时会遇到训练中从未见过的类别。字体识别正是典型的开放集场景:世界上有数十万款字体,不可能全部出现在训练集里,模型需要学会"字体通用特征"而非死记硬背类别标签。将 ArcFace 头接在 EfficientNet 等骨干网络上,等于把骨干的视觉编码能力和 ArcFace 的类间分离能力结合起来,让输出的 embedding 在语义上更贴近"字体身份",而非 ImageNet 骨干默认编码的物体类别或纹理信息。

那些尴尬翻车的瞬间

豆腐块(Tofu)。 17 万款字体的图库里,有些字体根本没有拉丁字形(高棉文、泰米尔文、纯阿拉伯文家族)。用拉丁样本词渲染时,它们产出一排排一模一样的 .notdef 方框。这些方框成了磁铁:任何方块状的东西都能以 99% 匹配上。最后开发者扫描了所有图库条目,把渲染结果由近乎相同的连通块组成的 453 款字体直接屏蔽。背后的教训是:去看你的图库图像本身,别只盯着指标。

字重盲区。 在引入家族级标签之前,一张加粗字体的照片,如果对应字体只有常规字重的类别,就根本匹配不上。事后看非常明显。

自作聪明的智能裁剪。 开发者写了个自动文字区域裁剪器。在一组标注数据上,它改善了 0/18 的查询结果,却把分数推到 95-98 的区间,还让延迟翻倍。紧凑的裁剪会让一切看起来都信心十足。它最终以默认关闭的状态发布,而"分数变高了"从此不再被当作任何结论的证据。

用 12 张样本做校准。 开发者用十几张手挑图片设置了质量门槛。放到 1152 条真实查询上,它拒绝了 27% 的合法上传。现在阈值一律在完整日志上校准,别无他选。

深色背景。 深色海报上的白字,和白底黑字的 embedding 完全不同。当图片平均亮度低于 110 时就反相处理,这是十行代码的改动,在 137 张基准图上带来了 +2 个正确结果——整个项目里最划算的一笔。

用市场模板图做训练。 开发者尝试把字体市场的设计师预览图加进训练数据,结果模型学会的是每位设计师的排版模板而不是字形本身,于是模板匹配模板。预览图是搜索数据,不是训练数据。

诚实的置信度

余弦相似度不等于置信度。一张完全没有文字的剪贴画也能得 96% 分。所以你看到的排序来自跨图像变体(完整图像加 OCR 行裁剪)的投票聚合,而非原始分数;当多个候选挤在一起时,UI 会显示"不确定,以下是最接近的近似字体"状态,而不是伪造一个 99%。

目前它过于谨慎。在下面的对比中,它在 12 张图里把正确字体放在第 1 位有 9 张,却仍对其中 7 张说"不确定"。这个校准是下一步要做的工作,而且照例要在真实查询日志上完成。

余弦相似度衡量的是两个向量方向的接近程度,取值范围 −1 到 1,与向量的绝对长度无关。在 embedding 空间中,它常被用作检索的相关性分数。但余弦相似度本质上是一个相对排序指标,并不具备概率意义:即使查询图片与数据库中所有字体都毫不相关,最近邻依然会返回一个接近 1 的高分——因为向量空间总存在"最近"的那个点。这就是"一张没有文字的剪贴画也能得 96% 分"的根本原因。将原始相似度直接展示为"置信度"会严重误导用户,需要额外的校准(calibration)步骤,例如通过 Platt scaling 或保序回归,将分数映射为有意义的概率估计,或者如本文所做的,用多图投票聚合来过滤低可信度的单次高分匹配。

12 张图的横向对比

开发者按真实使用场景渲染了 12 款已知的 Google Fonts:干净的、带纹理的、旋转的、透视变形的、模糊的、低对比度的、小尺寸 JPEG、深色背景。然后把这 12 个文件上传到所有能通过浏览器访问的免费识别器。评分标准是真实家族在 top-5 中的排名。

识别器Top-1Top-5备注
FontBoxDL(本项目)9 / 1212 / 12
开源识别模型(ResNet18,约1k家族)7 / 129 / 12对照片识别强,但丢了干净衬线体和马克笔体
WhatFontIs6 / 1210 / 12OCR 字母步骤;在小尺寸深色 JPEG 上崩溃
Font Squirrel Matcherator5 / 65 / 6约 15 次上传后被封,仅测了 6 张
WhatTheFont0 / 120 / 12仅收录商业字体,结构上无法命名免费字体;其付费近似结果合理

这是作者自己的测试、用自己的图片,所以应读作"这套方法行得通",而不是一张排行榜。诚实的弱点:手写体 / 脚本体。Lobster 在带纹理、旋转的背景上只排到第 4,前面压着三个近似字体。

浏览器插件本身

插件反倒是最薄的一层。基于 Manifest V3,不申请 host 权限。右键某张图片,选择"识别图中字体",插件只把图片 URL 发给服务器,服务器自行抓取图片(带常规的 SSRF 防护)并返回匹配结果。在你点击之前,页面上什么都不会被读取。因为用户反复要求,它还长出了一个悬停检查器,报告某个元素实际渲染用的字体(取自浏览器的 font-face 集合,而非 CSS 栈里的第一个名字),以及一个"本页面字体"列表。

移植到 Firefox 主要是清单文件的工作:用 background.scripts 替代 service worker、加一个 browser_specific_settings.gecko 块,以及 AMO 现在要求的数据采集声明。

对开发者而言,识别失败的案例才是现阶段唯一有价值的训练数据——手写字、霓虹灯、镀铬效果、极小截图,这些难啃的骨头才是模型下一步进化的方向。

#字体识别#图像识别字体#浏览器插件#ArcFace#字体识别器#EfficientNet#合成数据训练#font identifier
分享:

相关推荐