使用C++进行算法部署,将图像传入模型时,要对图像做一些变换,使其满足推理框架支持的图像数据格式。本文总结常用处理方法及其C++代码实现。
深度学习推理时,需要将图像像素值缩放到[0, 1]之间,并做标准化处理。
代码实现:
class NormalizeImage :{
public:
// TODO: 根据自己的模型数据需求添加构造函数
// ......
void Run(cv::Mat* im, ImageBlob* data);
private:
// CHW or HWC
std::vector<float> mean_;
std::vector<float> scale_;
bool is_scale_ = true;
std::string norm_type_ = "mean_std";
};
void NormalizeImage::Run(cv::Mat* im, ImageBlob* data)
{
double e = 1.0;
if (is_scale_) {
e /= 255.0;
}
(*im).convertTo(*im, CV_32FC3, e); // 转化成3通道float32
if (norm_type_ == "mean_std") {
for (int h = 0; h < im->rows; h++) {
for (int w = 0; w < im->cols; w++) {
im->at<cv::Vec3f>(h, w)[0] =
(im->at<cv::Vec3f>(h, w)[0] - mean_[0]) / scale_[0];
im->at<cv::Vec3f>(h, w)[1] =
(im->at<cv::Vec3f>(h, w)[1] - mean_[1]) / scale_[1];
im->at<cv::Vec3f>(h, w)[2] =
(im->at<cv::Vec3f>(h, w)[2] - mean_[2]) / scale_[2];
}
}
}
}
resize将推理图像,缩放到模型所支持的尺寸大小。根据需求,可以设定是否将图像按比例缩放。
class Resize :{
public:
// TODO: 根据自己的模型数据需求添加构造函数
// ......
// Compute best resize scale for x-dimension, y-dimension
std::pair<float, float> GenerateScale(const cv::Mat& im);
void Run(cv::Mat* im, ImageBlob* data);
private:
int interp_;
bool keep_ratio_; // 是否保持原图比例进行缩放
std::vector<int> target_size_;
std::vector<int> in_net_shape_;
};
void Resize::Run(cv::Mat* im, ImageBlob* data) {
auto resize_scale = GenerateScale(*im);
cv::resize(
*im, *im, cv::Size(), resize_scale.first, resize_scale.second, interp_);
data->in_net_shape_ = { static_cast<float>(im->rows),
static_cast<float>(im->cols) };
data->im_shape_ = {
static_cast<float>(im->rows), static_cast<float>(im->cols),
};
data->scale_factor_ = {
resize_scale.second, resize_scale.first,
};
}
std::pair<float, float> Resize::GenerateScale(const cv::Mat& im) {
std::pair<float, float> resize_scale;
int origin_w = im.cols;
int origin_h = im.rows;
if (keep_ratio_) {
int im_size_max = std::max(origin_w, origin_h);
int im_size_min = std::min(origin_w, origin_h);
int target_size_max =
*std::max_element(target_size_.begin(), target_size_.end());
int target_size_min =
*std::min_element(target_size_.begin(), target_size_.end());
float scale_min =
static_cast<float>(target_size_min) / static_cast<float>(im_size_min);
float scale_max =
static_cast<float>(target_size_max) / static_cast<float>(im_size_max);
float scale_ratio = std::min(scale_min, scale_max); // 以最小值作为缩放因子
resize_scale = { scale_ratio, scale_ratio };
}
else {
resize_scale.first = static_cast < float>(2.0);
resize_scale.second = static_cast <float>(2.0);
}
return resize_scale;
}
LetterBoxResize将图像按照原图比例缩放,然后短边用常数填充,使其尺寸达到模型所要求的大小。
代码示例:
class LetterBoxResize : {
public:
// TODO: 根据自己的模型数据需求添加构造函数
// ......
void Init(const YAML::Node& item) {
target_size_ = item["target_size"].as<std::vector<int>>();
}
float GenerateScale(const cv::Mat& im);
virtual void Run(cv::Mat* im, ImageBlob* data);
private:
std::vector<int> target_size_;
std::vector<int> in_net_shape_;
};
void LetterBoxResize::Run(cv::Mat* im, ImageBlob* data) {
float resize_scale = GenerateScale(*im); // 保持比例缩放
int new_shape_w = std::round(im->cols * resize_scale);
int new_shape_h = std::round(im->rows * resize_scale);
data->im_shape_ = { static_cast<float>(new_shape_h),
static_cast<float>(new_shape_w) };
float padw = (target_size_[1] - new_shape_w) / 2.;
float padh = (target_size_[0] - new_shape_h) / 2.;
int top = std::round(padh - 0.1);
int bottom = std::round(padh + 0.1);
int left = std::round(padw - 0.1);
int right = std::round(padw + 0.1);
cv::resize(
*im, *im, cv::Size(new_shape_w, new_shape_h), 0, 0, cv::INTER_AREA);
data->in_net_shape_ = {
static_cast<float>(im->rows), static_cast<float>(im->cols),
};
cv::copyMakeBorder(*im,
*im,
top,
bottom,
left,
right,
cv::BORDER_CONSTANT,
cv::Scalar(127.5));
data->in_net_shape_ = {
static_cast<float>(im->rows), static_cast<float>(im->cols),
};
data->scale_factor_ = {
resize_scale, resize_scale,
};
}
float LetterBoxResize::GenerateScale(const cv::Mat& im) {
int origin_w = im.cols;
int origin_h = im.rows;
int target_h = target_size_[0];
int target_w = target_size_[1];
float ratio_h = static_cast<float>(target_h) / static_cast<float>(origin_h);
float ratio_w = static_cast<float>(target_w) / static_cast<float>(origin_w);
float resize_scale = std::min(ratio_h, ratio_w);
return resize_scale;
}
对待推理图像做仿射变换。
class WarpAffine : public PreprocessOp {
public:
// TODO: 根据自己的模型数据需求添加构造函数
// ......
void Run(cv::Mat* im, ImageBlob* data);
private:
int input_h_;
int input_w_;
int interp_ = 1;
bool keep_res_ = true;
int pad_ = 31;
};
void GetAffineTrans(const cv::Point2f center,
const cv::Point2f input_size,
const cv::Point2f output_size,
cv::Mat* trans) {
cv::Point2f srcTri[3];
cv::Point2f dstTri[3];
float src_w = input_size.x;
float dst_w = output_size.x;
float dst_h = output_size.y;
cv::Point2f src_dir(0, -0.5 * src_w);
cv::Point2f dst_dir(0, -0.5 * dst_w);
srcTri[0] = center;
srcTri[1] = center + src_dir;
cv::Point2f src_d = srcTri[0] - srcTri[1];
srcTri[2] = srcTri[1] + cv::Point2f(-src_d.y, src_d.x);
dstTri[0] = cv::Point2f(dst_w * 0.5, dst_h * 0.5);
dstTri[1] = cv::Point2f(dst_w * 0.5, dst_h * 0.5) + dst_dir;
cv::Point2f dst_d = dstTri[0] - dstTri[1];
dstTri[2] = dstTri[1] + cv::Point2f(-dst_d.y, dst_d.x);
*trans = cv::getAffineTransform(srcTri, dstTri);
}
void WarpAffine::Run(cv::Mat* im, ImageBlob* data) {
cv::cvtColor(*im, *im, cv::COLOR_RGB2BGR);
cv::Mat trans(2, 3, CV_32FC1);
cv::Point2f center;
cv::Point2f input_size;
int h = im->rows;
int w = im->cols;
if (keep_res_) {
input_h_ = (h | pad_) + 1;
input_w_ = (w + pad_) + 1;
input_size = cv::Point2f(input_w_, input_h_);
center = cv::Point2f(w / 2, h / 2);
}
else {
float s = std::max(h, w) * 1.0;
input_size = cv::Point2f(s, s);
center = cv::Point2f(w / 2., h / 2.);
}
cv::Point2f output_size(input_w_, input_h_);
GetAffineTrans(center, input_size, output_size, &trans);
cv::warpAffine(*im, *im, trans, cv::Size(input_w_, input_h_));
data->in_net_shape_ = {
static_cast<float>(input_h_), static_cast<float>(input_w_),
};
}
根据onnxruntime等常用的推理框架的要求,要生成tensor,需要将图像数据根据通道,储存到一段连续的内存中。使用cv::extractChannel可以根据通道,将数据抽取一段内存中。
class Permute : {
public:
void Run(cv::Mat* im, ImageBlob* data);
};
void Permute::Run(cv::Mat* im, ImageBlob* data) {
(*im).convertTo(*im, CV_32FC3);
int rh = im->rows;
int rw = im->cols;
int rc = im->channels();
(data->im_data_).resize(rc * rh * rw);
float* base = (data->im_data_).data();
for (int i = 0; i < rc; ++i) {
cv::extractChannel(*im, cv::Mat(rh, rw, CV_32FC1, base + i * rh * rw), i);
} // 根据通道将图像数据抽取到内存中,用于生成tensor
}