移动端YOLOv11部署实战:模型转换与AR实时识别 简介面向移动端开发者的实战指南基于YOLOv11在iOS、Android双平台实现实时AR物体识别系统解决模型轻量化、格式转换、推理框架选型等部署难题覆盖从环境搭建、模型适配到性能优化的完整链路。资源包为单个PDF文档共2.3MB、52页支持目录章节跳转与左侧大纲快速定位图文呈现完整已有286人学习。文档核心价值在于完整展示YOLOv11算法原理到工程落地的全流程涵盖单阶段检测机制、Core ML与ARKit、TensorFlow Lite与ARCore集成细节、剪枝量化蒸馏优化、模型转ONNX与TensorRT加速以及帧率、内存、推理时间等性能监测调试方法并给出App Store与Google Play的部署发布流程以教育、零售、旅游、游戏四类AR案例与经验总结收尾对目标检测移动端落地极具参考价值。1. 移动端跑 YOLOv11不是把模型塞进手机就完事掏手机对着办公桌扫一圈屏幕上实时框出显示器、水杯、键盘还叠着 3D 标签——这个效果看起来不复杂但我拆过好几份所谓“移动端 YOLOv11 部署指南”后发现真正能落地的不多。模型能跑通和能用是两码事跑通是模型输出几个框能用在手机上还得满足帧率、发热、AR 锚点贴合这三大约束。YOLOv11 作为 Ultralytics 系的最新目标检测模型在精度和速度上比前代有实打实的提升但移动端集成是个完整链路——模型导出、格式转换、平台 SDK 对接、AR 场景标定每一步都有坑。这份指南面向的是想在 iOS、Android 上做实时 AR 物体识别的从业者不管是做扫码类工具、智能家居控制还是工业巡检辅助核心问题是一样的怎么把 YOLOv11 的检测结果稳定地叠进摄像头画面里。2. 模型出口从 YOLOv11 权重到 TFLite 与 Core ML 的转换链路2.1 先选推理格式TFLite、Core ML 和 NCNN 各自卡在哪儿移动端跑 YOLOv11绕不开格式选型。Android 阵营的常见选择是 TFLiteGoogle 官方生态CameraX、ML Kit 都能直接配合iOS 阵营没得选Core ML 是系统级框架配合 Vision 框架做检测前置处理最顺。但实际项目里我见过不少团队在 Android 上用 NCNN理由是某些旧设备上 TFLite 的 GPU delegate 兼容性差NCNN 的 Vulkan 加速反而稳定。这取决于目标用户机型如果覆盖千元机NCNN 值得一试如果只要求近两年的中高端机型TFLite 省事得多。另外YOLOv11 的官方仓库已经内置了导出脚本常见做法是先导出 TorchScript 再转平台格式或者直接用yolo export一步到位。但有一点要注意YOLOv11 有n、s、m、l、x几个尺寸变体移动端首推yolo11n实测在骁龙 8 Gen 2 上 TFLite 的 FP16 版本能跑到 3545ms 一帧的延迟yolo11s就会掉到 60ms 以上AR 场景已经感觉到卡顿。所以选型逻辑是能上n就不上s精度不够再加输入分辨率而不是换大模型。2.2 导出命令与关键参数IMGSZ、量化、批次的取舍以最常用的导出路径为例先把 PyTorch 权重转成 TFLite。命令如下yolo export modelyolo11n.pt formattflite imgsz640 int8 halfFalse转出来的yolo11n_saved_model文件夹里会包含model.tflite。这里imgsz640是输入尺寸TFLite 固定输入 shape运行时不能改。int8开启 PTQ 量化但注意它会把模型打成全整型部分旧设备上 TFLite 的 CPU 支持没问题GPU delegate 反而不支持 INT8这点后面在避坑章节再展开。如果是 iOS导出 Core MLyolo export modelyolo11n.pt formatcoreml imgsz640 nmsFalsenmsFalse是我故意加的。YOLOv11 的 Core ML 导出支持内置 NMS但内置 NMS 的参数不透明调试起来很痛苦。我一般习惯把原始 tensor 输出拿到外部自己写 NMS虽然多写几十行代码但置信度阈值和 IoU 阈值都能直接调。导出时如果不加nmsFalseiOS 端拿到的结果是VNRecognizedObjectObservation看着方便但置信度阈值在模型内部已经写死实际项目里容易被小目标漏检坑到。参数对照表如下参数TFLite 场景Core ML 场景说明imgsz固定 640 或 416固定 640 或 416越小越快小目标越容易丢int8真机 CPU 可用不推荐INT8 GPU delegate 兼容性差halfFP16 可开建议开内存减半精度掉得最少nms不涉及建议 False外部做 NMS 便于调参batch固定 1固定 1移动端没有 batch 概念2.3 输出 tensor 的布局解析8400 个候选框怎么读YOLOv11 在 640×640 输入下单尺度推理输出是84 × 8400还是1 × 84 × 8400取决于导出通道顺序。TFLite 和 Core ML 对这点的处理不完全一样。TFLite 端常见输出 shape 是[1, 84, 8400]Core ML 端有人导出来是[1, 8400, 84]。我自己的习惯是拿到模型后先用测试图片跑一次 dump 出 shape别靠猜。84 4框坐标 80COCO 类别数如果是自定义数据集换成 4 类别数。8400 是三个尺度 head 的候选框总和YOLOv11 在这个尺寸下是 8400 个 anchor-free 候选框。解码时需要注意 YOLOv11 的坐标格式它的输出中心点坐标是基于输入尺寸归一化的但不同导出方式对是否还原到原图坐标的处理不同。我踩过的一次坑是 TFLite 输出拿到手后xy 坐标直接用了归一化值往 AR 坐标上映射结果标签全偏到屏幕右下角。正确的做法是先乘回 640再按相机帧和输入尺寸的缩放比做一次换算后面 iOS 和 Android 代码里都会体现这一步。3. iOS 侧集成Core ML Vision ARKit 的实线接法3.1 初始化模型与请求VNCoreMLRequest 的正确姿势iOS 端不直接拿 Core ML 模型做前处理标准做法是包一层 Vision。Vision 帮你处理了缩放、颜色格式、裁剪策略省掉不少脏活。关键代码这样写import CoreML import Vision import ARKit private var visionModel: VNCoreMLModel { let config MLModelConfiguration() config.computeUnits .cpuAndGPU guard let model try? YOLOv11n(configuration: config).model, let vnModel try? VNCoreMLModel(for: model) else { fatalError(模型加载失败) } return vnModel }() private func detect(in pixelBuffer: CVPixelBuffer) { let request VNCoreMLRequest(model: visionModel) { req, error in guard let results req.results as? [VNRecognizedObjectObservation] else { return } DispatchQueue.main.async { self.updateARAnchors(with: results) } } request.imageCropAndScaleOption .scaleFill request.confidenceThreshold 0.35 let handler VNImageRequestHandler(pixelBuffer: pixelBuffer, orientation: .right) try? handler.perform([request]) }computeUnits设为.cpuAndGPU是 Core ML 在移动端最稳的组合纯.all在部分 A12 芯片上触发神经引擎反而慢。scaleFill会把输入拉伸到 640×640如果不想图片变形可以用scaleFit但会留黑边检测框坐标要额外校正。这里有个细节置信度阈值 0.35 是检测的初始门槛AR 场景里阈值太低会出现标签乱跳我会在后面的锚点管理那层再设一道 0.5 的显示门槛两层过滤。3.2 ARKit 叠加把 Vision 的 2D 框映射到 3D 场景Vision 的输出坐标归一化到原图原图来自 ARKit 的ARFrame.capturedImage这个 pixel buffer 的尺寸是传感器原生分辨率和屏幕显示分辨率不是一回事。映射时需要做一次比例换算extension ARSCNView { func convertVisionRect(_ rect: CGRect, to view: UIView) - CGRect { let widthRatio view.bounds.width / UIScreen.main.bounds.width let heightRatio view.bounds.height / UIScreen.main.bounds.height // 参数rect 是 Vision 归一化坐标原点在左下 let x rect.minX * widthRatio let y (1 - rect.maxY) * heightRatio let w rect.width * widthRatio let h rect.height * heightRatio return CGRect(x: x, y: y, width: w, height: h) } }拿到 2D 框后AR 标签不能直接贴在屏幕上否则就不是 AR。常见做法是拿框中心点做射线检测和 ARKit 的场景几何求交把标签锚定在物理表面上let centerPoint CGPoint(x: rect.midX, y: rect.midY) let hitResults arSCNView.hitTest(centerPoint, types: [.existingPlaneUsingExtent, .featurePoint]) if let hit hitResults.first { let anchorNode SCNNode() anchorNode.position SCNVector3(hit.worldTransform.columns.3.x, hit.worldTransform.columns.3.y, hit.worldTransform.columns.3.z) let textNode createLabelNode(text: labelString) anchorNode.addChildNode(textNode) arSCNView.scene.rootNode.addChildNode(anchorNode) }射线检测的.featurePoint类型没法保证稳定性所以标签会抖。倾向用.existingPlaneUsingExtent但前提是场景里先有平面检测结果。我一般会在ARSessionDelegate里持续追踪平面锚点而不是每次检测都现去 hitTest这样标签位置能稳定不少。另外AR 标签文字要用SCNText而不是SKLabelNode前者是 3D 节点能跟随场景光照不会有飘在屏幕上的违和感。3.3 帧率控制不要每帧都跑检测AR 会话默认 60fps 回调但 YOLOv11n 在 iPhone 12 上单帧推理就要 30ms 左右每帧都做检测会吃满 CPU掉电飞快。我的做法是设置节流lastDetectTime距离现在超过 100ms 才触发一次检测同时把 AR 标签平滑更新和检测解耦。这样检测 10fps、AR 渲染 60fps视觉上标签跟随很顺又没有性能压力。4. Android 侧集成TFLite CameraX ARCore 的完整接线4.1 CameraX 取帧与 ImageAnalysis 的背压处理Android 端用 CameraX 比 Camera2 省心太多ImageAnalysis自带背压模式能自动丢帧防止积压。关键配置val analysisConfig ImageAnalysis.Builder() .setTargetResolution(Size(640, 640)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysisConfig.setAnalyzer(executor) { imageProxy - val bitmap imageProxy.toBitmap().copy(Bitmap.Config.ARGB_8888, false) val result detect(bitmap) runOnUiThread { arFragment.setDetectedObjects(result) } imageProxy.close() }setTargetResolution(Size(640, 640))不是直接裁剪到 640而是告诉 CameraX 优先输出接近这个分辨率的一帧实际拿到的是 640×480 或 960×720 之类的尺寸需要再做一次缩放。STRATEGY_KEEP_ONLY_LATEST是关键如果检测线程处理不过来CameraX 会主动丢弃旧帧保证延时最低。imageProxy.close()一定不能漏漏一次就少一个 buffer跑几分钟后流就卡死了这个我踩过。4.2 TFLite Interpreter 的输入输出拼装与解码YOLOv11 导出的 TFLite 模型输入是[1, 640, 640, 3]的 float 数组需要把 Bitmap 缩放到 640×640、归一化到 01、再 RGB 排布private fun bitmapToInput(bitmap: Bitmap): FloatArray { val scaled Bitmap.createScaledBitmap(bitmap, 640, 640, true) val input FloatArray(640 * 640 * 3) val pixels IntArray(640 * 640) scaled.getPixels(pixels, 0, 640, 0, 0, 640, 640) for (i in pixels.indices) { val r (pixels[i] shr 16) and 0xFF val g (pixels[i] shr 8) and 0xFF val b pixels[i] and 0xFF input[i * 3] r / 255.0f input[i * 3 1] g / 255.0f input[i * 3 2] b / 255.0f } return input }推理调用val interpreter Interpreter(loadModelFile()) // 加载 assets 里的 .tflite val inputArray arrayOf(bitmapToInput(bitmap)) val outputShape intArrayOf(1, 84, 8400) val output Array(1) { Array(84) { FloatArray(8400) } } interpreter.run(inputArray, output)把run放进单独线程池Interpreter本身是线程安全的但 GPU delegate 的部分实现不是所以我的习惯是一个线程池串行跑推理。解码 NMS 的部分用标准实现取output[0]前 4 行是 cx, cy, w, h后面 80 行是类别分数。4.3 ARCore 锚点与标签放置平面检测优先于特征点Android 端的 AR 叠加用的是 ARCore 的AnchorNode。检测到物体的 2D 框中心之后用Frame.hitTest做射线求交val hits frame.hitTest(centerX, centerY) if (hits.isNotEmpty()) { val hit hits.first() // 优先平面命中 val anchor frame.createAnchor(hit.hitPose) val anchorNode AnchorNode(anchor) val label createLabelNode(text) anchorNode.addChild(label) arSceneView.scene.addChild(anchorNode) }ARCore 的hitTest返回结果按深度排序优先取HitTestResult.DepthPriority最高的那个但要注意如果场景没建立平面命中结果可能落在特征点上标签会漂。我的方案是开启Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL并且在平面未稳定前不创建锚点只显示 2D 检测框这样用户体验不割裂。5. 跨平台排查实录量化、NMS、尺寸和线程的五个坑5.1 现象iOS 端检测框的位置整体偏移到右上角原因Vision 的坐标原点在左下CVPixelBuffer的原始朝向和屏幕不一致直接用归一化坐标映射到ARSCNView时没有做 Y 轴翻转和方向校正。解决统一走VNImageRequestHandler的orientation参数传.right或根据设备方向动态计算拿到框后先做坐标翻转再进convertVisionRect。5.2 现象Android 上 TFLite 推理 300ms 一帧完全达不到实时原因默认走了 CPU 的 FP32 推理YOLOv11n 在 640×640 输入下 CPU 就是这个量级CPU 单线程跑大模型不现实。解决加载模型时指定 GPU delegateval gpuDelegate GpuDelegate() val options Interpreter.Options().addDelegate(gpuDelegate) interpreter Interpreter(loadModelFile(), options)但 GPU delegate 对算子有兼容限制遇到不支持的算子会静默 fallback 回 CPU所以性能还是上不去时要加日志确认 delegate 真的生效了。5.3 现象INT8 量化后小物体全部丢失大物体框也偏了原因YOLOv11 的检测头对量化误差很敏感PTQ 全量化直接把输出分布压垮了。解决改用 FP16 量化iPhone 上精度几乎无损Android 上可以试 per-channel INT8但多数情况 FP16 精度已经够。真要 INT8 就得做 QAT用 Ultralytics 的hDetect蒸馏分支这个改动量大非必要不碰。5.4 现象AR 标签在桌面上飘像沾了胶水没贴稳原因直接用 2D 框中心点做射线检测特征点命中导致锚点在空间里前后跳。解决限定只和已追踪平面求交另外加一个平滑滤波器锚点位置用上一次的加权平均// 一阶低通滤波alpha 越小越平滑但延迟越大 anchorScale 0.25f smoothedPos smoothedPos * (1 - anchorScale) rawPos * anchorScale5.5 现象App 切后台再回来模型推理报错或 AR 黑屏原因GPU delegate 和 ARCore session 在后台被系统回收回来没有重建。解决在onPause里释放InterpreteronResume时重新加载ARKit 的ARSession用run带resetTracking重置不要复用旧 session。Core ML 模型的MLModelConfiguration也要重新实例化。6. 帧率调优的最后一公里预热、双缓冲与低延迟 Trick新模型第一次推理总是奇慢因为 GPU 驱动要编译内核、缓存池要分配。实测 iPhone 上首次推理 300ms第二次就回落到 35ms。所以 App 启动后要跑一次空帧预热摄像头权限还没拿到时就用纯黑色图片喂一帧让 Core ML 和 GPU delegate 把预热走完。Android 上同理TFLite 的interpreter.run之前先跑一次相同 shape 的输入能省掉不少黑匣子时间。双缓冲是另一个必做项。推理线程和渲染线程不能抢同一个 bufferCameraX 的ImageAnalysis拿到帧后立刻拷贝出 Bitmap把原始ImageProxy交还系统推理完成后把结果投递到 AR 渲染线程的队列渲染线程只管消费队列里最新的结果不关心推理中间态。这样即使某一帧推理超时渲染线程也不会等。检测结果连续两帧相同的情况下第三帧自动跳过推理直接用上一帧结果做插值这是我调 AR 标签稳定性时发现的规律语义上物体没变检测就没必要重跑。锚点平滑再细一层的话要把运动趋势考虑进去。一阶低通滤波在物体快速移动时会拖尾我的做法是加一个速度项// velocity-aided 平滑 val alpha 0.18f val predicted smoothedPos velocity * 0.05f smoothedPos predicted * (1 - alpha) rawPos * alpha velocity smoothedPos - predicted这套组合下来iPhone 12 上 YOLOv11n 的端到端延迟能稳定在 45ms 左右Android 骁龙 8 Gen 2 稍低一点视觉上标签基本贴在物体上。最后说一个工程习惯每次改完模型或导出参数强制在真机上跑一遍 30 分钟温升测试记录帧率和发热曲线不要只在模拟器上验证。我从一次真机发热降频导致 AR 卡成 PPT 的翻车里长记性之后就再也没跳过这个环节。这这份 PDF 指南能省下不少自己摸索的时间希望帮到你。本文还有配套的精品资源点击获取