最近根据项目需要,需要用到透视变换,如果直接调用cv::warpPerspective()虽然可以直接完成任务,但是因为后期需要移植到GPU里,所以需要自己实现。(本想直接改写cv::warpPerspective()的,但是OpenCV源码又调用了各种函数而且还有乱七八糟的东西,所以才打算自己搞。)目前kernel函数还没改写完,过几天测试好了再更。由于透视变换矩阵是个常量,只需要计算一次,所以可以调用cv::getPerspectiveTransform(),实际上就是借用这个函数来求解一个八元八次线性方程组。
先上点数学公式,根据计算机视觉的多视图几何一书中的定义
和像素级的计算公式
这一点和OpenCV中计算透视矩阵的公式是一样的。
OpenCV计算透视矩阵的公式
* Calculates coefficients of perspective transformation
* which maps (xi,yi) to (ui,vi), (i=1,2,3,4):
*
* c00*xi + c01*yi + c02
* ui = ---------------------
* c20*xi + c21*yi + c22
*
* c10*xi + c11*yi + c12
* vi = ---------------------
* c20*xi + c21*yi + c22
*
* Coefficients are calculated by solving linear system:
* / x0 y0 1 0 0 0 -x0*u0 -y0*u0 \ /c00\ /u0\
* | x1 y1 1 0 0 0 -x1*u1 -y1*u1 | |c01| |u1|
* | x2 y2 1 0 0 0 -x2*u2 -y2*u2 | |c02| |u2|
* | x3 y3 1 0 0 0 -x3*u3 -y3*u3 |.|c10|=|u3|,
* | 0 0 0 x0 y0 1 -x0*v0 -y0*v0 | |c11| |v0|
* | 0 0 0 x1 y1 1 -x1*v1 -y1*v1 | |c12| |v1|
* | 0 0 0 x2 y2 1 -x2*v2 -y2*v2 | |c20| |v2|
* \ 0 0 0 x3 y3 1 -x3*v3 -y3*v3 / \c21/ \v3/
*
* where:
* cij - matrix coefficients, c22 = 1
*
所以代码是这样的:
for (int y = 0; y < image.rows; y = y + 1) { //x, y are src, u, v are dst.
for (int x = 0; x < image.cols; x = x + 1) {
int v = (int)((M10*x+M11*y+M12) / (M20*x+M21*y+M22));
int u = (int)((M00*x+M01*y+M02) / (M20*x+M21*y+M22));
if (v >=0 && u >= 0 && v < dst_size.height && u < dst_size.width) {
image_Perspective.at<cv::Vec3b>(v, u) = image.at<cv::Vec3b>(y, x);
}
}
}
原图是这样的:
效果是这样的:
这是直接coding的结果,显然需要优化,第一直觉是插值,但是这是一个非线形图,我还没想到一个对于非线性图片比较好的插值算法。导致这种结果的原因是,对源图像进行遍历,在做坐标变换之后,因为强制类型转换,所以导致有的相近的源点被映射成同一个目标点,于是在目标图像上,有的点就没有被赋值,所以还是黑色,像素值还是(0, 0, 0)。晚上,我梦到了一个优化的方法——用逆变换coding。
这是目标图像dst中的点(u,v)关于源图像src中的点(x,y)的函数(u=u(x, y), v=v(x, y)),现在给它写成src关于dst的函数(x=x(u,v), y=y(u, v)),就是无脑解一个二元线性方程组,不展示具体推倒细节了,结果是
原来好比是满城尽带黄金甲发哥的一句台词“朕给你,才是你的,朕不给,你不能抢。”然而,现在我就是要抢。具体就是,遍历目标图像,利用逆变换去源图像中索要像素值,这样每个像素点都能要到像素值。
利用逆变换得到的代码是这样的:
for (int v = 0; v < dst_size.height; v = v + 1) { //x, v are src, u, v are dst.
for (int u = 0; u < dst_size.width; u = u + 1) {
int x = (int)(((M02-u*M22)*(v*M21-M11)-(M12-v*M22)*(u*M21-M01)) / ((u*M20-M00)*(v*M21-M11)-(v*M20-M10)*(u*M21-M01)));
int y = (int)(((M02-u*M22)*(v*M20-M10)-(M12-v*M22)*(u*M20-M00)) / ((u*M21-M01)*(v*M20-M10)-(v*M21-M11)*(u*M20-M00)));
if (x >= 0 && y >=0 && x < image.cols && y < image.rows) {
image_Perspective.at<cv::Vec3b>(v, u) = image.at<cv::Vec3b>(y, x);
}
}
}
void warpPerspectiveTransform(const cv::Mat& src, cv::Mat& dst, const cv::Mat& TransformMatrix, const cv::Size& dstSize) {
const double M00 = TransformMatrix.at<double>(0, 0); //(0,0) X
const double M01 = TransformMatrix.at<double>(0, 1); // ---------------->
const double M02 = TransformMatrix.at<double>(0, 2); // |
const double M10 = TransformMatrix.at<double>(1, 0); // |
const double M11 = TransformMatrix.at<double>(1, 1); // |
const double M12 = TransformMatrix.at<double>(1, 2); // |
const double M20 = TransformMatrix.at<double>(2, 0); // |
const double M21 = TransformMatrix.at<double>(2, 1); // |
const double M22 = TransformMatrix.at<double>(2, 2); // Y\/
for (int v = 0; v < dstSize.height; v = v + 1) { //x, y are src, u, v are dst. Search in dst.
for (int u = 0; u < dstSize.width; u = u + 1) {
int y = (int)(((M02-u*M22)*(v*M20-M10)-(M12-v*M22)*(u*M20-M00)) / ((u*M21-M01)*(v*M20-M10)-(v*M21-M11)*(u*M20-M00)));
int x = (int)(((M02-u*M22)*(v*M21-M11)-(M12-v*M22)*(u*M21-M01)) / ((u*M20-M00)*(v*M21-M11)-(v*M20-M10)*(u*M21-M01)));
if (y >=0 && x >= 0 && y < src.rows && x < src.cols) {
dst.at<cv::Vec3b>(v, u) = src.at<cv::Vec3b>(y, x);
}
}
}
}
整个demo的代码:
#include
#include
#include
#include
#include
#include
#include
#include "device_launch_parameters.h"
#include
using namespace std;
using namespace cv;
void warpPerspectiveTransform(const cv::Mat& src, cv::Mat& dst, const cv::Mat& TransformMatrix, const cv::Size& dstSize) {
const double M00 = TransformMatrix.at<double>(0, 0);
const double M01 = TransformMatrix.at<double>(0, 1); //(0,0) X
const double M02 = TransformMatrix.at<double>(0, 2); // ------------->
const double M10 = TransformMatrix.at<double>(1, 0); // |
const double M11 = TransformMatrix.at<double>(1, 1); // |
const double M12 = TransformMatrix.at<double>(1, 2); // |
const double M20 = TransformMatrix.at<double>(2, 0); // |
const double M21 = TransformMatrix.at<double>(2, 1); // |
const double M22 = TransformMatrix.at<double>(2, 2); // Y\/
for (int v = 0; v < dstSize.height; v = v + 1) { //x, y are src, u, v are dst. Search in dst.
for (int u = 0; u < dstSize.width; u = u + 1) {
int y = (int)(((M02-u*M22)*(v*M20-M10)-(M12-v*M22)*(u*M20-M00)) / ((u*M21-M01)*(v*M20-M10)-(v*M21-M11)*(u*M20-M00)));
int x = (int)(((M02-u*M22)*(v*M21-M11)-(M12-v*M22)*(u*M21-M01)) / ((u*M20-M00)*(v*M21-M11)-(v*M20-M10)*(u*M21-M01)));
if (y >=0 && x >= 0 && y < src.rows && x < src.cols) {
dst.at<cv::Vec3b>(v, u) = src.at<cv::Vec3b>(y, x);
}
}
}
}
int main()
{
cv::Mat image = cv::imread("../warpPerspectiveTransform/front.jpg");
cv::Mat image_Perspective(720, 1280, CV_8UC3);
cv::Size dst_size = cv::Size(1280, 720);
cv::Point2f src_points[] = {
cv::Point2f(400, 360),
cv::Point2f(879, 360),
cv::Point2f(1279, 719),
cv::Point2f(0, 719)
};
cv::Point2f dst_points[] = {
cv::Point2f(0, 0),
cv::Point2f(dst_size.width - 1, 0),
cv::Point2f(dst_size.width - 1, dst_size.height - 1),
cv::Point2f(0, dst_size.height - 1)
};
cv::Mat tfMatrix = cv::getPerspectiveTransform(src_points, dst_points);
int row = dst_size.height;
int col = dst_size.width;
// 直接code的code
// for (int y = 0; y < image.rows; y = y + 1) { //x, y are src, u, v are dst.
// for (int x = 0; x < image.cols; x = x + 1) {
// int v = (int)((M10*x+M11*y+M12) / (M20*x+M21*y+M22));
// int u = (int)((M00*x+M01*y+M02) / (M20*x+M21*y+M22));
// if (v >=0 && u >= 0 && v < dst_size.height && u < dst_size.width) {
// image_Perspective.at(v, u) = image.at(y, x);
// }
// }
// }
warpPerspectiveTransform(image, image_Perspective, tfMatrix, dst_size);
cv::imshow("image_Perspective", image_Perspective);
cv::imwrite("../warpPerspectiveTransform/front_perspective.jpg", image_Perspective);
cv::waitKey(0);
return 0;
}
CMakeLists.txt:
cmake_minimum_required(VERSION 2.8)
project(warpPerspectiveTransform)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(include
${CUDA_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS}
)
link_directories(${OpenCV_LIBRARY_DIRS})
find_package(CUDA REQUIRED)
find_package(OpenCV REQUIRED)
#INCLUDE(/home/psdz/cmake-3.9.0/Modules/FindCUDA.cmake)
FILE(GLOB SOURCES "*.cu" "*.cpp" "*.c" "*.h")
set(CUDA_NVCC_FLAGS "-g -G")
CUDA_ADD_EXECUTABLE(${PROJECT_NAME} main.cu)
#add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})
这是个.cu文件,改成.cpp文件只需要把和CUDA有关的东西注释掉,然后把 #add_executable(${PROJECT_NAME} “main.cpp”) 这一句给取消注释。
(ps1:做透视变换的八个点是我随便选的)
(ps2:透视变换对整张图都有效果,而不仅仅是dst_points选中的区域)
希望改写成内核函数顺利。
更新:算法优化。既然利用透视变换的逆矩阵,那么可以在这里将dst_points和src_points颠倒个位置,求得的就是逆变换了。
cv::Mat inversal_tfMatrix = cv::getPerspectiveTransform(dst_points, src_points);
3月24日更新:最近研究视觉SLAM,看到视觉SLAM十四讲的7.3.3单应矩阵一节,发现单应矩阵和透视矩阵是一个东西!具体地,应该比较一下cv::findHomography()和cv::getPerspectiveTransform()这两个函数的计算结果。已经有人抢先了——>https://blog.csdn.net/abc20002929/article/details/8709902
4月6日更新:原来这种求dst目标值像素的方法叫做后向映射,而之前的方法叫做前向映射,借用哈工大许毅立硕士论文中的一张图。