如何将屏幕x,y(笛卡尔坐标)转换为3D世界空间准星移动角度(screenToWorld)?

4

最近我一直在研究计算机视觉和神经网络。
并且发现了一个实验性的物体检测方法,可以在3D应用程序中使用。
但是,令我惊讶的是 - 我面临着将一个坐标系转换为另一个坐标系的问题 (据我所知是笛卡尔坐标系到极坐标/球坐标系)

让我解释一下。
例如,我们有一个3D应用程序窗口的截图 (某个3D游戏)enter image description here

现在,使用Open-CV或神经网络,我能够检测到圆形球体 (游戏目标)
以及它们在游戏窗口中的X,Y坐标 (x,y偏移量)
enter image description here

如果我想要在给定的 X, Y 坐标内编程移动鼠标光标以瞄准其中一个目标,那么它只能在桌面环境下(移动桌面中的光标)工作。

但是当我切换到 3D 游戏并且我的鼠标光标现在位于 3D 游戏世界环境中时,它不起作用,也无法瞄准目标。

因此,我对这个问题进行了适当的研究。我发现,鼠标光标被锁定在 3D 游戏中。因此,我们不能使用 mouse_event win32 调用中的 MOUSEEVENTF_MOVE (0x0001) + MOUSEEVENTF_ABSOLUTE (0x8000) 标志来移动光标。

我们只能使用相对移动来编程移动鼠标。理论上,为了获得这种相对鼠标移动偏移量,我们可以计算检测结果与 3D 游戏窗口中心的偏移量。在这种情况下,如果目标点距离屏幕中心左侧 100px,则相对移动向量将是 (x=-100, y=0)

事实是,在3D游戏中,准星不会向左移动100像素,也不会瞄准给定的目标。但它会朝着给定的方向稍微移动一点。
之后,我对这个主题进行了更多的研究。据我所知,在3D游戏中,准星使用3D空间中的角度移动。具体来说,只有两个角度:水平移动角度和垂直移动角度。
因此,游戏引擎将我们的鼠标移动转换为给定3D世界空间内的移动角度。这就是在3D游戏中完成准星移动的方法。但我们无法访问它,我们只能通过外部调用win32来移动鼠标。

我决定以某种方式计算每度像素数(win32 相对鼠标移动需要使用的像素数量,以在游戏内将准星移动 1 度)。 为了做到这一点,我写下了一个简单的计算算法。 它是这样的: enter image description here

正如您所看到的,我们需要通过 win32 相对地水平移动鼠标 16400 像素,才能使游戏内的准星移动360度。 确实如此。 分别通过 16400/2 来移动准星 180 度。

接下来,我尝试将屏幕上的目标偏移坐标 X,Y 转换为百分比(从屏幕中央开始)。 然后再将它们转换为度数。

总体公式看起来是这样的:
(仅列出水平移动的示例):

w = 100  # screen width
x_offset = 10  # target x offset 
hor_fov = 106.26

degs = (hor_fov/2) * (x_offset /w)  # 5.313 degrees

实际上,它起作用了!
但并不完全如预期。
总体瞄准精度不同,取决于目标距离屏幕中央的距离。

我不太擅长三角学,但我可以说-极坐标/球面坐标肯定有关系。
因为我们只能水平和垂直地看到游戏世界的一部分。
这也被称为 FOV (视野)

因此,在给定的 3D 游戏中,我们仅能水平查看 106.26 度。
而纵向则是 73.74 度。

我的猜测是,我试图将坐标从线性系统转换为非线性系统。
结果导致整体精度不足。

我还尝试在 Python 中使用 math.atan
它可以工作,但精度仍然不够高。

下面是代码:

def point_get_difference(source_point, dest_point):
    # 1000, 1000
    # source_point = (960, 540)
    # dest_point = (833, 645)
    # result = (100, 100)

    x = dest_point[0]-source_point[0]
    y = dest_point[1]-source_point[1]

    return x, y

def get_move_angle__new(aim_target, gwr, pixels_per_degree, fov):
    game_window_rect__center = (gwr[2]/2, gwr[3]/2)
    rel_diff = list(point_get_difference(game_window_rect__center, aim_target))

    x_degs = degrees(atan(rel_diff[0]/game_window_rect__center[0])) * ((fov[0]/2)/45)
    y_degs = degrees(atan(rel_diff[1] / game_window_rect__center[0])) * ((fov[1]/2)/45)
    rel_diff[0] = pixels_per_degree * x_degs
    rel_diff[1] = pixels_per_degree * y_degs

    return rel_diff, (x_degs+y_degs)

get_move_angle__new((900, 540), (0, 0, 1920, 1080), 16364/360, (106.26, 73.74))
# Output will be: ([-191.93420990140876, 0.0], -4.222458785413539)
# But it's not accurate, overall x_degs must be more or less than -4.22...

有没有一种精确地将2D屏幕X,Y坐标转换为3D游戏十字准心移动度数的方法?
一定有办法,我就是想不出来...


这取决于游戏的渲染方法... 如果它是3D透视投影,您必须考虑znear值(或焦距)以及相机的FOVx,FOVy角度来调整您的角度计算。如果它是2.5D射线投射,例如Wolfenstein,则通常需要考虑在x轴上使用投影,通常使用cos(x_angle)或其反转而不是直接使用x坐标。 - Spektre
@Spektre,这基本上是默认的3D透视投影,相机使用近和远裁剪平面将所有内容渲染到2D平面(屏幕)上。问题在于,我们没有实际的“znear”和“zfar”值,也没有目标对象在3D空间中的距离、大小或位置信息。 - Abraham Tugalov
@AbrahamTugalov 奇怪的网站没有通知我...为了获得透视图,我建议尝试:FOVx = 2.0*atan(0.5*screen_x_resolution/znear); angle_x = atan(screen_x_from_center/znear); 所以你可能需要测量/拟合一些你不知道的东西来获取 znear、FOVx ... 另外 screen_x 可能需要缩放(通过屏幕分辨率和有时甚至是缩放比例)。如果你钩入游戏进程,你甚至可以从那里获取视频和深度缓冲区,从而可以获得目标的原始 3D 位置 - Spektre
@ardget,它可以工作,但仍不准确。您的解决方案遭受了与“Simon Lundberg”给出的答案相同的不准确性问题。我猜公式缺少某些东西(也许是PHI/Theta)。 - Abraham Tugalov
在我看来,这里的主要困难在于弄清游戏引擎如何将你的3D空间投影到2D屏幕上。如果找不到更直接的方法,可以在3D空间中散布几个点并记录它们的2D屏幕坐标。然后,希望不难弄清楚它使用的是哪种方法。 - Simon Goater
显示剩余2条评论
1个回答

0

屏幕中心和边缘之间的中点并不等于视野角度除以四。正如您所注意到的,这种关系是非线性的。

屏幕上分数位置(0-1)与屏幕中心之间的角度可以按以下方式计算。这是针对水平旋转(即围绕垂直轴)的,因此我们只考虑屏幕上的X位置。

# angle is the angle in radians that the camera needs to
# rotate to aim at the point

# px is the point x position on the screen, normalised by
# the resolution (so 0.0 for the left-most pixel, 0.5 for
# the centre and 1.0 for the right-most

# FOV is the field of view in the x dimension in radians
angle = math.atan((x-0.5)*2*math.tan(FOV/2))

对于100度的视野和零点x,这给了我们-50度的旋转(正好是视野的一半)。对于0.25的x(在边缘和中间之间),我们得到大约-31度的旋转。

注意2*math.tan(FOV/2) 部分对于任何特定的视野都是固定的,因此您可以提前计算并存储它。然后就变成了(假设我们将其命名为z):

angle = math.atan((x-0.5)*z)

对于x和y都这样做,它应该可以工作。

编辑/更新:

这是一个完整的函数。我已经测试过了,看起来它可以正常工作。

import math

def get_angles(aim_target, window_size, fov):
"""
    Get (x, y) angles from center of image to aim_target.

    Args:
        aim_target: pair of numbers (x, y) where to aim
        window_size: size of area (x, y)
        fov: field of view in degrees, (horizontal, vertical)

    Returns:
       Pair of floating point angles (x, y) in degrees
    """
    fov = (math.radians(fov[0]), math.radians(fov[1]))
    
    x_pos = aim_target[0]/(window_size[0]-1)
    y_pos = aim_target[1]/(window_size[1]-1)


    x_angle = math.atan((x_pos-0.5)*2*math.tan(fov[0]/2))
    y_angle = math.atan((y_pos-0.5)*2*math.tan(fov[1]/2))

    return (math.degrees(x_angle), math.degrees(y_angle))


print(get_angles(
    (0, 0), (1920, 1080), (100, 67.67)
), "should be around -50, -33.835")

print(get_angles(
    (1919, 1079), (1920, 1080), (100, 67.67)
), "should be around 50, 33.835")

print(get_angles(
    (959.5, 539.5), (1920, 1080), (100, 67.67)
), "should be around 0, 0")

print(get_angles(
    (479.75, 269.75), (1920, 1080), (100, 67.67)
), "should be around 30.79, 18.53")

看起来提供的公式有些缺失。 对于给定的输入 get_move_angle__new((441, 446), (0, 0, 1920, 1080), 16364/360, [106.26, 73.74]),输出角度必须更接近于 [-41.88894918065101, -8.580429158509922],但实际输出为 [-41.588949180651014, -12.680429158509924]。 正如您所见,进行了 [-0.30000000000000004, 4.100000000000001] 的修正。 在实践中,大多数情况下它可以完美地瞄准水平轴(但如果目标太远,则精度会降低)。 而垂直轴非常不准确(也许与 PHI/Theta 有关?)。 - Abraham Tugalov
还应该注意的是,垂直旋转(也称为“偏航”)与水平旋转(也称为“俯仰”)不同。垂直轴根本无法旋转360度。我猜它在某种程度上被限制在180度,因此准心只能看向顶部和底部。 - Abraham Tugalov
你有考虑到索引是从零开始到大小减一吗?也就是说,归一化不应该是 xpos/xsize,而应该是 xpos/(xsize-1) - Simon Lundberg
嗯,我对此没有解释。我在三角学方面相当自信。你正在检查的项目可能使用某种扭曲,这使得几乎不可能找出精确的公式。你可能需要使用启发式技术,如查找表或拟合多项式来进行校正。 - Simon Lundberg
也许吧,不确定。让我在我的3D应用程序中进行测试(我可以使用Unity或WebGL制作),然后告诉你是否是这种情况。 - Abraham Tugalov
显示剩余3条评论

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