4

还原被摄像机透视的纹理

 3 years ago
source link: https://www.skywind.me/blog/archives/1363
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

还原被摄像机透视的纹理

有人问如何还原被透视纹理?给你一张照片,还原照片上四个点所组成的平面的纹理该怎么做?我们可以从数学上推导一下,为了和三维图形的透视纹理映射对照,我们称照片上四个点在照片上的位置为“屏幕坐标”,那么可以发现:

空间中,三维坐标(x,y,z)和纹理坐标(u, v)承线性关系。根据该问题描述,可以理解为已知四个点的屏幕投影坐标(xi,yi),和对应纹理坐标(u,v),求整个纹理坐标系到屏幕坐标系的反向映射过程,即根据(u,v)求解(xi,yi)。

1. 按照纹理隐射的原理,同平面纹理坐标与空间坐标存在线性关系,设 a1-a12为常数,有:

a1 * x +  a2 * y +  a3 * z +  a4 = u ... 线性关系
a5 * x +  a6 * y +  a7 * z +  a8 = v ... 线性关系
a9 * x + a10 * y + a11 * z + a12 = 0 ... 平面方程

2. 求解上面的方程组,可以得到类似下面的关系,其中b1-b9为常数:

x = b1 * u + b2 * v + b3  
y = b4 * u + b5 * v + b6 
z = b7 * u + b8 * v + b9 

常数 b1-b9如果展开,就是9个关于a1-a12的等式,太长了,这里不展开,有兴趣可以自己求解。

3. 屏幕上投影坐标一般是:

           x
xi = D1 * --- + C1
           z
       
           x
yi = D2 * --- + C2
           z

因为同样一个透视投影矩阵下,能隐射成屏幕上同样形状纹理的平面,在空间中存在无穷多个,而且还存在不同透视投影矩阵下,同样屏幕投影的平面存在更多无穷多个。这里我们不用去求解每个平面,直接设置 D1 = D2 = 1 且 C1 = C2 = 0 有:

      x
xi = ---
      z
       
      x
yi = ---
      z

4. 展开等式:

       b1 * u + b2 * v + b3
xi =  ----------------------
       b7 * u + b8 * v + b9

       b4 * u + b5 * v + b6
yi =  ----------------------
       b7 * u + b8 * v + b9

改变一下,用矩阵 M的各个元素 m00 – m22来代替 b1-b9。

      m00 * u + m01 * v + m02
xi = -------------------------
      m20 * u + m21 * v + m22

      m10 * u + m11 * v + m12
yi = -------------------------
      m20 * u + m21 * v + m22

5. 于是整个问题很简单了,就是求矩阵 M。我们知道存在矩阵 M,使得:

/ m00 m01 m02 \   / u \
| m10 m11 m12 | . | v | = [ uo, vo, wo ] = [ xi * wo, yi * wo, wo ]
\ m20 m21 m22 /   \ 1 /

               m00 * u + m01 * v + m02
xi = uo / wo = ------------------------  .......(4)
               m20 * u + m21 * v + m22

               m10 * u + m11 * v + m12
yi = uo / wo = ------------------------  .......(5)
               m20 * u + m21 * v + m22

6. 再此基础上,我们已知屏幕上四个端点的位置(x0, y0)-> (x3, y3), 也知道他们对应的纹理坐标,(u0, v0) -> (u3, v3)。继续设 m22 = 1,可以列出一个方程:

/ u0 v0  1  0  0  0 -u0*x0 -v0*x0 \ /m00\ /x0\
| u1 v1  1  0  0  0 -u1*x1 -v1*x1 | |m01| |x1|
| u2 v2  1  0  0  0 -u2*x2 -v2*x2 | |m02| |x2|
| u3 v3  1  0  0  0 -u3*x3 -v3*x3 |.|m10|=|x3|
|  0  0  0 u0 v0  1 -u0*y0 -v0*y0 | |m11| |y0|
|  0  0  0 u1 v1  1 -u1*y1 -v1*y1 | |m12| |y1|
|  0  0  0 u2 v2  1 -u2*y2 -v2*y2 | |m20| |y2|
\  0  0  0 u3 v3  1 -u3*y3 -v3*y3 / \m21/ \y3/

对于这个方程组,四个端点的 x, y是已知的,同时纹理坐标 u, v也是已知的。那么我们解个方程就能求出矩阵 M的各个元素。现在你已经有了 m00 – m22,有了式子4,和式子5:

     / m00 m01 m02 \ 
M = |  m10 m11 m12  |   .....  已知
     \ m20 m21 m22 / 

      m00 * u + m01 * v + m02
xi = -------------------------    ... (4)
      m20 * u + m21 * v + m22

      m10 * u + m11 * v + m12
yi = -------------------------    ... (5)
      m20 * u + m21 * v + m22

7. 在已知 M矩阵的各个元素,已知纹理坐标(u, v),你想求对应的屏幕坐标(x, y),就可以用公式(4)和公式(5)来求解了。假设纹理为 256×256大小,最终求解原纹理图片的代码应该是:

for (int v = 0; v < 256; v++) {
     for (int u = 0; u < 256; u++) {
        int x = CalculateX(M, u, v);  // 带入式子(4)
          int y = CalculateY(M, u, v);  // 带入式子(5)
          texture[v * 256 + u] = GetPixel(x, y);
     }
}

8. 优化一下,上面每个点的乘除法太多,我们需要找找关系,设:

A = m00 * u + m01 * v + m02    
B = m10 * u + m11 * v + m12    
C = m20 * u + m21 * v + m22   

于是有:x = A / C, y = B / C,而随着 u坐标的均匀递增 A, B, C成线性变化,那么我们可以用差值来完成我们的优化:

for (int v = 0; v < 256; v++) {
    float A1 = m00 *   0 + m01 * v + m02;
    float A2 = m00 * 256 + m01 * v + m02;
    float B1 = m10 *   0 + m11 * v + m12;
    float B2 = m10 * 256 + m11 * v + m12;
    float C1 = m20 *   0 + m21 * v + m22;
    float C2 = m20 * 256 + m21 * v + m22;

    float dA = (A2 - A1) * (1 / 256.0);   // 计算步长
    float dB = (B2 - B1) * (1 / 256.0);   
    float dC = (C2 - C1) * (1 / 256.0);  

    float A = A1 + 0.5;  // 方便取整时四舍五入
    float B = B1 + 0.5;  
    float C = C1 + 0.5;

    uint32_t *pixel = &texture[v * 256];

    for (int u = 0; u < 256; u++) {
        float i = 1.0 / C;    // 减少一次除法
        int x = (int)(A * i);
        int y = (int)(B * i);
        pixel[0] = GetPixel(x, y);
        pixel++;
        A += dA;
        B += dB;
        C += dC;
    }
}

整个程序基本上就是这个样子了,内循环已经足够紧凑。再次观察等式关系,可以发现其实:dA = m00, dB = m10, dC = m20。还可以继续进一步化解外层循环。是不是和透视纹理绘制方法很像?一个正过程,一个逆过程而已。

注意1:越界处理,简单判断下越界坐标。

注意2:如果还需要进一步提高性能,可以尝试定点数优化计算过程,尝试继续展开循环。

241 total views, 1 view today

I like thisUnlike LikeI dislike thisUndislike 

Categories: 图形编程 Tags: 图形

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK