如何通过JavaCV DNN的RetinaFace模型获取人脸及关键点信息
问题解决:JavaCV DNN解析RetinaFace输出获取人脸及关键点
RetinaFace的输出包含3种尺度(stride8/16/32)的检测结果,每个尺度对应三类输出:边界框预测、关键点预测、分类概率。你当前用net.forward()获取的是合并输出,更清晰的方式是按输出层名称单独获取每个层的结果,这样更容易解析。
步骤1:按层名称获取单独输出
替换原有的Mat output = net.forward();,改为逐个获取每个输出层的结果:
// 遍历所有输出层名称,逐个获取结果 for (long i = 0; i < outLayerNames.size(); i++) { String layerName = outLayerNames.get(i).getString(); Mat layerOutput = net.forward(layerName); LOGGER.info("Layer {} output shape: {}x{}x{}x{}", layerName, layerOutput.size(0), layerOutput.size(1), layerOutput.size(2), layerOutput.size(3)); }
每个输出层的维度含义:
face_rpn_cls_prob_reshape_strideXX:维度为[1, 2, H, W],其中2代表背景/人脸的概率,H和W是该尺度下的特征图尺寸(stride32对应20x20,stride16对应40x40,stride8对应80x80)face_rpn_bbox_pred_strideXX:维度为[1, 4, H, W],4代表边界框的偏移量(dx, dy, dw, dh)face_rpn_landmark_pred_strideXX:维度为[1, 10, H, W],10代表5个关键点的x/y偏移量(左眼、右眼、鼻子、左嘴角、右嘴角)
步骤2:解析分类概率,筛选人脸候选框
对每个尺度的分类概率层,遍历每个特征点,筛选出人脸概率高于阈值(比如0.7)的候选框:
float confidenceThreshold = 0.7f; // 以stride32为例,其他尺度逻辑一致 Mat clsOutput = net.forward("face_rpn_cls_prob_reshape_stride32"); // 转换为float数组方便访问 float[] clsData = new float[(int)clsOutput.total()]; clsOutput.get(0, 0, clsData); int H = (int)clsOutput.size(2); int W = (int)clsOutput.size(3); int stride = 32; for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { // 获取人脸概率(索引为1,0是背景) float score = clsData[1 * H * W + y * W + x]; if (score > confidenceThreshold) { // 计算候选框的原始坐标(基于特征图的锚点) float anchorX = (x + 0.5f) * stride; float anchorY = (y + 0.5f) * stride; // 后续需要结合bbox_pred的偏移量计算真实框坐标 } } }
步骤3:解析边界框偏移量,计算真实人脸框
对应每个候选框,从face_rpn_bbox_pred_strideXX中获取偏移量,转换为真实的人脸框坐标:
Mat bboxOutput = net.forward("face_rpn_bbox_pred_stride32"); float[] bboxData = new float[(int)bboxOutput.total()]; bboxOutput.get(0, 0, bboxData); // 接上面的循环 float dx = bboxData[0 * H * W + y * W + x]; float dy = bboxData[1 * H * W + y * W + x]; float dw = bboxData[2 * H * W + y * W + x]; float dh = bboxData[3 * H * W + y * W + x]; // 计算真实框坐标(基于锚点和偏移量) float boxCenterX = anchorX + dx * 16; // 16是RetinaFace默认锚框基础宽度 float boxCenterY = anchorY + dy * 16; float boxWidth = (float)Math.exp(dw) * 16; float boxHeight = (float)Math.exp(dh) * 16; // 转换为左上角和右下角坐标 float x1 = boxCenterX - boxWidth / 2; float y1 = boxCenterY - boxHeight / 2; float x2 = boxCenterX + boxWidth / 2; float y2 = boxCenterY + boxHeight / 2; // 转换回原始图像尺寸 float scaleX = (float)image.cols() / 640; float scaleY = (float)image.rows() / 640; x1 *= scaleX; y1 *= scaleY; x2 *= scaleX; y2 *= scaleY;
步骤4:解析关键点坐标
从face_rpn_landmark_pred_strideXX中获取5个关键点的偏移量,计算真实坐标:
Mat landmarkOutput = net.forward("face_rpn_landmark_pred_stride32"); float[] landmarkData = new float[(int)landmarkOutput.total()]; landmarkOutput.get(0, 0, landmarkData); // 接上面的循环 float[] landmarks = new float[10]; for (int k = 0; k < 5; k++) { float lx = landmarkData[2*k * H * W + y * W + x]; float ly = landmarkData[(2*k+1) * H * W + y * W + x]; landmarks[2*k] = anchorX + lx * 16; landmarks[2*k+1] = anchorY + ly * 16; // 转换回原始图像尺寸 landmarks[2*k] *= scaleX; landmarks[2*k+1] *= scaleY; } // landmarks数组依次是:左眼x、左眼y、右眼x、右眼y、鼻子x、鼻子y、左嘴角x、左嘴角y、右嘴角x、右嘴角y
步骤5:非极大值抑制(NMS)去重
不同尺度会检测到重复的人脸框,需要用NMS去除重叠框:
// 收集所有符合条件的人脸框(x1,y1,x2,y2,score),转换为OpenCV格式 List<Rect> boxes = new ArrayList<>(); List<Float> scores = new ArrayList<>(); // (通过上面步骤收集所有符合阈值的框) // 执行NMS MatOfRect boxesMat = new MatOfRect(boxes.toArray(new Rect[0])); MatOfFloat scoresMat = new MatOfFloat(scores.toArray(new Float[0])); MatOfInt indices = new MatOfInt(); float nmsThreshold = 0.3f; opencv_imgproc.dnnNMSBoxes(boxesMat, scoresMat, confidenceThreshold, nmsThreshold, indices); // 获取NMS后的结果 int[] idxArray = indices.toArray(); for (int idx : idxArray) { Rect finalBox = boxes.get(idx); float finalScore = scores.get(idx); // 对应的关键点也需要同步筛选 }
内容的提问来源于stack exchange,提问作者Jeffy Zhang
相关产品推荐
相关产品推荐

