当前位置:   article > 正文

python快速实现地面点滤除工作_地面点滤除工具

地面点滤除工具

使用传统算法来滤除地面点,具体思路把点云划分为BEV上面的一个个seg和bin,每个bin里面取z_min一个点作为代表。

  1. import numpy as np
  2. class Processor:
  3. '''
  4. Module: Processor
  5. Args:
  6. n_segments(int): The number of fan-shaped regions divided by 360 degrees
  7. n_bins(int): The number of bins divided in a segment.
  8. r_max(float): The max boundary of lidar point.(meters)
  9. r_min(float): The min boundary of lidar point.(meters)
  10. line_search_angle(float): The angle for relative search in nearby segments.
  11. max_dist_to_line(float): The distance threshold of the non-ground object to the ground.(meters)
  12. max_slope(float): Local maximum slope of the ground.
  13. max_error(float): The max MSE to fit the ground.(meters)
  14. long_threshold(int): The max threshold of ground wave interval.
  15. max_start_height(float): The max height difference between hillside and ground.(meters)
  16. sensor_height(float): The distance from the lidar sensor to the ground.(meters)
  17. Call:
  18. Arg:
  19. vel_msg(numpy.ndarray): The raw local LiDAR cloud points in 3D(x,y,z).
  20. For example:
  21. vel_msg shapes [n_point, 3], with `n_point` refers to the number of cloud points,
  22. while `3` is the number of 3D(x,y,z) axis.
  23. vel_msg = array([[0.3, 0.1, 0.7],
  24. [0.6, 0.6, 0.5],
  25. [0.1, 0.4, 0.8],
  26. ... ... ...
  27. [0.5, 0.3, 0.6],
  28. [0.6, 0.3, 0.4]]
  29. Returns:
  30. vel_non_ground(numpy.ndarray): The local LiDAR cloud points after filter out ground info.
  31. '''
  32. def __init__(self, n_segments=60, n_bins=80, r_max=150, r_min=0.3,
  33. line_search_angle=0.2, max_dist_to_line=0.25,
  34. max_slope=2.0, max_error=0.1, long_threshold=3,
  35. max_start_height=0.2, sensor_height=1.73):
  36. self.n_segments = n_segments # number of segments
  37. self.n_bins = n_bins # number of bins in a segment
  38. self.r_max = r_max
  39. self.r_min = r_min
  40. self.line_search_angle = line_search_angle
  41. self.max_dist_to_line = max_dist_to_line
  42. self.max_slope = max_slope
  43. self.max_error = max_error
  44. self.long_threshold = long_threshold # int
  45. self.max_start_height = max_start_height
  46. self.sensor_height = sensor_height
  47. self.segment_step = 2 * np.pi / self.n_segments
  48. self.bin_step = (self.r_max - self.r_min) / self.n_bins
  49. self.segments = []
  50. self.seg_list = []
  51. def __call__(self, vel_msg):
  52. point5D = self.Model_Ground(vel_msg)
  53. vel_non_ground = self.Segment_Vel(point5D)
  54. return vel_non_ground
  55. def Model_Ground(self, vel_msg):
  56. point5D = self.project_5D(vel_msg)
  57. point5D = self.filter_out_range(point5D)
  58. # 根据seg模块排序。
  59. point5D = point5D[np.argsort(point5D[:, 3])]
  60. # np.unique去除数组中重复的元素,并从小到大返回一个新的数组
  61. self.seg_list = np.int16(np.unique(point5D[:, 3]))
  62. # 每个seg单独处理
  63. for seg_idx in self.seg_list:
  64. # 创建一个segmentation的实例
  65. segment = Segmentation(self.max_slope, self.max_error, self.long_threshold,
  66. self.max_start_height, self.sensor_height)
  67. # 取出当前segment的点进行处理
  68. point5D_seg = point5D[point5D[:, 3] == seg_idx]
  69. # min_z表示现在seg里的每一个bin的z的最小值
  70. min_z = segment.get_min_z(point5D_seg) # checked
  71. segment.fitSegmentLines(min_z) # checked
  72. # 对应当前segment的实例,一个segment实例里面有一个self.lines的数组属性
  73. self.segments.append(segment)
  74. return point5D
  75. def Segment_Vel(self, point5D):
  76. #point5D按seg顺序排好的,并且过滤了不在r范围内的点
  77. # 每个点的初始label
  78. label = np.zeros([point5D.shape[0]])
  79. #np.diff表示数组中相邻元素做减法,如10列向量,运算完得到9列。np.r_表示上下拼接两个矩阵,np.nonzero用来获取数组中非零元素的索引
  80. #这个slice_list是将point5D里每个seg的起点终点索引建立一个列表,根据列表便可以分开每个seg中的点
  81. slice_list = np.r_[np.nonzero(np.r_[1, np.diff(point5D[:, 3])])[0], len(point5D)]
  82. for i, seg_idx in enumerate(self.seg_list):
  83. #得到当前seg的实例以及seg里面的点
  84. segment = self.segments[i]
  85. point5D_seg = point5D[point5D[:, 3] == seg_idx]
  86. #得到当前seg里点的label
  87. non_ground = segment.verticalDistanceToLine(point5D_seg[:, [4, 2]]) # x,y -> d,z
  88. #将计算距离大于阈值的点设置为0
  89. non_ground[non_ground > self.max_dist_to_line] = 0
  90. step = 1
  91. #下面定义了一个函数,根据i和step可以得到一个下标值
  92. idx_search = lambda i, step: (i % len(self.seg_list) + step) % len(self.seg_list)
  93. #判断当前seg左右的几个seg,如果当前seg的bin与z满足隔壁的segline曲线,也定义为地面点。
  94. while step * self.segment_step < self.line_search_angle:
  95. segment_f = self.segments[idx_search(i, -step)]
  96. segment_b = self.segments[idx_search(i, step)]
  97. non_ground_b = segment_f.verticalDistanceToLine(point5D_seg[:, [4, 2]])
  98. non_ground_b[non_ground_b > self.max_dist_to_line] = 0
  99. non_ground_f = segment_b.verticalDistanceToLine(point5D_seg[:, [4, 2]])
  100. non_ground_f[non_ground_f > self.max_dist_to_line] = 0
  101. non_ground += non_ground_b + non_ground_f
  102. step += 1
  103. #当前seg的点赋值label,label为1表示是地面点
  104. label[slice_list[i]:slice_list[i + 1]] = non_ground == 0
  105. # vel_non_ground = point5D[label == 1][:, :3]
  106. vel_non_ground = point5D[label != 1][:, :3]
  107. return vel_non_ground
  108. def project_5D(self, point3D):
  109. '''
  110. Args:
  111. point3D: shapes (n_row, 3), while 3 represent x,y,z axis in order.
  112. Returns:
  113. point5D: shapes (n_row, 3+2), while 5 represent x,y,z,seg,bin axis in order.
  114. '''
  115. x = point3D[:, 0]
  116. y = point3D[:, 1]
  117. z = point3D[:, 2]
  118. # index mapping
  119. # arctan2输入的是两个点,输出范围为正负派。而arctan是输入一个,输出范围二分之一正负派
  120. angle = np.arctan2(y, x)
  121. # 因为输出是正负派,所以加上pai范围是0-2派。除以每份的角度。得到在哪一份
  122. segment_index = np.int32(np.floor((angle + np.pi) / self.segment_step)) # segment
  123. # 求每个点的平面距离,也就是半径
  124. radius = np.sqrt(x ** 2 + y ** 2)
  125. # 在rmin和rmax之间分为n份,看这个点落在哪一份
  126. bin_index = np.int32(np.floor((radius - self.r_min) / self.bin_step)) # bin
  127. # segment_index是一行n列,所以要先转置再按行叠加
  128. point5D = np.vstack([point3D.T, segment_index, bin_index]).T
  129. return point5D
  130. def filter_out_range(self, point5D):
  131. '''
  132. Args:
  133. point5D: shapes (n_row, 3+2), while 5 represent x,y,z,seg,bin axis in order.
  134. Returns:
  135. point5D: shapes (n_row_filtered, 5), while 5 represent x,y,z,seg,bin axis in order.
  136. '''
  137. radius = point5D[:, 4] # [x,y,z,seg,bin]
  138. # np.logical_and滤除掉不在半径范围里面的点。
  139. condition = np.logical_and(radius < self.r_max, radius > self.r_min)
  140. point5D = point5D[condition]
  141. return point5D
  142. class Segmentation:
  143. '''
  144. Args:
  145. max_slope(float): Local maximum slope of the ground.
  146. max_error(float): The max MSE to fit the ground.
  147. long_threshold(int): The max threshold of ground wave interval.
  148. max_start_height(float): The max height difference between hillside and ground.
  149. sensor_height(float): The distance from the lidar sensor to the ground.
  150. '''
  151. def __init__(self, max_slope=2.0, max_error=0.1, long_threshold=3,
  152. max_start_height=0.2, sensor_height=1.73):
  153. self.max_slope_ = max_slope
  154. self.max_error_ = max_error
  155. self.long_threshold_ = long_threshold # int
  156. self.max_start_height_ = max_start_height
  157. self.sensor_height_ = sensor_height
  158. self.matrix_new = np.array([[1, 0, 0], [0, 1, 0]])
  159. self.matrix_one = np.array([[0, 0, 1]])
  160. self.lines = []
  161. def get_min_z(self, point5D_seg):
  162. '''
  163. Args:
  164. point5D: shapes (n_row, 5), while 5 represent x,y,z,seg,bin axis in order.
  165. Returns:
  166. pointSBZ: shapes (n_row, 2), while 3 represent bin,z axis in order. Bin order sorted.
  167. '''
  168. # 当前seg里面的bin值
  169. bin_ = point5D_seg[:, 4]
  170. # 得到当前seg里面每个bin的z的最小值,返回的是[num_bin,2]2分别是bin_idx和z_min
  171. pointBZ = np.array([point5D_seg[bin_ == bin_idx].min(axis=0)[2:] for bin_idx in np.unique(bin_)])[:, [2, 0]]
  172. return pointBZ
  173. def fitLocalLine(self, cur_line_points, error=False):
  174. # @在numpy中表示矩阵相乘,第二个元素是向量的话会自动转置
  175. xy1 = np.array(cur_line_points) @ self.matrix_new + self.matrix_one
  176. # 此时的xy1表示(:(bin,z_min,1))
  177. A = xy1[:, [0, 2]]
  178. y = xy1[:, [1]]
  179. # 自变量是bin,因变量是z_min。np.linalg.lstsqyong'zui'xiao'er'chneg
  180. [[m], [b]] = np.linalg.lstsq(A, y, rcond=None)[0]
  181. if error:
  182. mse = (A @ np.array([[m], [b]]) - y) ** 2
  183. return [m, b], mse
  184. else:
  185. return [m, b]
  186. def verticalDistanceToLine(self, xy): # checked
  187. #传入的xy是当前seg里面的点,包含bin,z的属性
  188. kMargin = 0.1
  189. #创建当前seg点的label
  190. label = np.zeros(len(xy))
  191. #遍历当前seg实例里面的线
  192. for d_l, d_r, m, b in self.lines:
  193. #计算当前seg里面的点到拟合的线的距离
  194. distance = np.abs(m * xy[:, 0] + b - xy[:, 1])
  195. #得到在当前line范围里的bin
  196. con = (xy[:, 0] > d_l - kMargin) & (xy[:, 0] < d_r + kMargin)
  197. #将这些bin的distance赋值到label里
  198. label[con] = distance[con]
  199. return label.flatten()
  200. def fitSegmentLines(self, min_z):
  201. # 最小bin的点
  202. cur_line_points = [min_z[0]]
  203. long_line = False
  204. # 传感器的高度
  205. cur_ground_height = self.sensor_height_
  206. d_i = 1
  207. while d_i < len(min_z):
  208. # 存储的bin列表的最后一个
  209. lst_point = cur_line_points[-1]
  210. # 循环遍历中当前的bin
  211. cur_point = min_z[d_i]
  212. # 如果两者的bin离得远,则为长线
  213. if cur_point[0] - lst_point[0] > self.long_threshold_:
  214. long_line = True
  215. if len(cur_line_points) < 2:
  216. # 如果两者的bin之差小于阈值,同时保存的bin的z与地面高度小于非地面阈值
  217. # 意思就是地面点是连续的,如果两个bin里离得远或者第一个点离地面远,都重新保存第一个点
  218. if (cur_point[0] - lst_point[0] < self.long_threshold_) and abs(
  219. lst_point[1] - cur_ground_height) < self.max_start_height_:
  220. cur_line_points.append(cur_point)
  221. else:
  222. cur_line_points = [cur_point]
  223. else:
  224. # 直接把第三个点保存进去
  225. cur_line_points.append(cur_point)
  226. # 根据已有的点拟合bin和z之间的直线方程
  227. cur_line, mse = self.fitLocalLine(cur_line_points, True)
  228. # 如果最小二乘拟合的误差大于阈值或者斜率大于坡度阈值,或者是长线的话
  229. if (mse.max() > self.max_error_ or cur_line[0] > self.max_slope_ or long_line):
  230. #说明刚放入的这个点不是平面里面的,pop出来
  231. cur_line_points.pop()
  232. if len(cur_line_points) >= 3:
  233. #如果里面大于等于三个点,则直接拟合参数,放到self.lines里面,起始bin,结束bin,直线参数
  234. new_line = self.fitLocalLine(cur_line_points)
  235. self.lines.append([cur_line_points[0][0], cur_line_points[-1][0], *new_line]) # b boundary
  236. #根据拟合出来的直线方程更新当前的z也就是地面高度。其中的自变量选择的是最后一个bin
  237. cur_ground_height = new_line[0] * cur_line_points[-1][0] + new_line[1] # m*x+b
  238. long_line = False
  239. cur_line_points = [cur_line_points[-1]]
  240. #因为前面弹出了,所以d_i并没有参与这次计算,所以减一,让d_i与保留的上一个末尾点再判断一次
  241. d_i -= 1
  242. d_i += 1
  243. #当退出循环后,如果保存的点数大于2,说明也是一个线,保留下来。
  244. if len(cur_line_points) > 2:
  245. new_line = self.fitLocalLine(cur_line_points)
  246. self.lines.append([cur_line_points[0][0], cur_line_points[-1][0], *new_line])

原文链接:
yGitHub - SilvesterHsu/LiDAR_ground_removal: This is an implement of Fast Segmentation of 3D Point Clouds for Ground Vehicles

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/523433
推荐阅读
相关标签
  

闽ICP备14008679号