一般的思路:
由全景平面坐标转到三维球面坐标再转到立方体(三维笛卡尔坐标)上,由于三维球面坐标是极坐标,当其映射到三维笛卡尔坐标系时会存在空洞。这里可以这么简单地得到理解:假设在极坐标系中角分辨率是一定的,那么随着坐标值的增大其角分辨率对应的距离越大,会导致六面体的像素缺失。另一种解释是,球的表面积小于其外切正方体的面积,所以如果直接将球坐标系上的点投影到立方体上,会存在立方体像素缺失的情况。
如何避免上述空洞问题呢?一个思路是将上述过程反变换:将立方体投影到三维球面坐标,再在全景平面找到对应的像素坐标。因此根据这一思路可以将全景平面照片生成六面体照片,即立方体的六个面。
首先需要定义两个坐标系:
建立一个三维单位球,半径为1,其是全景照片的极坐标表示,建立单位球的外切正方体,其边长为2.
三维笛卡尔坐标系(相机坐标系):
x轴正方向朝右,y轴正方向朝前,z轴正方向朝上(右手系)。
假设六面体对应六张影像,设其分辨率为h,像素坐标为(u,v),则其对应的图像坐标为(x,y):
{
x
=
2
∗
u
h
−
1
y
=
2
∗
v
h
−
1
\begin{cases} x=\frac{2*u}{h}-1\\ y=\frac{2*v}{h}-1 \end{cases}
{x=h2∗u−1y=h2∗v−1
进一步得到(x,y)在三维笛卡尔坐标系中的坐标P(X,Y,Z):
进一步极坐标与笛卡尔坐标的关系为:
{
X
=
r
.
c
o
s
θ
.
c
o
s
α
Y
=
r
.
c
o
s
θ
.
s
i
n
α
Z
=
r
.
s
i
n
θ
\begin{cases} X=r.cos\theta.cos\alpha\\ Y=r.cos\theta.sin\alpha\\ Z=r.sin\theta \end{cases}
⎩
⎨
⎧X=r.cosθ.cosαY=r.cosθ.sinαZ=r.sinθ
θ
,
α
\theta,\alpha
θ,α为OP与xoy平面夹角,及OP在xoy平面投影与x轴夹角。
−
0.5
π
<
θ
<
0.5
π
,
−
π
<
α
<
π
-0.5\pi<\theta<0.5\pi,-\pi<\alpha<\pi
−0.5π<θ<0.5π,−π<α<π,r=1.
可得:
{
θ
=
a
r
c
s
i
n
(
Z
X
∗
X
+
Y
∗
Y
+
Z
∗
Z
)
α
=
a
r
c
t
a
n
(
Y
X
)
\begin{cases} \theta=arcsin(\frac{Z}{\sqrt{X*X+Y*Y+Z*Z}}) \\ \alpha=arctan(\frac{Y}{X}) \end{cases}
{θ=arcsin(X∗X+Y∗Y+Z∗ZZ)α=arctan(XY)
三维球坐标系与全景照片的关系如图所示(网图,符号与本文定义不符,忽略符合):
相当于把球先投影到圆柱侧面,再展开成平面,因此,
θ
\theta
θ表示全景平面的y值,
α
\alpha
α表示全景照片平面的x值。设全景照片分辨率m,n(m,n=高宽=1:2),其像素坐标为(u1,v1):
{
u
1
=
α
π
∗
n
2
+
n
2
v
1
=
−
θ
0.5
π
∗
m
2
+
m
2
\begin{cases} u1=\frac{\alpha}{\pi}*\frac{n}{2}+\frac{n}{2}\\ v1=\frac{-\theta}{0.5\pi}*\frac{m}{2}+\frac{m}{2} \end{cases}
{u1=πα∗2n+2nv1=0.5π−θ∗2m+2m
注意全景图像左上角对应
θ
=
0.5
π
,
α
=
−
π
\theta=0.5\pi,\alpha=-\pi
θ=0.5π,α=−π所以这里的y方向与全景图像坐标系y是相反的,所以使用
−
θ
-\theta
−θ。
通过上述过程就得到了从正方体像素平面坐标(u,v)->(X,Y,Z)->
(
θ
,
α
)
(\theta,\alpha)
(θ,α)->全景像素平面坐标(u1,v1),完成全景到6张正方体图像的转换。
代码实现:
#include <iostream>
#include<opencv2/opencv.hpp>
using namespace cv;
const double pi = 3.1415926;
void pano2cube(Mat &pano_img, int face_id)
{
Mat cube = Mat(512, 512, CV_8UC3);
for (size_t i = 0; i < cube.rows; i++)
{
for (size_t j = 0; j < cube.cols; ++j)
{
//get img coordinates
auto img_x = 2.0 * double(i) / double(cube.cols) - 1.0;
auto img_y = 2.0 * double(j) / double(cube.cols) - 1.0;
//transform img coordinates to camera coordinates(Cartesian coordinates)
double camera_x = 0.0;
double camera_y = 0.0;
double camera_z = 0.0;
switch (face_id)
{
case 0:
{
camera_x = img_x;
camera_y = -1.0 * img_y;
camera_z = 1;
break;
}
case 1:
{
camera_x = img_x;
camera_y = -1.0 * img_y;
camera_z = -1.0;
break;
}
case 2:
{
camera_x = img_x;
camera_y = 1.0;
camera_z = -1.0 * img_y;
break;
}
case 3:
{
camera_x = img_x;
camera_y = -1.0;
camera_z = -1.0 * img_y;
break;
}
case 4:
{
camera_x = -1.0;
camera_y = img_x;
camera_z = -1.0 * img_y;
break;
}
case 5:
{
camera_x = 1.0;
camera_y = img_x;
camera_z = -1.0 * img_y;
break;
}
default:
break;
}
double xyz_dist = std::pow((camera_x * camera_x + camera_y * camera_y+camera_z*camera_z), 0.5);
//pano polar coordinates
double theta = std::asin(camera_z/xyz_dist);
double alpha = std::atan2(camera_y, camera_x);//[-pi,pi]
//pano pixel coordinates
double u = alpha / pi * double(pano_img.cols) / 2.0 + double(pano_img.cols) / 2.0;
double v = -1.0 * theta / (0.5 * pi) * double(pano_img.rows) / 2.0 + double(pano_img.rows) / 2.0;
if (v>pano_img.rows ||u> pano_img.cols)
{
//std::cout << u << " " << v << std::endl;
continue;
}
cube.at<cv::Vec3b>(j, i)[0] = pano_img.at<cv::Vec3b>(v, u)[0];
cube.at<cv::Vec3b>(j, i)[1] = pano_img.at<cv::Vec3b>(v, u)[1];
cube.at<cv::Vec3b>(j, i)[2] = pano_img.at<cv::Vec3b>(v, u)[2];
}
}
cv::imwrite(std::to_string(face_id) + "cube.jpg", cube);
}
int main()
{
auto pano = imread("pano.jpg");
for (size_t i = 0; i < 6; i++)
{
pano2cube(pano, i);
}
std::cout << "Hello World!\n";
}
效果:
全景影像:
生成的六面体
参考:
1