点云和图像
像高 = EFL*tan (半FOV);EFL为焦距;FOV为视场角
视场角:Field of View 以光学仪器的镜头为顶点,以被测目标的物像可通过镜头的最大范围的两条边缘构成的夹角,称为视场角
FOV又分为 HFOV(水平)/VFOV(垂直)/DFOV(对角)
HFOV由Focal Length和senor的宽度(W)决定, VFOV由Focal Length和sensor的高度(H)决定。DFOV由Focal Length, W, H共同决定。
光学畸变(Opt distortion)和TV畸变(TV distortion):
点云的变换-原理和概念介绍
rotx(t) roty(t) rotz(t)
def transform_from_rot_trans(R, t):
"""Transforation matrix from rotation matrix and translation vector."""
R = R.reshape(3, 3)
t = t.reshape(3, 1)
return np.vstack((np.hstack([R, t]), [0, 0, 0, 1]))
### 图像和雷达
减去主点坐标,从公制坐标(m)转换为像素坐标
相机校准(camera calibration)
相机内参和畸变参数的过程是畸变校准(calibration), 拿着相机内参和畸变参数去消除相机图像失真的才叫校正(rectification
cv2.findChessboardCorners cv2.drawChessboardCorners cv2.calibrateCamera cv2.undistort
实现3D投影需要从完成这些坐标系的变换:
激光雷达世界坐标系->
相机坐标系(激光雷达和相机都在车辆坐标系中进行位置校准, 从而互相关联)->
像平面坐标系->像素坐标系.
像素平面坐标系(u,v)、 像平面坐标系(图像物理坐标第(x,y)、相机坐标系(Xc,Yc,Zc) 和世界坐标系(Xw,Yw,Zw)
深度图像的点m(u,v,z)转换到深度相机坐标系下的坐标点M(x,y,z)
欧几里得坐标系->齐次坐标系->欧几里得坐标系.
摄像机内外参
摄像机外参:决定摄像机坐标与世界坐标系之间相对位置关系-- 变换矩阵
摄像机内参:确定摄像机从三维空间到二维图像的投影关系
点云映射
np.delete(array,obj,axis)
axis 如果输入为0:按行删除 如果输入为1:按列删除
np.logical_and
# choose illegal index
bound_x = np.logical_and(index[:, 0] >= 0, index[:, 0] < image_size[0])
bound_y = np.logical_and(index[:, 1] >= 0, index[:, 1] < image_size[1])
bound_box = np.logical_and(bound_x, bound_y)
# filter point out of canvas
u,v,z = cam
u_out = np.logical_or(u<0, u>IMG_W)
v_out = np.logical_or(v<0, v>IMG_H)
outlier = np.logical_or(u_out, v_out)
cam = np.delete(cam,np.where(outlier),axis=1)
散点图
# 过滤了激光雷达背后深度为负的点云,保留图像宽高内点
plt.scatter()函数用于生成一个scatter散点图
matplotlib.pyplot.scatter(x, y, s=None, c=None, marker=None,
cmap=None, norm=None, vmin=None, vmax=None, alpha=None,
linewidths=None, verts=None, edgecolors=None, *, data=None, **kwargs)
其中
x, y → 散点的坐标,float or array-like, shape (n, )
s → 散点的面积,float or array-like, shape (n, ), optional
c → 散点的颜色(默认值为蓝色,'b',其余颜色同plt.plot( ))
marker → 散点样式(默认值为实心圆,'o',其余样式同plt.plot( ))
alpha → 散点透明度([0, 1]之间的数,0表示完全透明,1则表示完全不透明)
linewidths →散点的边缘线宽
edgecolors → 散点的边缘颜色
参考相机_坐标系中, 再进行图像共面对齐修正,
然后投影到相机0的像素坐标系中.
###说明
#以列为基准, 删除投影图像点中深度z<0(在投影图像后方)的点 #3xN
cam = np.delete(proj_lidar,np.where(proj_lidar[2,:]<0),axis=1)
cam[:2,:] /= cam[2,:] # 等价写法 cam[:2] /= cam[2] #
前两行元素分布除以第三行元素(归一化到相机坐标系z=1平面)(x=x/z, y =y/z)
代码示例
import open3d as o3d
import numpy as np
import matplotlib.pyplot as plt
import json
if __name__ == "__main__":
##############################################################
calib_path = r"./config.json"
with open(calib_path,"r") as f:
calib_config = json.loads(f.read())
img_base_calib = calib_config.get("config")
# Extrinsics
img_Tx = img_base_calib.get("img_Tx")
# Intrinsics
img_intrinsics = img_base_calib.get("img_intrinsics")
img_TX_cam_m = img_base_calib.get("img_TX_cam_m")
##############################################################
pcd_path = r"./123.pcd"
pcd = o3d.io.read_point_cloud(pcd_path)
pcd_points = np.asarray(pcd.points)
# reshape to homogeneous 4*N shape
point_hom = np.vstack([pcd_points[:,:3].T, [1]*len(pcd_points)])
print("Ppint_in_homo",point_hom.shape)
##############################################################
img_path = r"./123.jpg"
#读入的 图像形状是(H,W,C),读入的顺序是RGB
png = plt.imread(img_path)
IMG_H,IMG_W,_ = png.shape
# ##############################################################
# ###转换
# # # 世界坐标系 --> 相机坐标系内的坐标
camera_array = np.dot(img_Tx, point_hom)
# #删除深度x=0的点避免前后覆盖
camera_array = np.delete(camera_array,np.where(camera_array[2,:]<0),axis=1)
# 转到相机上
## 归一化到相机坐标系z=1平面 --> 像平面坐标系
point_Came_temp = camera_array / camera_array[2, :]
deep_color = camera_array[2,:]
## -->像素平面坐标系
points_pixel = np.dot(img_intrinsics, point_Came_temp[0:3,:])
# filter point out of canvas
u,v,z = points_pixel[0:3,]
u_out = np.logical_or(u<0, u>IMG_W)
v_out = np.logical_or(v<0, v>IMG_H)
outlier = np.logical_or(u_out, v_out)
points_pixel_limit = np.delete(points_pixel,np.where(outlier),axis=1)
u,v,z = points_pixel_limit[0:3,]
# to plot format
points_pixel = points_pixel.T
points_pixel = points_pixel.astype(np.int64)
#plot the data
plt.axis([0,IMG_W,IMG_H,0])
plt.imshow(png)
plt.scatter(points_pixel[:, 0], points_pixel[:, 1], s=0.5, c=deep_color , cmap='rainbow_points', alpha=0.7)
# imshow需要的是RGB顺序
plt.show()
图像像素和点云点之间的匹配
准确描述和检测 2D 和 3D 关键点对于建立跨图像和点云的对应关系至关重要
建立粗粒度匹配-- 建立细粒度的对应关系
联合特征描述和关键点检测
示例
def points_lidar2image(points, tr_velo_to_cam, P2):
'''
points: shape=(N, 8, 3)
tr_velo_to_cam: shape=(4, 4)
P2: shape=(4, 4)
return: shape=(N, 8, 2)
'''
# points = points[:, :, [1, 2, 0]]
extended_points = np.pad(points, ((0, 0), (0, 0), (0, 1)), 'constant', constant_values=1.0) # (N, 8, 4)
# (N, 8, 4)
camera_points = np.dot(extended_points , tr_velo_to_cam.T)
# (N, 8, 4)
image_points = np.dot(camera_points , P2.T)
# 删除投影图像点中深度z<0(在投影图像后方)的点
image_points = np.delete(image_points,np.where(image_points[:, :, 2:3]<0),axis=0)
# 归一化到相机坐标系z=1平面 --> 像平面坐标系
image_points = image_points[:, :, :2] / image_points[:, :, 2:3]
return image_points
if __name__ == "__main__":
bboxes = bbox3d2corners(gt_3d_box)
image_points = points_lidar2image(bboxes, RT,P2=K)
gt_img = vis_img_3d(img_png, image_points, rt=True)
参考
https://ksimek.github.io/perspective_camera_toy.html
pykitti工具 KITTI 数据集oxts 文件读取 https://blog.csdn.net/qq_26623879/article/details/103595531
https://github.com/azureology/kitti-velo2cam
Lidar to camera projection of KITTI https://github.com/utiasSTARS/pykitti/blob/master/pykitti/utils.py
根据标定的相机和激光雷达的外参,使用python将3D激光雷达点云映射到图像上 https://juejin.cn/post/7094064714778411021
参数功能可视化 https://ksimek.github.io/perspective_camera_toy.html
点云投影 https://blog.csdn.net/shyjhyp11/article/details/113180123
3D点云 (Lidar)检测入门篇 - PointPillars PyTorch实现 https://zhuanlan.zhihu.com/p/521277176
【ROS】利用ROS将KITTI数据集点云数据投影到2D图像 https://copyfuture.com/blogs-details/202207150551110747