CoreML模型没问题,是照片太大了:端侧AI的图像管线陷阱

端侧视觉计数不准时,先别换模型——分块推理和正确的合并策略往往就能把召回率从14提升到173。
文章记录了一个端侧计算机视觉的典型故障:开发者用小型 YOLO/CoreML 模型在设备端统计牲畜数量,一张约 250 只羊的 1200 万像素照片却只检测出 14 只。问题不在模型本身,而在于整幅高分辨率照片被直接缩放至模型输入尺寸,远处的小目标仅剩几个像素,检测器无从恢复。修复方案是无聊但有效的分块推理:保留全帧推理,同时叠加 3×3、25% 重叠的切片,把切片坐标映射回原图,丢弃被切片边缘切断的框,并用类无关 NMS 合并结果。同一张照片的检测数从 14 提升到 173,且近距离简单场景的计数未被虚增。文章强调合并步骤才是隐藏的坑——分块场景下仅靠 IoU 不够,还需 intersection-over-smaller 与类无关合并。核心教训是:在责怪模型前先检查你实际喂给了它什么像素,端侧 AI 中图像管线往往是最廉价有效的优化点。
一个普通的计算机视觉故障:模型按要求工作,但图像管线让任务无法完成。

一位开发者分享了他的经历:应用需要在设备端用一个小型YOLO/CoreML模型统计牲畜数量,无网络、无账户、无服务端兜底。这种约束有用,但意味着手机端管线必须诚实地面对模型实际看到的东西。
250只羊,模型只数出14只
最初的代码路径看起来合理:
let request = VNCoreMLRequest(model: model)
request.imageCropAndScaleOption = .scaleFit
handler.perform([request])
一张图进去,检测结果出来。
然后用一张真实的牧场照片测试——画面里大约有250只羊。计数返回14。
这不是模型的问题。一张1200万像素的照片被压缩进了模型输入。等羊群到达网络时,远处的羊每只只有几个像素。检测器已经没有什么聪明的东西可以恢复了。
分块推理:不换模型,换输入尺寸
修复方案很无聊,但比换模型好得多:
- 先跑一遍完整图像
- 在同一张图上跑重叠的切片
- 把切片相对坐标的检测框映射回图像坐标
- 丢弃被内部切片边缘切断的框
- 用类无关NMS合并剩余检测
关键点是,分块改变了模型看到的物体尺寸,但没有改变模型本身。
let regions = [CGRect(x: 0, y: 0, width: 1, height: 1)]
+ Self.tiles(aspect: aspect, across: 3, overlap: 0.25)
for region in regions {
request.regionOfInterest = region
}
在一张4:3的照片上,这变成12个切片加一次全帧推理。推理次数更多,但仍在设备端,而且对于静态照片仍然只需要不到一秒。
同一张牧场照片从14个检测提升到173个。不完美,但突然有用了。近距离的母鸡照片保持稳定,这一点同样重要:修复在尺度是问题的地方提升了召回率,却没有在容易计数的场景中虚增数量。
什么是 regionOfInterest
VNCoreMLRequest 的 regionOfInterest(ROI)是 Vision 框架提供的一个属性,取值是归一化坐标(0~1)表示的 CGRect,默认 .zero 表示整幅图像。设置后,Vision 会先把该矩形区域从原图中裁剪出来,再按 imageCropAndScaleOption 缩放并送入模型。这意味着每次推理实际输入的像素尺寸可能不同,但模型本身(输入张量形状、权重)完全不变。正因为 ROI 只是改变了「馈送给模型的像素」,分块推理才成为不触及模型结构的廉价优化手段。需要注意的是,坐标归一化基于原图方向,旋转或镜像的图像要先统一方向,否则 ROI 会切错位置。
imageCropAndScaleOption 的取舍
imageCropAndScaleOption 决定 Vision 如何把图像适配模型要求的输入尺寸,常见取值有 .centerCrop、.scaleFit、.scaleFill 和 .scaleFitRotate90CCW。.centerCrop 保持长宽比但裁掉边缘,容易在宽幅照片边缘丢目标;.scaleFit 保留整幅画面但会在短边补灰边,等效缩小物体;.scaleFill 会拉伸变形。文章中 1200 万像素照片缩放入模型(通常是 416×416 或 640×640)时,单只远处的羊可能只占几个像素,这属于典型的「极端尺度下采样」问题,而非模型能力不足。选择 .scaleFit 配合分块,可以让每个切片内的物体占更多像素,本质上是在输入层补偿尺度损失。
合并步骤才是隐藏的坑
不明显的部分是合并步骤。
默认的仅基于IoU的NMS在分块时不够用。一个部分头部框可能位于整个动物框内部,IoU很低,所以还需要基于intersection-over-smaller进行合并。同时把合并做成类无关的,因为同一只动物可能在不同推理中以两个标签返回。数动物,不是数标签争论。
这个经验很简单:在责怪模型之前,检查你实际给了它什么像素。
对于端侧AI,模型往往是系统中最不灵活的部分。围绕它的图像管线才是廉价优化隐藏的地方。
IoU 与 intersection-over-smaller(IoS)
NMS 常用 IoU(交并比)衡量两个框的重叠:交叠面积除以并集面积。但当一个框被另一个框完全包含时——比如分块场景中一个「部分头部」框落在「整只动物」框内部——交叠面积虽大,并集却等于大框,IoU 会明显偏小,导致本应合并的框被保留下来。intersection-over-smaller(IoS)改成交叠面积除以较小框的面积,只要小框大部分落在大框内,IoS 就接近 1,从而正确触发合并。分块推理时同一目标可能在相邻切片中分别以局部框和全局框出现,IoS 正是为此类「包含式重叠」设计的补充判据。把它与类无关合并结合,即可避免同一动物因标签或尺度不一致被重复计数。
类无关 NMS(class-agnostic NMS)
标准 NMS 通常按类别分别执行,只有当两个框类别相同且 IoU 超阈值时才抑制。但在分块推理中,同一目标在不同切片、不同上下文下可能被模型赋予不同标签(例如「羊」与「动物」),按类独立过滤就会把同一实体保留成多个框。类无关 NMS 忽略类别、对全部候选框统一做重叠抑制,再决定保留哪个标签。对以「计数」为目标的场景,这一策略至关重要:计数的对象是物理实体,而不是分类争议。代价是可能抑制掉一些类别不同但空间重叠的真实目标,因此需要根据任务权衡阈值和是否保留类别信息。
相关推荐

WorkBuddy保姆级教程:半小时跑通自动化工作流
WorkBuddy 自动化工作流保姆级拆解:为什么大多数人只把它用成聊天软件,27 个支点课程与 12 个模板如何帮助新手半小时跑通落地实操,附使用边界提醒。

Codex云端模式详解:环境配置与适用场景
本文详细解析Codex云端模式的工作原理、环境配置步骤及适用场景,包括如何创建环境、连接GitHub仓库、设置定时任务,并对比云端与本地模式的差异,帮助开发者高效利用Codex。

Claude Code免费使用真相:三条路与一个骗局
Claude Code 免费使用是否可行?本文拆解 Pro、Max 的真实成本(含汇率与税费)、社区免费方案的技术原理与隐私代价,以及假安装包、钓鱼页面等安全陷阱,并给出访客通行证、开源贡献者计划和本地模型三条合法免费路径。