当前位置:网站首页>OpenCv入门(二)——仿射变换和透视变换
OpenCv入门(二)——仿射变换和透视变换
2022-04-22 23:34:00 【郑烯烃快去学习】
为什么要图像重映射?我们可以把每个像素的位置重新映射到新的位置,这可用来创建图像特效,或者修正因镜片等原因导致的图像扭曲。
如何实现?使用OpenCv的remap函数,首先需要定义在重映射处理中使用的映射参数,然后把映射参数应用到输入图像。很明显,定义映射参数的方式将决定产生的效果。这里定义一个转换函数,在图像上创建波浪形效果:
// 重映射图像,创建波浪形效果
void wave(const cv::Mat &image, cv::Mat &result) {
// 映射参数
cv::Mat srcX(image.rows,image.cols,CV_32F);
cv::Mat srcY(image.rows,image.cols,CV_32F);
// 创建映射参数
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++) {
// (i,j)像素的新位置
srcX.at<float>(i,j)= j; // 保持在同一列
// 原来在第 i 行的像素,现在根据一个正弦曲线移动
srcY.at<float>(i,j)= i+5*sin(j/10.0);
}
}
// 应用映射参数
cv::remap(image, // 源图像
result, // 目标图像
srcX, // x 映射
srcY, // y 映射
cv::INTER_LINEAR); // 填补方法
}
程序实现原理:
重映射是通过修改像素的位置,生成一个新版本的图像,为了构建新图像,需要指定目标图像中每个像素的原始位置。我们需要的映射函数应该能根据像素的新位置得到像素的原始 位置。这个转换过程描述了如何把新图像的像素映射回原始图像,因此称为反向映射。
在opencv中可以用两个映射参数来说明反向映射参数来说明反向映射:一个针对x坐标,一个针对y坐标,它们都使用浮点数型的cv::Mat实例来表示:
// 映射参数
cv::Mat srcX(image.rows,image.cols,CV_32F); // x 方向
cv::Mat srcY(image.rows,image.cols,CV_32F); // y 方向
这些矩阵大小决定了目标图像的大小。用下面的代码可以从原始图像获得目标图像中(i,j)像素的值:
( srcX.at<float>(i,j) , srcY.at<float>(i,j) )
对于水平翻转,可以改变左右两边的坐标值,也可以实现:
// 创建映射参数
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++) {
// 水平翻转
srcX.at<float>(i,j)= image.cols-j-1;
srcY.at<float>(i,j)= i;
}
}
要实现效果只需要调用remap函数:
// 应用映射参数
cv::remap( image, // 源图像
result, // 目标图像
srcX, // x 方向映射
srcY, // y 方向映射
cv::INTER_LINEAR); // 插值法
(1)使用图像实现逆透视(鸟瞰图)
参考博客:逆透视变换(IPM)多种方式及代码总结 - 古月居
在自动/辅助驾驶中,车道线的检测非常重要。要在前视摄像头拍摄的图像中,由于透视效应的存在,本来平行的事物,在图像中确实相交的。而IPM变换就是消除这种透视的效应,所以也叫逆透视。
IPM分为三种:透视变换、仿射变换、单应性变换。
-
透视变换:不能保证物体形状的"平行性"。透视变换是将一个平面投射到另一个平面,就是把一张图片投射到另一张图片,求的是同一张图片到它的投影图片之间的变换。
-
仿射变换:透视变换的特殊形式。保证物体形状的”平值性“和”平行性",一般为平移旋转等操作。
-
单应性变换:由三维空间拍摄两张不同的图片来获取关键点,求的是该图片到另一个角度图片的变换,但是变换过后还是这张图片,没有变成另一个角度的图片,变换过后和另一张图片还是不同,因为三维空间得到的背景不同,所以变换后并不能获取关键点对的另一张图片。
IPM变换方法:
-
输入:至少四个对应点对,不能有三点及其以上共线,不需要知道摄像机参数或者平面位置的任何信息。
-
数学原理:利用点对,求解逆透视变换矩,其中map_matrix是一个3×3矩阵,所以可以构建一个线性方程组进行求解。其实就是我们上面说的类似于卷积的操作了。如果大于4个点,可采用ransc的方法进行求解,一边具有更好的稳定性。


-
选点方法:一般采取手动选取,或者利用消影点(图像上平行线的交点,也叫消失点,vanish point)选取。
实现目标及其实现代码:

从拍照视角变成鸟瞰图,是自动驾驶领域和机器人导航种常用的手段,以便在该平面上进行规划和导航。这种变换常常用到透视变换,但我们今天在讲解透视变换时,需要普及一下其他的变换,包括平移、旋转、错切以及仿射变换。
基础的变换有如下:
-
平移
对于矩阵的平移怎么做?对每一个像素点坐标进行平移,就是在相应的矩阵上x,y都加一个变量。
-
放缩
就是将矩阵图像放缩n倍,也就是长宽各乘一个变量。
-
旋转
对矩阵(图片)进行旋转,关于旋转的数学推导在后边的仿射会介绍:
-
错切
关于公式的推导:
关于y方向的错切:

相应的数学表达:

关于x轴方向的错切:

那么相应的数学表达:

之后把其两者合起来:

那么我们就可以实现如下的四种变换:

那么等式右边就是仿射变换矩阵,是由原图像进行平移、旋转、放缩、错切之后得来的。
那么我们的仿射变换跟透视变换到底有什么区别?
仿射变换是可以将矩形变换成平行四边形(即变换后各边依旧平行),而透视变换可以变成任意不规则四边形。所以仿射变换是透视变换的子集。
仿射变换其实就是单纯对图片进行缩放,倾斜和旋转,因此图片不论如何变换,线之间的平行性是不变的。而透视变换,则是当观察者的视角发生变化时物体发生透视变换,此变换允许造成透视形变。
那么如何实现?
(2)仿射变换
opencv中给出了仿射变换的函数接口:
warpAffine(
InputArray src, //输入图像
OutputArray dst, //输出图像
InputArray M, //仿射计算矩阵
Size dsize, //输出图像大小
int flags = INIET_LINEAR, //插值方法
int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar()
)
这函数的前两个函数就不用详细说了,那么第三个矩阵m是什么,第三个参数其实就是要我们输入上方的2*3的仿射计算矩阵:

这个东西可以去掉最后一行。
这个函数的推导在上面,就不多叙述了。这里的核心思想是:坐标系中某个点的旋转可以等价地去旋转坐标轴。
再用这张图看看:

我们需要将原坐标系原点(Xs0,Ys0)旋转至新坐标原点。然后要完成旋转操作,旋转操作是基于原点的,也就是图中的蓝色坐标。
我们如何把那点P转移到我们需要的坐标轴上?并且在新建的坐标上确定我们的坐标位置?
那就是一系列的几何知识了:
基于数学,我们可以通过简单的立体几何知识确定P在新坐标系中的坐标。P在新坐标中的X坐标和Y坐标分别是:
那么由矩阵来表示就是这样:

那么我们完成了旋转操作,那么接下来的平移:

那么这个矩阵就出来了。
那么推导是这样的,那怎么去实现?opencv提供了计算仿射矩阵的函数接口:
getAffineTransform(
const Point2f* src, //输入图像的点集
const Point2f* dst //输出图像的点集
)
我们需要输入三对点集。也就是上面的矩阵,要有六个变量,因此至少需要列六个等式才可以算出该矩阵。我们需要找输入图像和输出图像上一一对应的三对点(3个x,y对应计算式)来作为输入。
(3)透视变换原理
仿射变换是在二维空间中的旋转,平移和缩放。而透视变换则是在三维空间中视角的变化。
opencv给的透视变换的函数接口:
void warPerspective(
InputArray src, //输入图像
OutputArray dst, //输出图像
InputArray M, //输入透视变换矩阵M
Size dsize,
int flags = INTER_LINEAR,
int borderMode=BORDER_CONSTANT,
const Scalar& borderValue=Scalar()
);
和仿射变换基本相同,不同的是输入透视变换矩阵M大小为3*3:

上面的矩阵的未知量比仿射变换的矩阵多了一个透视变换矩阵T3(两个未知量),因此我们一共有八个未知量,所以需要给下面的透视变换矩阵的函数提供四对以上的点来求解:
Mat cv::getPerspectiveTransform (
const Point2f src[], //输入图像点集
const Point2f dst[], //输出图像点集
);
T1为线性变换完成旋转,错切和放缩,T2完成平移操作。T3就是设了两个变量来表示映射关系。
取原图上的四点,之后计算该四点变换后的位置。就是把那个像梯形的东西,转换为长方形。
代码如下:
//逆透视
cv::Mat road = cv::imread("./image/cross.jpg");
cv::imshow("原来", road);
//存放要更改的图片
cv::Mat dstImage(1500, 1500, CV_16F);
cv::Point2f imPts[4], objPts[4], mypoint[4];
//画原来的点
mypoint[0].x = 166; mypoint[0].y = 195;
mypoint[1].x = 482; mypoint[1].y = 195;
mypoint[2].x = 80; mypoint[2].y = 350;
mypoint[3].x = 568; mypoint[3].y = 350;
//你想要变成的点
objPts[0].x = 500; objPts[0].y = 200;
objPts[1].x = 1000; objPts[1].y = 200;
objPts[2].x = 500; objPts[2].y = 0;
objPts[3].x = 1000; objPts[3].y = 0;
//计算透视变换矩阵
cv::Mat H = cv::getPerspectiveTransform(mypoint, objPts);
//进行透视变换
cv::warpPerspective(road, dstImage, H, dstImage.size());
//画出透视变换后的四个点
circle(dstImage, objPts[0], 9, cv::Scalar(0, 0, 255), 3);
circle(dstImage, objPts[1], 9, cv::Scalar(0, 0, 255), 3);
circle(dstImage, objPts[2], 9, cv::Scalar(0, 0, 255), 3);
circle(dstImage, objPts[3], 9, cv::Scalar(0, 0, 255), 3);
cv::namedWindow("dddd", cv::WINDOW_FREERATIO);
cv::imshow("dddd", dstImage);

0x04 直方图统计像素
在单通道灰度图像种,每个像素都有一共0(黑色)~255(白色)的整数。对于每个灰度,都有不同数量的像素分布在图像内,具体取决于图片内容。
-
直方图是一个简单的表格,表示一幅图像(优势是一组图像)中具有某个值的像素的数量。所以直方图一共有256个项目,也叫箱子(bin)。
实现:
指定一个专门用于处理单通道的类:
class HistogramID {
private:
int histSize[1]; //直方图中箱子的数量
float hranges[2]; //值范围
const float* ranges[1]; //值范围的指针
int channels[1]; //要检查的通道数量
public:
HistogramID()
{
//准备一维直方图的默认参数
histSize[0] = 256; //个数
hranges[0] = 0.0; //min
hranges[1] = 256.0; //max
ranges[0] = hranges; //指向它
channels[0] = 0; //先关注通道0
}
};
计算灰度直方图:
//计算灰度直方图
cv::Mat HistogramID::getHistogram(const cv::Mat& image) {
cv::Mat hist;
//使用calcHist函数计算一维直方图
cv::calcHist(&image, 1, //仅为一幅图像的直方图
channels, //使用的通道
cv::Mat(), //不使用掩码
hist, //作为结果的直方图
1, //这是一维的直方图
histSize, //箱子数量
ranges //像素值的范围
);
return hist;
}
使用:
//计算灰度直方图
HistogramID VV;
cv::Mat histo = VV.getHistogram(image);
//遍历数组,可得到对应的灰度的个数
for (int i = 0; i < 256; i++)
std::cout << "Value" << i << "=" << histo.at<float>(i) << std::endl;
可能这么看不大直观,可以使用画图画出来:
//创建一个表示直方图的图像
cv::Mat getImageOfHistogram(const cv::Mat& hist, int zoom)
{
//取得箱子值的最大值和最小值
double maxVal = 0;
double minVal = 0;
cv::minMaxLoc(hist, &minVal, &maxVal, 0, 0);
//取值直方图
int histSize = hist.rows;
//用于显示直方图的方形图像
cv::Mat histImg(histSize * zoom,histSize * zoom, CV_8U, cv::Scalar(255));
//设置最高点为100%的箱子个数
int hpt = static_cast<int>(1.00 * histSize);
//为每个箱子画垂直线
for (int h = 0; h < histSize; h++)
{
float binVal = hist.at<float>(h);
if (binVal > 0)
{
int intensity = static_cast<int>(binVal * hpt / maxVal);
cv::line(histImg, cv::Point(h * zoom, histSize * zoom),
cv::Point(h * zoom, (histSize - intensity) * zoom),
cv::Scalar(0), zoom);
}
}
return histImg;
}
//画出直方图
cv::Mat HistogramID::getHistogramImage(const cv::Mat& image,int zoom) {
//计算出直方图
cv::Mat hist =getHistogram(image);
//创建图像
return getImageOfHistogram(hist,zoom);
}
使用:
//以图像形式显示直方图
cv::namedWindow("Histogram");
cv::imshow("Histogram",VV.getHistogramImage(image));

从上面图形化的直方图可以看出,在黑白两种颜色都有一个很大的尖峰,这两部分像素也分别对应了图像的背景和前景。所以在这两部分之间的那个值其实就是我们需要的阈值。我们取直方图中在升高为顶峰之前的最小值的位置(灰度值为100)对其进行阈值处理,可得到二值化图像:
//输出二值化图像
cv::Mat thresholded;
cv::threshold(
image,
thresholded,
100,
255,
cv::THRESH_BINARY //阈值化类型
);
cv::imshow("thresholded", thresholded);
但是可以发现,这种方法算出来的二值化图像,实在是太多噪点了:

版权声明
本文为[郑烯烃快去学习]所创,转载请带上原文链接,感谢
https://blog.csdn.net/Alkaid2000/article/details/124318758
边栏推荐
- MPP架构概念
- How to show the ability of structure in the interview? 200 real interview questions +100 classic architecture cases disassembly attached
- 【经验分享】分享 MangoPapa 的论文学习经验
- 网狐U3D客户端游戏配置加载失败Couldn‘t connect to server解决
- JUC 全套(1)
- LeetCode 414. The third largest number (simple, array) day13
- [perseverance challenge] PCIe asks and answers every day (filed on March 2022)
- 分享两道最近做的比较经典的OJ题(排列子序列+字符串中找出连续最长的数字串)
- [dvcon2020] acceleration of low power design level verification based on signoff abstract model
- Cache and buffer
猜你喜欢

grid_map(6):grid-mapping-in-ROS编译运行

一个关于混淆的 Native 崩溃分析

visual studio 2019恢复默认设置

Method of shrinking master-slave nodes in redis cluster cluster

LeetCode 1446 - 1449

Webrtc series - webrtc Foundation (VII) NAT, stun and turn (2)
![[perseverance challenge] PCIe ask and answer every day (filed on February 2022)](/img/f5/986fed0345053371b464972997c276.png)
[perseverance challenge] PCIe ask and answer every day (filed on February 2022)
![[newcoder] week 20220422](/img/cd/3d24d3b5be26166ab94742da8c46dd.png)
[newcoder] week 20220422
![[leetcode refers to offer 54. The k-th node of the binary search tree (simple)]](/img/99/f76139a54826bbd56f150a3eccd216.png)
[leetcode refers to offer 54. The k-th node of the binary search tree (simple)]

Pytorch 卷积核填充和步幅、多输入多输出通道、池化层
随机推荐
伦敦金在哪里开户安全些?
想开户交易农产品期货如玉米、豆粕等,该如何开户?
存储器简介
2022硬刚PLUS德施曼年度峰会:多款领航旗舰发布,引领高端市场
PCIe reference clock architecture
Login function & test point extraction of new article function and test case writing
Classical question: a pair of rabbits give birth to a pair of rabbits every month from the third month after birth. The little rabbit grows to another pair of rabbits every month after the third month
[dvcon2020] simulation acceleration method based on multithreaded UVM test platform
Yolov1 paper notes
线程池(通俗易懂)
JUC 全套(1)
Excel VBA multi condition screening and summary statistics
【LeetCode 剑指 Offer 54. 二叉搜索树的第k大节点(简单)】
[hctf 2018] admin flash session forgery
If you want to open an account to trade agricultural products futures, such as corn and soybean meal, how to open an account?
【笔记】PCIe LTSSM 状态转移
SQL数据库基础知识
Hello, I want to open an account for crude oil futures. How can I open an account for futures safely and reliably?
51 单片机学习_4-2 数码管动态显示
JS有红,白,黑三球若干个,其中红,白球共25个,白黑共31个,红黑共28个,求三种球各多少个。