如何将球面坐标转换为等经纬度投影坐标?

16

简化问题

如何将一个球面坐标(θ,φ)转换为等射线投影(也称为“地理投影”)上的位置(x,y)?

其中:

  • x是经度,水平位置,范围为-180到180度。
  • y是纬度,垂直位置,范围从-90到90度。
  • θ是theta角,水平角度(度数),从(0,0,0)向球体表面上的点的矢量。
  • φ是phi角,垂直角度(度数),从(0,0,0)向球体表面上的点的矢量。

下面是原始问题,在我还不太理解问题时,但我认为它仍然很好地展示了这个解决方案的实际应用。

背景

编辑:原问题标题为:如何将拍摄的带有角度的照片转换为全景照片的一部分?

如果我想以这样的方式转换以任意给定角度拍摄的照片,那么我应该采取哪些步骤才能将结果(扭曲/变形)图像放置在等射线投影、立方体贴图或任何全景照片投影的相应特定位置上?

无论哪种投影方式都足够简单,因为有大量资源可以在不同投影之间进行转换。我只是不知道如何从实际照片到这样的投影中进行步骤。

可以安全地假设相机将保持固定位置,并可以在任何方向旋转。我认为所需的数据可能类似于:

  • 物理相机的水平角度[-180,+ 180](例如+ 140deg)。
  • 物理相机的垂直角度 [-90, +90](例如-30度)。
  • 照片分辨率 w x h(例如1280x720像素)。
  • 照片的水平角度(例如70度)。
  • 照片的垂直角度(例如40度)。
  • 镜头校正的a、b、c参数(见下文)。

我有这些数据,我猜第一步是进行镜头校正,以使所有应该直线的线都是实际上的直线。这可以使用imagemagick桶形畸变来完成,其中您只需要填写三个参数:a、b和c。应用于图像以进行校正的转换很简单。

我卡在了下一步。可能是我没有完全理解它,或者搜索引擎没有帮助我,因为大多数结果都是关于在已经给定的投影之间转换或使用先进的应用程序智能地将照片拼接在一起。这些结果没有帮助我回答我的问题。

编辑:我想也许一张图片会更好地解释它:

问题是给定的照片红色不能放入等距投影中而不进行转换。下图说明了这个问题。

Equirectangular projection with a "normal" photo

因此,我需要将红色转换为绿色蓝色显示转换的差异,但这取决于水平/垂直角度。


在最简单的情况下,等经纬投影不会改变任何数字。投影的x坐标等于经度θ,投影的y坐标等于纬度φ。常数因子被选择为选择显示零扭曲的纬度。在您的应用程序中,您可能需要进行后续缩放/平移以获取像素坐标。 - tiwo
图像拼接通常涉及“高级”校正,因为在实践中,精确的投影参数(θ、φ以及相机参数)是未知且难以获取的。 - tiwo
@tiwo,不是很清楚,我已经解决了这个问题,但是我还没有找到时间发布答案。下周会处理。生成的投影不是像素精确的,但足够好看,不需要在各个照片的边缘进行缩放(我应该添加模糊边缘来进一步减少转换伪影)。 - Yeti
@Yeto 很高兴听到这个消息,听起来是一个不错的实用解决方案 :-) - tiwo
2个回答

7
如果照片是从固定点拍摄的,相机只能在该点周围绕其偏航和俯仰旋转。那么我们可以考虑任意半径的球体(为了数学计算,强烈建议使用半径为1)。这张照片将成为该球体上的矩形形状(从相机的视角来看)。

地平线情况

如果您正在观察地平线(赤道),则垂直像素代表纬度,而水平像素代表经度。对于地平线简单全景照片,问题不大:

Looking at the wireframe of the geo-sphere from (0,0,0)

这里我们大致看一下我们的世界地平线。也就是说,相机的角度为va = ~0。因此,这很容易理解,因为如果我们知道照片的宽度为70度,高度为40度,那么我们也知道经度范围将约为70度,纬度范围为40度。
如果我们不关心轻微的失真,那么从照片中计算出任何像素(x,y)(longitude,latitude)的公式就很容易了。
photo_width_deg = 70
photo_height_deg = 30
photo_width_px = 1280
photo_height_px = 720
ha = 0
va = 0
longitude = photo_width_deg * (x - photo_width_px/2) / photo_width_px + ha
latitude = photo_height_deg * (y - photo_height_px/2) / photo_height_px + va

问题

但是当我们将相机垂直移动得更多时,这种近似根本不起作用:

Looking at the wireframe of the geo-sphere from (0,0,0)

那么,如何在给定拍摄照片的垂直/水平角度(va, ha)的情况下,将图片中的一个像素从(x, y)转换为经纬度坐标(longitude, latitude)呢?
解决方案
对我有帮助的重要思路是:你基本上有两个球体:
1.以相机为中心的照片球体。
2.以经度/纬度坐标为基础的地理球体(等距投影球体)。
您知道照片球体上一点的球面坐标,并想知道该点在不同相机角度下在地理球体上的位置。
真正的问题
我们必须认识到,仅使用球面坐标进行两个球体之间的任何计算是困难的笛卡尔坐标系的数学要简单得多。在笛卡尔坐标系中,我们可以轻松地绕任意轴旋转,使用乘以坐标向量[x,y,z]旋转矩阵来获得旋转后的坐标。 警告: 这里非常重要的是要知道有关x轴、y轴和z轴含义的不同约定。不确定哪个轴是垂直轴,哪个指向何处。你只需为自己画一个图并决定这一点。如果结果有误,可能是因为弄混了这些。对于球面坐标的thetaphi也是如此。

真正的解决方案

因此,关键在于将照片球形坐标系转换为笛卡尔坐标系,然后应用旋转,最后再回到球面坐标系:

  1. 取照片上的任意像素,并计算其相对于照片中心的水平和垂直角度。
  2. 将照片球面坐标系转换为笛卡尔坐标系([x,y,z]向量)。
  3. 对坐标应用旋转矩阵,就像相机被旋转(ha,va)一样。
  4. 将笛卡尔坐标系转换回球面坐标系,这些将是您的经度和纬度。

示例代码

// Photo resolution
double img_w_px = 1280;
double img_h_px = 720;
// Camera field-of-view angles
double img_ha_deg = 70;
double img_va_deg = 40;
// Camera rotation angles
double hcam_deg = 230;
double vcam_deg = 60;
// Camera rotation angles in radians
double hcam_rad = hcam_deg/180.0*PI;
double vcam_rad = vcam_rad/180.0*PI;
// Rotation around y-axis for vertical rotation of camera
Matrix rot_y = {
    cos(vcam_rad), 0, sin(vcam_rad),
    0, 1, 0,
    -sin(vcam_rad), 0, cos(vcam_rad)
};
// Rotation around z-axis for horizontal rotation of camera
Matrix rot_z = {
    cos(hcam_rad), -sin(hcam_rad), 0,
    sin(hcam_rad), cos(hcam_rad), 0,
    0, 0, 1
};

Image img = load('something.png');
for(int i=0;i<img_h_px;++i)
{
    for(int j=0;j<img_w_px;++j)
    {
        Pixel p = img.getPixelAt(i, j);

        // Calculate relative position to center in degrees
        double p_theta = (j - img_w_px / 2.0) / img_w_px * img_w_deg / 180.0 * PI;
        double p_phi = -(i - img_h_px / 2.0) / img_h_px * img_h_deg / 180.0 * PI;

        // Transform into cartesian coordinates
        double p_x = cos(p_phi) * cos(p_theta);
        double p_y = cos(p_phi) * sin(p_theta);
        double p_z = sin(p_phi);
        Vector p0 = {p_x, p_y, p_z};

        // Apply rotation matrices (note, z-axis is the vertical one)
        // First vertically
        Vector p1 = rot_y * p0;
        Vector p2 = rot_z * p1;

        // Transform back into spherical coordinates
        double theta = atan2(p2[1], p2[0]);
        double phi = asin(p2[2]);

        // Retrieve longitude,latitude
        double longitude = theta / PI * 180.0;
        double latitude = phi / PI * 180.0;

        // Now we can use longitude,latitude coordinates in many different projections, such as:
        // Polar projection
        {
            int polar_x_px = (0.5*PI + phi)*0.5 * cos(theta) /PI*180.0 * polar_w;
            int polar_y_px = (0.5*PI + phi)*0.5 * sin(theta) /PI*180.0 * polar_h;
            polar.setPixel(polar_x_px, polar_y_px, p.getRGB());
        }
        // Geographical (=equirectangular) projection
        {
            int geo_x_px = (longitude + 180) * geo_w;
            int geo_y_px = (latitude + 90) * geo_h;
            geo.setPixel(geo_x_px, geo_y_px, p.getRGB());
        }
        // ...
    }
}

请注意,这只是某种伪代码。建议使用矩阵库来处理矩阵和向量的乘法和旋转。


polar_w、polar_h、geo_w 和 geo_h 是什么? - PvDev
1
@PvDev 这些只是最终投影的宽度和高度,将度数转换为像素。例如,考虑这张地球贴图的等经纬度投影。如果我们有一个以度为单位的经度和纬度,并且想要在该图像上画出该点,则geo_w_px为2058,geo_h_px为1036。但是,首先我们需要对度数进行归一化,因此我们执行(longitude+180)/360.0*geo_w_px。但是在伪代码中,我预先计算了geo_wgeo_w_px/360(对于polar_wpolar_h等也是同样的思路)。 - Yeti
谢谢您的回复。实际上,我正在将您的代码转换为Node.js,用于球面到等距投影的转换。 - PvDev
@PvDev 是的,但是为此您实际上不需要使用旋转,您可以直接一对一映射。您只需要一个将 (x,y) 从您的鱼眼图像转换为 (纬度, 经度) 的函数,然后再有一个将这个纬度、经度转换为您的等距投影输出图像上的像素的函数(这就是 geo_w_px 是如何计算的)。但是,请提出一个新问题,因为您的情况似乎与此不同。 - Yeti
你有没有查看我的问题?还有什么更新吗? - PvDev
显示剩余2条评论

1
只有地平线不会出现垂直畸变。不幸的是,它只是一条细线。随着畸变程度的增加,到达顶部或底部的距离越来越远,畸变程度也会随之增加。

它不像桶形畸变那样是恒定的,而是取决于地平线的垂直距离。

我认为最好的方法是从侧面观察两种类型相机和它们所投影的目标,从那里进行三角函数计算数学。

请注意,对于70mm的图片,您需要知道拍摄的角度(或估计角度)。


我已经考虑过根据图片的垂直偏移和拍摄角度来手动拉伸图片。这也是我的第一反应。但不幸的是,这并没有让我更接近解决方案。我认为我真的需要正确的数学方法才能正确地完成这项任务。现在我正在思考这个问题:我需要从平面投影到任何360度查看器的截图中反转步骤。但是,我不确定如何反转此过程,我不擅长几何数学。(顺便说一下,相机角度约为70x40度) - Yeti
这里不仅仅是简单的拉伸,拉伸因子应该按像素(而不是整个图像)应用于距离地平线的距离有关的因子。对于每个垂直像素来说,这个因子都会不同。 - Peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接