本文首发地址 http://h89.cn/archives/322.html

在 Android 端部署 YOLOv8 目前有两条主流技术路线,分别适用于不同场景:

方案 技术栈 难度 性能 适用场景
路线一 TFLite + CameraX 良好 快速上线、迭代频繁的业务
路线二 NCNN + JNI/NDK 中高 极佳 对帧率和功耗有极致要求的场景

注意:本文档标题为 YOLOv8,若你手头是 YOLOv5/v7 或更早的 YOLO-Nano/SSD-MobileNet 等模型,预处理和输出解析逻辑会有差异,需对照模型结构做相应调整。


一、模型转换与优化

无论选择哪条路线,第一步都是将 Ultralytics 训练好的 .pt 模型转换为移动端可用的格式。

1.1 导出 TFLite(路线一)

Ultralytics 已内置 TFLite 导出,一行命令即可完成:

# 导出 FP16 TFLite(推荐,速度与精度的平衡)
yolo export model=yolov8n.pt format=tflite half

# 或导出 INT8 量化模型(体积最小,需指定代表性数据集)
yolo export model=yolov8n.pt format=tflite int8 data=coco128.yaml

导出后会得到 yolov8n_float16.tfliteyolov8n_full_integer_quant.tflite,直接放入 Android 项目的 assets/ 目录即可。

关键参数说明

  • half:FP16 量化,模型体积减半,推理速度提升,精度损失极小。
  • int8:INT8 全整数量化,体积最小,但可能需要校准数据集,且部分低端设备支持不佳。

1.2 导出 NCNN(路线二)

NCNN 不直接支持 PyTorch,通常通过 ONNX 中转:

# 1. 导出 ONNX(固定 batch=1,opset 建议 12+)
yolo export model=yolov8n.pt format=onnx opset=12 simplify

# 2. 使用 onnx2ncnn 转换
onnx2ncnn yolov8n.onnx yolov8n.param yolov8n.bin

# 3.(可选)FP16 优化
ncnnoptimize yolov8n.param yolov8n.bin yolov8n-opt.param yolov8n-opt.bin 1

更优方案:如果环境允许,推荐使用 pnnx 直接将 .pt 转为 NCNN,能更好地处理 YOLOv8 的解耦头(Decoupled Head)和动态 Shape 问题,减少后处理代码复杂度。


二、路线一:TFLite + CameraX(推荐新手)

2.1 环境配置

在模块级 build.gradle 中添加依赖:

dependencies {
    // CameraX
    val cameraxVersion = "1.4.1"
    implementation("androidx.camera:camera-core:$cameraxVersion")
    implementation("androidx.camera:camera-camera2:$cameraxVersion")
    implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
    implementation("androidx.camera:camera-view:$cameraxVersion")

    // TensorFlow Lite(版本请以 https://github.com/tensorflow/tensorflow/releases 为准)
    implementation("org.tensorflow:tensorflow-lite:2.16.1")
    implementation("org.tensorflow:tensorflow-lite-gpu:2.16.1")
    implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
}

android {
    aaptOptions {
        noCompress("tflite")  // 禁止压缩模型文件
    }
}

2.2 图像预处理

YOLOv8 要求输入为 640×640,并采用 Letterbox 填充(保持长宽比,不足部分用灰色 [114, 114, 114] 填充),最后将像素值归一化到 [0, 1]

// 注意:此处使用 ResizeWithCropOrPadOp 是简化写法,并非严格的 Letterbox。
// 严格 Letterbox 需手动等比缩放后用灰色(114,114,114)填充,TFLite Support 库无现成 Op。
// 对于大多数场景,此简化写法精度损失可接受;若发现边缘目标漏检,建议改用手动 Letterbox。
fun preprocess(bitmap: Bitmap): TensorImage {
    val imageProcessor = ImageProcessor.Builder()
        .add(ResizeWithCropOrPadOp(640, 640))
        .add(ResizeOp(640, 640, ResizeOp.ResizeMethod.BILINEAR))
        .add(NormalizeOp(0f, 255f))                    // 除以 255
        .add(CastOp(DataType.FLOAT32))
        .build()

    var tensorImage = TensorImage.fromBitmap(bitmap)
    tensorImage = imageProcessor.process(tensorImage)
    return tensorImage
}

2.3 模型推理与后处理

YOLOv8 的 TFLite 模型输出形状通常为 [1, 84, 8400]84 = 4 个坐标 + 80 个 COCO 类别),需要转置后做 Sigmoid 和 NMS。

class YoloV8Detector(context: Context) {
    private val interpreter: Interpreter
    private val inputSize = 640

    init {
        val options = Interpreter.Options().apply {
            numThreads = 4
            // 若设备支持 GPU,可启用 GPU Delegate
            // addDelegate(GpuDelegate())
        }
        val model = loadModel(context, "yolov8n_float16.tflite")
        interpreter = Interpreter(model, options)
    }

    fun detect(bitmap: Bitmap): List<Detection> {
        // 1. 预处理
        val input = preprocess(bitmap).tensorBuffer

        // 2. 推理(输出形状 [1, 84, 8400])
        val outputShape = intArrayOf(1, 84, 8400)
        val outputBuffer = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32)
        interpreter.run(input.buffer, outputBuffer.buffer.rewind())

        // 3. 解析输出并执行 NMS
        return parseOutput(outputBuffer.floatArray, bitmap.width, bitmap.height)
    }

    private fun parseOutput(floatArray: FloatArray, imgW: Int, imgH: Int): List<Detection> {
        val detections = mutableListOf<Detection>()
        val numAnchors = 8400
        val numClasses = 80

        for (i in 0 until numAnchors) {
            // 提取第 i 个 anchor 的数据
            val x = floatArray[i]
            val y = floatArray[numAnchors + i]
            val w = floatArray[numAnchors * 2 + i]
            val h = floatArray[numAnchors * 3 + i]

            // 找最大类别分数
            var maxScore = 0f
            var classId = 0
            for (c in 0 until numClasses) {
                val score = floatArray[numAnchors * (4 + c) + i]
                if (score > maxScore) {
                    maxScore = score
                    classId = c
                }
            }

            if (maxScore > CONFIDENCE_THRESHOLD) {
                // 将 640×640 的坐标映射回原始图像尺寸(注意 Letterbox 的偏移补偿)
                val scale = min(640f / imgW, 640f / imgH)
                val scaledW = imgW * scale
                val scaledH = imgH * scale
                val dx = (640 - scaledW) / 2
                val dy = (640 - scaledH) / 2

                val left = (x - w / 2 - dx) / scale
                val top = (y - h / 2 - dy) / scale
                val right = (x + w / 2 - dx) / scale
                val bottom = (y + h / 2 - dy) / scale

                detections.add(Detection(left, top, right, bottom, maxScore, classId))
            }
        }
        return applyNMS(detections)
    }
}

常见坑点

  • 若使用 Ultralytics 导出的 End2End TFLite 模型(带 NMS),输出形状可能不同,后处理会大幅简化,请通过 Netron 查看模型输出节点确认。
  • 坐标映射时务必补偿 Letterbox 的灰边偏移,否则检测框会整体偏移。

三、路线二:NCNN + JNI(性能优先)

3.1 NDK / CMake 配置

build.gradle 中启用 CMake:

android {
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
    ndkVersion = "25.2.9519653"
}

CMakeLists.txt 示例:

cmake_minimum_required(VERSION 3.22)
project("yolov8ncnn")

set(ncnn_DIR ${CMAKE_SOURCE_DIR}/ncnn-20240102-android-vulkan/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)

add_library(yolov8ncnn SHARED native-lib.cpp yolov8.cpp)
find_library(log-lib log)

target_link_libraries(yolov8ncnn ncnn jnigraphics ${log-lib})

依赖获取

  • NCNN Android 预编译库:ncnn releases 下载带 Vulkan 的版本。
  • OpenCV Android SDK:建议从 OpenCV 官网 下载,通过 Android Studio 的 "Import Module" 导入,而不是使用不存在的 Maven 坐标。

3.2 JNI 层实现(修正版)

初学者常见错误:示例代码在每次 detect() 内都执行 ncnn::Net yolov8; yolov8.load_param(...)。这会导致每次推理都重新读取磁盘并解析模型结构,实时性完全不可接受。正确做法是在类初始化时加载一次,后续反复复用。

以下给出修正后的核心结构:

// yolov8.h
class YoloV8 {
public:
    int load(const char* paramPath, const char* binPath);
    int detect(const ncnn::Mat& rgb, std::vector<Object>& objects,
               float probThreshold = 0.25f, float nmsThreshold = 0.45f);
private:
    ncnn::Net net;  // 模型只加载一次,反复复用
    int targetSize = 640;
};
// yolov8.cpp
int YoloV8::load(const char* paramPath, const char* binPath) {
    net.clear();
    net.opt.use_vulkan_compute = true;  // 启用 Vulkan GPU 加速
    net.opt.num_threads = 4;            // 多核并行
    int ret = net.load_param(paramPath);
    if (ret != 0) return ret;
    return net.load_model(binPath);
}

int YoloV8::detect(const ncnn::Mat& rgb, std::vector<Object>& objects,
                   float probThreshold, float nmsThreshold) {
    // 1. Letterbox 缩放
    int w = rgb.w;
    int h = rgb.h;
    float scale = std::min(targetSize / (float)w, targetSize / (float)h);
    int dstW = static_cast<int>(w * scale);
    int dstH = static_cast<int>(h * scale);

    ncnn::Mat in = ncnn::Mat::from_pixels_resize(
        rgb.data, ncnn::Mat::PIXEL_RGB, w, h, dstW, dstH);

    // 填充灰边到 640x640(注意:以下为伪代码示意,实际应使用 ncnn::copy_make_border)
    int dx = (targetSize - dstW) / 2;
    int dy = (targetSize - dstH) / 2;
    ncnn::Mat in_pad;
    ncnn::copy_make_border(in, in_pad, dy, targetSize - dstH - dy, 
                           dx, targetSize - dstW - dx, 
                           ncnn::BORDER_CONSTANT, 114.f);

    // 归一化 [0,1]
    const float normVals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
    in_pad.substract_mean_normalize(0, normVals);

    // 2. 推理
    ncnn::Extractor ex = net.create_extractor();
    ex.input("in0", in_pad);  // 输入节点名需与模型一致

    ncnn::Mat out;
    ex.extract("out0", out);  // 输出节点名需与模型一致

    // 3. 解析 YOLOv8 输出 (anchor-free, 解耦头)
    // out 维度: [1, 84, 8400] 或 NCNN 优化后的多尺度输出
    // 此处省略具体解析循环,核心逻辑与 TFLite 路线类似:
    //    - 提取 x, y, w, h + 80 类分数
    //    - 找最大类别分数
    //    - 做 NMS
    // 详细实现可参考下方 Demo 项目

    return 0;
}

3.3 Kotlin 调用层

class NativeDetector {
    init {
        System.loadLibrary("yolov8ncnn")
    }
    external fun initModel(paramPath: String, binPath: String): Boolean
    external fun detect(bitmap: Bitmap): Array<Detection>
}

与 Camera 结合

  • 若使用 CameraX,在 ImageAnalysis.Analyzer 中获取 ImageProxy,转换为 Bitmapncnn::Mat 后传入 JNI。
  • 若使用 Camera2,需自行管理 ImageReader 和线程,代码量较大,不推荐新项目使用。

四、性能优化技巧

优化项 TFLite 路线 NCNN 路线
GPU 加速 GpuDelegate() use_vulkan_compute = true
量化推理 INT8 / FP16 FP16 (ncnnoptimize --fp16)
多线程 numThreads = 4 num_threads = 4
输入分辨率 降低到 320×320(需重新训练/导出) 同左
后台线程 使用 Executors.newSingleThreadExecutor() 做推理 同左
内存复用 复用 ByteBuffer / TensorBuffer 复用 ncnn::Mat

特别提醒

  • 千万不要在主线程执行推理,无论模型多小,都会掉帧甚至触发 ANR。
  • 对于实时预览场景,若单帧推理耗时 > 33ms(< 30 FPS),建议开启 ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST 丢弃旧帧,避免延迟累积。

五、调试与测试

5.1 常见问题排查

现象 可能原因 排查方法
模型加载失败 路径错误、模型版本与库不匹配 确认 assets 未压缩;用 Netron 查看模型 opset
检测框整体偏移 Letterbox 补偿计算错误 对比预处理后的图片与原始图片的宽高比
帧率低 未开启量化/GPU、输入分辨率过高 Android Profiler 查看 CPU 占用;逐步降低分辨率测试
大量误检/漏检 置信度阈值不当、预处理归一化错误 固定测试图片,对比 PC 端与移动端输出值
JNI Crash Native 内存越界、Mat 维度不匹配 adb logcat | grep DEBUG 查看 backtrace

5.2 验证工具

  • Netron:查看 TFLite / ONNX / NCNN 模型的输入输出节点名称与形状。
  • Android Profiler:监控 CPU、内存、GPU 占用。
  • adb shell dumpsys meminfo <package>:查看详细内存分配。

六、参考实现

项目 技术栈 说明
ncnn-android-yolov8 NCNN + JNI YOLOv8 基础检测,含完整 NMS 和绘制逻辑
ncnn-android-yolov8-seg NCNN + JNI YOLOv8 分割任务
ultralytics/examples TFLite / CoreML / ONNX 官方导出脚本,TFLite Android 示例
android/camera-samples/CameraXTfLite TFLite + CameraX Google 官方 CameraX + TFLite 图像分类示例,可迁移到检测任务

模型转换参考https://github.com/Digital2Slave/ncnn-android-yolov8-seg/wiki/Convert-yolov8-model-to-ncnn-model


总结

  • 如果追求快速落地,优先选择 TFLite + CameraX 路线:Ultralytics 官方原生支持导出,无需编写 C++,CameraX 大幅简化相机开发。
  • 如果追求极限性能,选择 NCNN + JNI 路线:Vulkan GPU 加速和多线程优化空间更大,但开发和调试成本显著增加。
  • 无论哪条路线,预处理(Letterbox + 归一化)和后处理(NMS + 坐标映射) 都是最容易出错的环节,建议在 PC 端用 Python 写好单元测试,确保数值与移动端完全一致后再上真机调试。

本文链接:YOLOv8 在 Android 端集成实践 - https://h89.cn/archives/322.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: YOLOv8, NCNN, tensorflow

🎓 呈言英语 智能英语学习平台
📚单词学习 🎧听说练习 📖阅读理解 ✏️拼写练习 🌟 AI智能推荐 · 科学记忆曲线
🚀 立即开始免费学习

添加新评论