【翻译】Leapmotion-python开发官方文档(10)

相机图像

LeapMotion控制器采用双目红外相机作为跟踪传感器。你可以使用Controller.images或者Frame.images来访问由相机采集到的图像。这些函数提供ImageList对象,包含Image对象。Controller.images提供一组最新的图像。Frame.images提供用于分析生成当前数据帧的那一组图像会比直接从Controller对象产生的图像稍延迟一点。
从相机中采集到的一幅图像。高亮网格点标记出了图像中明显而又复杂的畸变。
这些图像可以用来:
头戴式显示器视频显示。
增强现实
计算机视觉。
Image 通过用接口提供了包括传感器亮度值的缓冲器和包括相机校准映射矩阵,可以用来校准镜头畸变和其他的光学畸变。

图像接口基础

从Controller.images或者Frame.images获得ImageList对象。Controller.images函数会返回最新获得的照片。Frame.images返回与当前帧相关的数据帧。因为处理数据帧会花一点时间,所以会落后于从Controller获得的图像。(在未来的版本中,数据帧速率可能从相机帧速率去耦,所以差距会变得更大)。从Controller中获取的图像有最小的延迟,但是可能和当前数据帧的跟踪数据不匹配。当使用Controller.images你可以重新实现Listener对象中的On_images()回调函数。当一组新的图像准备好时Controller就会调用你的Listener.on_images()回调函数。
图像数据是一组像素值数组。数据格式通过Image.format值返回。一般当前只有一个格式在使用。“INFRARED”格式每个像素使用一个字节,为传感器位置定义了亮度测量。你可以用灰度图显示红外格式的图像。

图象畸变

当一束光进入LeapMotion的摄像头时,镜片将光弯曲(折射)使光打在传感器上,传感器将其记录为一个特定像素位置的灰度值。当然,没有镜头是完美的,所以一束光不可能打在传感器的光学理想点上。校准映射矩阵提供修正这一畸变的数据。使你能够计算出真实的光线原始角度,你可以利用这个修正后的角度生成无畸变图像,同时利用双摄像头中获得的角度,你能够通过三角测量找到图像中特定物体的真实位置。注意,校准映射矩阵只会修正镜头畸变而不会修正透视变形。
为了校正图像,带有畸变的图像数据可以传送到一个着色器程序中,其可以对校准使用插值函数并应用到传入镜头的光中。为了获得一个小点集的角度,你可以使用Image.warp()函数(但是这个函数在高帧率情况下转换全位图时效率不够)
失真数据基于LeapMotion摄像头的视野角度。image类提供了一些函数,Image.ray_scale_x和Image.ray_scal_y能够按比例的使视角扩大来确保畸变映射矩阵能够应用到整个视野中。大概当前LeapMotion设备能有150°可视范围i,意味着一束穿过镜头的光最大坡度不超过4/1。

150°视角对应于±4的坡度。(75°的切线大约是4).
上图展示了重建畸变矫正矩阵。每个像素的亮度值源于从特定方向进入相机的光线。图像通过计算每个像素表示的水平和竖直坡度,同时利用校正映射矩阵在图像数据中找到真实的亮度值。图像的红色区域代表无亮度的区域(实际的视角小于150°)

图像方向

在LeapMotion座标系统中图像顶部一直指向z轴的负半轴方向。默认的,LeapMotion软件自动适应此座标系统这样手就是从Z轴的正半轴方向进入图像得了。(用户可以在LeapMotion的控制面板中使自动方向失效。)在手进入LeapMotion视野之前,无法知道图像的方向。因为用户可能正常放置设备也有可能以任意方向放置设备(即,设备长的那一侧的绿色LED灯朝向一个方向或者其他)。如果用户朝相反的方向放置设备,那么图像会颠倒,直到有手进入LeapMotion的视野中(或者将设备转过来)

获得原始图像

在你获得图像数据之前,你必须使用Controller.set_policy()函数设置POLICY_IMAGES标志。出于保护隐私,每个使用者也必须在LeapMotion控制面板中使能关键字使得任何程序可以访问相机的原始数据。
为了获得图像数据,用户应使用Controller,images或Frame.images函数。因为LeapMotion设备有两个摄像头。这些函数能够返回一个ImageList对象包含两张图象(这一点在将来可能会变,如果多个LeapMotion可以同时工作)。
图像编号从零开始,先左相机的图像后右相机的图像。注意设备的左右方向可以基于用户放入手的方向自动检测。自动方向检测使能设置在LeapMotion的控制面板中。
一旦你获得了一个Image对象,你可以从数据缓冲中获得8位的亮度值。这个缓冲器的大小等于Image.width乘以Image.height乘以Image.bytes_per_pixel。图像的长和宽跟随当前控制器的操作模式的变化而变化,这可以逐帧的变化。注意在强健模式下图像变为原来的一半。

获得校准映射矩阵

校准映射矩阵可以用来修正由镜头曲率及其他原因产生的图像畸变。这个映射矩阵是一个64*64的分格。每一个点都包括两个32位的值,所以缓冲大小为128乘以64乘以4.你可以通过使用Image.distortion函数获得该矩阵的缓冲。
每一个在缓冲区的点指出了在原始图像中对应于修正后的亮度值的像素的位置。有效的座标是归一化了的,值在0与1之间的小数。校准映射矩阵中单个元素的值范围在[-0.6,2.3],但是低于0和高于1的座标是不合法的,在使用校准数据时要将[0,1]范围之外的值抛弃。
为转换像素座标要与图像的宽或者长相乘。对于那些位于校准网格点之间的点,你可以使用相邻网格点的插值。相机镜头有很大的视角和畸变。因于此,在座标网格中并不是所有的点都是有效点。下面的渲染图用颜色值展示了镜头校准数据。左边图是X方向的值,右边是Y方向的。

红色值是位于图像之外的映射值
校准映射矩阵的大小可能在将来发生改变,所以Image类提供了网格尺寸(通过使用.distortion_width和.distirtion_height)实际上是网格点的两倍(宽)。包括校准数据的缓冲器的长度是distortion_width乘以 distortion_height乘以4字节。

图像射线校准

你可以用一下两种方法校准图像:
1使用Image.warp()和Image.rectify()函数。
2直接使用Image.distortion缓冲区里的数据
warp()和rectify()函数是较简单的方法但是在CPU上对图像进行逐像素处理还是相对来说很慢的。如果你只是校正少量的点,或者你不需要实时处理数据或者你不能使用GPU的着色器时你可以使用这个函数。畸变缓冲区被设计成在高帧率的情况下使用GPU着色器同时可以矫正整张原始图像。

使用Image.warp()校正

Image.warp()获取光线方向返回原始图像数据中的像素座标,转换成指定记录该光线方向上的亮度值。

使用32位ARGB纹理编码畸变数据

如果每分量32位的纹理格式在你的平台上不受支持,你可以在x轴和y轴使用一个分离的纹理查找值以及将浮点值编码成多个8位颜色分量在使用他们查找原始亮度值之前你需要将这些值解码。
对纹理中浮点型数据编码的一个共同的方法是将输入分解成4个低精度值然后再着色器中恢复。例如你能够将浮点型数据编码成拥有8比特分量的颜色数据:(注C++乱入中...)
Color encodeFloatRGBA(float input)
{
    input = (input + 0.6)/2.3; //scale the input value to the range [0..1]
    float r = input;
    float g = input * 255;
    float b = input * 255 * 255;
    float a = input * 255 * 255 * 255;

    r = r - (float)Math.floor(r);
    g = g - (float)Math.floor(g);
    b = b - (float)Math.floor(b);
    a = a - (float)Math.floor(a);

    return Color(r, g, b, a);
}

为了重组在片断着色器中的值,你可以在纹理中查找对应值并进行倒数操作。为避免丢失太多精度,将x,y畸变值编码成独立的纹理。一旦失真指数从纹理中采样和解码,你能够在相机图像纹理中找到校正后的亮度值。(注:GLSL乱入中)
uniform sampler2D texture;
uniform sampler2D vDistortion;
uniform sampler2D hDistortion;

varying vec2 distortionLookup;
varying vec4 vertColor;
varying vec4 vertTexCoord;

const vec4 decoderCoefficients = vec4(1.0, 1.0/255.0, 1.0/(255.0*255.0), 1.0/(255.0*255.0*255.0));

void main() {
  vec4 vEncoded = texture2D(vDistortion, vertTexCoord.st);
  vec4 hEncoded = texture2D(hDistortion, vertTexCoord.st);
  float vIndex = dot(vEncoded, decoderCoefficients) * 2.3 - 0.6;
  float hIndex = dot(hEncoded, decoderCoefficients) * 2.3 - 0.6;

  if(vIndex >= 0.0 && vIndex <= 1.0
        && hIndex >= 0.0 && hIndex <= 1.0)
  {
      gl_FragColor = texture2D(texture, vec2(hIndex, vIndex)) * vertColor;
  } else {
      gl_FragColor = vec4(1.0, 0, 0, 1.0); //show invalid pixels as red
  }
}

使用双线性插值校正

在使用着色器不可行的情况下,你可能会使用优化双线性插值来快速矫正图像来代替使用warp()函数。(正如对待任何优化,你需要使用优化测试来验证你的结果)

重新调用含有64X64网格元素的畸变映射矩阵,想象这些网格均匀的散布在你的目标图像中。(元素[0,0]在左下方角点同时[64,64]在右上方)。每一个元素包括水平座标与垂直座标用于确定传感器图像数据中的图像数据,以找到目标图像中像素的记录亮度。为了寻找到畸变网格元素间的像素的亮度值,你应当利用该像素周围的4个网格点进行插值计算。

用于在目标图像中寻找给定像素的畸变修正亮度值的基本算法:

1.找到目标像素周围四个校正网格点。

2.计算基于目标点距离网格点的距离计算插值权重。

3.查找这四个网格点的每一个的水平值和垂直值(译者注:x座标?y座标?)

4.使用基于距离的权重因子计算水平值得双线性插值。

5.为纵向值重复插值计算。

6.拒绝那些横向座标或纵向座标不在[0,1]范围之内的。

7.反归一化这些值,使他们变成原始传感器数据中的像素座标。

8.查找传感器值所计算的像素座标。

9.在目标图像中为原始座标设置亮度值。

在Python中遍历图像像素进行双线性插值是很慢的。你可以使用OpenCV提供的函数进行插值计算。第一将畸变数据转换成cv2.remap()函数能使用的数据格式:

import cv2, Leap, math, ctypes
import numpy as np

def convert_distortion_maps(image):

    distortion_length = image.distortion_width * image.distortion_height
    xmap = np.zeros(distortion_length/2, dtype=np.float32)
    ymap = np.zeros(distortion_length/2, dtype=np.float32)

    for i in range(0, distortion_length, 2):
        xmap[distortion_length/2 - i/2 - 1] = image.distortion[i] * image.width
        ymap[distortion_length/2 - i/2 - 1] = image.distortion[i + 1] * image.height

    xmap = np.reshape(xmap, (image.distortion_height, image.distortion_width/2))
    ymap = np.reshape(ymap, (image.distortion_height, image.distortion_width/2))

    #resize the distortion map to equal desired destination image size
    resized_xmap = cv2.resize(xmap,
                              (image.width, image.height),
                              0, 0,
                              cv2.INTER_LINEAR)
    resized_ymap = cv2.resize(ymap,
                              (image.width, image.height),
                              0, 0,
                              cv2.INTER_LINEAR)

    #Use faster fixed point maps
    coordinate_map, interpolation_coefficients = cv2.convertMaps(resized_xmap,
                                                                 resized_ymap,
                                                                 cv2.CV_32FC1,
                                                                 nninterpolation = False)

    return coordinate_map, interpolation_coefficients

然后将这些map和相应的图像传给cv2.map()函数:

def undistort(image, coordinate_map, coefficient_map, width, height):
    destination = np.empty((width, height), dtype = np.ubyte)

    #wrap image data in numpy array
    i_address = int(image.data_pointer)
    ctype_array_def = ctypes.c_ubyte * image.height * image.width
    # as ctypes array
    as_ctype_array = ctype_array_def.from_address(i_address)
    # as numpy array
    as_numpy_array = np.ctypeslib.as_array(as_ctype_array)
    img = np.reshape(as_numpy_array, (image.height, image.width))

    #remap image to destination
    destination = cv2.remap(img,
                            coordinate_map,
                            coefficient_map,
                            interpolation = cv2.INTER_LINEAR)

    #resize output to desired destination size
    destination = cv2.resize(destination,
                             (width, height),
                             0, 0,
                             cv2.INTER_LINEAR)
    return destination


注意你应当避免每一帧都转换畸变映射矩阵,只有当一个新的设备接入时、图像反转方向时(当手从相反的方向进入)或者设备被重新校准此矩阵才会发生改变。下面代码只会进行一次转换畸变映射矩阵(所以无法处理当畸变映射矩阵发生变化的情况)


def run(controller):
    maps_initialized = False
    while(True):
        frame = controller.frame()
        image = frame.images[0]
        if image.is_valid:
            if not maps_initialized:
                left_coordinates, left_coefficients = convert_distortion_maps(frame.images[0])
                right_coordinates, right_coefficients = convert_distortion_maps(frame.images[1])
                maps_initialized = True

            undistorted_left = undistort(image, left_coordinates, left_coefficients, 400, 400)
            undistorted_right = undistort(image, right_coordinates, right_coefficients, 400, 400)

            #display images
            cv2.imshow('Left Camera', undistorted_left)
            cv2.imshow('Right Camera', undistorted_right)

            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

def main():
    controller = Leap.Controller()
    controller.set_policy_flags(Leap.Controller.POLICY_IMAGES)
    try:
        run(controller)
    except KeyboardInterrupt:
        sys.exit(0)
        
if __name__ == '__main__':
    main()

在图像上画出追踪数据

直接在相机图像上画出LeapMotion的追踪数据十分简单。如果你曾将原始图像数据·绘制为位图,你可以通过使用warp()函数找到像素与LeapMotion位置的对应关系。
将LeapMotion座标系中一个位置转换成水平和垂直斜度(以相机的视角),需要知道相机与LeapMotion座标系统的原点之间的距离。对于目前的版本,对于每一侧来说x轴的偏移量是20mm。因为相机在x轴,所以在z轴上没有偏移量。斜度是在相机中的图像平面的距离——x座标是水平斜度;z轴是垂直斜度。由到图像平面的距离分割。下面的图展示了水平斜度的关系。


左相机的计算过程。加上偏移距离代替了减去右相机的对应值。

只要你知道光线的斜率,你就能通过调用warp()函数获得像素的座标。

注意:偏移值可能因为LeapMotion控制器的不同形式的因素而不同,但是目前没有方法从API中获得这个值。

如果你显示了校正后的图像数据,那么将追踪数据与图像数据相关联取决于你显示图像的方式。对于3D视角,使用不变的缩放因子和纹理贴图的校正后的位置显示图像是有问题的。对于其他形式的显示,你必须根据你矫正图像的方式将代表LeapMotion中一个位置的光线斜率转换成目标图像像素座标

计算图象特征的方向向量

使用Image.rectify()获得图像特征方向向量。Image.rectify()返回一个向量包含水平和竖直斜率(以相机原点定义)给出了在原始图像数据中的像素座标。

如果你能以足够的精度辩认出图像中的相同的特征,你可以使用从两个摄像机获得的斜率值计算出3D位置/


头戴式显示模式

LeapMotion服务/守护程序提供了当LeapMotion附着于一个头戴式显示器上时优化跟踪数据的模式。在这个模式下LeapMotion软件会从上方而不是底部检测手。但是,手心是朝向LeapMotion传感器还是背离传感器是有二义性的,设置这个模式使得LeapMotion软件会初始化手部模型使得它默认手心是背向传感器的。这样有利于将LeapMotion设备安装在头戴式显示器的表面作为控制器。

为了在你的应用中开启这个模式,你需要开启优化HMD方法

controller.set_policy(Leap.Controller.POLICY_OPTIMIZE_HMD)

对于那些不能安装在HMD上的硬件,这个方法是会被拒绝的,如那些嵌在笔记本上或者键盘上的设备。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章