4、图像投影变换

4.1 原理

前文我们已经说过,每幅图像是相机在不同角度下拍摄得到的,它们并不在同一个投影平面上,如果对重叠部分直接进行拼接,则会破坏实际场景的视觉一致性。所以我们需要在拼接之前,对图像进行投影变换,即对图像进行扭曲变形。

设图像中某像素点的二维坐标为(x, y),它所对应的世界坐标为(X, Y, Z),两者之间的关系为:

(70)

式中,R为旋转矩阵,K为相机的内参数矩阵。像素点可以映射到不同的表面上,最简单的是映射到平面上,设(u, v)为映射后的二维坐标,则

(71)

如果世界坐标有平移,并且投影图像有尺度的变化,则式71改写为:

(72)

式中,s表示尺度,与相机焦距成正比,t1t2t3表示三个坐标轴的位移。

上面的变换是由源图像变换到投影图像上,即由(x, y)映射为(u, v),我们称为正向投影。如果是由投影图像变换为源图像,我们称为反向投影。反向投影的公式为:

(73)

(74)

平面投影简单,但拼接图像较多时,视觉效果并不好。另一种常见的投影方式是柱面投影。柱面投影是以相机为圆柱中心点,相机焦距为半径的一个柱面作为投影面。它的投影图像与投影到的圆柱表面的位置无关,柱面全景图像可在水平方向上满足360度环视,具有较好的视觉效果,并且柱面投影也符合我们对相机位置的假设(相机只做旋转动作)。柱面投影后的坐标为:

(75)

柱面投影的反向映射关系为:

(76)

由(X, Y, Z)得到(x, y)的公式也是式74。

球面投影是将图像序列投影到以一点为坐标中心的球面上。人的眼睛在看东西时的原理就类似于球面投影,因此,以视点为中心的球面投影模型是最自然的投影模型。但是球面投影模型也存在着一些缺点,比如球面上的像素点不是行列均匀排列的关系,球面不能展开成平面,这些都使得很多图像处理算法很难用在平面投影上。球面投影的正向映射为:

(77)

球面投影的反向映射为:

(78)

立方体投影是为了克服球形投影缺点而提出的投影模型。这种投影模型的优点是方便计算机处理与储存图像。立方体投影的正向映射为:

(79)

立方体投影的反向映射为:

(80)

鱼眼投影图像具有较大的视角,非常适用于导航、监视和检测等方面。它的正向映射为:

(81)

鱼眼投影的反向映射为:

(82)

在图像拼接过程中,我们首先需要把图像进行正向映射,又因为最终图像还是要在平面上进行展示,所以还是需要再进行反向映射。最终被映射到的平面就是全景图像所在的平面,这是因为在上一步,我们已经通过最大生成树得到了基准图像,相机的内参数都是基于该基准图像的,所以所有的图像最终都映射到了该基准图像所在的平面上,这样就构成了一幅全景图像。

4.2 源码

RotationWarper类是只处理因旋转而引起的图像扭曲的接口类,它是RotationWarperBase类的基类:

template <class P>
class CV_EXPORTS RotationWarperBase : public RotationWarper
{
public://表示投影图像的像素点,pt为源像素点,它通过P.mapForward函数得到投影点(该函数的返回值),K为相机的内参数,R为相机的旋转矩阵,通过P.setCameraParams函数设置Point2f warpPoint(const Point2f &pt, const Mat &K, const Mat &R);//由给定的相机数据建立投影关系,src_size为源图像区域,xmap和ymap是分别表示坐标值由两次映射的值,该函数返回投影图区域Rect buildMaps(Size src_size, const Mat &K, const Mat &R, Mat &xmap, Mat &ymap);//表示由源图src经buildMaps函数得到投影图像dst,interp_mode和border_mode分别表示投影时用到的插值算法和边界扩展方法,该函数返回dst在最终的全景图像投影后的左上角坐标Point warp(const Mat &src, const Mat &K, const Mat &R, int interp_mode, int border_mode,Mat &dst);//与buildMaps函数相类似,只不过该函数使用的是P.mapForward函数void warpBackward(const Mat &src, const Mat &K, const Mat &R, int interp_mode, int border_mode,Size dst_size, Mat &dst);//表示确定扭曲图像区域Rect warpRoi(Size src_size, const Mat &K, const Mat &R);float getScale() const { return projector_.scale; }    //得到尺度void setScale(float val) { projector_.scale = val; }    //设置尺度protected:// Detects ROI of the destination image. It's correct for any projection.//该虚函数用于得到目标图像的区域virtual void detectResultRoi(Size src_size, Point &dst_tl, Point &dst_br);// Detects ROI of the destination image by walking over image border.// Correctness for any projection isn't guaranteed.//该函数仅由源图像的边界得到目标图像的区域void detectResultRoiByBorder(Size src_size, Point &dst_tl, Point &dst_br);P projector_;    //表示投影的方法
};

下面我们给出RotationWarperBase类中主要函数的介绍:

template <class P>
Point2f RotationWarperBase<P>::warpPoint(const Point2f &pt, const Mat &K, const Mat &R)
//pt表示投射的源点
//K表示相机的内参数
//R表示相机的旋转参数
//该函数返回投射点
{projector_.setCameraParams(K, R);    //设置相机参数Point2f uv;    //表示投射映射点projector_.mapForward(pt.x, pt.y, uv.x, uv.y);    //前向投影,得到投射点return uv;    //返回投射点
}
template <class P>
Rect RotationWarperBase<P>::buildMaps(Size src_size, const Mat &K, const Mat &R, Mat &xmap, Mat &ymap)
//src_size表示源图的区域
//K表示相机的内参数
//R表示相机的旋转参数
//xmap和ymap分别表示返回横纵坐标的前向映射后再反向映射的值
//该函数返回投影后的区域尺寸
{projector_.setCameraParams(K, R);    //设置相机参数Point dst_tl, dst_br;    //表示投影区域的左上角坐标和右下角坐标//得到映射后的左上角坐标dst_tl和右下角坐标dst_brdetectResultRoi(src_size, dst_tl, dst_br); //创建xmap和ymap矩阵大小xmap.create(dst_br.y - dst_tl.y + 1, dst_br.x - dst_tl.x + 1, CV_32F);ymap.create(dst_br.y - dst_tl.y + 1, dst_br.x - dst_tl.x + 1, CV_32F);float x, y;    //表示反向投影映射后的x轴和y轴坐标值//遍历投影区域,再进行反向映射for (int v = dst_tl.y; v <= dst_br.y; ++v) {for (int u = dst_tl.x; u <= dst_br.x; ++u){//反向投影projector_.mapBackward(static_cast<float>(u), static_cast<float>(v), x, y);xmap.at<float>(v - dst_tl.y, u - dst_tl.x) = x;    //赋值ymap.at<float>(v - dst_tl.y, u - dst_tl.x) = y;    //赋值}}return Rect(dst_tl, dst_br);    //返回投影映射区域
}
template <class P>
Point RotationWarperBase<P>::warp(const Mat &src, const Mat &K, const Mat &R, int interp_mode, int border_mode,Mat &dst)
//src表示源图
//K表示相机内参数
//R表示相机的旋转参数
//interp_mode表示插值模式
//border_mode表示边界扩充模式
//dst表示投影映射图
//该函数返回投影映射图的左上角在基准图像坐标系下的坐标,即全景图像坐标系下的坐标
{Mat xmap, ymap;Rect dst_roi = buildMaps(src.size(), K, R, xmap, ymap);    //调用buildMaps函数dst.create(dst_roi.height + 1, dst_roi.width + 1, src.type());    //创建大小//按xmap和ymap对src进行重映射,得到dstremap(src, dst, xmap, ymap, interp_mode, border_mode);return dst_roi.tl();    //返回左上角坐标
}
template <class P>
Rect RotationWarperBase<P>::warpRoi(Size src_size, const Mat &K, const Mat &R)
//src表示源图
//K表示相机内参数
//R表示相机的旋转参数
//返回投影矩形区域
{projector_.setCameraParams(K, R);    //设置相机参数Point dst_tl, dst_br;detectResultRoi(src_size, dst_tl, dst_br);    //得到映射区域return Rect(dst_tl, Point(dst_br.x + 1, dst_br.y + 1));    //返回映射矩形区域
}
template <class P>
void RotationWarperBase<P>::detectResultRoi(Size src_size, Point &dst_tl, Point &dst_br)
//src_size表示源图像区域
//dst_tl和dst_br分别表示返回得到的投影后区域的左上角坐标和右下角坐标
{//下面4个变量分别表示左上角和右下角x轴和y轴的值float tl_uf = std::numeric_limits<float>::max();    //先初始化为最大值float tl_vf = std::numeric_limits<float>::max();    //先初始化为最大值float br_uf = -std::numeric_limits<float>::max();    //先初始化为最小值float br_vf = -std::numeric_limits<float>::max();    //先初始化为最小值float u, v;for (int y = 0; y < src_size.height; ++y)    //遍历源图区域{for (int x = 0; x < src_size.width; ++x){//前向映射projector_.mapForward(static_cast<float>(x), static_cast<float>(y), u, v);tl_uf = std::min(tl_uf, u); tl_vf = std::min(tl_vf, v);    //更新左上角坐标br_uf = std::max(br_uf, u); br_vf = std::max(br_vf, v);    //更新右下角坐标}}//得到最终的左上角和右下角坐标dst_tl.x = static_cast<int>(tl_uf);dst_tl.y = static_cast<int>(tl_vf);dst_br.x = static_cast<int>(br_uf);dst_br.y = static_cast<int>(br_vf);
}
template <class P>
void RotationWarperBase<P>::detectResultRoiByBorder(Size src_size, Point &dst_tl, Point &dst_br)
{//下面4个变量分别表示左上角和右下角x轴和y轴的值float tl_uf = std::numeric_limits<float>::max();    //先初始化为最大值float tl_vf = std::numeric_limits<float>::max();    //先初始化为最大值float br_uf = -std::numeric_limits<float>::max();    //先初始化为最小值float br_vf = -std::numeric_limits<float>::max();    //先初始化为最小值float u, v;for (float x = 0; x < src_size.width; ++x)    //遍历源图的横坐标{projector_.mapForward(static_cast<float>(x), 0, u, v);    //上边映射tl_uf = std::min(tl_uf, u); tl_vf = std::min(tl_vf, v);br_uf = std::max(br_uf, u); br_vf = std::max(br_vf, v);//下边映射projector_.mapForward(static_cast<float>(x), static_cast<float>(src_size.height - 1), u, v);tl_uf = std::min(tl_uf, u); tl_vf = std::min(tl_vf, v);br_uf = std::max(br_uf, u); br_vf = std::max(br_vf, v);}for (int y = 0; y < src_size.height; ++y)    //遍历源图的纵坐标{projector_.mapForward(0, static_cast<float>(y), u, v);    左边映射tl_uf = std::min(tl_uf, u); tl_vf = std::min(tl_vf, v);br_uf = std::max(br_uf, u); br_vf = std::max(br_vf, v);//右边映射projector_.mapForward(static_cast<float>(src_size.width - 1), static_cast<float>(y), u, v);tl_uf = std::min(tl_uf, u); tl_vf = std::min(tl_vf, v);br_uf = std::max(br_uf, u); br_vf = std::max(br_vf, v);}//得到坐标dst_tl.x = static_cast<int>(tl_uf);dst_tl.y = static_cast<int>(tl_vf);dst_br.x = static_cast<int>(br_uf);dst_br.y = static_cast<int>(br_vf);
}

投影方法的基类结构为:

struct CV_EXPORTS ProjectorBase
{//设置相机参数,该函数见后面的介绍void setCameraParams(const Mat &K = Mat::eye(3, 3, CV_32F),const Mat &R = Mat::eye(3, 3, CV_32F),const Mat &T = Mat::zeros(3, 1, CV_32F));float scale;    //表示尺度float k[9];    //表示相机的内参数矩阵K,用向量形式表示//在前面的程序分析中,我们已强调过,程序中所表示的旋转矩阵r其实是公式中旋转矩阵R的逆float rinv[9];    //表示相机旋转矩阵r的逆(就是R),用向量形式表示float r_kinv[9];    //表示rK-1(就是R-1K-1),用向量形式表示float k_rinv[9];    //表示Kr-1(就是KR),用向量形式表示float t[3];    //表示三个方向的平移量
};

设置相机的内外参数:

void ProjectorBase::setCameraParams(const Mat &K, const Mat &R, const Mat &T)
//K表示相机的内参数
//R表示相机的旋转参数
//T表示相机的平移量
{//确保三个输入参数正确CV_Assert(K.size() == Size(3, 3) && K.type() == CV_32F);CV_Assert(R.size() == Size(3, 3) && R.type() == CV_32F);CV_Assert((T.size() == Size(1, 3) || T.size() == Size(3, 1)) && T.type() == CV_32F);Mat_<float> K_(K);    //复制//把矩阵形式的K转换为向量形式的kk[0] = K_(0,0); k[1] = K_(0,1); k[2] = K_(0,2);k[3] = K_(1,0); k[4] = K_(1,1); k[5] = K_(1,2);k[6] = K_(2,0); k[7] = K_(2,1); k[8] = K_(2,2);Mat_<float> Rinv = R.t();    //得到r的逆,即R-1//得到向量形式的rinvrinv[0] = Rinv(0,0); rinv[1] = Rinv(0,1); rinv[2] = Rinv(0,2);rinv[3] = Rinv(1,0); rinv[4] = Rinv(1,1); rinv[5] = Rinv(1,2);rinv[6] = Rinv(2,0); rinv[7] = Rinv(2,1); rinv[8] = Rinv(2,2);Mat_<float> R_Kinv = R * K.inv();    //得到rK-1,即R-1K-1//得到向量形式的r_kinvr_kinv[0] = R_Kinv(0,0); r_kinv[1] = R_Kinv(0,1); r_kinv[2] = R_Kinv(0,2);r_kinv[3] = R_Kinv(1,0); r_kinv[4] = R_Kinv(1,1); r_kinv[5] = R_Kinv(1,2);r_kinv[6] = R_Kinv(2,0); r_kinv[7] = R_Kinv(2,1); r_kinv[8] = R_Kinv(2,2);Mat_<float> K_Rinv = K * Rinv;    //得到Kr-1,即KR//得到向量形式的k_rinvk_rinv[0] = K_Rinv(0,0); k_rinv[1] = K_Rinv(0,1); k_rinv[2] = K_Rinv(0,2);k_rinv[3] = K_Rinv(1,0); k_rinv[4] = K_Rinv(1,1); k_rinv[5] = K_Rinv(1,2);k_rinv[6] = K_Rinv(2,0); k_rinv[7] = K_Rinv(2,1); k_rinv[8] = K_Rinv(2,2);Mat_<float> T_(T.reshape(0, 3));    //复制//把矩阵形式的T转换为向量形式的tt[0] = T_(0,0); t[1] = T_(1,0); t[2] = T_(2,0);
}

各种扭曲方法都是以RotationWarperBase为基类,各种投影方法都是以ProjectorBase为基类,扭曲和投影是一一对应的,通过不同的投影算法实现不同的图像扭曲。Opencv实现了许多投影算法,有平面、柱面、球面、鱼眼、立方体、压缩直线、压缩直线人像、panini(弯曲)、panini人像、Mercator(正轴等角柱面)、横向Mercator、球面人像、柱面人像、平面人像等投影算法。只要投影算法掌握了,通过映射得到图像扭曲就很容易,下面我们就重点介绍平面、柱面、球面、鱼眼、立方体这几种投影类,这些投影类主要就是实现了正向和反向映射算法。

平面投影:

inline
void PlaneProjector::mapForward(float x, float y, float &u, float &v)    //正向
//x和y表示源图像的坐标
//u和v表示投影图像的坐标
{//式70float x_ = r_kinv[0] * x + r_kinv[1] * y + r_kinv[2];float y_ = r_kinv[3] * x + r_kinv[4] * y + r_kinv[5];float z_ = r_kinv[6] * x + r_kinv[7] * y + r_kinv[8];//式72x_ = t[0] + x_ / z_ * (1 - t[2]);y_ = t[1] + y_ / z_ * (1 - t[2]);//式72u = scale * x_;v = scale * y_;
}
inline
void PlaneProjector::mapBackward(float u, float v, float &x, float &y)    //反向
//u和v表示投影图像的坐标
//x和y表示源图像的坐标
{u = u / scale - t[0];v = v / scale - t[1];//式73float z;x = k_rinv[0] * u + k_rinv[1] * v + k_rinv[2] * (1 - t[2]);y = k_rinv[3] * u + k_rinv[4] * v + k_rinv[5] * (1 - t[2]);z = k_rinv[6] * u + k_rinv[7] * v + k_rinv[8] * (1 - t[2]);//式74x /= z;y /= z;
}

柱面投影:

inline
void CylindricalProjector::mapForward(float x, float y, float &u, float &v)    //正向
{//式70float x_ = r_kinv[0] * x + r_kinv[1] * y + r_kinv[2];float y_ = r_kinv[3] * x + r_kinv[4] * y + r_kinv[5];float z_ = r_kinv[6] * x + r_kinv[7] * y + r_kinv[8];//式75u = scale * atan2f(x_, z_);v = scale * y_ / sqrtf(x_ * x_ + z_ * z_);
}
inline
void CylindricalProjector::mapBackward(float u, float v, float &x, float &y)    //反向
{u /= scale;v /= scale;float x_ = sinf(u);float y_ = v;float z_ = cosf(u);//式76float z;x = k_rinv[0] * x_ + k_rinv[1] * y_ + k_rinv[2] * z_;y = k_rinv[3] * x_ + k_rinv[4] * y_ + k_rinv[5] * z_;z = k_rinv[6] * x_ + k_rinv[7] * y_ + k_rinv[8] * z_;//式74if (z > 0) { x /= z; y /= z; }else x = y = -1;
}

球面投影:

inline
void SphericalProjector::mapForward(float x, float y, float &u, float &v)    //正向
{//式70float x_ = r_kinv[0] * x + r_kinv[1] * y + r_kinv[2];float y_ = r_kinv[3] * x + r_kinv[4] * y + r_kinv[5];float z_ = r_kinv[6] * x + r_kinv[7] * y + r_kinv[8];//式77u = scale * atan2f(x_, z_);float w = y_ / sqrtf(x_ * x_ + y_ * y_ + z_ * z_);v = scale * (static_cast<float>(CV_PI) - acosf(w == w ? w : 0));
}
inline
void SphericalProjector::mapBackward(float u, float v, float &x, float &y)    //反向
{u /= scale;v /= scale;//式78float sinv = sinf(static_cast<float>(CV_PI) - v);float x_ = sinv * sinf(u);float y_ = cosf(static_cast<float>(CV_PI) - v);float z_ = sinv * cosf(u);float z;x = k_rinv[0] * x_ + k_rinv[1] * y_ + k_rinv[2] * z_;y = k_rinv[3] * x_ + k_rinv[4] * y_ + k_rinv[5] * z_;z = k_rinv[6] * x_ + k_rinv[7] * y_ + k_rinv[8] * z_;//式74if (z > 0) { x /= z; y /= z; }else x = y = -1;
}

立方体投影:

inline
void StereographicProjector::mapForward(float x, float y, float &u, float &v)    //正向
{//式70float x_ = r_kinv[0] * x + r_kinv[1] * y + r_kinv[2];float y_ = r_kinv[3] * x + r_kinv[4] * y + r_kinv[5];float z_ = r_kinv[6] * x + r_kinv[7] * y + r_kinv[8];//式79float u_ = atan2f(x_, z_);float v_ = (float)CV_PI - acosf(y_ / sqrtf(x_ * x_ + y_ * y_ + z_ * z_));float r = sinf(v_) / (1 - cosf(v_));u = scale * r * cos(u_);v = scale * r * sin(u_);
}
inline
void StereographicProjector::mapBackward(float u, float v, float &x, float &y)    //反向
{u /= scale;v /= scale;float u_ = atan2f(v, u);float r = sqrtf(u*u + v*v);float v_ = 2 * atanf(1.f / r);float sinv = sinf((float)CV_PI - v_);float x_ = sinv * sinf(u_);float y_ = cosf((float)CV_PI - v_);float z_ = sinv * cosf(u_);//式80float z;x = k_rinv[0] * x_ + k_rinv[1] * y_ + k_rinv[2] * z_;y = k_rinv[3] * x_ + k_rinv[4] * y_ + k_rinv[5] * z_;z = k_rinv[6] * x_ + k_rinv[7] * y_ + k_rinv[8] * z_;//式74if (z > 0) { x /= z; y /= z; }else x = y = -1;
}

鱼眼投影:

inline
void FisheyeProjector::mapForward(float x, float y, float &u, float &v)    //正向
{//式70float x_ = r_kinv[0] * x + r_kinv[1] * y + r_kinv[2];float y_ = r_kinv[3] * x + r_kinv[4] * y + r_kinv[5];float z_ = r_kinv[6] * x + r_kinv[7] * y + r_kinv[8];//式81float u_ = atan2f(x_, z_);float v_ = (float)CV_PI - acosf(y_ / sqrtf(x_ * x_ + y_ * y_ + z_ * z_));u = scale * v_ * cosf(u_);v = scale * v_ * sinf(u_);
}
inline
void FisheyeProjector::mapBackward(float u, float v, float &x, float &y)    //反向
{u /= scale;v /= scale;//式82float u_ = atan2f(v, u);float v_ = sqrtf(u*u + v*v);float sinv = sinf((float)CV_PI - v_);float x_ = sinv * sinf(u_);float y_ = cosf((float)CV_PI - v_);float z_ = sinv * cosf(u_);float z;x = k_rinv[0] * x_ + k_rinv[1] * y_ + k_rinv[2] * z_;y = k_rinv[3] * x_ + k_rinv[4] * y_ + k_rinv[5] * z_;z = k_rinv[6] * x_ + k_rinv[7] * y_ + k_rinv[8] * z_;//式74if (z > 0) { x /= z; y /= z; }else x = y = -1;
}

前面介绍了映射变换算法的几个重要函数,下面给出编写映射变换程序时还需要用到的一些其他类。

WarperCreator类表示映射变换的生成器:
class WarperCreator
{
public:virtual ~WarperCreator() {}//生成映射变换器,scale表示映射尺度virtual Ptr<detail::RotationWarper> create(float scale) const = 0;
};

具体算法的映射变换器类都是WarperCreator类的子类,如平面映射PlaneWarper,柱面映射CylindricalWarper,球面映射SphericalWarper等等,它们的内容基本相似,我们只给出PlaneWarper类:

class PlaneWarper : public WarperCreator
{
public:Ptr<detail::RotationWarper> create(float scale) const { return new detail::PlaneWarper(scale); }
};

4.3 应用

图像映射投影变换的应用:

#include "opencv2/core/core.hpp"
#include "highgui.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/nonfree/nonfree.hpp"
#include "opencv2/legacy/legacy.hpp"#include "opencv2/stitching/detail/autocalib.hpp"
#include "opencv2/stitching/detail/blenders.hpp"
#include "opencv2/stitching/detail/camera.hpp"
#include "opencv2/stitching/detail/exposure_compensate.hpp"
#include "opencv2/stitching/detail/matchers.hpp"
#include "opencv2/stitching/detail/motion_estimators.hpp"
#include "opencv2/stitching/detail/seam_finders.hpp"
#include "opencv2/stitching/detail/util.hpp"
#include "opencv2/stitching/detail/warpers.hpp"
#include "opencv2/stitching/warpers.hpp"#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
using namespace cv;
using namespace std;
using namespace detail;int main(int argc, char** argv)
{vector<Mat> imgs;    //输入图像Mat img = imread("1.jpg");imgs.push_back(img);img = imread("2.jpg");imgs.push_back(img);Ptr<FeaturesFinder> finder;    //特征检测finder = new SurfFeaturesFinder();vector<ImageFeatures> features(2);(*finder)(imgs[0], features[0]);(*finder)(imgs[1], features[1]);vector<MatchesInfo> pairwise_matches;    //特征匹配BestOf2NearestMatcher matcher(false, 0.3f, 6, 6);matcher(features, pairwise_matches);HomographyBasedEstimator estimator;    //相机参数评估vector<CameraParams> cameras;estimator(features, pairwise_matches, cameras);for (size_t i = 0; i < cameras.size(); ++i){Mat R;cameras[i].R.convertTo(R, CV_32F);cameras[i].R = R;}Ptr<detail::BundleAdjusterBase> adjuster;    //光束平差法,精确相机参数adjuster = new detail::BundleAdjusterReproj(); adjuster->setConfThresh(1);(*adjuster)(features, pairwise_matches, cameras);vector<Mat> rmats;for (size_t i = 0; i < cameras.size(); ++i)rmats.push_back(cameras[i].R.clone());waveCorrect(rmats, WAVE_CORRECT_HORIZ);    //波形校正for (size_t i = 0; i < cameras.size(); ++i)cameras[i].R = rmats[i];vector<Point> corners(2);    //表示映射变换后图像的左上角坐标vector<Mat> masks_warped(2);    //表示映射变换后的图像掩码vector<Mat> images_warped(2);    //表示映射变换后的图像vector<Size> sizes(2);    //表示映射变换后的图像尺寸vector<Mat> masks(2);    //表示源图的掩码for (int i = 0; i < 2; ++i)    //初始化源图的掩码{masks[i].create(imgs[i].size(), CV_8U);    //定义尺寸大小masks[i].setTo(Scalar::all(255));    //全部赋值为255,表示源图的所有区域都使用}Ptr<WarperCreator> warper_creator;    //定义图像映射变换创造器//warper_creator = new cv::PlaneWarper();    //平面投影//warper_creator = new cv::CylindricalWarper();    //柱面投影//warper_creator = new cv::SphericalWarper();    //球面投影//warper_creator = new cv::FisheyeWarper();    //鱼眼投影warper_creator = new cv::StereographicWarper();    //立方体投影//定义图像映射变换器,设置映射的尺度为相机的焦距,所有相机的焦距都相同Ptr<RotationWarper> warper = warper_creator->create(static_cast<float>(cameras[0].focal));for (int i = 0; i < 2; ++i){Mat_<float> K;cameras[i].K().convertTo(K, CV_32F);    //转换相机内参数的数据类型//对当前图像镜像投影变换,得到变换后的图像以及该图像的左上角坐标corners[i] = warper->warp(imgs[i], K, cameras[i].R, INTER_LINEAR, BORDER_REFLECT, images_warped[i]); sizes[i] = images_warped[i].size();    //得到尺寸//得到变换后的图像掩码warper->warp(masks[i], K, cameras[i].R, INTER_NEAREST, BORDER_CONSTANT, masks_warped[i]);}//通过掩码,只得到映射变换后的图像for(int k =0;k<2;k++){for(int i=0;i<sizes[k].height;i++){for(int j=0;j<sizes[k].width;j++){if(masks_warped[k].at<uchar>(i, j)==0)    //掩码{images_warped[k].at<Vec3b>(i, j)[0]=0;images_warped[k].at<Vec3b>(i, j)[1]=0;images_warped[k].at<Vec3b>(i, j)[2]=0;}}}}imwrite("warp1.jpg", images_warped[0]);imwrite("warp2.jpg", images_warped[1]);return 0;
}

1.jpg和2.jpg仍然是第2.3节程序中的那两幅图像,则经过立体投影后的图像为:

     

图10 立体投影图

Opencv2.4.9源码分析——Stitching(四)相关推荐

  1. Opencv2.4.9源码分析——Stitching(二)

    2.计算单应矩阵 2.1 原理 在得到了图像特征点以后,我们就可以根据这些特征点,实现图像匹配,即得到重叠区域.而要把多幅图像拼接成一幅图像,就需要以某幅图像为基准,把其他图像映射到该图像所在的平面. ...

  2. Opencv2.4.9源码分析——Stitching(五)

    5.曝光补偿 5.1 原理 即使通过几何投影,图像之间可以做到很好的拼接,但如果不同图像之间有不同的曝光程度,那么拼接图像中的重叠部分也会出现明显的边缘,这样就使图像看起来十分不自然.因此,我们还需要 ...

  3. Opencv2.4.9源码分析——Stitching(一)

    相机镜头所呈现出的景物要比人类的视觉系统所看到的景物要狭小得多,因此一幅图像不可能捕获到我们所看到的整个景物.全景图像拼接给出了这个问题的解决办法,它是把图像间重叠部分拿出来拼接起来,从而得到一幅更大 ...

  4. Opencv2.4.9源码分析要点摘录

    以下摘录自 http://blog.csdn.net/zhaocj?viewmode=contents Opencv2.4.9源码分析要点摘录 Boosting AdaBoost的计算步骤: 1.设有 ...

  5. Flume 1.7 源码分析(四)从Source写数据到Channel

    Flume 1.7 源码分析(一)源码编译 Flume 1.7 源码分析(二)整体架构 Flume 1.7 源码分析(三)程序入口 Flume 1.7 源码分析(四)从Source写数据到Channe ...

  6. spring源码分析第四天------springmvc核心原理及源码分析

    spring源码分析第四天------springmvc核心原理及源码分析 1.基础知识普及 2. SpringMVC请求流程 3.SpringMVC代码流程 4.springMVC源码分析 4.1 ...

  7. Anbox源码分析(四)——Anbox渲染原理(源码分析)

    Anbox源码分析(四) 上篇文章我们从源码分析了一下Anbox是怎样一步步的准备了OpenGL ES的渲染环境的,这篇文章,我们继续分析Android的渲染指令是如何到达宿主机进行渲染的. 宿主机端 ...

  8. shardingsphere源码分析(四)-- 改写引擎

    shardingsphere源码分析(四)-- 改写引擎 shardingsphere源码分析(四)-- 改写引擎 官方介绍 debug 总结 shardingsphere源码分析(四)-- 改写引擎 ...

  9. kube-scheduler源码分析(四)之 findNodesThatFit

    本文个人博客地址:https://www.huweihuang.com/kubernetes-notes/code-analysis/kube-scheduler/findNodesThatFit.h ...

最新文章

  1. java gui 案例_JavaGui入门—布局的嵌套使用附实例
  2. 隔空操作之通过简单计算识别手的挥动反向
  3. 数据意识上的“代沟”
  4. python_目录结构
  5. 天池大赛, Storm
  6. oracle 升级前备份,rac(exadata)升级前的备份及LVM快照的恢复
  7. 尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm ?简单好用!源码揭秘!
  8. Android之解决PC浏览器上传表单文件到手机服务器read数据错误导致有时候下载到手机的文件打开文字错乱问题
  9. 解决Vscode提示bodyparser已被弃用的问题
  10. 在CentOS6和CentOS7安装epel仓库-最简单的方法
  11. 纽微特荒唐事:都知道是找人顶罪,竟没人敢指正
  12. sqlserver企业版秘钥_SQLserver 2012下载 (附密钥)
  13. Exadata一体机故障回顾
  14. hadoop源码编译(从0到1一步步教你如何编译,适用于任何hadoop版本)
  15. 概率论————思维导图(上岸必备)(数字特征)
  16. java计算2个日期的天数时间差
  17. mysql给一张表做快照_MySQL之快照读
  18. python多个判断条件_python if not in 多条件判断代码
  19. 夜神模拟器——最好用的安卓模拟器
  20. 现场直击大数据行业应用实践

热门文章

  1. 视频直播制作软件:MimoLive Mac v5.2b2
  2. d1,d2,d3 error
  3. dumpsys alarm 格式解读
  4. 浏览器地址栏无法直接使用Google搜索问题
  5. 【动画图解微积分笔记】 (一) -1.概述 (附B站视频)
  6. 初学者建模和布线技巧
  7. 那些主流的淘宝客引流方法有哪些?
  8. 刘利刚-什么是计算机图形学?
  9. 认证模式之Digest模式
  10. 如何把视频转换成gif动图