前言
部署基于深度学习的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,具体实现如下:
image_padded = np.ones([self.input_size[0], self.input_size[1], 3], dtype=np.float32) * 114.0
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)
image_padded[:int(image.shape[0] * r), :int(image.shape[1] * r), :] = image_resized
img = image_padded[:, :, ::-1]
img = (img - self.mean)/self.std
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]]]]]])
图像处理的顺序,首先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模块非常简单:
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++中图像预处理的首选函数了。
|