关于 τ \tau τ的确定是通过实验得到的,其实也可以通过卡方分布得到,但是卡方分布的要求较多,实际中并不满足卡方的要求,大多数是通过实验得到的。
迭代次数N的确定
( 1 − ( 1 − e ) s ) N = 1 − p N = l o g ( 1 − p ) l o g ( 1 − ( 1 − e ) s ) (1-(1-e)^s)^N=1-p \\ N=\frac{log(1-p)}{log(1-(1-e)^s)} (1−(1−e)s)N=1−pN=log(1−(1−e)s)log(1−p)
其中参数如下
e:一个点是外点的概率,这个给一个大概的值就可以了
s:解出一个模型至少需要多少个数据点,平面的话s为3
N:迭代的次数
p:作N次迭代,至少能够取到一个好的sample(没有外点,只有内点)的概率,大多数设为0.9,0.99,当然0.99的迭 代次数要比0.9的要多。
其中上述的图为p=0.99的结果。例如图中的1177表示为,当模型参数至少需要 s = 8 s=8 s=8个点,一个点是外点的概率是 e = 50 e=50% e=50, p = 0.99 p=0.99 p=0.99的情况下,迭代次数 N = 1177 N=1177 N=1177
#确认迭代次数
if math.log(1 - (1 - e) ** s) < sys.float_info.min:
N = N_regular
else:
N = math.log(1 - p) / math.log(1 - (1 - e) ** s)
ids = random.sample(range(0, t), 3)
p1, p2, p3 = X[ids]
# 判断是否共线
L = p1 - p2
R = p2 - p3
if 0 in L or 0 in R:
continue
else:
if L[0] / R[0] == L[1] / R[1] == L[2] / R[2]:
continue
# 计算平面参数
#参考A(x-x1)+B(y-y1)+C(z-z1)=0,代入(x2,y2,z2)和(x3,y3.z3)
a = (p2[1] - p1[1]) * (p3[2] - p1[2]) - (p2[2] - p1[2]) * (p3[1] - p1[1]);
b = (p2[2] - p1[2]) * (p3[0] - p1[0]) - (p2[0] - p1[0]) * (p3[2] - p1[2]);
c = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]);
d = 0 - (a * p1[0] + b * p1[1] + c * p1[2]);
其中平面模型的创建
设过 p 1 ( x 0 , y − y 0 , z 0 ) p_1(x_0,y-y_0,z_0) p1(x0,y−y0,z0)的平面方程为
A ( x − x 0 ) + B ( y − y 0 ) + C ( z − z 0 ) = 0 A(x-x_0)+B(y-y_0)+C(z-z_0)=0 A(x−x0)+B(y−y0)+C(z−z0)=0
因为 p 2 ( x 1 , y 1 , z 1 ) , p 3 ( x 2 , y 2 , z 2 ) p_2(x_1,y_1,z_1),p_3(x_2,y_2,z_2) p2(x1,y1,z1),p3(x2,y2,z2)都在这个平面上,那么
A ( x 1 − x 0 ) + B ( y 1 − y 0 ) + C ( z 1 − z 0 ) = 0 A ( x 2 − x 0 ) + B ( y 2 − y 0 ) + C ( z 2 − z 0 ) = 0 A(x_1-x_0)+B(y_1-y_0)+C(z_1-z_0)=0 \\ A(x_2-x_0)+B(y_2-y_0)+C(z_2-z_0)=0 A(x1−x0)+B(y1−y0)+C(z1−z0)=0A(x2−x0)+B(y2−y0)+C(z2−z0)=0
化成矩阵方程为
[ x − x 0 y − y 0 z − z 0 x − x 1 y − y 1 z − z 1 x − x 2 y − y 2 z − z 2 ] [ A B C ] = 0 \begin{bmatrix} {x - {x_0}}&{y - {y_0}}&{z - {z_0}}\\ {x - {x_1}}&{y - {y_1}}&{z - {z_1}}\\ {x - {x_2}}&{y - {y_2}}&{z - {z_2}} \end{bmatrix}\begin{bmatrix} A\\ B\\ C \end{bmatrix} = 0 ⎣⎡x−x0x−x1x−x2y−y0y−y1y−y2z−z0z−z1z−z2⎦⎤⎣⎡ABC⎦⎤=0
那么要使 [ A , B , C ] T [A,B,C]^T [A,B,C]T为非零解,那么
[ x − x 0 y − y 0 z − z 0 x − x 1 y − y 1 z − z 1 x − x 2 y − y 2 z − z 2 ] = 0 \begin{bmatrix} {x - {x_0}}&{y - {y_0}}&{z - {z_0}}\\ {x - {x_1}}&{y - {y_1}}&{z - {z_1}}\\ {x - {x_2}}&{y - {y_2}}&{z - {z_2}} \end{bmatrix}=0 ⎣⎡x−x0x−x1x−x2y−y0y−y1y−y2z−z0z−z1z−z2⎦⎤=0
进而推导出代码的样子。
dis = abs(a * X[:, 0] + b * X[:, 1] + c * X[:, 2] + d) / (a**2 + b**2 + c**2) ** 0.5
idset = []
for i, d in enumerate(dis):
if d < tao:
idset.append(i)
核心公式为点到平面的距离
d i s t = ∣ A x 0 + B y 0 + C z 0 + D ∣ A 2 + B 2 + C 2 dist=\frac{{|A{x_0} + B{y_0} + C{z_0} + D|}}{{\sqrt {{A^2} + {B^2} + {C^2}} }} dist=A2+B2+C2∣Ax0+By0+Cz0+D∣
其中代码中的tao就是认为设置的 τ \tau τ
其实上面的一些代码分析,RANSAC算法就已经结束了,但是RANSAC得到的结果不一定准确,那么通过最小二乘的方法对RANSAC算法进一步优化
上面左图是通过RANSAC得到的内点,右图是通过最小二乘进一步对得到的内点进行优化,使得结果更准确
def PlaneLeastSquare(X: np.ndarray):
# z=ax+by+c,return a,b,c
A = X.copy()
b = np.expand_dims(X[:, 2], axis=1) #(M,)tuple类型变成 M*1的array类型
A[:, 2] = 1
# 通过X=(AT*A)-1*AT*b直接求解
A_T = A.T
A1 = np.dot(A_T, A)
A2 = np.linalg.inv(A1)
A3 = np.dot(A2, A_T)
x = np.dot(A3, b)
return x
其中X就是RANSAC得到的内点数,在这里优化的平面模型是 a x + b y + c = z ax+by+c=z ax+by+c=z,化成矩阵如下
A = [ x 0 y 0 1 x 1 y 1 1 . . . . . . . . . x n y n 1 ] x = [ a b c ] b = [ z 0 z 1 . . . z n ] A =\begin{bmatrix} {{x_0}}&{{y_0}}&1\\ {{x_1}}&{{y_1}}&1\\ {...}&{...}&{...}\\ {{x_n}}&{{y_n}}&1 \end{bmatrix} \\ x =\begin{bmatrix} a\\ b\\ c \end{bmatrix}\\ b =\begin{bmatrix} {{z_0}}\\ {{z_1}}\\ {...}\\ {{z_n}} \end{bmatrix} \\ A=⎣⎢⎢⎡x0x1...xny0y1...yn11...1⎦⎥⎥⎤x=⎣⎡abc⎦⎤b=⎣⎢⎢⎡z0z1...zn⎦⎥⎥⎤
那么就是经典的最小二乘问题 A x = b Ax=b Ax=b, x x x的解析解为 x = ( A T A ) − 1 A b x=(A^TA)^{-1}Ab x=(ATA)−1Ab,文中的代码也是这样操作的。
if len(idset) > t * (1 - e):
break
其中idset是RANSAC得到的内点集合,当某次RANSAC算法算出的inliner数大于等于(1-e)乘以原始点云数,RANSAC算法就可以终止了。即
T ≥ ( 1 − e ) × t o t a l _ n u m _ o f _ d a t a _ p o i n t s T \ge (1-e) \times total\_num\_of\_data\_points T≥(1−e)×total_num_of_data_points
代码也是上述的复现
# 文件功能:
# 1. 从数据集中加载点云数据
# 2. 从点云数据中滤除地面点云
# 3. 从剩余的点云中提取聚类
import numpy as np
import os
import struct
from sklearn import cluster, datasets, mixture
from mpl_toolkits.mplot3d import Axes3D
import random
import open3d
import math
import sys
from scipy.spatial import KDTree
import sklearn.cluster
#读取bin文件获取三维数据
def read_velodyne_bin(path):
pc_list = []
with open(path, 'rb') as f:
content = f.read()
pc_iter = struct.iter_unpack('ffff', content)
for idx, point in enumerate(pc_iter):
pc_list.append([point[0], point[1], point[2]])
return np.asarray(pc_list, dtype=np.float32)
#最小二乘函数,优化提取的平面
#输入参数:待优化的点云数
#输出参数:最小二乘拟合的平面参数(a,b,c)
def PlaneLeastSquare(X: np.ndarray):
# z=ax+by+c,return a,b,c
A = X.copy()
b = np.expand_dims(X[:, 2], axis=1) #(M,)tuple类型变成 M*1的array类型
A[:, 2] = 1
# 通过X=(AT*A)-1*AT*b直接求解
A_T = A.T
A1 = np.dot(A_T, A)
A2 = np.linalg.inv(A1)
A3 = np.dot(A2, A_T)
x = np.dot(A3, b)
return x
#RANSAC算法,提取平面
#输入参数 X:输入的点云数据,tao:外点距离平面的距离因子,e:一个点是外点的概率,N_regular:默认迭代次数
#输出参数:内点,外点,平面的四参数(a,b,c,d)
def PlaneRANSAC(X: np.ndarray, tao: float, e=0.4, N_regular=100):
# return plane ids
t = X.shape[0]
s = X.shape[1]
count = 0
p = 0.99
dic = {}
#确认迭代次数
if math.log(1 - (1 - e) ** s) < sys.float_info.min:
N = N_regular
else:
N = math.log(1 - p) / math.log(1 - (1 - e) ** s)
# 开始迭代
while count < N:
ids = random.sample(range(0, t), 3)
p1, p2, p3 = X[ids]
# 判断是否共线
L = p1 - p2
R = p2 - p3
if 0 in L or 0 in R:
continue
else:
if L[0] / R[0] == L[1] / R[1] == L[2] / R[2]:
continue
# 计算平面参数
#参考A(x-x1)+B(y-y1)+C(z-z1)=0,代入(x2,y2,z2)和(x3,y3.z3)
a = (p2[1] - p1[1]) * (p3[2] - p1[2]) - (p2[2] - p1[2]) * (p3[1] - p1[1]);
b = (p2[2] - p1[2]) * (p3[0] - p1[0]) - (p2[0] - p1[0]) * (p3[2] - p1[2]);
c = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]);
d = 0 - (a * p1[0] + b * p1[1] + c * p1[2]);
dis = abs(a*X[:, 0] + b*X[:, 1]+ c*X[:, 2]+d) / (a**2 + b**2 + c** 2) ** 0.5
idset = []
for i, d in enumerate(dis):
if d < tao:
idset.append(i)
# 再使用最小二乘法,得到更加准确的a,b,c,d
p = PlaneLeastSquare(X[idset])
a, b, c, d = p[0], p[1], -1, p[2]
dic[len(idset)] = [a, b, c, d]
if len(idset) > t * (1 - e):
break
count += 1
parm = dic[max(dic.keys())]
a, b, c, d = parm
dis = abs(a*X[:, 0] + b*X[:, 1]+ c*X[:, 2]+d) / (a**2 + b**2 + c** 2) ** 0.5
idset = []
odset = []
for i, d in enumerate(dis):
if d < tao:
idset.append(i)
else:
odset.append(i)
print("地面的内点数,外点数:",len(idset),len(odset))
return np.array(idset),np.array(odset),a,b,c,d
#点云旋转--显示
def rotate_view(vis):
ctr = vis.get_view_control()
ctr.rotate(0.0, 10.0)
return False
if __name__ == '__main__':
##原始数据的加载
#读取数据
origin_points = read_velodyne_bin("000000.bin")
#建立点云对象
pcd=open3d.geometry.PointCloud()
#加载点云数据
pcd.points=open3d.utility.Vector3dVector(origin_points)
#改变点云颜色
c=[0,0,0]
cs=np.tile(c,(origin_points.shape[0],1))
pcd.colors=open3d.utility.Vector3dVector(cs)
#添加坐标系
FOR1 = open3d.geometry.TriangleMesh.create_coordinate_frame(size=5, origin=[0, 0, 0])
#显示点云
open3d.visualization.draw_geometries([FOR1,pcd])
print("原始数据的个数、维数:",origin_points.shape[0],origin_points.shape[1])
##通过RANSAC算法获得点云的地面
planeids,othersids,pa,pb,pc,pd=PlaneRANSAC(origin_points,0.35)
planedata = origin_points[planeids]
planepcd = open3d.geometry.PointCloud()
planepcd.points = open3d.utility.Vector3dVector(planedata)
c = [0, 0, 255]
cs = np.tile(c, (planedata.shape[0], 1))
planepcd.colors = open3d.utility.Vector3dVector(cs)
otherdata = origin_points[othersids]
otherpcd = open3d.geometry.PointCloud()
otherpcd.points = open3d.utility.Vector3dVector(otherdata)
c = [255, 0, 0]
cs = np.tile(c, (otherdata.shape[0], 1))
otherpcd.colors = open3d.utility.Vector3dVector(cs)
open3d.visualization.draw_geometries([planepcd, otherpcd])
print("地面方程为a,b,c,d:",pa[0],pb[0],pc,pd)
##sklearn库下的DBSCAN进行聚类
Css = sklearn.cluster.DBSCAN(eps=0.50, min_samples=4).fit(otherdata)
ypred = np.array(Css.labels_)
print("聚类的个数:",ypred.shape[0])
ddraw = []
colorset = [[222, 0, 0], [0, 224, 0], [0, 255, 255], [222, 244, 0], [255, 0, 255], [128, 0, 0]]
for cluuus in set(ypred):
kaka = np.where(ypred == cluuus)
ppk = open3d.geometry.PointCloud()
ppk.points = open3d.utility.Vector3dVector(otherdata[kaka])
if len(ppk.points)<10:
continue
c = colorset[cluuus % 3]
if cluuus == -1:
c = [0, 0, 0]
cs = np.tile(c, (otherdata[kaka].shape[0], 1))
ppk.colors = open3d.utility.Vector3dVector(cs)
ddraw.append(ppk)
ddraw.append(planepcd)
open3d.visualization.draw_geometries(ddraw)
#点云绕着轴旋转
#open3d.visualization.draw_geometries_with_animation_callback(ddraw,rotate_view)
注意:最后一行代码打开,会发现神奇的东西哦
数据集取自KITTI的激光雷达数据,就其中的两个数据集进行仿真,效果如下
KITTI数据集比较大,需要完全的KITTI数据集,加我QQ:2822908208
000000.bin密码7875
原始数据
RANSAC提取地面
DBSCAN进行聚类
000001.bin密码7875
原始数据
RANSAC提取地面
DBSCAN进行聚类