AutoCV第十课:3D基础

news2024/11/16 1:27:29

3D基础

前言

手写 AI 推出的全新保姆级从零手写自动驾驶 CV 课程,链接。记录下个人学习笔记,仅供自己参考。

本次课程我们来学习下 nuScenes 数据集的可视化。

课程大纲可看下面的思维导图。

在这里插入图片描述

1. nuScenes数据集

明确下我们本次学习的目的:将 nuScenes 数据集中 sample 的 Lidar 点云投影到 Camera 图像上

在具体实现之前有一些基本概念需要我们了解,由于每个传感器安装在车辆的不同位置,为了方便描述它们之间的关系需要建立不同的坐标系,同时还需要对传感器进行标定,获取它们位置的相对关系,方便后续进行多传感器融合。

我们先来简单了解下几种常见坐标系

  • 全局坐标系 (global coordinate)

    • 可以简单的认为,车辆在 t0 时刻的位置是全局坐标系的原点
  • 车体坐标系 (ego_pose/ego coordinate)

    • 以车体为原点的坐标系
  • 传感器坐标系

    • lidar 坐标系
    • camera 坐标系
    • radar 坐标系

接下来我们再来简单了解下几种传感器的标定 (calibration)

  • lidar 标定获得的结果是:
    • lidar 相对于 ego 而言的位置 (translation) 和旋转 (rotation)
    • translation 使用 3 个 float 数字表示
      • 相对于 ego 而言的位置
    • rotation 使用 4 个 float 数字表示即四元数
  • camera 标定获得的结果是:
    • camera 相对于 ego 而言的位置 (translation) 和旋转 (rotation)
    • 相机内参 camera intrinsic (3d->2d平面)
    • 相机畸变参数 (目前 nuScenes 数据集不考虑)
    • translation 使用 3 个 float 数字表示
      • 相对于 ego 而言的位置
    • rotation 使用 4 个 float 数字表示即四元数

我们再来看看如何将 Lidar 的点云数据转换到 Camera 上面呢?你可能会说那就将 Lidar 坐标系先转换到 ego 坐标系再将 ego 坐标系转换到 Camera 坐标是不就行了吗?那这样转换其实是有问题的

那存在什么问题呢?我们不难发现 Lidar 和 Camera 这两种传感器的频率不同,也就是捕获数据的时间不同。假设当前 lidar 捕获的 timestamp 是 t0,对应的车体坐标系为 ego_pose0,而 camera 捕获的 timestamp 是 t1,对应的车体坐标系为 ego_pose1,因此考虑时间问题,最终的转换应该如下:

lidar_points -> ego_pose0 -> global -> ego_pose1 -> camera -> intrinsic -> image

2. 代码实现

我们需要熟悉 nuScenes 数据集中的数据格式,后续具体代码实现的过程中需要经常参考 图2-1

在这里插入图片描述

图2-1 nuScenes Data Format

2.1 前置工作

在正式开始前需要安装 nuScenes 的包,安装指令如下:

pip install nuscenes-devkit

在安装过程中,遇到了如下问题:

在这里插入图片描述

图2-2 nuscenes-devkit安装问题

解决方案可参考这篇博文,比较简单大家按步骤走就行

2.2 初始化nuScenes类

利用 nuscenes 库来初始化 nuScenes 类并进行样本处理

示例代码如下:

# pip install nuscenes-devkit
from nuscenes.nuscenes import NuScenes
from pyquaternion import Quaternion
import numpy as np
import cv2
import os

# 构建nuScenes类
version = "v1.0-mini"
dataroot = "D:\\Data\\nuScenes"
nuscenes = NuScenes(version, dataroot, verbose=False)

sample = nuscenes.sample[0]

上述示例代码导入了一些库包,并初始化了NuScenes类,对第一个样本进行了打印

输出如下,可以看到我们打印 nuScenes 第一个样本中的内容,它的结果是一个字典,后续我们需要按照字典中的 对应的 token 去访问对应的值

{'token': 'ca9a282c9e77460f8360f564131a8af5', 'timestamp': 1532402927647951, 'prev': '', 'next': '39586f9d59004284a7114a68825e8eec', 'scene_token': 'cc8c0bf57f984915a77078b10eb33198', 'data': {'RADAR_FRONT': '37091c75b9704e0daa829ba56dfa0906', 'RADAR_FRONT_LEFT': '11946c1461d14016a322916157da3c7d', 'RADAR_FRONT_RIGHT': '491209956ee3435a9ec173dad3aaf58b', 'RADAR_BACK_LEFT': '312aa38d0e3e4f01b3124c523e6f9776', 'RADAR_BACK_RIGHT': '07b30d5eb6104e79be58eadf94382bc1', 'LIDAR_TOP': '9d9bf11fb0e144c8b446d54a8a00184f', 'CAM_FRONT': 'e3d495d4ac534d54b321f50006683844', 'CAM_FRONT_RIGHT': 'aac7867ebf4f446395d29fbd60b63b3b', 'CAM_BACK_RIGHT': '79dbb4460a6b40f49f9c150cb118247e', 'CAM_BACK': '03bea5763f0f4722933508d5999c5fd8', 'CAM_BACK_LEFT': '43893a033f9c46d4a51b5e08a67a1eb7', 'CAM_FRONT_LEFT': 'fe5422747a7d4268a4b07fc396707b23'}, 'anns': ['ef63a697930c4b20a6b9791f423351da', '6b89da9bf1f84fd6a5fbe1c3b236f809', '924ee6ac1fed440a9d9e3720aac635a0', ...]} 

2.3 获取lidar数据

从上述样本打印的结果可知,数据都存储在 'data' 中,而 lidar 的数据对应的键的 token 是 LIDAR_TOP,因此 lidar 的数据可通过下面的示例代码获取:

# 获取lidar的数据
lidar_token = sample["data"]["LIDAR_TOP"]
lidar_sample_data = nuscenes.get('sample_data', lidar_token)
print(lidar_sample_data)
lidar_file = os.path.join(dataroot, lidar_sample_data['filename'])

# 加载点云数据
lidar_points = np.fromfile(lidar_file, dtype=np.float32).reshape(-1, 5)

通过字典中的键获取 token,通过 token 来获取 lidar 数据,值得注意的是我们通过 lidar 文件的路径读取点云数据后会将其 reshape 成 (-1,5) 的形式,其中 5 分别代码一个点云数据所代表的 x、y、z、intensity、ring_index

输出如下:

# lidar_sample_data
{'token': '9d9bf11fb0e144c8b446d54a8a00184f', 'sample_token': 'ca9a282c9e77460f8360f564131a8af5', 'ego_pose_token': '9d9bf11fb0e144c8b446d54a8a00184f', 'calibrated_sensor_token': 'a183049901c24361a6b0b11b8013137c', 'timestamp': 1532402927647951, 'fileformat': 'pcd', 'is_key_frame': True, 'height': 0, 'width': 0, 'filename': 'samples/LIDAR_TOP/n015-2018-07-24-11-22-45+0800__LIDAR_TOP__1532402927647951.pcd.bin', 'prev': '', 'next': '0cedf1d2d652468d92d23491136b5d15', 'sensor_modality': 'lidar', 'channel': 'LIDAR_TOP'}

# lidar_points
[[-3.1243734e+00 -4.3415368e-01 -1.8671920e+00  4.0000000e+00
   0.0000000e+00]
 [-3.2906363e+00 -4.3220678e-01 -1.8631892e+00  1.0000000e+00
   1.0000000e+00]
 [-3.4704101e+00 -4.3068862e-01 -1.8595628e+00  2.0000000e+00
   2.0000000e+00]
 ...
 [-1.4129141e+01  4.9357712e-03  1.9857219e+00  8.0000000e+01
   2.9000000e+01]
 [-1.4120683e+01  9.8654358e-03  2.3199446e+00  7.5000000e+01
   3.0000000e+01]
 [-1.4113669e+01  1.4782516e-02  2.6591547e+00  4.0000000e+01
   3.1000000e+01]]

2.4 lidar坐标转换

获取完 lidar 数据后我们需要通过一些坐标变换将点云数据从 lidar 坐标系转换到 global 坐标系,转换流程是 lidar -> ego -> global,先将 lidar 坐标系转换到 ego 坐标系,再将 ego 坐标系转换到 global 坐标系,具体实现代码如下:

# lidar坐标变换
# 获取lidar相对于车体坐标系而言的translation和rotation标定数据
lidar_calibrated_data = nuscenes.get("calibrated_sensor", lidar_sample_data["calibrated_sensor_token"])
print(lidar_calibrated_data)

def get_matrix(calibrated_data, inverse=False):
    # 返回的结果是 lidar->ego 坐标系的变换矩阵
    output = np.eye(4)
    output[:3, :3] = Quaternion(calibrated_data["rotation"]).rotation_matrix
    output[:3,  3] = calibrated_data["translation"]
    if inverse:
        output = np.linalg.inv(output)
    return output

# lidar->ego 变换矩阵
lidar_to_ego = get_matrix(lidar_calibrated_data)
print(lidar_to_ego)

# 获取lidar数据时对应的车体位姿
ego_pose = nuscenes.get("ego_pose", lidar_sample_data["ego_pose_token"])
print(ego_pose)
# ego->global 变换矩阵
ego_to_global = get_matrix(ego_pose)
print(ego_to_global)
# lidar->global 变换矩阵
lidar_to_global = ego_to_global @ lidar_to_ego
print(lidar_to_global)

# x, y, z -> x, y, z, 1 转换为齐次坐标,方便做矩阵相乘
hom_points = np.concatenate([lidar_points[:, :3], np.ones((len(lidar_points), 1))], axis=1)
# lidar_points -> global
global_points = hom_points @ lidar_to_global.T
print(global_points)

上述代码中我们通过 lidar_sample_data 中的 'calibrated_sensor_token' 获取到了标定时的 translation 和 rotation,通过函数 get_matrix 获取变换矩阵,其中 rotation 表示旋转变换而 translation 表示位置变换

那你可能会问 'rotation' 中只包含4个值,为什么能表示为 3x3 的旋转矩阵呢?这是因为 rotation 的 4 个值,它们代表着四元数,通过一定的变换可以转换为旋转矩阵,假设四元数 q \mathbf{q} q 的向量形式为 q = ( w , x , y , z ) \mathbf{q} = (w,x,y,z) q=(w,x,y,z),则对应于四元数 q \mathbf{q} q 的旋转矩阵计算如下:
R q = [ 1 − 2 y 2 − 2 z 2 2 x y − 2 w z 2 x z + 2 w y 2 x y + 2 w z 1 − 2 x 2 − 2 z 2 2 y z − 2 w x 2 x z − 2 w y 2 y z + 2 w x 1 − 2 x 2 − 2 y 2 ] \mathbf{R_q}=\left[\begin{array}{ccc}1-2y^2-2z^2&2xy-2wz&2xz+2wy\\ 2xy+2wz&1-2x^2-2z^2&2yz-2wx\\ 2xz-2wy&2yz+2wx&1-2x^2-2y^2\end{array}\right] Rq= 12y22z22xy+2wz2xz2wy2xy2wz12x22z22yz+2wx2xz+2wy2yz2wx12x22y2
更多细节描述:四元数和旋转(Quaternion & rotation)

在上述代码中我们首先计算了 lidar->ego 的变换矩阵,然后计算了 ego->global 的变换矩阵,将二者相乘后得到了 lidar->global 的变换矩阵,将该变换矩阵应用到每个点云上就可以得到最终的 global_points,值得注意的是,为了方便矩阵相乘我们将 lidar_points 数据转换成为了齐次坐标

输出如下:

# lidar_calibrated_data
{'token': 'a183049901c24361a6b0b11b8013137c', 'sensor_token': 'dc8b396651c05aedbb9cdaae573bb567', 'translation': [0.943713, 0.0, 1.84023], 'rotation': [0.7077955119163518, -0.006492242056004365, 0.010646214713995808, -0.7063073142877817], 'camera_intrinsic': []}

# lidar_to_ego
[[ 0.00203327  0.99970406  0.02424172  0.943713  ]
 [-0.99998053  0.00217566 -0.00584864  0.        ]
 [-0.00589965 -0.02422936  0.99968902  1.84023   ]
 [ 0.          0.          0.          1.        ]]

# ego_pose0
{'token': '9d9bf11fb0e144c8b446d54a8a00184f', 'timestamp': 1532402927647951, 'rotation': [0.5720320396729045, -0.0016977771610471074, 0.011798001930183783, -0.8201446642457809], 'translation': [411.3039349319818, 1180.8903791765097, 0.0]}

# ego_to_global0
[[-3.45552926e-01  9.38257989e-01  1.62825160e-02  4.11303935e+02]
 [-9.38338111e-01 -3.45280305e-01 -1.74097708e-02  1.18089038e+03]
 [-1.07128245e-02 -2.12945025e-02  9.99715849e-01  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  1.00000000e+00]]

# lidar_to_global
[[-9.39038386e-01 -3.43803850e-01  2.41312207e-03  4.11007796e+02]
 [ 3.43468398e-01 -9.38389802e-01 -3.81318685e-02  1.17997282e+03]
 [ 1.53743323e-02 -3.49784571e-02  9.99269802e-01  1.82959727e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  1.00000000e+00]]

# global_points
[[ 4.14086460e+02  1.17937830e+03 -6.90804743e-02  1.00000000e+00]
 [ 4.14241928e+02  1.17931922e+03 -6.77048676e-02  1.00000000e+00]
 [ 4.14410229e+02  1.17925591e+03 -6.68980550e-02  1.00000000e+00]
 ...
 [ 4.24278696e+02  1.17503955e+03  3.59647049e+00  1.00000000e+00]
 [ 4.24269865e+02  1.17502509e+03  3.93040672e+00  1.00000000e+00]
 [ 4.24262408e+02  1.17500995e+03  4.26930490e+00  1.00000000e+00]]

2.5 camera坐标变换

在之前的分析中我们已经实现了将 lidar 坐标系的点云数据转换到了 global 坐标系下,要将其投影到 camera 下面还要计算 global 到 camera 的坐标变换,下面我们就来实现,具体代码如下:

# camera坐标变换
cameras = ['CAM_FRONT_LEFT', 'CAM_FRONT', 'CAM_FRONT_RIGHT', 'CAM_BACK_LEFT', 'CAM_BACK', 'CAM_BACK_RIGHT']

for cam in cameras:
    # 获取camera的数据
    camera_token = sample["data"][cam]
    camera_data  = nuscenes.get("sample_data", camera_token)
    print(camera_data)
    # 加载图像
    image_file = os.path.join(dataroot, camera_data["filename"])
    image = cv2.imread(image_file)
    
    # 获取camera数据时对应的车体位姿
    camera_ego_pose = nuscenes.get("ego_pose", camera_data["ego_pose_token"])
    # global->ego 变换矩阵
    global_to_ego = get_matrix(camera_ego_pose, True)

    # 获取camera相对于车体坐标系而言的translation、rotation和camera_intrinsic标定数据
    camera_calibrated_data = nuscenes.get("calibrated_sensor", camera_data["calibrated_sensor_token"])
    print(camera_calibrated_data)
    # ego->camera 变换矩阵
    ego_to_camera = get_matrix(camera_calibrated_data, True)
    camera_intrinsic = np.eye(4)
    camera_intrinsic[:3, :3] = camera_calibrated_data["camera_intrinsic"]

    # global->camera_ego_pose->camera->image
    global_to_image = camera_intrinsic @ ego_to_camera @ global_to_ego

    image_points = global_points @ global_to_image.T
    image_points[:, :2] /= image_points[:, [2]] # 归一化
    
    # 过滤z<=0的点,因为它在图像平面的后面,形不成投影
    # z > 0
    for x, y in image_points[image_points[:, 2] > 0, :2].astype(int):
        cv2.circle(image, (x, y), 3, (255, 0, 0), -1, 16)

    cv2.imwrite(f"{cam}.jpg", image)

在上述示例代码中我们通过一系列的坐标变换最终实现了 global->camera 的变换,且 camera 的 3D->2D 变换是通过相机内参矩阵变换得到的。

image_points[:, :2] /= image_points[:, [2]] 这行代码是将 x,y 坐标值除以 z 深度值,实现了归一化处理,目的是将点云投影坐标归一化到图像平面上的单位坐标系,以便后续的绘制和处理。

输出如下:

# camera_data
{'token': 'fe5422747a7d4268a4b07fc396707b23', 'sample_token': 'ca9a282c9e77460f8360f564131a8af5', 'ego_pose_token': 'fe5422747a7d4268a4b07fc396707b23', 'calibrated_sensor_token': '75ad8e2a8a3f4594a13db2398430d097', 'timestamp': 1532402927604844, 'fileformat': 'jpg', 'is_key_frame': True, 'height': 900, 'width': 1600, 'filename': 'samples/CAM_FRONT_LEFT/n015-2018-07-24-11-22-45+0800__CAM_FRONT_LEFT__1532402927604844.jpg', 'prev': '', 'next': '48f7a2e756264647bcf8870b02bf711f', 'sensor_modality': 'camera', 'channel': 'CAM_FRONT_LEFT'}

# camera_calibrated_data
{'token': '75ad8e2a8a3f4594a13db2398430d097', 'sensor_token': 'ec4b5d41840a509984f7ec36419d4c09', 'translation': [1.52387798135, 0.494631336551, 1.50932822144], 'rotation': [0.6757265034669446, -0.6736266522251881, 0.21214015046209478, -0.21122827103904068], 'camera_intrinsic': [[1272.5979470598488, 0.0, 826.6154927353808], [0.0, 1272.5979470598488, 479.75165386361925], [0.0, 0.0, 1.0]]}

绘制的图像如下:

在这里插入图片描述

图2-3 CAM_FRONT_LEFT点云投影结果

当然我们也可以把 3D 框的信息绘制上去,代码如下所示:

from nuscenes.utils.data_classes import Box
for token in sample["anns"]:
    annotation = nuscenes.get("sample_annotation", token)
    # print(annotation)
    box = Box(annotation['translation'], annotation['size'], Quaternion(annotation['rotation']))
    # print(box)
    # 坐标变换
    corners = box.corners().T # 3x8 => 立体框角点
    global_corners = np.concatenate([corners, np.ones((len(corners), 1))], axis=1)
    image_based_corners = global_corners @ global_to_image.T
    image_based_corners[:, :2] /= image_based_corners[:, [2]] # 归一化
    image_based_corners = image_based_corners.astype(np.int32)

    # 长方体12条棱
    ix, iy = [0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7, 1, 2, 3, 0, 5, 6, 7, 4]
    for p0, p1 in zip(image_based_corners[ix], image_based_corners[iy]):
        if p0[2] <= 0 or p1[2] <= 0: continue # 过滤z<0
            cv2.line(image, (p0[0], p0[1]), (p1[0], p1[1]), (0, 255, 0), 2, 16)

在这里插入图片描述

图2-4 CAM_FRONT_LEFT点云投影+3D框结果

2.5 完整示例代码

完整的示例代码如下:

# pip install nuscenes-devkit
from nuscenes.nuscenes import NuScenes
from pyquaternion import Quaternion
import numpy as np
import cv2
import os

# 构建nuScenes类
version = "v1.0-mini"
dataroot = "D:\\Data\\nuScenes"
nuscenes = NuScenes(version, dataroot, verbose=False)

sample = nuscenes.sample[0]

# 获取lidar的数据
lidar_token = sample["data"]["LIDAR_TOP"]
lidar_sample_data = nuscenes.get('sample_data', lidar_token)
lidar_file = os.path.join(dataroot, lidar_sample_data['filename'])

# 加载点云数据
lidar_points = np.fromfile(lidar_file, dtype=np.float32).reshape(-1, 5)

# lidar坐标变换
# 获取lidar相对于车体坐标系而言的translation和rotation标定数据
lidar_calibrated_data = nuscenes.get("calibrated_sensor", lidar_sample_data["calibrated_sensor_token"])
# print(lidar_calibrated_data)

def get_matrix(calibrated_data, inverse=False):
    # 返回的结果是 lidar->ego 坐标系的变换矩阵
    output = np.eye(4)
    output[:3, :3] = Quaternion(calibrated_data["rotation"]).rotation_matrix
    output[:3,  3] = calibrated_data["translation"]
    if inverse:
        output = np.linalg.inv(output)
    return output

# lidar->ego 变换矩阵
lidar_to_ego = get_matrix(lidar_calibrated_data)
# print(lidar_to_ego)

# 获取lidar数据时对应的车体位姿
ego_pose = nuscenes.get("ego_pose", lidar_sample_data["ego_pose_token"])
# print(ego_pose)
# ego->global 变换矩阵
ego_to_global = get_matrix(ego_pose)
# print(ego_to_global)
# lidar->global 变换矩阵
lidar_to_global = ego_to_global @ lidar_to_ego
# print(lidar_to_global)

# x, y, z -> x, y, z, 1 转换为齐次坐标,方便做矩阵相乘
hom_points = np.concatenate([lidar_points[:, :3], np.ones((len(lidar_points), 1))], axis=1)
# lidar_points -> global
global_points = hom_points @ lidar_to_global.T
# print(global_points)

# camera坐标变换
cameras = ['CAM_FRONT_LEFT', 'CAM_FRONT', 'CAM_FRONT_RIGHT', 'CAM_BACK_LEFT', 'CAM_BACK', 'CAM_BACK_RIGHT']

for cam in cameras:
    # 获取camera的数据
    camera_token = sample["data"][cam]
    camera_data  = nuscenes.get("sample_data", camera_token)
    # print(camera_data)
    # 加载图像
    image_file = os.path.join(dataroot, camera_data["filename"])
    image = cv2.imread(image_file)
    
    # 获取camera数据时对应的车体位姿
    camera_ego_pose = nuscenes.get("ego_pose", camera_data["ego_pose_token"])
    # global->ego 变换矩阵
    global_to_ego = get_matrix(camera_ego_pose, True)

    # 获取camera相对于车体坐标系而言的translation、rotation和camera_intrinsic标定数据
    camera_calibrated_data = nuscenes.get("calibrated_sensor", camera_data["calibrated_sensor_token"])
    # print(camera_calibrated_data)
    # ego->camera 变换矩阵
    ego_to_camera = get_matrix(camera_calibrated_data, True)
    camera_intrinsic = np.eye(4)
    camera_intrinsic[:3, :3] = camera_calibrated_data["camera_intrinsic"]

    # global->camera_ego_pose->camera->image
    global_to_image = camera_intrinsic @ ego_to_camera @ global_to_ego

    image_points = global_points @ global_to_image.T
    image_points[:, :2] /= image_points[:, [2]] # 归一化
    
    # 过滤z<=0的点,因为它在图像平面的后面,形不成投影
    # z > 0
    for x, y in image_points[image_points[:, 2] > 0, :2].astype(int):
        cv2.circle(image, (x, y), 3, (255, 0, 0), -1, 16)

    from nuscenes.utils.data_classes import Box
    for token in sample["anns"]:
        annotation = nuscenes.get("sample_annotation", token)
        # print(annotation)
        box = Box(annotation['translation'], annotation['size'], Quaternion(annotation['rotation']))
        # print(box)
        # 坐标变换
        corners = box.corners().T # 3x8 => 立体框角点
        global_corners = np.concatenate([corners, np.ones((len(corners), 1))], axis=1)
        image_based_corners = global_corners @ global_to_image.T
        image_based_corners[:, :2] /= image_based_corners[:, [2]] # 归一化
        image_based_corners = image_based_corners.astype(np.int32)

        # 长方体12条棱
        ix, iy = [0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7, 1, 2, 3, 0, 5, 6, 7, 4]
        for p0, p1 in zip(image_based_corners[ix], image_based_corners[iy]):
            if p0[2] <= 0 or p1[2] <= 0: continue # 过滤z<0
            cv2.line(image, (p0[0], p0[1]), (p1[0], p1[1]), (0, 255, 0), 2, 16)

    cv2.imwrite(f"{cam}.jpg", image)

3. 需要补充的知识

学习本次课程我们还需要补充一些额外的知识

  • 坐标变换
    • 最详细、最完整的相机标定讲解
    • 手写激光雷达与相机标定代码实践
  • 齐次坐标系
    • 深入理解齐次坐标及其作用
  • 四元数
    • 四元数-基本概念
    • 四元数和旋转(Quaternion & rotation)
  • 小孔成像模型
    • 三维重建-相机几何模型和投影矩阵
    • 相机小孔成像模型(逐步推导详解)
  • 时间同步
    • lidar和camera时间同步

总结

本次课程学习了可视化 nuScenes 数据集,将 lidar 点云数据投影到 camera 图像上。在实现的过程中需要我们去了解坐标变换、四元数等相关知识,总的来说可视化代码好理解,但是一些相关的基础知识却需要我们自己去了解掌握。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/637471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ThinkPHP3.2.3通过局域网手机访问项目

折腾一上午&#xff0c; 试了nginx&#xff0c; 试了修改Apache的httpd.conf 试了关闭代理 试了手动配置网络 试了关闭防火墙 试了添加防火墙入站出站规则 问了五个ChatGPT 都没解决。 记录一下 wampserver3.0.4 Apache2.4.18 PHP 5.6.19 MySQL 5.7.11 所有服务启…

交换机上云MACC方式

步骤1、尝试ping通114.114.114.114 步骤2、尝试ping cloud.ruije.com.cn 若不通&#xff0c;配置dns&#xff1a;ip name-server 223.5.5.5 步骤3、设备开启cwmp功能 Ruijie#conf t Ruijie(config)#cwmp Ruijie(config-cwmp)#acs url http://118.190.126.198/service/tr069s…

Jmeter对数据库批量增删改查

目录 前言&#xff1a; 一、主要配置元件介绍 二、共有元件数据配置如下 前言&#xff1a; JMeter可以通过JDBC请求实现对数据库的批量增删改查。JDBC请求模拟了一个JDBC请求&#xff0c;它是连接池中的一个虚拟用户。JDBC请求可以定义SQL语句和预编译参数&#xff0c;…

【100个高大尚求职简历】简历模板+修改教程+行业分类简历模板 (涵盖各种行业) (简历模板+编辑指导+修改教程)

文章目录 1 简历预览2 简历下载 很多人说自己明明投了很多公司的简历&#xff0c;但是都没有得到面试邀请的机会。自己工作履历挺好的&#xff0c;但是为什么投自己感兴趣公司的简历&#xff0c;都没有面试邀请的机会。反而是那些自己没有投递的公司&#xff0c;经常给自己打电…

一文详解!教你如何在Jmeter里添加Get请求

目录 前言&#xff1a; 第一步&#xff0c;添加线程组 第二步&#xff0c;添加HTTP请求 第三步&#xff0c;添加监视器 前言&#xff1a; 前提条件&#xff1a;Jmeter已安装且已配置好&#xff1b;运行Jmeter&#xff0c;打开界面。 在JMeter中添加一个GET请求非常简…

使用uniapp的扩展组件,在微信小程序中出现报错如何解决

在 vue-cli 项目中可以使用 npm 安装 uni-ui 库 &#xff0c;或者直接在 HBuilderX 项目中使用 npm 。 注意 cli 项目默认是不编译 node_modules 下的组件的&#xff0c;导致条件编译等功能失效 &#xff0c;导致组件异常 需要在根目录创建 vue.config.js 文件 &#xff0c;增…

视频播放失败?

&#x1f4f1;1.手机端: 重新下载下客户端即可 &#x1f4bb;2.电脑端: 重新下载客户端->鼠标右键管理员方式打开

管理类联考入栏需看

逻辑 技巧篇 管理类联考•逻辑——解题技巧汇总 真题篇 按年份分类 2010 年一月联考逻辑真题 2011 年一月联考逻辑真题 2012 年一月联考逻辑真题 2013 年一月联考逻辑真题 2014 年一月联考逻辑真题 2015 年一月联考逻辑真题 2016 年一月联考逻辑真题 2017 年一月联考逻辑真…

服务日志性能调优,由log引出一系列的事故

只有被线上服务问题毒打过的人才明白日志有多重要&#xff01; 谁赞成&#xff0c;谁反对&#xff1f;如果你深有同感&#xff0c;那恭喜你是个社会人了&#xff1a;&#xff09; 日志对程序的重要性不言而喻&#xff0c;轻巧、简单、无需费脑&#xff0c;程序代码中随处可见…

分布式架构之EasyES---和 Mybatis用法相似,太方便了

一、EasyES是什么&#xff1f; Easy-Es&#xff08;简称EE&#xff09;是一款基于ElasticSearch(简称Es)官方提供的RestHighLevelClient打造的ORM开发框架&#xff0c;在 RestHighLevelClient 的基础上,只做增强不做改变&#xff0c;为简化开发、提高效率而生,您如果有用过Myb…

ETF薛斯通道抄底指标表

ETF薛斯通道抄底指标表(20230611) 小白也能懂的薛斯通道抄底指标以及公式(附源码) 名称规模(亿)上市日期delta医药创新ETF5606000.1882022-03-150.72医疗创新ETF51682011.8472021-07-010.75生物药ETF1598396.8282021-02-221.1生物医药ETF15985928.5592021-07-071.17疫苗ETF1596…

LVS+Keepalived 高可用群集

LVSKeepalived 高可用群集 一、LVSKeepalived 高可用群集1、LVS2、工作原理3、Keepalived的特性、特点&#xff1a;4、Keepalived实现原理剖析5、VRRP &#xff08;虚拟路由冗余协议&#xff09; 二、LVSKeepalived 高可用群集部署1、配置负载调度器&#xff08;192.168.184.10…

004:vue中安装使用Mock来模拟数据(详细教程)

第004个 查看专栏目录: 按照VUE知识点 ------ 按照element UI知识点 echarts&#xff0c;openlayers&#xff0c;cesium&#xff0c;leaflet&#xff0c;mapbox&#xff0c;d3&#xff0c;canvas 免费交流社区 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏…

Linux 高可用群集HA LVS+Keepalived高可用 NGINX高可用

Keepalived及其工作原理 Keepalived 是一个基于VRRP协议针对LVS负载均衡软件设计的&#xff0c;通过监控集群中各节点的状态以实现LVS服务高可用的软件&#xff0c;可以解决静态路由出现的单点故障问题。 Keepalived除了能够管理LVS软件&#xff0c;还可以对NGINX haproxy MyS…

HbuilderX--小程序运行配置

安装 HbuilderX 官网下载安装程序 【传送门】 微信小程序开发者工具官网下载 【传送门】 小程序配置 ① 点击顶部工具按钮跳出弹框&#xff0c;弹框第一个设置或者直接使用快捷键 ctrlalt, ② 在配置页面点击运行配置往下划&#xff0c;其余配置如下 微信小程序 将小程序的…

[迁移学习]域自适应

一、定义 1.源域和目标域 源域(Source)和目标域(Target)之间不同但存在联系(different but related)。迁移学习的人物是从源域学习到知识并使其在目标域中取得较好的成绩。 迁移学习可以分为正迁移(postive transfer)和负迁移(negtive transfer)&#xff0c;划分依据是迁移学习…

有哪些好用抠图软件?这几种抠图工具简单又高效

有什么好用的抠图软件呢&#xff1f;通过抠图技术将不同的元素组合在一起&#xff0c;创造出独特的艺术效果。我们日常中也会经常需要进行照片抠图&#xff0c;如拍出的照片背景不满意&#xff0c;想要抠出图片中的人物放到新的背景中&#xff0c;这些都是需要进行抠图才能够完…

微服务_Hystrix

在每个服务中引用该组件&#xff0c;监控当前组件。可被GateWay、Fegin集成。简介 作用&#xff1a;防止服务雪崩 Hystrix是一个由Netflix开源的容错框架&#xff0c;它主要用于分布式系统中的服务间通信。Hystrix通过在调用服务的过程中添加各种容错机制&#xff0c;来保护系…

hbuilderX uni-app 自定义快捷键无效、无法生效解决方法(附:好用的常用的快捷键自定义代码片段)

在最后加上 ,"override": true 才能让原有默认的快捷键被覆盖 好用的常用的快捷键自定义代码片段 [//打开快捷键设置{"key": "altshiftk","command": "workbench.action.openGlobalKeybindings","override": tr…

信息专业求职个人简历最新版

信息专业求职个人简历最新版1 个人信息 _ 性 别&#xff1a; 男 婚姻状况&#xff1a; 已婚 民 族&#xff1a; 汉族 户 籍&#xff1a; 江苏-宜兴 年 龄&#xff1a; 34 现所在地&#xff1a; 江苏-宜兴 身 高&#xff1a; 175cm 希望地区&#xff1a; 江苏-常州、 江苏-…