手机取代了黄铜塞子
手机取代了黄铜塞子
在爱丁堡郊外一个铁皮隧道里,作者每周三傍晚穿着不舒服的夹克趴在地上打靶。真正让他感到疲惫的并非射击本身,而是那套繁琐的计分仪式——用黄铜塞子一个个填补弹孔、借助测环尺判定环数。作为一名 iOS 工程师,他决定用手机摄像头和计算机视觉来终结这一切。
这篇文章的妙处在于,它表面上讲的是用技术替代机械劳动,实则揭示了一个更深层的命题:当你想用 AI 识别"什么东西不见了"时,你面对的是"负空间"识别这一根本性挑战。弹孔是纸上被移除的部分,而绝大多数视觉模型训练的是识别"存在的事物",不是"缺失的事物"。作者从 Vision 框架的过度检测一路摸索到 OpenCV 的传统图像处理,最终找到了一个结合径向亮度剖面的几何解法。
这是一篇关于"用工程思维解决实际问题"的好文章——它不堆砌术语,而是带你一步步走过从灵感到落地的全部思考过程。
几个月来,我每周三傍晚都待在爱丁堡郊外一个铁皮隧道里,穿着一件看起来很可笑(也同样不舒服)的夹克。我躺在地上数呼吸,然后沿着靶场走下去,在横梁下低头躲避。每隔五米,地面上就用白漆写着 DUCK,横梁上也贴着写着"DUCK"的标语,但我偶尔还是会撞到头——忙着查看计分卡。
如果不走运,一发子弹打在了环线附近,你就需要借助工具了。你走到一托盘 Greggs 香肠卷1旁——我们地处北方,赞助商也是本地品牌——找到一个装着各种尺寸黄铜塞的木盒子,选一个合适的,小心地把它塞进弹孔里(最好只塞一次,以免撕裂),塞进去的位置就是你的得分。

子弹会把纸向内推,所以即使环线本身没被破坏,只要(塞子的)凸缘超出外环,你就得算更低的分数。
射击本身很有趣。但数环数、撞脑袋、塞塞子这套仪式必须终结。
我去那里的起因是烹饪。
我几十年前开始迷上烹饪,逐渐变得愈发痴迷:从羞涩地尝试复刻我去过的每一家高级餐厅的菜品,到建造自动化熟成柜(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)。物体检测器训练的是"应该存在的东西",所以要用它们来寻找"原来存在但已被移除的东西",并不那么直接。

将 Vision 的环检测和物体检测应用于一张 NSRA 靶纸。
我又试了几种更明显的方法:灰度化、反转图像、添加和去除噪声,但即使其他一切正常,落在环线上的弹孔还是会变成太小而无法识别的碎片。
更好的方法是将靶纸视为一个几何已知的物体:先找到环线结构,然后再在其中寻找弹孔。我承认这次不打算重新发明轮子了,于是去查了查其他方案。
端口及其他
2012 年的一篇论文
作为计算机视觉问题,射击靶标计分已经足够平淡到有人发表过相关论文。我找到了 Rudzinski 和 Luckner(华沙工业大学)2012 年发表的《Automatic Scoring of Shooting Targets with Tournament Precision》(自动竞赛精度射击靶标计分),该论文声称能检测 99% 的弹孔。
有几个注意事项:该方法针对低分辨率图像进行了优化,但要求使用平坦的 ISSF 靶纸4、相机角度低、标注不与弹孔相似,且主要针对气枪射击设计。气枪铅弹会在纸上留下饼干切割器般整齐的圆孔,而 0.22 口径子弹在 25 码距离上则会留下参差不齐的边缘。
我逐步复现了该论文。如果你不想阅读原文,可以概括为四个步骤:擦除环线,泛洪填充以找到弹孔形状,运行 Prewitt 边缘检测器,使用霍夫变换拟合圆。

从灰度靶纸开始

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

- 泛洪填充

- 检测边缘 4. 使用霍夫变换拟合圆。
Vision 框架没有 Prewitt 边缘检测器,所以我同时引入了 OpenCV,前三个步骤运行良好。但步骤四有一个问题。NSRA 靶纸在四个基本方位标注了环值——9 环的北、东、南、西方向各有一个"9"——而霍夫变换也会将这些数字拟合成圆形。
此外,环线擦除后的月牙形碎片有时太小,无法进行泛洪填充,所以我最终使用了 V 值径向强度剖面——从牛眼中心向外选取一条带状区域,沿该区域采样亮度,寻找白色环线交叉处的峰值。峰值位置就是环线半径。

- 选取一条带状区域。

- 采样亮度。

- 绘制峰值。
结合 Vision 的 VNDetectContoursRequest 和周长过滤器,我平均每张靶纸能检测出五个弹孔中的四个——即 80% 的准确率,仍有很长的路要走,而且我们还没有遇到重叠弹孔等边界情况。
也想在你的应用中使用 Vision 和 CoreML 吗?来聊聊 →
引入机器学习
射击者会在靶纸上做各种事情。他们会写名字、加日期、在接近的弹孔周围画圈。大多数靶纸有撕裂的订书钉孔,有时多发子弹在纸上留下的孔洞过于接近,以至于合并成一个泛洪填充区域。
我最初的尝试是为每种情况添加启发式规则,每隔一周带着新的一批靶纸回来,再次调整参数,但这几乎不可扩展也不可持续。
于是我再次回到 Google,找到了一篇 2023 年底发表的论文5。作者声称平均精度达 96.5%,但专注于弹孔检测,并通过边界框类别读出环值。
我无法为我的靶纸手动准备边界框类别,但我已经有了可用的几何方法,这要归功于 Rudzinski 和 Luckner。我所缺少的是可靠的弹孔检测。YOLO 论文很好地解决了弹孔检测问题,但将几何部分留给了一个假设:靶纸是完美对齐的。

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

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

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

- NMS(非极大值抑制)为每个弹孔保留最强的边界框。
自然地,我合并了两种方法:OpenCV 处理结构几何——牛眼、椭圆、环线、透视变换。YOLOv8 进行弹孔定位,与 MDPI 论文相同的架构,在我自己的数据集上进行了微调。推理时丢弃学习模型的类别预测;环值来自弹孔中心到牛眼中心的距离对比几何环线半径。
完成标注和训练后,我使用 coremltools 将模型导出为 CoreML——Xcode 导入后的最终包大小为 22.4 MB。

Xcode CoreML 预览中打包的检测器,运行在我自己的一张靶纸上。
计分
坐标映射回
两个组件都就绪后,我本以为计分是最容易的部分。大多数情况下确实如此,只是我在坐标空间上反复浪费时间,并且完全忘记了透视变换的问题。

以一定角度拍摄的照片会将每个牛眼变成椭圆,径向强度剖面需要后处理才能正确拟合它们。
Vision 返回的边界框范围在 0-1 之间,原点在左下角,但 UIKit 的原点在左上角,因此很容易出现偏移一环的误差——这种误差看起来足够合理,以至于让人误以为是变换方法以外的问题。
一旦坐标对齐,计算看起来很简单。不过,测环尺的存在是有原因的——检测到的弹孔比实际制造它的子弹要小,因为纸张被撕裂并向内挤压。CoreML 返回的是可见撕裂纸张区域的边界框,而非子弹本身,因此计分时需要的是弹孔中心加上子弹半径——即子弹到达的最远点。
子弹半径
0.22 口径子弹直径为 0.22 英寸(显然),而 NSRA .22 靶纸为 2.05 英寸,所以几何上子弹半径应为牛眼直径的 10.87%。
不过纸张撕裂得没那么干净。此外,我以前在物理学和机器学习方面的表现都糟糕透顶,所以在重新阅读两篇论文后,我放弃了理论方法,开始凭经验调整乘数——即改变常数,在我手动计分的靶纸上重新运行测试。30% 对我而言效果不错(即牛眼直径的 14.13%),但我很想知道正确的方法——如果你了解,请给我留言。

每个撕裂区域周围的红色矩形是 CoreML 检测到的内容;黄色圆形是用于计分的弹着点位置。
超越测环尺
开始六个月后,我仍然觉得自己枪法不佳,但现在大多数时候我知道原因了——姿势、扳机压力,或在击发时呼吸。不过这些都是靠感受而非测量的东西,这也正是初学者一开始会打分组靶纸的原因。靶纸上弹群分布的形状能告诉你很多信息——从扳机扣压和击发时呼吸等常见问题,到步枪本身的问题。6

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

穿过中心的纵向长条——击发时呼吸。
比赛靶纸是按已知顺序逐牛眼射击的,所以只要知道哪个牛眼先打,就能将精度相对于位置作图,从而发现趋势。我经常看到自己在中间段漂移,然后在快结束时——当我注意到靶纸快用完了——又重新收紧。
我踏上这个旅程的初衷是想降低测环尺(并停止带回家一堆打过的靶纸),但当计分功能实现时,我变得更感兴趣的是不仅要自动化计分,还要自动化反馈——经过几个月积累的计分靶纸,我可以将所有弹着点叠加到累计热力图上观察趋势,或者对比我在全部四支公用步枪上的表现。
我甚至终于能证明,上场前几分钟吃一个甜甜圈平均会让我的成绩下降 7 分(满分为 100)。这可能是安慰剂效应,但我当前的理论是它提高了我的血糖水平(你可能看出来了,我不仅物理学得不好,生物学也不行——不过我已经尽力了)。
最后我把它打包成了一个离线优先的小应用——最初是为俱乐部里的伙伴们做的,但实际上是为任何想让自己的 烹饪 射击日常更有趣一点的人准备的。不过我也了解到世界上大部分人并不怎么关心 NSRA 靶纸,所以我一直在慢慢添加对其他项目和靶纸的支持。

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
-
大西洋这岸最好的精品糕点(而且他们也做甜甜圈!)。 ↩
-
《枪支法案》允许土地所有者在"监督"你使用的前提下把他们的枪借给你,几个世纪以来人们就是这样在他们的庄园里狩猎的(顺便说一句,这叫鹿狩——hunting 是给骑马的有钱蠢货干的,shooting 是给穿粗花呢的有钱蠢货干的)。 ↩
-
我写代码时就已经多次搬起石头砸自己的脚了,想想我要是拿了真枪能干出什么事。 ↩
-
与 NSRA 相比,ISSF 靶纸有不同的牛眼尺寸、环数、不同的环区以及不同的背景
↩ -
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 ↩
-
一个干净紧密但偏离牛眼几环的弹群通常意味着,如果不是归零问题,你本可以完美命中牛眼——这个解释我觉得很安慰(但我的伙伴们不同意)。 ↩
-
我终有一天会回来的。
↩