OpenCV 学习总结.番外篇:图像预处理接口 cv::dnn::blobFromImage()

目录

  • 前言
  • OpenCV 学习总结.番外篇 cv::dnn::blobFromImage()
    • python中的图像预处理
      • 常用的图像预处理方法
      • 强大的Numpy
      • YOLOX通过Numpy进行预处理
      • 被埋没的cv2.dnn.blobFromImage()
    • C++中的图像预处理
      • Mat数据重排
      • 懒人必备之cv::dnn::blobFromImage()接口

前言

部署基于深度学习的CV算法时,几乎都会涉及到输入图像预处理

在python中,图像预处理的任务基本上被numpy承包了。

But,当部署需要通过C++实现时,问题就出现了:numpy不能用,怎么样把图像转换为模型接受的格式呢?

OpenCV 学习总结.番外篇 cv::dnn::blobFromImage()

python中的图像预处理

首先需要知道为什么有图像预处理这个步骤,包括但不限于以下原因:

  • 数据集内的图片尺寸不一定相同,而训练时的输入尺寸必须固定
  • 模型输入尺寸不能太大,否则训练占用大量内存显存
  • 多通道图像的各个通道对训练目标的贡献不一定相同
  • 将训练数据放到同一个分布维度上,有利于提升泛化能力
  • 部分CV任务对光照敏感,需要通过图像处理进行调整
  • 数据增强相关

而神经网络根据数据集学习出来了一种拟合方法,在预测时,用于预测的数据自然也要符合数据集的分布。

常用的图像预处理方法

  • 尺寸修改 (resize)
  • 颜色通道互换 (swap)
  • 图像标准化 (normalize)
  • 图像维度互换 (transpose)
  • 增加维度 (expand)
  • 图像截取(crop)
  • 图像补充 (padding)
  • 数组重排 (reshape)

强大的Numpy

python中常用的图像处理库opencv和PIL都可以与numpy数组无缝衔接,pytorch、tf、paddle等深度学习框架也能够直接转换numpy数据,并且numpy数组切片和处理都十分强大,因此几乎垄断了CV里的预处理和后处理实现方法。

YOLOX通过Numpy进行预处理

YOLOX是经典目标检测系列YOLO的最新算法,它在训练和推理时的图像预处理步骤为:resize=>padding=>swap=>normalize=>transpose=>expand,具体实现如下:

# input_size是模型需要的输入尺寸,image是cv2.imread的结果
image_padded = np.ones([self.input_size[0], self.input_size[1], 3], dtype=np.float32) * 114.0
# resize
r = min(self.input_size[0]/image.shape[0], self.input_size[1]/image.shape[1])
image_resized = cv2.resize(image, (int(image.shape[1] * r), int(image.shape[0] * r)), cv2.INTER_LINEAR)
# padding
image_padded[:int(image.shape[0] * r), :int(image.shape[1] * r), :] = image_resized
# swap RB channels
img = image_padded[:, :, ::-1]
# normalize
img = (img - self.mean)/self.std
# expand
img = np.expand_dims(img.transpose(2, 0, 1), axis=0)

model_input = np.ascontiguousarray(img, dtype=np.float32)

熟悉了numpy切片操作就能轻易完成YOLOX的图像预处理步骤。

被埋没的cv2.dnn.blobFromImage()

实际上opencv本身也提供了一个预处理接口blobFromImage,python中的原型如下:

cv2.dnn.blobFromImage(image[, scalefactor[, size[, mean[, swapRB[, crop[, ddepth]]]]]])
# scalefactor:缩放因子
# size:resize的尺寸
# mean: 通道均值
# swapRB: 交换RB通道
# crop: 截取区域
# ddepth: 输出图像数据格式,cv2.CV_8U or cv2.CV_32F

图像处理的顺序,首先resize,然后swapRB,接着减mean,再乘以scalefactor。

最后将返回一个blob,即四维数组,shape为[1, channels, size[0], size[1]]。

其实这个blob也是一个numpy.ndarray,因此可以被numpy完美替代(numpy能做的,它不一定能搞定)

C++中的图像预处理

C++中没有numpy一样方便的数组切片和访问的方法,访问opencv Mat格式的方法依赖指针,因此涉及到维度的预处理操作就显得格外复杂。

例如YOLOX和YOLOv5模型中的Foucs模块,由于许多AI芯片并不支持这个focus的切片操作(硬件op算子稍落后于学术),因此必须在图像预处理时提前实现这个操作。

在python中,这个focus模块非常简单:

# focus模块
img_focus = np.concatnate([image[::2, ::2, :], image[1::2, ::2, :], image[::2, 1::2, :], image[1::2, 1::2, :]], axis=-1)

但是C++ opencv Mat的实现就非常麻烦,需要借助到指针进行Mat数据重排

Mat数据重排

focus操作实际上是把一张图片拆成四张等大小图(等同于downsample),然后把这四张小图从上到下拼接起来,因此同时涉及到尺寸(downsample)、通道(拼接)两个方面的预处理。

那么数据重排的思路就是,将原Mat宽高中的downsample出的数据,贴到新Mat的通道上,C++实现如下

Mat focusImage(vector<Mat> srcChannels) {
    Mat focusMat(YOLOArgs::modelHeight/2, YOLOArgs::modelWidth/2, CV_32FC(12), 0.0);
    int startPt[4][2] = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}, startX, startY;
    for (size_t i = 0; i < YOLOArgs::modelHeight / 2; i++)
    {
        for (size_t j = 0; j < YOLOArgs::modelWidth / 2; j++)
        {
            for (size_t k = 0; k < 4; k++)
            {
                for (size_t s = 0; s < srcChannels.size(); s++)
                {
                    startX = startPt[k][0];
                    startY = startPt[k][1];
                    focusMat.at<Vec<float, 12>>(i, j)[4 * s + k] = srcChannels[s].at<float>(2 * i + startY, 2 * j + startX);
                }
            }
        }
    }
    return focusMat;
}

上面这个代码用了四个for循环,和python numpy相比可以说是非常麻烦了。

懒人必备之cv::dnn::blobFromImage()接口

虽然focus模块在其它模型中并不常见,但是另一个预处理操作则十分常见,即transpose,将模型输入格式由NHWC转换为NCHW。

根据上面的Mat数据重排思路,transpose也需要三个for循环来进行重排。此时,在python里坐冷板凳的blobFromImage()接口就派上用场了。

cv::dnn::blobFromImage()除了transpose外,还能完成resize,swapRB,scale,crop,可以说是C++中图像预处理的首选函数了。

你可能感兴趣的:(OpenCV,python,opencv,计算机视觉)