Closed Huang-yihao closed 11 months ago
As I mentioned in #2, nuScenes was built w/ Velodyne HDL-32E (namely 32 beams).
import numba
import numpy as np
@numba.jit(nopython=True, parallel=False)
def scatter(array, index, value):
for (h, w), v in zip(index, value):
array[h, w] = v
return array
def load_points_as_images(
point_path: str,
scan_unfolding: bool = True,
H: int = 32,
W: int = 1024,
min_depth: float = 0.0,
max_depth: float = 100.0,
):
points = np.fromfile(point_path, dtype=np.float32).reshape((-1, 5))
ring_id = points[:, [4]]
xyz = points[:, :3]
x = xyz[:, [0]]
y = xyz[:, [1]]
z = xyz[:, [2]]
intensity = points[:, [3]]
depth = np.linalg.norm(xyz, ord=2, axis=1, keepdims=True)
mask = (depth >= min_depth) & (depth <= max_depth)
points = np.concatenate([xyz, intensity, depth, mask], axis=1)
if scan_unfolding:
grid_h = 31 - ring_id.astype(np.int32) # upside down
else:
fup, fdown = np.deg2rad(10.67), np.deg2rad(-30.67)
elevation = np.arcsin(z / depth) + abs(fdown)
grid_h = 1 - elevation / (fup - fdown)
grid_h = np.floor(grid_h * H).clip(0, H - 1).astype(np.int32)
azimuth = -np.arctan2(y, x) # [-pi,pi]
grid_w = (azimuth / np.pi + 1) / 2 % 1 # [0,1]
grid_w = np.floor(grid_w * W).clip(0, W - 1).astype(np.int32)
order = np.argsort(-depth.squeeze(1))
grid = np.concatenate((grid_h, grid_w), axis=1)
proj_points = np.zeros((H, W, 4 + 2), dtype=points.dtype)
proj_points = scatter(proj_points, grid[order], points[order])
return proj_points.astype(np.float32)
import matplotlib.pyplot as plt
point_path = "v1.0-mini/samples/LIDAR_TOP/n008-2018-08-01-15-16-36-0400__LIDAR_TOP__1533151603547590.pcd.bin"
kwargs = dict(scan_unfolding=True, W=512)
image = load_points_as_images(point_path, **kwargs)
fig, ax = plt.subplots(image.shape[-1], 1, figsize=(10, 5), constrained_layout=True)
fig.suptitle(kwargs)
for i in range(image.shape[-1]):
ax[i].imshow(image[..., i], cmap="turbo", interpolation="none")
ax[i].axis("off")
ax[i].set_title("XYZIDM"[i])
plt.axis("off")
plt.show()
Thank you very much for your quick reply! Could you please not close the issue? For I can report the problem of trying depth_to_point_map function
I have revised the depth_to_point map function as your suggestion.
# Velodyne HDL-32E
H, W = 32, 512
h_up, h_down = 10, -30
w_left, w_right = 180, -180
device = "cpu"
elevation = 1 - torch.arange(H, device=device) / H # [0, 1]
elevation = elevation * (h_up - h_down) + h_down # [-30, 10]
azimuth = 1 - torch.arange(W, device=device) / W # [0, 1]
azimuth = azimuth * (w_left - w_right) + w_right # [-180, 180]
[elevation, azimuth] = torch.meshgrid([elevation, azimuth], indexing="ij")
angle = torch.stack([elevation, azimuth])[None].deg2rad()
def depth_to_point_map(depth,angle):
assert depth.dim() == 4
grid_cos = torch.cos(angle)
grid_sin = torch.sin(angle)
grid_x = depth * grid_cos[:, [0]] * grid_cos[:, [1]]
grid_y = depth * grid_cos[:, [0]] * grid_sin[:, [1]]
grid_z = depth * grid_sin[:, [0]]
return torch.cat((grid_x, grid_y, grid_z), dim=1)
If I use the depth map generated by the previous load_pts_as_img function, and put the depth map into depth_to_point map function to obtain the points. I expect the points generated by depth_to_point map function to be the same or close to the points data that put into the load_pts_as_img function (i.e., load from bin file). But I find the shape is not consistent and the points generated by depth_to_point map function cannot be put into the load_pts_as_img function again. Could you please point out the error I made? Thank you very much!
range_image = load_points_as_images(points, scan_unfolding=True, H=target_H, W=target_W,min_depth=dmin,max_depth=dmax)
depth_for_next = TF.to_tensor(range_image[...,3]).unsqueeze(0)
output_points = depth_to_point_map(depth_for_next,angle)
points_for_next = output_points.flatten(2).squeeze().numpy().transpose(1,0)
ring_id_for_next = np.expand_dims(range_image[:, :, [4]].flatten(), axis=1)
intensity_for_next = np.expand_dims(range_image[:, :, [3]].flatten(), axis=1)
data_for_next = np.concatenate((points_for_next, ring_id_for_next,intensity_for_next), axis=1)
range_image_new = load_points_as_images(data_for_next, scan_unfolding=True, H=target_H, W=target_W,min_depth=dmin,max_depth=dmax)
[-30.67, 10.67]
, not [-30, 10]
.load_points_as_images
has (x, y, z, intensity, ring_id)
-order channels, not (x, y, z, ring_id, intensity)
.Thank you for the response. I have adjusted the code, but find the range image strange.
H, W = 32, 512
h_up, h_down = 10.67, -30.67
w_left, w_right = 180, -180
device = "cpu"
elevation = 1 - torch.arange(H, device=device) / H # [0, 1]
elevation = elevation * (h_up - h_down) + h_down # [-30, 10]
azimuth = 1 - torch.arange(W, device=device) / W # [0, 1]
azimuth = azimuth * (w_left - w_right) + w_right # [-180, 180]
[elevation, azimuth] = torch.meshgrid([elevation, azimuth], indexing="ij")
angle = torch.stack([elevation, azimuth])[None].deg2rad()
def depth_to_point_map(depth,angle):
assert depth.dim() == 4
grid_cos = torch.cos(angle)
grid_sin = torch.sin(angle)
grid_x = depth * grid_cos[:, [0]] * grid_cos[:, [1]]
grid_y = depth * grid_cos[:, [0]] * grid_sin[:, [1]]
grid_z = depth * grid_sin[:, [0]]
return torch.cat((grid_x, grid_y, grid_z), dim=1)
depth_for_next = TF.to_tensor(range_image[...,3]).unsqueeze(0)
output_points = depth_to_point_map(depth_for_next,angle)
compare_points = np.fromfile(pcl_path, dtype=np.float32).reshape((-1, 5))
points_for_next = output_points.flatten(2).squeeze().numpy().transpose(1,0)
new_x = np.pad(points_for_next[:,0],(0,compare_points.shape[0]-points_for_next.shape[0]),'constant',constant_values = (0))
new_y = np.pad(points_for_next[:,1],(0,compare_points.shape[0]-points_for_next.shape[0]),'constant',constant_values = (0))
new_z = np.pad(points_for_next[:,2],(0,compare_points.shape[0]-points_for_next.shape[0]),'constant',constant_values = (0))
new_x = np.expand_dims(new_x,axis = 1)
new_y = np.expand_dims(new_y,axis = 1)
new_z = np.expand_dims(new_z,axis = 1)
points_for_next = np.concatenate((new_x,new_y,new_z),axis=1)
intensity_for_next = compare_points[:, [3]]
ring_id_for_next = compare_points[:, [4]]
data_for_next = np.concatenate((points_for_next, intensity_for_next, ring_id_for_next), axis=1)
range_image = load_points_as_images(data_for_next, scan_unfolding=True, H=target_H, W=target_W,min_depth=dmin,max_depth=dmax)
The output is like the follow.
I am confused about which setting is wrong. Could you please point out the error from the output?
I have revised the code to be somewhat correct. But the range map seems a bit lack information for there are black lines in the range map. Is this the problem of the depth_to_point_map function? That is, the method can only obtain this accuracy from the given point cloud.
# Velodyne HDL-32E
H, W = 32, 512
h_up, h_down = 10.67, -30.67
w_left, w_right = 180, -180
device = "cpu"
elevation = 1 - torch.arange(H, device=device) / H # [0, 1]
elevation = elevation * (h_up - h_down) + h_down # [-30.67, 10.67]
azimuth = 1 - torch.arange(W, device=device) / W # [0, 1]
azimuth = azimuth * (w_left - w_right) + w_right # [-180, 180]
[elevation, azimuth] = torch.meshgrid([elevation, azimuth], indexing="ij")
angle = torch.stack([elevation, azimuth])[None].deg2rad()
def depth_to_point_map(depth,angle):
assert depth.dim() == 4
grid_cos = torch.cos(angle)
grid_sin = torch.sin(angle)
grid_x = depth * grid_cos[:, [0]] * grid_cos[:, [1]]
grid_y = depth * grid_cos[:, [0]] * grid_sin[:, [1]]
grid_z = depth * grid_sin[:, [0]]
return torch.cat((grid_x, grid_y, grid_z), dim=1)
depth_for_next = TF.to_tensor(range_image[...,4]).unsqueeze(0)
output_points = depth_to_point_map(depth_for_next,angle)
compare_points = np.fromfile(pcl_path, dtype=np.float32).reshape((-1, 5))
points_for_next = output_points.flatten(2).squeeze().numpy().transpose(1,0)
repeat_radio = int(compare_points.shape[0]/points_for_next.shape[0])+1
new_x = points_for_next[:,0].repeat(repeat_radio)[:compare_points.shape[0]]
new_y = points_for_next[:,1].repeat(repeat_radio)[:compare_points.shape[0]]
new_z = points_for_next[:,2].repeat(repeat_radio)[:compare_points.shape[0]]
new_x = np.expand_dims(new_x,axis = 1)
new_y = np.expand_dims(new_y,axis = 1)
new_z = np.expand_dims(new_z,axis = 1)
intensity_for_next = range_image[:, :, [3]].flatten().repeat(repeat_radio)[:compare_points.shape[0]]
ring_id_for_next = range_image[:, :, [4]].flatten().repeat(repeat_radio)[:compare_points.shape[0]]
intensity_for_next = np.expand_dims(intensity_for_next,axis = 1)
ring_id_for_next = np.expand_dims(ring_id_for_next,axis = 1)
data_for_next = np.concatenate((new_x,new_y,new_z,intensity_for_next,ring_id_for_next),axis=1)
range_image = load_points_as_images(data_for_next, scan_unfolding=None, H=target_H, W=target_W,min_depth=dmin,max_depth=dmax)
That's because you switched the scan_unfolding
to None in the second projection, which ignores the ring IDs. If you want a consistent result, keep the ring IDs to project back, or the spherical projection (scan_unfolding=False
) is recommended for both the first and the second projections as many papers, although the line artifact remains. The scan unfolding (scan_unfolding=True
) can produce a dense image like raw scan data but the angular spacing is a bit disturbed, which can be inconsistent in re-projection like your example.
[The image is from https://github.com/ltriess/kitti_scan_unfolding]
Besides, both your depth
and ring_id
are from range_image[..., 4]
depth_for_next = TF.to_tensor(range_image[...,4]).unsqueeze(0)
ring_id_for_next = range_image[:, :, [4]].flatten().repeat(repeat_radio)[:compare_points.shape[0]]
Thank you for the response!
def scatter(array, index, value):
for (h, w), v in zip(index, value):
if np.max(array[h, w])==0:
array[h, w] = v
else:
if h-1>=0:
if np.max(array[h-1, w])==0:
array[h-1, w] = v
if h+1<array.shape[0]:
if np.max(array[h+1, w])==0:
array[h+1, w] = v
return array
The spherical projection & letting the holes be is the most popular approach, to my understanding.
Thank you! According to your suggestion, maybe calculating ring id from the point cloud is a better choice for me. Because I need to achieve range view images with no holes according to my research. Then, how can I calculate ring IDs from point clouds? I can only get xyz information from the generated point cloud and achieve intensity and depth information from the range view map and this information seems not directly related to ring id (with my knowledge)?
I refer to the https://github.com/VincentCheungM/Run_based_segmentation/issues/3#issuecomment-452686184 for the calculation of ring_id, but the method requires the data to be sorted. Since I am not familiar with KITTI data and Nuscenes data, could you please tell me how to sort the point cloud generated by the range map to fit the requirements of the ring_id calculation algorithm?
In nuScenes, the ring IDs (vertical pixel index) take (0, 1, 2, ..., 31) numbers, and each is paired with an elevation angle $\phi$ of a laser beam. Basically, the ID/angle can only be identified by converting Cartesian coordinates (x, y, z) into spherical coordinates (r, $\phi$, $\theta$); you don't need to sort them. However, the analyzed ring IDs might differ from the original beam IDs depending on sensor calibration. The method you addressed tackles this problem. Fortunately, the officially released points are already sorted in scan order like (0, 0, 0, ..., 31, 31, 31, 31) where each ID group is sorted by azimuth order, so you just have to cut at boundaries and stack them to make range images. This is the scan unfolding (compare it with my code).
In conclusion, if you just have unsorted point clouds without the given IDs, the method has no advantage. Of course, you can compute (r, $\phi$, $\theta$), assign IDs, and finally sort the points, but the result is the same as the simple spherical projection, since the IDs are from the angles.
The (x, y, z) -> ring ID part is already here:
fup, fdown = np.deg2rad(10.67), np.deg2rad(-30.67)
elevation = np.arcsin(z / depth) + abs(fdown)
grid_h = 1 - elevation / (fup - fdown)
grid_h = np.floor(grid_h * H).clip(0, H - 1).astype(np.int32)
Thank you for the response. I do not fully understand this https://github.com/kazuto1011/dusty-gan-v2/issues/3#issuecomment-1788512972. But I get the information that, with the scan_unfolding=False, I can get the ring_id by
fup, fdown = np.deg2rad(10.67), np.deg2rad(-30.67)
elevation = np.arcsin(z / depth) + abs(fdown)
grid_h = 1 - elevation / (fup - fdown)
grid_h = np.floor(grid_h * H).clip(0, H - 1).astype(np.int32)
This ring_id can only support me to obtain range map like
There is no better method to obtain a range map without hole with the unsorted points generated by depth_to_point_map function. Is the understanding correct?
Yes, I think so. Try adjusting the angles [-30.67, 10.67] to make the image a bit better.
Thank you very much!
Hello, I revised the function with your suggestion and generated some range view maps. But I do not know whether they are correct or not. The load_pts_as_img is
And the main code is
The generated grid map is as follows.
The xyz is as follows.
The reflectance map is as follows.
I think the maps seem somewhat correct. But there are two questions (1) Is the height and width correct? The range map seems exist some region without information. (2) Do I need to normalize the XYZ, depth and reflectance?
Thank you very much for the help!