OpenCV C++案例,全景拼接及人脸打马赛克

涛涛CV

共 8631字,需浏览 18分钟

 ·

2022-06-10 10:20

关注涛涛CV,设置星标,更新错过




作者Zero___Chen

来源blog.csdn.net/Zero___Chen

本文将使用OpenCV C++ 进行图像全景拼接。


目前使用OpenCV对两幅图像进行拼接大致可以分为两类。

一、使用OpenCV内置API Stitcher 进行拼接。

二、使用特征检测算法匹配两幅图中相似的点、计算变换矩阵、最后对其进行透视变换就可以了。


一、OpenCV Stitcher

image_left

f195d177a78eb4ab4bd188362f7e2131.webp

image_right

b5b76a4cd226cca6969a4739371cce53.webp

原图如图所示。本案例的需求是将上述两幅图片拼接成一幅图像。首先使用OpenCV提供的Stitcher进行拼接。关于Stitcher的具体原理请大家自行查找相关资料。


1.功能源码

bool OpenCV_Stitching(Mat image_left, Mat image_right){  //将待拼接图片放进容器里面  vectorimages;  images.push_back(image_left);  images.push_back(image_right);
//创建Stitcher模型 Ptrstitcher = Stitcher::create();
Mat result; Stitcher::Status status = stitcher->stitch(images, result);// 使用stitch函数进行拼接
if (status != Stitcher::OK) return false;
imshow("OpenCV图像全景拼接", result);
return true;}


2.效果

7400ff5de8a4caa05ad28357fcf921f6.webp

这就是使用OpenCV 内置Stitcher拼接出来的效果。


二、图像全景拼接

1.特征检测

使用方法二进行图像全景拼接。由于我们需要用到SIFT或SURF特征检测算子,故我们需要提前配置nonfree模块,关于如何配置nonfree模块请大家自行查阅资料,网上教程也有很多。


目前网上教程大致流程归为:


1、使用特征检测算子提取两幅图像的关键点,然后进行特征描述子匹配。我这里使用的是SURF算子。当然SIFT等其他特征检测算子也可以。

  //创建SURF特征检测器  int Hessian = 800;  Ptrdetector = SURF::create(Hessian);
//进行图像特征检测、特征描述 vectorkeypoint_left, keypoint_right; Mat descriptor_left, descriptor_right; detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left); detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);
//使用FLANN算法进行特征描述子的匹配 FlannBasedMatcher matcher; vectormatches;  matcher.match(descriptor_left, descriptor_right, matches);

     

5c5f9e0f6c13d500859be067d58921cc.webp

如图为使用FLANN算法进行特征描述子匹配的结果。我们需要把那些匹配程度高的关键点筛选出来用以下面计算两幅图像的单应性矩阵。


2、筛选出匹配程度高的关键点

  double Max = 0.0;  for (int i = 0; i < matches.size(); i++)  {    //float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。    double dis = matches[i].distance;    if (dis > Max)    {      Max = dis;    }  }
//筛选出匹配程度高的关键点 vectorgoodmatches; vectorgoodkeypoint_left, goodkeypoint_right; for (int i = 0; i < matches.size(); i++) { double dis = matches[i].distance; if (dis < 0.15*Max) { /* 以右图做透视变换 左图->queryIdx:查询点索引(查询图像) 右图->trainIdx:被查询点索引(目标图像) */ //注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx //int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。 goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt); //int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。 goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt); goodmatches.push_back(matches[i]); }  }

   

如图为image_left筛选出来的关键点。

d606af7817471cba5d6bee7c00bbd988.webp

如图为image_right筛选出来的关键点。

79b9f088308b4b0da1d597b6b8c21ebb.webp

从上图可以看出,我们已经筛选出image_left,image_right共有的关键点部分。接下来,我们需要使用这两个点集计算两幅图的单应性矩阵。


2.计算单应性矩阵

3、计算单应性变换矩阵

  //获取图像right到图像left的投影映射矩阵,尺寸为3*3  //注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left  H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);


3.透视变换

4、根据计算出来的单应性矩阵对image_right进行透视变换


  //对image_right进行透视变换  warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows));  imshow("透视变换", WarpImg);

a5a556e7ba3f66126ce4b3d636f3c1b2.webp

如图所示为image_right进行透视变换得到的结果。


4.图像拼接

5、根据上述操作,我们已经得到了经透视变换的WarpImg,接下来只需将image_left与WarpImg拼接起来就可以了。


//将image_left拷贝到透视变换后的图片上,完成图像拼接image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows)));imshow("图像全景拼接", DstImg);


5.功能源码

bool Image_Stitching(Mat image_left, Mat image_right, Mat& H, Mat & WarpImg, Mat &DstImg, bool draw){  //创建SURF特征检测器  int Hessian = 800;  Ptrdetector = SURF::create(Hessian);
//进行图像特征检测、特征描述 vectorkeypoint_left, keypoint_right; Mat descriptor_left, descriptor_right; detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left); detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);
//使用FLANN算法进行特征描述子的匹配 FlannBasedMatcher matcher; vectormatches; matcher.match(descriptor_left, descriptor_right, matches);
double Max = 0.0; for (int i = 0; i < matches.size(); i++) { //float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。 double dis = matches[i].distance; if (dis > Max) { Max = dis; } }
//筛选出匹配程度高的关键点 vectorgoodmatches; vectorgoodkeypoint_left, goodkeypoint_right; for (int i = 0; i < matches.size(); i++) { double dis = matches[i].distance; if (dis < 0.15*Max) { /* 以右图做透视变换 左图->queryIdx:查询点索引(查询图像) 右图->trainIdx:被查询点索引(目标图像) */ //注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx //int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。 goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt); //int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。 goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt); goodmatches.push_back(matches[i]); } }
//绘制特征点 if (draw) { Mat result; drawMatches(image_left, keypoint_left, image_right, keypoint_right, goodmatches, result); imshow("特征匹配", result);
Mat temp_left = image_left.clone(); for (int i = 0; i < goodkeypoint_left.size(); i++) { circle(temp_left, goodkeypoint_left[i], 3, Scalar(0, 255, 0), -1); } imshow("goodkeypoint_left", temp_left);
Mat temp_right = image_right.clone(); for (int i = 0; i < goodkeypoint_right.size(); i++) { circle(temp_right, goodkeypoint_right[i], 3, Scalar(0, 255, 0), -1); } imshow("goodkeypoint_right", temp_right); }
//findHomography计算单应性矩阵至少需要4个点 /* 计算多个二维点对之间的最优单映射变换矩阵H(3x3),使用MSE或RANSAC方法,找到两平面之间的变换矩阵 */ if (goodkeypoint_left.size() < 4 || goodkeypoint_right.size() < 4) return false;

//获取图像right到图像left的投影映射矩阵,尺寸为3*3 //注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);
//对image_right进行透视变换 warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows)); imshow("透视变换", WarpImg);
DstImg = WarpImg.clone(); //将image_left拷贝到透视变换后的图片上,完成图像拼接 image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows))); imshow("图像全景拼接", DstImg);
return true;}


6.效果

a4ca57e656708745ac047e8160f8e464.webp

如上图所示,我们已经完成了图像拼接。但是从上图可以看出,拼接效果有明显的拼接缝,故我们还需要对拼接结果进行图像融合,使拼接效果看起来更自然。


三、图像融合

如图所示,我们需要对红线区域进行融合。即将image_left与WarpImg重叠区域进行像素融合,使拼接效果过渡更加自然。

2068a3478446b1da2432d5146d2a1741.webp

//图像融合,使得拼接自然bool OptimizeSeam(Mat H, Mat& image_left, Mat& WarpImg, Mat& DstImg){  //透视变换左上角(0,0,1)  Mat V2 = (Mat_<double>(3, 1) << 0.0, 0.0, 1.0);  Mat V1 = H * V2;  int left_top = V1.at<double>(0, 0) / V1.at<double>(2, 0);  if (left_top < 0)left_top = 0;
//透视变换左下角(0,src.rows,1) V2 = (Mat_<double>(3, 1) << 0.0, image_left.rows, 1.0); V1 = H * V2; int left_bottom = V1.at<double>(0, 0) / V1.at<double>(2, 0); if (left_bottom < 0)left_bottom = 0;
int start = MAX(left_top, left_bottom);//开始位置,即重叠区域的左边界
double Width = (image_left.cols - start);//重叠区域的宽度
//line(WarpImg, Point(start, 0), Point(start, WarpImg.rows), Scalar(0, 0, 255), 2); //line(WarpImg, Point(image_left.cols, 0), Point(image_left.cols, WarpImg.rows), Scalar(0, 0, 255), 2); //图像加权融合,通过改变alpha修改image_left与WarpImg像素权重,达到融合效果 double alpha = 1.0; for (int i = 0; i < DstImg.rows; i++) { for (int j = start; j < image_left.cols; j++) { for (int c = 0; c < 3; c++) { //如果图像WarpImg像素为0,则完全拷贝image_left if (WarpImg.at(i, j)[c] == 0) { alpha = 1.0; } else { double l = Width - (j - start); //重叠区域中某一像素点到拼接缝的距离 alpha = l / Width; } DstImg.at(i, j)[c] = image_left.at(i, j)[c] * alpha + WarpImg.at(i, j)[c] * (1.0 - alpha); } } }
imshow("图像融合", DstImg); return true;}

1.效果

bfafc1367b942600d74e4e5125b8f653.webp

如图为最终融合效果,基本看不出拼接缝啦。


四、源码

#include#include#include#includeusing namespace std;using namespace cv;using namespace cv::xfeatures2d;
//1、使用特征检测算法找到两张图像中相似的点,计算变换矩阵//2、将图像right透视变换后得到的图片与图像left拼接

bool Image_Stitching(Mat image_left, Mat image_right, Mat& H, Mat & WarpImg, Mat &DstImg, bool draw){ //创建SURF特征检测器 int Hessian = 800; Ptrdetector = SURF::create(Hessian);
//进行图像特征检测、特征描述 vectorkeypoint_left, keypoint_right; Mat descriptor_left, descriptor_right; detector->detectAndCompute(image_left, Mat(), keypoint_left, descriptor_left); detector->detectAndCompute(image_right, Mat(), keypoint_right, descriptor_right);
//使用FLANN算法进行特征描述子的匹配 FlannBasedMatcher matcher; vectormatches; matcher.match(descriptor_left, descriptor_right, matches);
double Max = 0.0; for (int i = 0; i < matches.size(); i++) { //float distance –>代表这一对匹配的特征点描述符(本质是向量)的欧氏距离,数值越小也就说明两个特征点越相像。 double dis = matches[i].distance; if (dis > Max) { Max = dis; } }
//筛选出匹配程度高的关键点 vectorgoodmatches; vectorgoodkeypoint_left, goodkeypoint_right; for (int i = 0; i < matches.size(); i++) { double dis = matches[i].distance; if (dis < 0.15*Max) { /* 以右图做透视变换 左图->queryIdx:查询点索引(查询图像) 右图->trainIdx:被查询点索引(目标图像) */ //注:对image_right图像做透视变换,故goodkeypoint_left对应queryIdx,goodkeypoint_right对应trainIdx //int queryIdx –>是测试图像的特征点描述符(descriptor)的下标,同时也是描述符对应特征点(keypoint)的下标。 goodkeypoint_left.push_back(keypoint_left[matches[i].queryIdx].pt); //int trainIdx –> 是样本图像的特征点描述符的下标,同样也是相应的特征点的下标。 goodkeypoint_right.push_back(keypoint_right[matches[i].trainIdx].pt); goodmatches.push_back(matches[i]); } }
//绘制特征点 if (draw) { Mat result; drawMatches(image_left, keypoint_left, image_right, keypoint_right, goodmatches, result); imshow("特征匹配", result);
Mat temp_left = image_left.clone(); for (int i = 0; i < goodkeypoint_left.size(); i++) { circle(temp_left, goodkeypoint_left[i], 3, Scalar(0, 255, 0), -1); } imshow("goodkeypoint_left", temp_left);
Mat temp_right = image_right.clone(); for (int i = 0; i < goodkeypoint_right.size(); i++) { circle(temp_right, goodkeypoint_right[i], 3, Scalar(0, 255, 0), -1); } imshow("goodkeypoint_right", temp_right); }
//findHomography计算单应性矩阵至少需要4个点 /* 计算多个二维点对之间的最优单映射变换矩阵H(3x3),使用MSE或RANSAC方法,找到两平面之间的变换矩阵 */ if (goodkeypoint_left.size() < 4 || goodkeypoint_right.size() < 4) return false;

//获取图像right到图像left的投影映射矩阵,尺寸为3*3 //注意顺序,srcPoints对应goodkeypoint_right,dstPoints对应goodkeypoint_left H = findHomography(goodkeypoint_right, goodkeypoint_left, RANSAC);
//对image_right进行透视变换 warpPerspective(image_right, WarpImg, H, Size(image_right.cols + image_left.cols, image_right.rows)); imshow("透视变换", WarpImg);
DstImg = WarpImg.clone(); //将image_left拷贝到透视变换后的图片上,完成图像拼接 image_left.copyTo(DstImg(Rect(0, 0, image_left.cols, image_left.rows))); imshow("图像全景拼接", DstImg);
return true;}

//图像融合,使得拼接自然bool OptimizeSeam(Mat H, Mat& image_left, Mat& WarpImg, Mat& DstImg){ //透视变换左上角(0,0,1) Mat V2 = (Mat_<double>(3, 1) << 0.0, 0.0, 1.0); Mat V1 = H * V2; int left_top = V1.at<double>(0, 0) / V1.at<double>(2, 0); if (left_top < 0)left_top = 0;
//透视变换左下角(0,src.rows,1) V2 = (Mat_<double>(3, 1) << 0.0, image_left.rows, 1.0); V1 = H * V2; int left_bottom = V1.at<double>(0, 0) / V1.at<double>(2, 0); if (left_bottom < 0)left_bottom = 0;
int start = MAX(left_top, left_bottom);//开始位置,即重叠区域的左边界
double Width = (image_left.cols - start);//重叠区域的宽度
//line(WarpImg, Point(start, 0), Point(start, WarpImg.rows), Scalar(0, 0, 255), 2); //line(WarpImg, Point(image_left.cols, 0), Point(image_left.cols, WarpImg.rows), Scalar(0, 0, 255), 2); //图像加权融合,通过改变alpha修改image_left与WarpImg像素权重,达到融合效果 double alpha = 1.0; for (int i = 0; i < DstImg.rows; i++) { for (int j = start; j < image_left.cols; j++) { for (int c = 0; c < 3; c++) { //如果图像WarpImg像素为0,则完全拷贝image_left if (WarpImg.at(i, j)[c] == 0) { alpha = 1.0; } else { double l = Width - (j - start); //重叠区域中某一像素点到拼接缝的距离 alpha = l / Width; } DstImg.at(i, j)[c] = image_left.at(i, j)[c] * alpha + WarpImg.at(i, j)[c] * (1.0 - alpha); } } }
imshow("图像融合", DstImg); return true;}
bool OpenCV_Stitching(Mat image_left, Mat image_right){ //将待拼接图片放进容器里面 vectorimages; images.push_back(image_left); images.push_back(image_right);
//创建Stitcher模型 Ptrstitcher = Stitcher::create();
Mat result; Stitcher::Status status = stitcher->stitch(images, result);// 使用stitch函数进行拼接
if (status != Stitcher::OK) return false;
imshow("OpenCV图像全景拼接", result);
return true;}
int main(){
Mat image_left = imread("left.jpg"); Mat image_right = imread("right.jpg"); if (image_left.empty() || image_right.empty()) { cout << "No Image!" << endl; system("pause"); return -1; }
Mat H, WarpImg, DstImg; if (Image_Stitching(image_left, image_right, H, WarpImg, DstImg, false)) { if (!OptimizeSeam(H, image_left, WarpImg, DstImg)) { cout << "Image fusion is not possible! " << endl; } } else { cout << "can not stitching the image!" << endl; }

if (!OpenCV_Stitching(image_left, image_right)) { cout << "can not stitching the image!" << endl; }
waitKey(0); system("pause"); return 0;}



总结

本文使用OpenCV C++进行图像全景拼接,关键步骤有以下几点。

1、使用特征检测算子提取两幅图像的关键点,然后进行特征描述子匹配。

2、筛选出匹配程度高的关键点计算两幅图的单应性矩阵。

3、利用计算出来的单应性矩阵对其中一张图片进行透视变换。

4、将透视变换的图片与另一张图片进行拼接。

5、将拼接得到的结果进行融合。




本文将使用OpenCV C++ 对人脸部位打上马赛克

实现步骤其实很简单。

1、人脸检测。

2、图像像素修改


一、人脸检测

c77b066cfdf89c44464de06237e8c44b.webp

原图如图所示。本案例的需求是将人脸部位打上马赛克。所以,第一步我们首先需要检测出人脸的位置。OpenCV提供harr级联检测器检测人脸。

  //人脸检测配置文件  string harr_file = "haarcascade_frontalface_default.xml";
//创建人脸检测器 CascadeClassifier detector; detector.load(harr_file);
//人脸检测 vectorfaces;  detector.detectMultiScale(src, faces, 1.15);

b082cbad94d9ef63d8905cd944e79dc5.webp

如图所示,我们已经定位到了人脸所在位置。接下来为了实现马赛克效果,仅需要将人脸区域像素值进行修改就可以了。


二、马赛克效果

具体原理请看源码注释。

  int step = 10;//步长
for (int t = 0; t < faces.size(); t++) { int x = faces[t].tl().x; //人脸矩形框起点x坐标 int y = faces[t].tl().y;//人脸矩形框起点y坐标 int width = faces[t].width;//人脸矩形框宽 int height = faces[t].height;//人脸矩形框高
//仅对人脸区域进行像素修改。遍历人脸矩形框区域像素,并对其进行修改 for (int i = y; i < (y + height); i += step) { for (int j = x; j < (x + width); j += step) { //将人脸矩形框再细分为若干个小方块,依次对每个方块修改像素(相同方块赋予相同灰度值) for (int k = i; k < (step + i); k++) { for (int m = j; m < (step + j); m++) { //对矩形区域像素值进行修改,rgb三通道 for (int c = 0; c < 3; c++) { src.at(k, m)[c] = src.at(i, j)[c]; } } } } }  }

三、效果显示

4303a65130ba6fc1231336a27f0fefb5.webp


四、源码

#include#includeusing namespace std;using namespace cv;
bool Generate_Mosaic(Mat &src,vector&faces){ if (faces.empty())return false;
int step = 10;//步长
for (int t = 0; t < faces.size(); t++) { int x = faces[t].tl().x; //人脸矩形框起点x坐标 int y = faces[t].tl().y;//人脸矩形框起点y坐标 int width = faces[t].width;//人脸矩形框宽 int height = faces[t].height;//人脸矩形框高
//仅对人脸区域进行像素修改。遍历人脸矩形框区域像素,并对其进行修改 for (int i = y; i < (y + height); i += step) { for (int j = x; j < (x + width); j += step) { //将人脸矩形框再细分为若干个小方块,依次对每个方块修改像素(相同方块赋予相同灰度值) for (int k = i; k < (step + i); k++) { for (int m = j; m < (step + j); m++) { //对矩形区域像素值进行修改,rgb三通道 for (int c = 0; c < 3; c++) { src.at(k, m)[c] = src.at(i, j)[c]; } } } } } }
return true;}

//对单张图片打马赛克bool Picture_Demo(Mat src){ //人脸检测配置文件 string harr_file = "haarcascade_frontalface_default.xml";
//创建人脸检测器 CascadeClassifier detector; detector.load(harr_file);
//人脸检测 vectorfaces; detector.detectMultiScale(src, faces, 1.1, 5);
if (!Generate_Mosaic(src, faces))return false; imshow("test", src); waitKey(0); return true;}

//对视频打马赛克bool Video_Demo(){ //人脸检测配置文件 string harr_file = "haarcascade_frontalface_default.xml";
//创建人脸检测器 CascadeClassifier detector; detector.load(harr_file);
VideoCapture cap; cap.open(0); if (!cap.isOpened()) { cout << "can not open the camera!" << endl; }
Mat frame; while (cap.read(frame)) { flip(frame, frame, 1);
//人脸检测 vectorfaces; detector.detectMultiScale(frame, faces, 1.1, 5);
if (Generate_Mosaic(frame, faces)) { imshow("Demo", frame); } char key = waitKey(10); if (key == 27)break; }
cap.release();
return true;}

int main(){ Mat src = imread("person.jpg"); if (src.empty()) { cout << "No Image!" << endl; system("pause"); return -1; }
//Picture_Demo(src); Video_Demo();
system("pause"); return 0;}


总结

本文使用OpenCV C++对人脸部位进行打码,关键步骤有以下几点。

1、人脸检测。首先需要定位人脸区域。

2、对人脸区域进行像素灰度值修改。



总结:

10年机器视觉网站,5年人工智能网站

2019经历总结2018视觉总结

项目感悟赚钱思路项目视频

课程:

《机器视觉:应用讲解》一总体概述二相机篇三镜头篇四光源篇五光学系统选型六视觉开发软件七相机标定技术八项目案例解析九视觉公司分析十产业发展情况

笔记:

《智能革命》《人工智能》《AI•未来》《好好赚钱》《韭菜的自我修养》读书笔记

行业: 

服务机器人公司,机器视觉公司,自动驾驶公司,ADAS公司总结, 防疫机器人发展腾讯未来交通

SLAM:

Vslam方案+源码,语义SLAM与深度相机SLAM和导航避障视觉SLAM总结

秦学英《三维物体的识别与跟踪》章国锋《视觉SLAM》申抒含《基于图像的三维建模》姜翰青《RGB -D SLAM》记录笔记

视觉SLAM的建图课件3课件2课件1

机器视觉:

毫米波雷达雷达视觉融合2021视觉研讨会2020上海研讨会双目和激光的三维重建2021视觉市场研究太阳能行业应用

机器视觉基本概念笔记,记录五,记录四,记录三,记录二,记录一

图像处理:

图像处理基本概念笔记,记录八,记录七,记录六 ,记录五,记录四 ,记录三,记录二 ,记录二,记录一

欢迎支持,点击在看,分享

浏览 160
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报