You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用OpenCV DNN从YOLOv8 ONNX模型提取边界框、置信度与类别标签

YOLOv8 ONNX输出解析与OpenCV后处理方案

一、理解YOLOv8 ONNX输出结构

你的模型输出是float32[1,5,8400],各维度含义明确:

  • 1:单张图片输入的批次大小
  • 5:每个候选框的5个参数,顺序为框中心x坐标、框中心y坐标、框宽度、框高度、置信度分数
  • 8400:模型生成的所有候选框总数

注:如果是多分类模型(如COCO 80类),输出维度通常为[1,85,8400](5个框参数+80个类别概率),你的输出为5维,说明是单分类模型或转换时仅保留了置信度参数。

二、通用解析与后处理步骤

1. 提取输出张量数据

将OpenCV返回的三维Mat转换为可遍历的数组,方便逐个读取候选框参数。

2. 坐标转换

模型输出的是基于640x640输入尺寸的相对坐标,需转换为原始图像的绝对坐标:

  • 把x_center、width乘以原始图像宽度,y_center、height乘以原始图像高度
  • 将中心坐标转为左上角/右下角坐标:
    x1 = x_center - width/2
    y1 = y_center - height/2
    x2 = x_center + width/2
    y2 = y_center + height/2

3. 置信度过滤

设置阈值(如0.5),筛除置信度低于阈值的无效候选框。

4. 非极大值抑制(NMS)

用OpenCV内置的Dnn.NMSBoxes去除重叠度高的重复框,保留最优检测结果。

三、适配OpenCV的Kotlin实现代码

修改你的Detector类,加入完整的输出解析与后处理逻辑:

import org.opencv.core.*
import org.opencv.dnn.Dnn
import org.opencv.dnn.Net
import org.opencv.imgproc.Imgproc
import org.opencv.android.Utils
import android.graphics.Bitmap
import java.io.File
import java.io.FileOutputStream

class Detector(private val context: Context) {
    private var net: Net? = null
    // 可根据需求调整的参数
    private val confThreshold = 0.5f // 置信度过滤阈值
    private val nmsThreshold = 0.4f // NMS重叠度阈值
    private val inputSize = Size(640.0, 640.0)

    // 检测结果数据类
    data class DetectionResult(
        val x1: Float,
        val y1: Float,
        val x2: Float,
        val y2: Float,
        val confidence: Float,
        val classId: Int = 0 // 单分类默认类别ID为0
    )

    fun detect(frame: Bitmap): List<DetectionResult> {
        val results = mutableListOf<DetectionResult>()
        val originalWidth = frame.width.toFloat()
        val originalHeight = frame.height.toFloat()

        // 图像预处理
        val mat = Mat()
        val resizedBitmap = Bitmap.createScaledBitmap(frame, inputSize.width.toInt(), inputSize.height.toInt(), true)
        Utils.bitmapToMat(resizedBitmap, mat)
        Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2RGB)
        val inputBlob = Dnn.blobFromImage(mat, 1.0/255.0, inputSize, Scalar(0.0), true, false)
        net?.setInput(inputBlob)
        val outputBlob = net?.forward() ?: return results

        // 解析输出张量
        val outputMat = outputBlob.reshape(1, 5) // 将1x5x8400转为5x8400的二维Mat
        val data = FloatArray(outputMat.total().toInt())
        outputMat.get(0, 0, data)

        val boxes = mutableListOf<Rect2d>()
        val confidences = mutableListOf<Float>()

        // 遍历所有候选框
        for (i in 0 until 8400) {
            val offset = i * 5
            val xCenter = data[offset] * originalWidth
            val yCenter = data[offset + 1] * originalHeight
            val width = data[offset + 2] * originalWidth
            val height = data[offset + 3] * originalHeight
            val confidence = data[offset + 4]

            // 置信度过滤
            if (confidence >= confThreshold) {
                val x1 = xCenter - width / 2
                val y1 = yCenter - height / 2
                boxes.add(Rect2d(x1, y1, x2 - x1, y2 - y1))
                confidences.add(confidence)
            }
        }

        // 执行NMS去重
        val indices = Mat()
        Dnn.NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices)

        // 提取最终有效检测结果
        for (i in 0 until indices.rows()) {
            val idx = indices.get(i, 0)[0].toInt()
            val box = boxes[idx]
            results.add(
                DetectionResult(
                    x1 = box.x.toFloat(),
                    y1 = box.y.toFloat(),
                    x2 = (box.x + box.width).toFloat(),
                    y2 = (box.y + box.height).toFloat(),
                    confidence = confidences[idx]
                )
            )
        }

        // 释放资源
        mat.release()
        outputMat.release()
        indices.release()
        return results
    }

    fun setupDetector() {
        val modelFile = File(context.cacheDir, MODEL_NAME)
        if (!modelFile.exists()) {
            try {
                context.assets.open(MODEL_NAME).use { inputStream ->
                    FileOutputStream(modelFile).use { outputStream ->
                        outputStream.write(inputStream.readBytes())
                    }
                }
                net = Dnn.readNetFromONNX(modelFile.absolutePath)
                // 可选:设置推理后端(如CPU/GPU/NPU)
                // net?.setPreferableBackend(Dnn.DNN_BACKEND_OPENCV)
                // net?.setPreferableTarget(Dnn.DNN_TARGET_CPU)
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        } else {
            net = Dnn.readNetFromONNX(modelFile.absolutePath)
        }
    }

    companion object {
        private const val MODEL_NAME = "model.onnx"
    }
}

四、多分类模型适配(若后续扩展)

如果模型为多分类(输出维度[1,85,8400]),只需调整解析逻辑:

  • 每个候选框参数为x_center, y_center, width, height, confidence, class1_prob...classN_prob
  • 遍历候选框时,找到概率最高的类别ID,将类别概率与置信度相乘得到最终分数
  • 修改DetectionResult类,加入className字段(需提前准备类别名称映射表)

关键注意事项

  • 预处理的归一化规则、输入尺寸、通道顺序必须与模型训练时一致(YOLOv8默认RGB通道、归一化到0-1、640x640输入)
  • 坐标转换必须使用原始图像的尺寸,而非模型输入尺寸
  • 调整confThreshold和nmsThreshold可平衡检测精度与召回率

内容的提问来源于stack exchange,提问作者kallis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 10:18:14