张春成

V2

2022/09/02阅读:17主题:默认主题

鱼眼看世界-V2鱼眼看世界-V2

鱼眼看世界-V2

很好奇鱼眼镜头里的“扭曲”是如何生成的,于是就有了这个模拟计算。

本文对之前的内容进行了两个方面的扩展,一是考虑了空间角的影响;二是通过变换还原真实的视野内容。


鱼眼镜头

偶然看到了这个搞全景相机的公司,Instra360。

全景相机,运动相机 - 影石Insta360官网,360度全景运动相机

它勾起了我一直以来的好奇,那就是全景相机的图像为什么是以这样的方式扭曲的?或者更具体一点的问题是,线性变换为什么会造成类似球面的扭曲?

Untitled
Untitled

我们首先假设真实的物理场景是一个简单的幕布,幕布用方格的方式进行染色

其中,下标代表求模数

经过颜色映射,即可以得到周期性方格的纹理图样。我们进一步假设这个图样在整个空间中构成一个平面,该平面与视线垂直。

texture.jpg
texture.jpg

接下来,我们假设有一个鱼眼镜头,那么在它的视野中,每个像素的位置可以用一对角度表示

其中,两个代数分别代表视线与水平和垂直轴之间的夹角,你可以想象成眼睛左右扫视和上下运动时视线的变化,由于眼睛始终在前方,因此它的视野范围不会超过180度。也因此,视线与平面的交点可以表示为三角函数的形式

在映射完成后,只要将位置代入到纹理方程,就可以计算得到该视线角度的颜色值,绘制如下。可以看到,虽然每条线都仍然是直的,但对角线的部分已经能看出扭曲

texture-in-fish-eye.jpg
texture-in-fish-eye.jpg

接下来,我将纹理稍微换个方向,就可以便这个现象更明显一些

texture.jpg
texture.jpg
texture-in-fish-eye.jpg
texture-in-fish-eye.jpg

空间角的修正

之前的分析是将两个坐标的角度当作相互独立的变量来进行计算,这样的好处是计算过程较为简单,但问题是不能真实反应视线的角度。

举个例子,你也可以自己尝试一下,首先抬头 45 度,然后再向右转头 30 度,请问这时从水平平面上看,你的视线是向右转了 30 度吗?显然不是,因为向右转的 30 度是空间角,它在水平面上的映射角可能远大于 30 度。

或者你可以这样想,想象一张A4纸,一个角固定在平面上,对角向上抬起 45 度。在这个过程中,你会纸张的轮廓在平面的投影逐渐从方形变成菱形,而靠近平面的角度及其dui jiao在不断变大,另外两个角度在逐渐变小,就是这个原理。

再或者讨论一种极端的情况,那就是先抬头90度,之后再向左右转头。这时,无论怎么转头,无论左右转头的角度有多么小,从平面的映射上看,这个转角永远等于180度。这其实就代表映射过程中产生了一个极点。

过程有点绕,但计算很简单。由于这个过程是可解析的,因此可以通过综合使用解析几何和正弦定理方法对映射的角度进行求解,(过程可见程序的 Version 2 部分)。

在经过空间角的修正后,从鱼眼镜头中得到的等势线图就是下面图的样子。有点像透视,也有消失点,但透视线不是直线。

texture-in-fish-eye-v2.jpg
texture-in-fish-eye-v2.jpg

反变换

接下来的问题就是,

在鱼眼镜头中,真实世界会变成什么样子?

这个问题不好用文字回答,但是可以绘图。因为只要通过上面介绍的等势线映射方法就可以反推出实际的视野图样。

最终得到的图样如下图所示,它大概是说,靠近视野中心的内容会显得稍微放大,而靠近左、右端点的内容则会收敛于各自的端点,收敛的过程呈现圆弧状扭曲。

虽然有点奇怪,但至少在几何上它是这个样子的。然而更加奇怪的是,人的眼球也是圆的,那为什么人的视觉很少“注意”到这个现象呢?这是个问题。

texture-in-fish-eye-v3.jpg
texture-in-fish-eye-v3.jpg

代码

以下是实现计算和绘图的代码

import numpy as np
import plotly.express as px
import matplotlib.pyplot as plt
from tqdm.auto import tqdm
from scipy.spatial.transform import Rotation as R

# Picture size
width = 500
height = 500

# Cycle length of the texture
length = 50

# Distance between fish eye and scene 
distance = 100

# Generate and draw the texture
mat = np.zeros((height, width))

for x in tqdm(range(width)):
    for y in range(height):
        v = max(x % length, y % length)
        # Texture 2 in 45 degrees
        # v = (x + y) % length
        mat[y, x] = v

fig = px.imshow(mat, title='Texture')
fig.write_image('texture.jpg')
fig.show()

# Generate and draw the fish-eye view

y_angles = np.linspace(-np.pi/2, np.pi/2, height)
x_angles = np.linspace(-np.pi/2, np.pi/2, width)

fish_mat = np.zeros((height, width))

for yj, ya in tqdm(enumerate(y_angles)):
    y = distance * np.tan(ya)
    for xj, xa in enumerate(x_angles):
        x = distance * np.tan(xa)
        v = max(x % length, y % length)
    # Texture 2 in 45 degrees
    # v = (x + y) % length
        fish_mat[yj, xj] = v

fig = px.imshow(fish_mat, title='Texture in fish-eye')
fig.write_image('texture-in-fish-eye.jpg')
fig.show()

# Generate and draw the fish-eye view in Version 2

y_angles = np.linspace(-np.pi/2, np.pi/2, height)
x_angles = np.linspace(-np.pi/2, np.pi/2, width)

fish_mat = np.zeros((height, width))

for yj, ya in tqdm(enumerate(y_angles)):
    y = distance * np.tan(ya)
    x0 = 0
    z0 = -distance
    v0 = (x0, y, z0)
    
    c = np.cos(ya)
    if c == 0:
        d0 = distance
    else:
        d0 = distance / c
    
    for xj, xa in enumerate(x_angles):
        if xa == 0:
            continue
            
        v1 = (xa, 00)
        cos = np.dot(v0, v1) / np.linalg.norm(v0) / np.linalg.norm(v1)
        acos = np.arccos(cos)
        
        angle = np.pi - acos - xa
        
        s = np.sin(angle)
        if s == 0:
            continue
        x = np.sin(xa) / np.sin(angle) * d0

        v = max(x % length, y % length)
        # Texture 2 in 45 degrees
        # v = (x + y) % length
        
        fish_mat[yj, xj] = v
        

fig = px.imshow(fish_mat, title='Texture in fish-eye, V2')
fig.write_image('assets/texture-in-fish-eye-v2.jpg')
fig.show()

# Generate and draw the fish-eye view in Version 3

y_angles = np.linspace(-np.pi/2, np.pi/2, height)
x_angles = np.linspace(-np.pi/2, np.pi/2, width)

# Along which axis, the x- and y-angle rotates
y_axis = np.array([100])
x_axis = np.array([0-10])

vec = np.array([00, -distance])

fish_mat = np.zeros((height, width))

for yj, ya in tqdm(enumerate(y_angles)):
    ry = R.from_rotvec(y_axis * ya)
    _x_axis = ry.apply(x_axis)
    _vec = ry.apply(vec)
    _vec = vec.copy()
    
    for xj, xa in enumerate(x_angles):
        rx = R.from_rotvec(_x_axis * xa)
        __vec = rx.apply(_vec)
        
        if np.abs(__vec[2]) < 1e-3:
            continue
            
        __vec *= (-distance / __vec[2])
        
        x, y, _ = __vec
        v = max(x % length, y % length)
        # Texture 2 in 45 degrees
        # v = (x + y) % length
        
        fish_mat[yj, xj] = v
        
        pass
    
fig = px.imshow(fish_mat, title='Texture in fish-eye, V3')
fig.write_image('assets/texture-in-fish-eye-v3.jpg')
fig.show()

分类:

后端

标签:

后端

作者介绍

张春成
V2