1. 为什么 ONNX 转 Core ML 总在第一步卡住
如果你正在做 iOS 端侧对象检测,大概率会遇到这个组合:训练或下载的是 YOLO 的 ONNX 模型,部署目标却是 iPhone 上的 Core ML。中间这一步转换,看起来只是一行ct.converters.onnx.convert,实际动手时却经常卡在环境、opset、输入节点名、预处理参数这些细节上。我试过把 YOLOv3、v4、v2 挨个往 Core ML 里塞,最后能顺利跑通预测的,反而是结构更朴素的 YOLOv2。
这篇聚焦一件事:把 ONNX 格式的 YOLO 对象检测模型转成.mlmodel,并让它在 Python 侧完成一次可验证的推理。适合已经会用 Conda、写过一点 Python、准备把检测模型放到 iOS App 里的开发者。读完你能拿到一份可复制的转换脚本骨架、一份config.toml示例,以及用统一 Key 通道验证模型加载与输出的具体动作。
对象检测和图像分类的区别在于,分类只告诉你“图里有没有手提箱”,检测还要告诉你“手提箱在哪、框多大”。YOLO 的思路是把输入图像切成网格,每个网格单元预测若干边界框和类别,一次前向传播就出结果。YOLOv2 用 13×13 的网格、每格 5 个框,合计 845 个候选框。理解这个输出结构,后面排查转换问题会轻松很多。
2. 转换前的环境与 TaoToken 统一 Key 准备
2.1 用 Conda 固定转换环境
Core ML 转换对版本很敏感,coremltools、onnx、protobuf三者版本错位就会报各种看不懂的错。建议单独建环境,不要和日常环境混用。
conda create -n coreml python=3.8 -y conda activate coreml pip install coremltools==5.2 onnx==1.12 onnxruntime==1.12 pillow requestsmacOS 10.15+、Xcode 11.7+ 是这套流程比较稳的基线。coremltools5.x 对 ONNX 的支持相对完整,再往上版本对旧 opset 的兼容反而可能变差。
2.2 为什么这里要引入 TaoToken
转换本身是本地计算,不需要联网。但转换完之后,你通常要做两件事:一是拉取或核对模型文件、比对不同来源的 ONNX 版本;二是用统一的 API 通道去验证模型加载和推理结果是否正常。如果每个环节都去单独配一套 Key 和地址,调试成本会很高。
TaoToken 在这里的角色是统一入口:一个 Key 覆盖模型对话、编码辅助、API 调用等通道。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。你可以在控制台创建 Key,然后在脚本里通过环境变量注入,避免把密钥写死在代码里。
export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"注意:Key 只放在环境变量或本地配置文件里,不要提交到 Git。转换脚本本身不依赖网络,但验证环节会用到这个通道。
3. 可复制的转换配置骨架
3.1 config.toml 示例
把路径、输入节点名、预处理参数抽到配置文件里,换模型时只改配置,不动脚本。
[model] onnx_path = "./models/yolov2-coco-9.onnx" mlmodel_path = "./models/yolov2-coco-9.mlmodel" input_node = "input.1" input_size = 416 minimum_ios = "13" [preprocess] image_scale = 0.00392156862745098 # 1/255 is_bgr = false [taotoken] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY"image_scale写成1/255.0的十进制值,是为了避免某些版本解析 TOML 浮点时出现精度问题。is_bgr = false表示按 RGB 通道顺序处理,YOLOv2 的 COCO 预训练模型用的是 RGB。
3.2 转换脚本骨架
import os import toml import onnx import coremltools as ct from PIL import Image from urllib.request import urlopen cfg = toml.load("config.toml") onnx_path = cfg["model"]["onnx_path"] input_node = cfg["model"]["input_node"] with open(onnx_path, "rb") as f: model_onnx = onnx.load(f) print(model_onnx.graph.input) cml_model = ct.converters.onnx.convert( model=onnx_path, image_input_names=[input_node], preprocessing_args={ "image_scale": cfg["preprocess"]["image_scale"], "is_bgr": cfg["preprocess"]["is_bgr"], }, minimum_ios_deployment_target=cfg["model"]["minimum_ios"], ) cml_model.save(cfg["model"]["mlmodel_path"]) print(cml_model)这里最容易踩的坑是image_input_names必须传列表。如果你写成image_input_names=input_node,转换不会报错,但preprocessing_args里的缩放会被静默忽略。结果就是模型能跑,输出却完全不对,你会花很久怀疑人生。方括号一定要加上。
3.3 检查输入输出形状
转换完成后,先别急着写 iOS 代码,在 Python 里把模型结构打印出来确认。
spec = cml_model.get_spec() print("输入:", spec.description.input) print("输出:", spec.description.output)YOLOv2 转换后输入应该是 416×416 的 RGB 图像,输出是形状(1, 425, 13, 13)的多维数组。425 这个数字的来源是:每个网格单元 5 个框,每个框 4 个坐标 + 1 个置信度 + 80 个类别概率,即5 × (4 + 1 + 80) = 425。13×13 就是网格数。看懂这个结构,下一篇解码输出时就不会迷路。
4. 验证请求与成功结果
4.1 准备一张测试图
用一张公开的 COCO 风格图片,裁剪成正方形再缩放到 416×416,避免长宽比失真影响框的位置。
def load_and_scale(image_url, size=416): image = Image.open(urlopen(image_url)).convert("RGB") w, h = image.size min_dim = min(w, h) x0 = (w - min_dim) // 2 y0 = (h - min_dim) // 2 box = (x0, y0, x0 + min_dim, y0 + min_dim) return image.crop(box).resize((size, size)) image = load_and_scale("https://c2.staticflickr.com/4/3393/3436245648_c4f76c0a80_o.jpg") pred = cml_model.predict({input_node: image}) print(pred[list(pred.keys())[0]].shape)如果输出形状是(1, 425, 13, 13),说明转换和加载都成功了。这一步的意义在于:在进入 Xcode 之前,先在 Python 侧确认模型是活的,把问题范围缩小。
4.2 用 TaoToken 通道做一次结果核对
当你需要把推理结果和另一路模型输出做比对,或者让编码助手帮你检查解码逻辑时,可以走统一通道。模型对话入口在 https://taotoken.net/api ,控制台里可以管理 Key:https://taotoken.net/console 。如果你后续要做长期的编码和 Agent 工作流,Coding Plan 会更合适:https://taotoken.net/coding-plan 。
import os, requests resp = requests.post( f"{os.environ['TAOTOKEN_BASE_URL']}/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json={ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "解释 YOLOv2 输出 (1,425,13,13) 的含义"}], }, timeout=30, ) print(resp.json()["choices"][0]["message"]["content"])这一步不是转换的必需环节,但当你对输出结构不确定时,用它快速核对比自己翻论文快得多。接入文档在 https://taotoken.net/doc ,API Key 管理在 https://taotoken.net/api-keys 。
5. 本篇常见错排查
5.1 转换报 opset 不支持
现象是Unsupported ONNX op或opset version not supported。原因通常是模型用了较新的 opset,而当前coremltools版本不支持。解决办法有两个:降级模型到 YOLOv2 这类结构简单的版本,或者用onnx.version_converter把 opset 降到 11 以下再转。
python -c "import onnx; m=onnx.load('model.onnx'); m=onnx.version_converter.convert_version(m, 11); onnx.save(m, 'model_op11.onnx')"5.2 输入节点名写错
model_onnx.graph.input打印出来可能是input.1、images、input等不同名字。写错不会报错,但预测时数据对不上。务必先打印再填配置。
5.3 预处理缩放被忽略
前面提过的方括号问题。判断方法:转换后看spec.description.input里有没有imageType字段。如果没有,说明图像输入没被正确识别,缩放参数自然也没生效。
5.4 输出全是乱值
多半是通道顺序或缩放系数不对。YOLOv2 用 RGB、缩放到 [0,1];如果你的模型训练时用的是 BGR 或 [0,255],就要改is_bgr和image_scale。改完重新转换,不要在原模型上反复预测。
5.5 保存后加载失败
.mlmodel保存成功但 Xcode 里打不开,通常是minimum_ios_deployment_target设得太低或太高。iOS 13 是这套流程的稳妥值,设成 12 可能缺算子,设成 15 又可能和你的 Xcode 版本不匹配。
6. 下一步与工具入口
到这里,你已经拿到了一个能在 Python 侧正常预测的 Core ML 模型。输出(1, 425, 13, 13)还只是原始张量,里面混着框坐标、置信度和类别概率,需要解码才能变成人能看懂的检测结果。下一篇会专门讲怎么把这堆数字拆成边界框和标签。
如果你在解码逻辑上想快速验证思路,可以用模型对话通道跑几个小例子:https://taotoken.net/api 。需要管理多个 Key 或查看调用量,去控制台:https://taotoken.net/console 。长期做 iOS 端侧模型转换和 Agent 工作流的话,Coding Plan 能省掉反复配环境的麻烦:https://taotoken.net/coding-plan 。Claude Code 相关的接入配置在 https://taotoken.net/claudecode-anthropic ,接入文档统一在 https://taotoken.net/doc 。
转换这一步的经验就一句话:先把 Python 侧的预测跑通,再去碰 Xcode。模型在 Python 里输出形状对了,iOS 那边基本只是搬运工作;反过来,如果 Python 侧都没验证就打包进 App,排错会痛苦十倍。