如何用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/2y1 = y_center - height/2x2 = x_center + width/2y2 = 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
相关产品推荐
相关产品推荐

