3 of 432

手机取代了黄铜塞子

V
Vadim Drobinin
2026-04-23
https://drobinin.com

手机取代了黄铜塞子

在爱丁堡郊外一个铁皮隧道里,作者每周三傍晚穿着不舒服的夹克趴在地上打靶。真正让他感到疲惫的并非射击本身,而是那套繁琐的计分仪式——用黄铜塞子一个个填补弹孔、借助测环尺判定环数。作为一名 iOS 工程师,他决定用手机摄像头和计算机视觉来终结这一切。

这篇文章的妙处在于,它表面上讲的是用技术替代机械劳动,实则揭示了一个更深层的命题:当你想用 AI 识别"什么东西不见了"时,你面对的是"负空间"识别这一根本性挑战。弹孔是纸上被移除的部分,而绝大多数视觉模型训练的是识别"存在的事物",不是"缺失的事物"。作者从 Vision 框架的过度检测一路摸索到 OpenCV 的传统图像处理,最终找到了一个结合径向亮度剖面的几何解法。

这是一篇关于"用工程思维解决实际问题"的好文章——它不堆砌术语,而是带你一步步走过从灵感到落地的全部思考过程。


几个月来,我每周三傍晚都待在爱丁堡郊外一个铁皮隧道里,穿着一件看起来很可笑(也同样不舒服)的夹克。我躺在地上数呼吸,然后沿着靶场走下去,在横梁下低头躲避。每隔五米,地面上就用白漆写着 DUCK,横梁上也贴着写着"DUCK"的标语,但我偶尔还是会撞到头——忙着查看计分卡。

如果不走运,一发子弹打在了环线附近,你就需要借助工具了。你走到一托盘 Greggs 香肠卷1旁——我们地处北方,赞助商也是本地品牌——找到一个装着各种尺寸黄铜塞的木盒子,选一个合适的,小心地把它塞进弹孔里(最好只塞一次,以免撕裂),塞进去的位置就是你的得分。

使用测环尺的示例;via Gary Anderson, DCM 所著《测环尺入门》(A Primer on Scoring Gauges)

子弹会把纸向内推,所以即使环线本身没被破坏,只要(塞子的)凸缘超出外环,你就得算更低的分数。

射击本身很有趣。但数环数、撞脑袋、塞塞子这套仪式必须终结。


我去那里的起因是烹饪。

我几十年前开始迷上烹饪,逐渐变得愈发痴迷:从羞涩地尝试复刻我去过的每一家高级餐厅的菜品,到建造自动化熟成柜(automated curing chamber)。不是买现成的麹(koji),而是自己培养霉菌,用真空腔体机(chamber vacuum)给拉面面团补水,用低温循环器(immersion circulator)加热蛋白酶(protease)和蚱蜢来制作鱼露(garum)。

后来我迷上了熟食肉品(charcuterie),这意味着要弄来整只动物尸体自己屠宰。当我决定认真对待烹饪肉类时,我觉得应该学学打猎。我之前从没真正握过枪,虽然英国人对执照情有独钟,也不喜欢枪(我们更喜欢刀),但猎鹿既不需要狩猎执照也不需要步枪执照2。马鹿(Red deer)本质上是一种害兽——它们吃林木的速度超过了树木再生的速度,而且没有天敌——所以猎杀它们几乎没有任何限制。

不过你确实需要一支步枪,而且最好知道怎么用3——于是我就每周去两次靶场,趴在那张垫子上。这和潜行猎鹿的纪律性完全不同:开枪,换靶纸,吃个甜甜圈,重复。半年后,在我尚未射杀一头鹿、离鹿肉牛排(venison steak)还有一段距离的路上,我倒是胖了几磅,而且大部分晚上都在寻找合适尺寸的测环尺(scoring gauge)。

实在厌倦了,我想不如把这个过程自动化。

负空间(negative space)

我是一名 iOS 工程师,所以先从原生 iOS 方案入手:Apple 的 Vision 框架已经存在了一段时间,内置了现成的检测器,可用于物体识别、人物分割、文字识别甚至条形码扫描,但它不断把图像中的随机部分标记为弹孔——从靶心的小圆点到某个环线的碎片。

弹孔是一种负空间(negative space)。物体检测器训练的是"应该存在的东西",所以要用它们来寻找"原来存在但已被移除的东西",并不那么直接。

一张 NSRA 靶纸的特写,每个检测到的特征上都叠有粉色的计算机视觉标记:同心计分环、中心瞄准十字、小型印刷计分数字以及四个方位上的注释方块。靶纸上的实际弹孔很小,且大多未被标记。

将 Vision 的环检测和物体检测应用于一张 NSRA 靶纸。

我又试了几种更明显的方法:灰度化、反转图像、添加和去除噪声,但即使其他一切正常,落在环线上的弹孔还是会变成太小而无法识别的碎片。

更好的方法是将靶纸视为一个几何已知的物体:先找到环线结构,然后再在其中寻找弹孔。我承认这次不打算重新发明轮子了,于是去查了查其他方案。


端口及其他

2012 年的一篇论文

作为计算机视觉问题,射击靶标计分已经足够平淡到有人发表过相关论文。我找到了 Rudzinski 和 Luckner(华沙工业大学)2012 年发表的《Automatic Scoring of Shooting Targets with Tournament Precision》(自动竞赛精度射击靶标计分),该论文声称能检测 99% 的弹孔。

有几个注意事项:该方法针对低分辨率图像进行了优化,但要求使用平坦的 ISSF 靶纸4、相机角度低、标注不与弹孔相似,且主要针对气枪射击设计。气枪铅弹会在纸上留下饼干切割器般整齐的圆孔,而 0.22 口径子弹在 25 码距离上则会留下参差不齐的边缘。

我逐步复现了该论文。如果你不想阅读原文,可以概括为四个步骤:擦除环线,泛洪填充以找到弹孔形状,运行 Prewitt 边缘检测器,使用霍夫变换拟合圆。

牛眼灰度图:一条白色测环线笔直穿过两个弹孔。

从灰度靶纸开始

环线已移除:每个弹孔现在分裂为环线两侧的两个新月形碎片。

  1. 擦除环线:弹孔分裂为两个月牙形。

泛洪填充结果:白色背景上的四个暗色小碎片,每个都低于最小区域阈值。

  1. 泛洪填充

碎片的 Prewitt 边缘:只有轮廓,没有完整的圆形可供霍夫变换拟合。

  1. 检测边缘 4. 使用霍夫变换拟合圆。

Vision 框架没有 Prewitt 边缘检测器,所以我同时引入了 OpenCV,前三个步骤运行良好。但步骤四有一个问题。NSRA 靶纸在四个基本方位标注了环值——9 环的北、东、南、西方向各有一个"9"——而霍夫变换也会将这些数字拟合成圆形。

此外,环线擦除后的月牙形碎片有时太小,无法进行泛洪填充,所以我最终使用了 V 值径向强度剖面——从牛眼中心向外选取一条带状区域,沿该区域采样亮度,寻找白色环线交叉处的峰值。峰值位置就是环线半径。

一个带有同心白色测环的黑色牛眼,上面叠加了一条从中心向右边缘延伸的橙色水平带状区域。

  1. 选取一条带状区域。

同一个牛眼和带状区域;带有橙色边框的白色小圆圈标记了带状区域与白色环线相交的每个点。

  1. 采样亮度。

沿带状区域的亮度条形图:五个高的橙色条(每个对应一次环线交叉)位于平坦基线上;最后一个条颜色较浅。

  1. 绘制峰值。

结合 Vision 的 VNDetectContoursRequest 和周长过滤器,我平均每张靶纸能检测出五个弹孔中的四个——即 80% 的准确率,仍有很长的路要走,而且我们还没有遇到重叠弹孔等边界情况。

也想在你的应用中使用 Vision 和 CoreML 吗?来聊聊 →

引入机器学习

射击者会在靶纸上做各种事情。他们会写名字、加日期、在接近的弹孔周围画圈。大多数靶纸有撕裂的订书钉孔,有时多发子弹在纸上留下的孔洞过于接近,以至于合并成一个泛洪填充区域。

我最初的尝试是为每种情况添加启发式规则,每隔一周带着新的一批靶纸回来,再次调整参数,但这几乎不可扩展也不可持续。

于是我再次回到 Google,找到了一篇 2023 年底发表的论文5。作者声称平均精度达 96.5%,但专注于弹孔检测,并通过边界框类别读出环值。

我无法为我的靶纸手动准备边界框类别,但我已经有了可用的几何方法,这要归功于 Rudzinski 和 Luckner。我所缺少的是可靠的弹孔检测。YOLO 论文很好地解决了弹孔检测问题,但将几何部分留给了一个假设:靶纸是完美对齐的。

一个带有三个白色弹孔的牛眼靶纸,弹孔位于 9 环和 8 环上,上面叠加了一个将图像划分为等尺寸方格的灰色虚线网格。

  1. 图像变为网格:每个面片是一个单元格。

同一张带网格的靶纸。三个网格单元格——各包含一个弹孔中心——被标记为绿色并带有勾号。

  1. 单元格投票:弹孔在里面吗?

绿色的"是"单元格现在在每个弹孔周围画出了粉色虚线边界框。各个边界框略微偏移并相互重叠。

  1. "是"单元格绘制各自的边界框;相邻单元格的边界框略有不同并相互重叠。

干净的结果:每个弹孔一个实心绿色边界框,没有网格,没有重叠的候选框。

  1. NMS(非极大值抑制)为每个弹孔保留最强的边界框。

自然地,我合并了两种方法:OpenCV 处理结构几何——牛眼、椭圆、环线、透视变换。YOLOv8 进行弹孔定位,与 MDPI 论文相同的架构,在我自己的数据集上进行了微调。推理时丢弃学习模型的类别预测;环值来自弹孔中心到牛眼中心的距离对比几何环线半径。

完成标注和训练后,我使用 coremltools 将模型导出为 CoreML——Xcode 导入后的最终包大小为 22.4 MB。

Xcode 中名为 'BulletHoleDetector' 的 CoreML 模型预览,22.4 MB,目标平台 iOS 15+。右侧是一张 .22 口径靶纸,上面有手写倒置的 VAZ #5 标注。五个白色边界框标记了检测到的弹孔——牛眼上方一个高处,中心附近三个相互接触,左下方一个。

Xcode CoreML 预览中打包的检测器,运行在我自己的一张靶纸上。

计分

坐标映射回

两个组件都就绪后,我本以为计分是最容易的部分。大多数情况下确实如此,只是我在坐标空间上反复浪费时间,并且完全忘记了透视变换的问题。

一张十牛眼 NSRA 竞赛靶纸,以轻微角度拍摄。每个检测到的牛眼周围画有绿色椭圆;边缘散布着数十个红色小圆圈,是装饰性印刷点被误标为弹孔的结果。左上角显示总分:40。

以一定角度拍摄的照片会将每个牛眼变成椭圆,径向强度剖面需要后处理才能正确拟合它们。

Vision 返回的边界框范围在 0-1 之间,原点在左下角,但 UIKit 的原点在左上角,因此很容易出现偏移一环的误差——这种误差看起来足够合理,以至于让人误以为是变换方法以外的问题。

一旦坐标对齐,计算看起来很简单。不过,测环尺的存在是有原因的——检测到的弹孔比实际制造它的子弹要小,因为纸张被撕裂并向内挤压。CoreML 返回的是可见撕裂纸张区域的边界框,而非子弹本身,因此计分时需要的是弹孔中心加上子弹半径——即子弹到达的最远点。

子弹半径

0.22 口径子弹直径为 0.22 英寸(显然),而 NSRA .22 靶纸为 2.05 英寸,所以几何上子弹半径应为牛眼直径的 10.87%。

不过纸张撕裂得没那么干净。此外,我以前在物理学和机器学习方面的表现都糟糕透顶,所以在重新阅读两篇论文后,我放弃了理论方法,开始凭经验调整乘数——即改变常数,在我手动计分的靶纸上重新运行测试。30% 对我而言效果不错(即牛眼直径的 14.13%),但我很想知道正确的方法——如果你了解,请给我留言

之前那张下方 NSRA 靶纸,每个弹孔都有标注:红色矩形紧密包围每个弹孔的撕裂纸张区域(CoreML 检测到的边界框),每个矩形内部有一个较小的橙色圆形,表示子弹的实际中心(用于计分的到达范围)。

每个撕裂区域周围的红色矩形是 CoreML 检测到的内容;黄色圆形是用于计分的弹着点位置。


超越测环尺

开始六个月后,我仍然觉得自己枪法不佳,但现在大多数时候我知道原因了——姿势、扳机压力,或在击发时呼吸。不过这些都是靠感受而非测量的东西,这也正是初学者一开始会打分组靶纸的原因。靶纸上弹群分布的形状能告诉你很多信息——从扳机扣压和击发时呼吸等常见问题,到步枪本身的问题。6

带有同心计分环的靶面。约十个白色弹孔紧密聚集在中心偏右下位置,触及 8 环和 9 环。

紧集群,中心偏右下——扳机扣压。

带有同心计分环的靶面。弹着点形成一条纵向长条,从 10 环上方穿过中心一直延伸到下方。

穿过中心的纵向长条——击发时呼吸。

比赛靶纸是按已知顺序逐牛眼射击的,所以只要知道哪个牛眼先打,就能将精度相对于位置作图,从而发现趋势。我经常看到自己在中间段漂移,然后在快结束时——当我注意到靶纸快用完了——又重新收紧。

我踏上这个旅程的初衷是想降低测环尺(并停止带回家一堆打过的靶纸),但当计分功能实现时,我变得更感兴趣的是不仅要自动化计分,还要自动化反馈——经过几个月积累的计分靶纸,我可以将所有弹着点叠加到累计热力图上观察趋势,或者对比我在全部四支公用步枪上的表现。

我甚至终于能证明,上场前几分钟吃一个甜甜圈平均会让我的成绩下降 7 分(满分为 100)。这可能是安慰剂效应,但我当前的理论是它提高了我的血糖水平(你可能看出来了,我不仅物理学得不好,生物学也不行——不过我已经尽力了)。

最后我把它打包成了一个离线优先的小应用——最初是为俱乐部里的伙伴们做的,但实际上是为任何想让自己的 烹饪 射击日常更有趣一点的人准备的。不过我也了解到世界上大部分人并不怎么关心 NSRA 靶纸,所以我一直在慢慢添加对其他项目和靶纸的支持。

iPhone 上 Notch 应用的短循环演示:从上方拍摄的比赛靶纸,牛眼自动检测,弹孔定位,每个牛眼旁写出了分数。

iPhone 上的 Notch,正在为比赛靶纸计分。


在我对自己有足够信心去猎鹿(deer stalking)之前,我就出发去了加拿大。

现在每周一和周三的晚上,有另一个人躺在普雷斯顿潘斯的那张垫子上7,躲避着同样的横梁,在靶纸之间数着呼吸。那个装测环尺的木盒仍然在桌子上——大多数伙计们说,这已经不是第一次有人觉得自己能比那把老黄铜塞规做得更好了。

测环尺的真正工作是解决争议。当两名射手对一枚边界弹着点有分歧时,测环尺说了算,因为规则就是这么定的。他们俩谁都不在乎我最先进的计算机视觉模型。

很可能,你身边也有类似的东西——一件在你出生之前就已经在履行职责的工具,试图替换它显得很愚蠢。

我们也许无法让它们退休。但我渴望制造出二十年后仍会出现在别人桌上的东西,耐用得足以让人也傻到想去替换它们。

想在应用中添加设备端机器学习?我可以帮忙 →


术语表

原文 中文
automated curing chamber 自动化熟成柜
bounding box 边界框
breathing through the shot 击发时呼吸
bull / bullseye 牛眼
chamber vacuum 真空腔体机
charcuterie 熟食肉品
deer stalking 猎鹿
fine-tune 微调
flood-fill 泛洪填充
garum 鱼露
Gary Anderson Gary Anderson
grouping card 分组靶纸
heat map 热力图
heuristic 启发式规则
Hough transform 霍夫变换
immersion circulator 低温循环器
inference 推理
koji
negative space 负空间
NMS (non-maximum suppression) NMS(非极大值抑制)
non-maximum suppression 非极大值抑制
NSRA NSRA
offline-first 离线优先
perspective transform 透视变换
Prewitt edge detector Prewitt 边缘检测器
protease 蛋白酶
radial-intensity profile 径向强度剖面
Red deer 马鹿
ring line 环线
scoring gauge 测环尺
trigger pull 扳机扣压
venison steak 鹿肉牛排

此文章由 AI 翻译

Footnotes

  1. 大西洋这岸最好的精品糕点(而且他们也做甜甜圈!)。

  2. 《枪支法案》允许土地所有者在"监督"你使用的前提下把他们的枪借给你,几个世纪以来人们就是这样在他们的庄园里狩猎的(顺便说一句,这叫鹿狩——hunting 是给骑马的有钱蠢货干的,shooting 是给穿粗花呢的有钱蠢货干的)。

  3. 我写代码时就已经多次搬起石头砸自己的脚了,想想我要是拿了真枪能干出什么事。

  4. 与 NSRA 相比,ISSF 靶纸有不同的牛眼尺寸、环数、不同的环区以及不同的背景
    ISSF 气步枪靶纸,Guvava,CC BY-SA 3.0,来自 Wikimedia Commons

  5. Z. Ali 等人,"Application of YOLOv8 and Detectron2 for Bullet Hole Detection and Score Calculation from Shooting Cards",AI,第 5 卷,第 1 期,72-90,2024 年。10.3390/ai5010005

  6. 一个干净紧密但偏离牛眼几环的弹群通常意味着,如果不是归零问题,你本可以完美命中牛眼——这个解释我觉得很安慰(但我的伙伴们不同意)。

  7. 我终有一天会回来的。
    普雷斯顿潘斯的射击场:一条长长的室内靶道,射击垫在纸质靶架前排成一排。