部署基于深度学习的CV算法时,几乎都会涉及到输入图像预处理。
在python中,图像预处理的任务基本上被numpy承包了。
But,当部署需要通过C++实现时,问题就出现了:numpy不能用,怎么样把图像转换为模型接受的格式呢?
首先需要知道为什么有图像预处理这个步骤,包括但不限于以下原因:
而神经网络根据数据集学习出来了一种拟合方法,在预测时,用于预测的数据自然也要符合数据集的分布。
python中常用的图像处理库opencv和PIL都可以与numpy数组无缝衔接,pytorch、tf、paddle等深度学习框架也能够直接转换numpy数据,并且numpy数组切片和处理都十分强大,因此几乎垄断了CV里的预处理和后处理实现方法。
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的图像预处理步骤。
实际上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++中没有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数据重排
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相比可以说是非常麻烦了。
虽然focus模块在其它模型中并不常见,但是另一个预处理操作则十分常见,即transpose,将模型输入格式由NHWC转换为NCHW。
根据上面的Mat数据重排思路,transpose也需要三个for循环来进行重排。此时,在python里坐冷板凳的blobFromImage()接口就派上用场了。
cv::dnn::blobFromImage()除了transpose外,还能完成resize,swapRB,scale,crop,可以说是C++中图像预处理的首选函数了。