在《OpenVINO获取模型输入节点信息》获得了如下结论:图像的Resize、Normalize、transpose、expand等预处理功能需要手动写一个preprocess函数来实现。
在Python中,由于存在强大的Numpy工具包,使得数组的切片、访问、广播都很容易实现,如下面代码所示:
def preprocess(self, image_file):
"""
输入的图像进行预处理,包括resize, 归一化
并返回模型所需的3个输入
image: 预处理后的图像数据
im_shape: 预处理后的图像大小
scale_factor: 原图在处理后,分别在高和宽上的缩放系数
"""
import cv2
def resize(im, height, width, interp=cv2.INTER_CUBIC):
scale_h = height / float(im.shape[0])
scale_w = width / float(im.shape[1])
im = cv2.resize(
im, None, None, fx=scale_w, fy=scale_h, interpolation=interp)
return im, scale_h, scale_w
def normalize(im, mean, std, is_scale=True):
if is_scale:
im = im / 255.0
im -= mean
im /= std
return im
im = cv2.imread(image_file)
if im is None:
raise Exception("Can not read image file: {} by cv2.imread".format(
image_file))
im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
im, scale_h, scale_w = resize(im, self.model_input_shape[0],
self.model_input_shape[1])
im = normalize(im, self.mean, self.std)
# 数据格式由HWC转为NCHW
im = im.transpose((2, 0, 1))
im = np.expand_dims(im, axis=0)
但是,在C++中,没有Numpy一样的工具包,访问OpenCV Mat对象必须依赖指针,或者按像素处理,这就使得涉及维度的预处理操作格外复杂,例如,没有数组广播操作的C++,要归一化图像,必须手动处理每个像素,如下所示。这样的代码开发,显然过于复杂。
void NormalizeImage::Run(cv::Mat* im, ImageBlob* data) {
double e = 1.0;
if (is_scale_) {
e /= 255.0;
}
(*im).convertTo(*im, CV_32FC3, e);
for (int h = 0; h < im->rows; h++) {
for (int w = 0; w < im->cols; w++) {
im->at(h, w)[0] =
(im->at(h, w)[0] - mean_[0]) / scale_[0];
im->at(h, w)[1] =
(im->at(h, w)[1] - mean_[1]) / scale_[1];
im->at(h, w)[2] =
(im->at(h, w)[2] - mean_[2]) / scale_[2];
}
}
}
另外,将模型输入格式由HWC转换为NCHW的操作,即先transpose,再expand,Python 中只需两行代码,如下所示:
# 数据格式由HWC转为NCHW
im = im.transpose((2, 0, 1))
im = np.expand_dims(im, axis=0)
但C++中,需要中指针来重排数据,非常麻烦,如下所示:
Mat RGBImg, ResizeImg;
cvtColor(MatBGRImage, RGBImg, COLOR_BGR2RGB);
cv::resize(RGBImg, ResizeImg, Size(224, 224));
// mean_rgb = [0.485, 0.456, 0.406]
// std_rgb = [0.229, 0.224, 0.225]
int channels = ResizeImg.channels(), height = ResizeImg.rows, width = ResizeImg.cols;
float* nchwMat = (float*)malloc(channels * height * width * sizeof(float));
memset(nchwMat, 0, channels * height * width * sizeof(float));
// Convert HWC to CHW and Normalize
float mean_rgb[3] = {0.485, 0.456, 0.406};
float std_rgb[3] = {0.229, 0.224, 0.225};
uint8_t* ptMat = ResizeImg.ptr(0);
int area = height * width;
for (int c = 0; c < channels; ++c)
{
for (int h = 0; h < height; ++h)
{
for (int w = 0; w < width; ++w)
{
int srcIdx = c * area + h * width + w;
int divider = srcIdx / 3; // 0, 1, 2
for (int i = 0; i < 3; ++i)
{
nchwMat[divider + i * area] = static_cast((ptMat[srcIdx] * 1.0f/255.0f - mean_rgb[i]) * 1.0f/std_rgb[i] );
}
}
}
}
为了方便AI开发者实现图像预处理,OpenCV在3.3之后的版本,提供了一个blobFromImage函数还实现图像预处理,其作用如下:
Creates 4-dimensional blob from image. Optionally resizes and crops image from center, subtract mean values, scales values by scalefactor, swap Blue and Red channels.
由上可知,blobFromImage函数的作用主要是用来对图片进行减均值和缩放,并交换蓝红通道的预处理:
- 整体像素值减去平均值(mean)
- 通过缩放系数(scalefactor)对图片像素值进行缩放
- 交换蓝红通道(可选)
范例程序代码:
// OpenVINO Sample code for PPYOLOv2
#include
#include
#include
飞桨模型输入概要:
- input node's name and shape:
a. image: [B, C, H, W],输入图片, RGB格式
b. im_shape: [B, 2], 图片resize之后的模型尺寸,h, w
c. scale_factor: [B, 2], 图片resize的缩放因子,scale_y, scale_x - 图片均值mean一般是[0.485, 0.456, 0.406],方差std一般是[0.229, 0.224, 0.225],百度模型全用的是imagenet的mean和std
- is_scale表示在做图片归一化之前,图片的值先除以255.
- interp的方法默认是2,即cv2.INTER_LINEAR
飞桨模型输出概要:飞桨模型输出名字在经过ONNX转化后经过前向的pass优化的,所以最终输出的名字对不上
参考链接:PaddleDetection模型导出教程