点云转BEV鸟瞰图

http://ronny.rest/tutorials/module/pointclouds_01/point_cloud_birdseye/

点云数据

点云数据可表示为[N,3]的数组,N点云个数,每行代表一个点,至少至少使用3个值表示,如(x,y,z)
在这里插入图片描述

图像与点云坐标

蓝色为图像坐标系,橙色为点云坐标系,如下图所示:
在这里插入图片描述

图像坐标系:

  • 原点为左上角,y向下,x向右
  • 坐标值始终为正,且为整数值

点云坐标:

  • 正x轴向前,正y轴向左,正z轴向上
  • 点云中的坐标值可以是正数或负数
  • 坐标可以采用实数编号的值

创建点云数据的鸟瞰视图

在这里插入图片描述
通常只需要提取点云的感兴趣区域

由于在俯视数据,要将其转换为图像,因此要使用与图像坐标轴更加一致的方向。下面,我指定了我想要关注的相对于原点的值范围。 原点左侧的任何内容都将被视为负数,而右侧的任何内容都将被视为正数。 点云的 x 轴将被解释为向前方向(这将是我们鸟瞰图像的向上方向)。

下面的代码将感兴趣的矩形设置为在原点两侧跨越 10m,在其前面跨越 20m

side_range=(-10, 10)     # left-most to right-most
fwd_range=(0, 20)       # back-most to forward-most

创建一个过滤器,仅保留实际位于指定的矩形内的点。

# EXTRACT THE POINTS FOR EACH AXIS
x_points = points[:, 0]
y_points = points[:, 1]
z_points = points[:, 2]

# FILTER - To return only indices of points within desired cube
# Three filters for: Front-to-back, side-to-side, and height ranges
# Note left side is positive y axis in LIDAR coordinates
f_filt = np.logical_and((x_points > fwd_range[0]), (x_points < fwd_range[1]))
s_filt = np.logical_and((y_points > -side_range[1]), (y_points < -side_range[0]))
filter = np.logical_and(f_filt, s_filt)
indices = np.argwhere(filter).flatten()

# KEEPERS
x_points = x_points[indices]
y_points = y_points[indices]
z_points = z_points[indices]

将点位置映射到像素位置

目前,我们有一堆实数值的点。 为了这些值映射到整数。 我们可以天真地将所有 x 和 y 值类型转换为整数,但最终可能会丢失很多分辨率。 例如,如果这些点的测量单位是米,那么每个像素将代表点云中一个 1x1 米的矩形,我们将丢失任何小于此的细节。 如果您有类似山景的点云,这可能会很好。 但如果你想捕捉到更精细的细节,并能识别出人类、汽车,甚至更小的物体,那么这种方法就不好用了。

但是,可以稍微修改上述方法,以便我们获得所需的分辨率水平。 我们可以先缩放数据,然后再类型转换为整数。 例如,如果测量单位是米,我们想要 5 厘米的分辨率,我们可以执行以下操作:

res = 0.05
# CONVERT TO PIXEL POSITION VALUES - Based on resolution
x_img = (-y_points / res).astype(np.int32)  # x axis is -y in LIDAR
y_img = (-x_points / res).astype(np.int32)  # y axis is -x in LIDAR

平移原点

x和y值仍有负数,还不能投影到图像上,因此还需要平移数据,使得(0,0)位置的数据最小。

# SHIFT PIXELS TO HAVE MINIMUM BE (0,0)
# floor and ceil used to prevent anything being rounded to below 0 after shift
x_img -= int(np.floor(side_range[0] / res))
y_img += int(np.ceil(fwd_range[1] / res))

验证数据是否全是正的:

>>> x_img.min()
7
>>> x_img.max()
199
>>> y_img.min()
1
>>> y_img.max()
199

像素值

到这里,已经使用点数据来指定图像中的x和y位置,现在需要做的是指定这些像素位置填充的值。一种方法是填充高度数据。
有两件事要注意:

  • 像素值应为整数
  • 像素值应该是0-255范围内的值

可以从数据中获取最小和最大高度值,并重新缩放至0-255范围。另一种方法是,设置我们想要关注的高度值范围,并且高于或低于该范围的任何值都被设置为最小值和最大值。这种方法很有用,因为它允许我们从感兴趣的区域获得最大限度的细节。

在下面的代码中,将范围设置为原点下方2米,原点上方半米。

height_range = (-2, 0.5)  # bottom-most to upper-most

# CLIP HEIGHT VALUES - to between min and max heights
pixel_values = np.clip(a = z_points,
                           a_min=height_range[0],
                           a_max=height_range[1])

接下来,将这些值重新缩放到0到25​​5之间,并将数据类型转换为整数。

def scale_to_255(a, min, max, dtype=np.uint8):
    """ Scales an array of values from specified min, max range to 0-255
        Optionally specify the data type of the output (default is uint8)
    """
    return (((a - min) / float(max - min)) * 255).astype(dtype)

# RESCALE THE HEIGHT VALUES - to be between the range 0-255
pixel_values  = scale_to_255(pixel_values, min=height_range[0], max=height_range[1])

创建图像数组

现在我们已经准备好实际创建图像,我们只需初始化一个数组,其尺寸取决于我们在矩形中想要的值的范围和我们选择的分辨率。 然后我们使用我们转换为像素位置的 x 和 y 点值来指定数组中的索引,并将我们在上一小节中选择作为像素值的值分配给这些索引。

# INITIALIZE EMPTY ARRAY - of the dimensions we want
x_max = 1+int((side_range[1] - side_range[0])/res)
y_max = 1+int((fwd_range[1] - fwd_range[0])/res)
im = np.zeros([y_max, x_max], dtype=np.uint8)

# FILL PIXEL VALUES IN IMAGE ARRAY
im[y_img, x_img] = pixel_values

可视化

目前,图像存储为numpy数组。如果希望将其可视化,可以将其转换为PIL图像。

# CONVERT FROM NUMPY ARRAY TO A PIL IMAGE
from PIL import Image
im2 = Image.fromarray(im)
im2.show()

在这里插入图片描述

人类并不善于分辨灰色和阴影之间的区别,因此可以使用光谱颜色映射来更容易地分辨出差异。可以在matplotlib中做到这一点。

import matplotlib.pyplot as plt
plt.imshow(im, cmap="spectral", vmin=0, vmax=255)
plt.show()

实际上,这种方式生成的图像与PIL绘制的图像具有完全相同的信息量,因此机器学习算法能够区分高度差异,即使我们人类不能非常清楚地看到差异

完整代码

import numpy as np


# ==============================================================================
#                                                                   SCALE_TO_255
# ==============================================================================
def scale_to_255(a, min, max, dtype=np.uint8):
    """ Scales an array of values from specified min, max range to 0-255
        Optionally specify the data type of the output (default is uint8)
    """
    return (((a - min) / float(max - min)) * 255).astype(dtype)


# ==============================================================================
#                                                         POINT_CLOUD_2_BIRDSEYE
# ==============================================================================
def point_cloud_2_birdseye(points,
                           res=0.1,
                           side_range=(-10., 10.),  # left-most to right-most
                           fwd_range = (-10., 10.), # back-most to forward-most
                           height_range=(-2., 2.),  # bottom-most to upper-most
                           ):
    """ Creates an 2D birds eye view representation of the point cloud data.

    Args:
        points:     (numpy array)
                    N rows of points data
                    Each point should be specified by at least 3 elements x,y,z
        res:        (float)
                    Desired resolution in metres to use. Each output pixel will
                    represent an square region res x res in size.
        side_range: (tuple of two floats)
                    (-left, right) in metres
                    left and right limits of rectangle to look at.
        fwd_range:  (tuple of two floats)
                    (-behind, front) in metres
                    back and front limits of rectangle to look at.
        height_range: (tuple of two floats)
                    (min, max) heights (in metres) relative to the origin.
                    All height values will be clipped to this min and max value,
                    such that anything below min will be truncated to min, and
                    the same for values above max.
    Returns:
        2D numpy array representing an image of the birds eye view.
    """
    # EXTRACT THE POINTS FOR EACH AXIS
    x_points = points[:, 0]
    y_points = points[:, 1]
    z_points = points[:, 2]

    # FILTER - To return only indices of points within desired cube
    # Three filters for: Front-to-back, side-to-side, and height ranges
    # Note left side is positive y axis in LIDAR coordinates
    f_filt = np.logical_and((x_points > fwd_range[0]), (x_points < fwd_range[1]))
    s_filt = np.logical_and((y_points > -side_range[1]), (y_points < -side_range[0]))
    filter = np.logical_and(f_filt, s_filt)
    indices = np.argwhere(filter).flatten()

    # KEEPERS
    x_points = x_points[indices]
    y_points = y_points[indices]
    z_points = z_points[indices]

    # CONVERT TO PIXEL POSITION VALUES - Based on resolution
    x_img = (-y_points / res).astype(np.int32)  # x axis is -y in LIDAR
    y_img = (-x_points / res).astype(np.int32)  # y axis is -x in LIDAR

    # SHIFT PIXELS TO HAVE MINIMUM BE (0,0)
    # floor & ceil used to prevent anything being rounded to below 0 after shift
    x_img -= int(np.floor(side_range[0] / res))
    y_img += int(np.ceil(fwd_range[1] / res))

    # CLIP HEIGHT VALUES - to between min and max heights
    pixel_values = np.clip(a=z_points,
                           a_min=height_range[0],
                           a_max=height_range[1])

    # RESCALE THE HEIGHT VALUES - to be between the range 0-255
    pixel_values = scale_to_255(pixel_values,
                                min=height_range[0],
                                max=height_range[1])

    # INITIALIZE EMPTY ARRAY - of the dimensions we want
    x_max = 1 + int((side_range[1] - side_range[0]) / res)
    y_max = 1 + int((fwd_range[1] - fwd_range[0]) / res)
    im = np.zeros([y_max, x_max], dtype=np.uint8)

    # FILL PIXEL VALUES IN IMAGE ARRAY
    im[y_img, x_img] = pixel_values

    return im
  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

令狐少侠、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值