MediaPipe 是谷歌在 CVPR2019会议上开源的一个感知和增强现实的框架。该框架基于图,可在移动设备、工作站和服务器上跨平台运行,并支持移动 GPU 加速。

BlazeFace 为其中一个重要的组件,因为检测是绝大多数计算机视觉应用的入口。过往检测算法总是从提升性能出发,即便关注轻量级检测器的文章仍不免要在通用数据集上刷一下榜。而谷歌这篇紧贴实际需求,考虑场景限制条件,打造出一款小而专的人脸检测器。BlazeFace 输入设置为 128 × 128 128 \times 128 128×128,仅检测图中较大的人脸。换个角度看,它也可以在局部区域内检测小脸,即不考虑目标交叉情况下的跟踪。

face_detection_android_gpu

BlazeFace 是一个轻量且性能出色的人脸检测器,为移动 GPU 推理量身定制。它在旗舰设备上的运行速度为200-1000+ FPS。这种超实时性能使其能够应用于任何增强现实管道,该管道需要准确的面部感兴趣区域作为任务特定模型的输入,例如2D/3D 面部关键点或几何估计,人脸特征或表情分类,以及人脸区域分割。BlazeFace 的贡献包括:

  • 一个轻量级的特征提取网络,其灵感来自于 MobileNetV1/V2;
  • 一种对 GPU 友好的 SSD 锚点改良方案;
  • 一种改进的限制分辨率策略替代非极大值抑制。

由于前置和后置相机的焦距以及拍摄物体的典型尺寸不同,BlazeFace 为二者分别建立了模型。除了预测轴对齐的人脸矩形外,BlazeFace 模型还可以生成6个人脸关键点(眼睛中心、耳屏点、嘴部中心和鼻尖)坐标,以便估计面部旋转(滚动角度)。这样可以将旋转的面部矩形传递到视频处理管道的后续特定任务阶段,从而减轻后续处理步骤中显著平移和旋转不变性要求。

BlazeFace 模型构建时考虑下面讨论的四个重要设计因素:

增大感受野
作者注意到深度可分离卷积计算由它们的逐点部分支配。对于 s × s × c s\times s \times c s×s×c 输入张量, k × k k \times k k×k 深度卷积涉及 s 2 c k 2 s^2 c k^2 s2ck2 次乘加运算,而后续 1 × 1 1 \times 1 1×1 卷积到 d d d 输出通道包含 s 2 c d s^2 c d s2cd 次操作,是深度部分的 d / k 2 d / k^2 d/k2 倍。

在实践中,例如, Apple iPhone X 上的 Metal Performance Shaders 实现,对于 56 × 56 × 128 56\times 56\times 128 56×56×128 张量,16位浮点运算中的 3 × 3 3\times 3 3×3 深度卷积需要0.07 ms,而随后从128到128通道的 1 × 1 1\times 1 1×1 卷积为0.3 ms,是前者的4.3倍(由于固定成本和内存访问因素,这不如纯算术运算计数差异那么显著)。

这一观察表明,增加深度部分的核尺寸相对成本小。为达到特定的感受野大小,BlazeFace 在 bottleneck 中使用 5 × 5 5\times 5 5×5 卷积,以增加卷积核大小来减少所需的 bottleneck 数量。如下图所示:

blocks

  • 考虑到网络前期 bottleneck 的中间张量通道较少,BlazeFace 设计出与 ResNet 中 BasicBlock 类似的 Single BlazeBlock,以便残差连接在“扩展”(增加)的通道分辨率上执行。

  • 网络深层使用 double BlazeBlock ,降低中间通道的同时进一步加速了感受野大小的增长(参见上面的右图)。

特征提取器
BlazeFace 必须考虑较小范围的目标尺度,才能具备较低的计算需求。前置摄像头模型的特征提取器如表 Table 4所示,采用 128 × 128 128\times 128 128×128 像素的 RGB 输入,由一个2D 卷积以及后跟的5个 Single BlazeBlock 和6个 Double BlazeBlock 组成。最大张量深度(通道分辨率)为96,而最低空间分辨率为 8 × 8 8\times 8 8×8(而 SSD 将分辨率一直降低到 1 × 1 1\times 1 1×1)。

在这里插入图片描述
锚点方案

通常的做法是根据目标尺度范围在多个分辨率级别定义锚点。积极的下采样也是优化计算资源的手段。典型的 SSD 模型使用来自 1 × 1 1\times 1 1×1 2 × 2 2\times 2 2×2 4 × 4 4\times 4 4×4 8 × 8 8\times 8 8×8 16 × 16 16\times 16 16×16 特征图大小的预测。然而,池化金字塔网络(Pooling Pyramid Network,PPN)架构的成功意味着在达到某个特征映射分辨率之后,额外的计算可能是多余的。

相对于 CPU 计算,GPU 的一个关键特性是调度特定层计算的显著固定成本,对于流行的 CPU 定制架构固有的深度低分辨率层来说,这一点变得相对重要。例如,在一个实验中,作者观察到,在 MobileNetv1 的4.9ms 推理时间中,只有3.9ms 用于实际的 GPU 着色器计算。

考虑到这一点,BlazeFace 采用了另一种锚点方案:

  • 仅将特征图下采样到 8 × 8 8\times 8 8×8
  • 8 × 8 8\times 8 8×8 中的6个锚点替换原来 8 × 8 8\times 8 8×8 4 × 4 4\times 4 4×4 2 × 2 2\times 2 2×2 分辨率中的每个像素对应的2个锚点。
  • 由于人脸长宽比的变化有限,将锚框固定为1:1纵横比。

anchors
后处理
由于 BlazeFace 的特征提取器没有将分辨率降低到 8 × 8 8\times 8 8×8 以下,因此与给定目标重叠的锚点数量会随着目标大小的增加而显着增加。在典型的非极大值抑制场景中,只有一个锚点“胜出”并用作最终的算法结果。当将这种模型应用于后续视频帧时,预测倾向于在不同锚框之间波动并且表现出时间抖动(人类可感知的噪声)。

为了最大限度地减少这一现象,BlazeFace抑制算法替换为混合策略,该策略将边界框的回归参数估计为重叠预测之间的加权平均值。它几乎不会给原始的NMS算法带来额外的成本。对于这里的人脸检测任务,该调整使精度提高10%。

之前很多人不求甚解,仅根据文中表格创建网络即宣称做出算法的实现。试问检测器没有后处理,仅靠网络能运行吗?细节根本对不上。考虑到谷歌本身就已放出代码,以上不得不令人唏嘘。

WeightedNonMaxSuppression

WeightedNonMaxSuppression 即文中 blending 策略的实现。以加权方式替换原有 NMS 不是首创,但这里的实现和其他方法不完全一样,以分数作为权重进行调和。

IndexedScores 将得分及其索引成对存储。

Detection 可记录检测或跟踪的结果。
跳过分数低于阈值的检测。
Location 维护 LocationData
OverlapSimilarity 首先从 Location 提取相对框,计算两个 Location 之间的重叠相似性。它假定 Location 中已经有一个相对的框表示,因此不需要帧宽和高度来进一步标准化。

将重叠率大于阈值的检测收集到candidates

    IndexedScores remained_indexed_scores;
    remained_indexed_scores.assign(indexed_scores.begin(),
                                   indexed_scores.end());

    IndexedScores remained;
    IndexedScores candidates;
    output_detections->clear();
    while (!remained_indexed_scores.empty()) {
      const auto& detection = detections[remained_indexed_scores[0].first];
      if (options_.min_score_threshold() > 0 &&
          detection.score(0) < options_.min_score_threshold()) {
        break;
      }

      remained.clear();
      candidates.clear();
      const Location location(detection.location_data());
      // This includes the first box.
      for (const auto& indexed_score : remained_indexed_scores) {
        Location rest_location(detections[indexed_score.first].location_data());
        float similarity =
            OverlapSimilarity(options_.overlap_type(), rest_location, location);
        if (similarity > options_.min_suppression_threshold()) {
          candidates.push_back(indexed_score);
        } else {
          remained.push_back(indexed_score);
        }
      }

candidates中的边框和关键点按得分加权取均值。

      auto weighted_detection = detection;
      if (!candidates.empty()) {
        const int num_keypoints =
            detection.location_data().relative_keypoints_size();
        std::vector<float> keypoints(num_keypoints * 2);
        float w_xmin = 0.0f;
        float w_ymin = 0.0f;
        float w_xmax = 0.0f;
        float w_ymax = 0.0f;
        float total_score = 0.0f;
        for (const auto& candidate : candidates) {
          total_score += candidate.second;
          const auto& location_data =
              detections[candidate.first].location_data();
          const auto& bbox = location_data.relative_bounding_box();
          w_xmin += bbox.xmin() * candidate.second;
          w_ymin += bbox.ymin() * candidate.second;
          w_xmax += (bbox.xmin() + bbox.width()) * candidate.second;
          w_ymax += (bbox.ymin() + bbox.height()) * candidate.second;

          for (int i = 0; i < num_keypoints; ++i) {
            keypoints[i * 2] +=
                location_data.relative_keypoints(i).x() * candidate.second;
            keypoints[i * 2 + 1] +=
                location_data.relative_keypoints(i).y() * candidate.second;
          }
        }
        auto* weighted_location = weighted_detection.mutable_location_data()
                                      ->mutable_relative_bounding_box();
        weighted_location->set_xmin(w_xmin / total_score);
        weighted_location->set_ymin(w_ymin / total_score);
        weighted_location->set_width((w_xmax / total_score) -
                                     weighted_location->xmin());
        weighted_location->set_height((w_ymax / total_score) -
                                      weighted_location->ymin());
        for (int i = 0; i < num_keypoints; ++i) {
          auto* keypoint = weighted_detection.mutable_location_data()
                               ->mutable_relative_keypoints(i);
          keypoint->set_x(keypoints[i * 2] / total_score);
          keypoint->set_y(keypoints[i * 2 + 1] / total_score);
        }
      }
      remained_indexed_scores = std::move(remained);
      output_detections->push_back(weighted_detection);
    }

参考资料:

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐