RANSAC拟合直线和平面

一、拟合直线

clc;clear all;close all;

%%%二维直线拟合
%%%生成随机数据
%内点
mu=[0 0];  %均值
S=[1 2.5;2.5 8];  %协方差
data1=mvnrnd(mu,S,200);   %产生200个高斯分布数据
%外点
mu=[2 2];
S=[8 0;0 8];
data2=mvnrnd(mu,S,100);     %产生100个噪声数据
%合并数据
data=[data1',data2'];
iter = 100; 

 %%% 绘制数据点
 figure;plot(data(1,:),data(2,:),'o');hold on; % 显示数据点
 number = size(data,2); % 总点数
 bestParameter1=0; bestParameter2=0; % 最佳匹配的参数
 sigma = 1;
 pretotal=0;     %符合拟合模型的数据的个数

 for i=1:iter
 %%% 随机选择两个点
     idx = randperm(number,2); 
     sample = data(:,idx); 

     %%%拟合直线方程 y=kx+b
     line = zeros(1,3);
     x = sample(:, 1);
     y = sample(:, 2);

     %k=(y(1)-y(2))/(x(1)-x(2));      %直线斜率
     %b = y(1) - k*x(1);
     k=(y(2)-x(2))/(y(1)-x(1));
     b= x(2)-k*x(1);
     line = [k -1 b]

     mask=abs(line*[data; ones(1,size(data,2))]);    %求每个数据到拟合直线的距离
     total=sum(mask<sigma);              %计算数据距离直线小于一定阈值的数据的个数

     if total>pretotal            %找到符合拟合直线数据最多的拟合直线
         pretotal=total;
         bestline=line;          %找到最好的拟合直线
    end  
 end
 %显示符合最佳拟合的数据
mask=abs(bestline*[data; ones(1,size(data,2))])<sigma;    
hold on;
k=1;
for i=1:length(mask)
    if mask(i)
        inliers(1,k) = data(1,i);
        k=k+1;
        plot(data(1,i),data(2,i),'+');
    end
end

 %%% 绘制最佳匹配曲线
 bestParameter1 = -bestline(1)/bestline(2);
 bestParameter2 = -bestline(3)/bestline(2);
 xAxis = min(inliers(1,:)):max(inliers(1,:));
 yAxis = bestParameter1*xAxis + bestParameter2;
 plot(xAxis,yAxis,'r-','LineWidth',2);
%  title(['bestLine:  y =  ',num2str(bestParameter1),'x + ',num2str(bestParameter2)]);

二、拟合平面

clc;clear all;close all;

%%%三维平面拟合
%%%生成随机数据
%内点
mu=[0 0 0];  %均值
S=[2 0 4;0 4 0;4 0 8];  %协方差
data1=mvnrnd(mu,S,300);   %产生200个高斯分布数据
%外点
mu=[2 2 2];
S=[8 1 4;1 8 2;4 2 8];  %协方差
data2=mvnrnd(mu,S,100);     %产生100个噪声数据
%合并数据
data=[data1',data2'];
iter = 1000; 

%%% 绘制数据点
 figure;plot3(data(1,:),data(2,:),data(3,:),'o');hold on; % 显示数据点
 number = size(data,2); % 总点数
 bestParameter1=0; bestParameter2=0; bestParameter3=0; % 最佳匹配的参数
 sigma = 1;
 pretotal=0;     %符合拟合模型的数据的个数

for i=1:iter
 %%% 随机选择三个点
     idx = randperm(number,3); 
     sample = data(:,idx); 

     %%%拟合直线方程 z=ax+by+c
     plane = zeros(1,3);
     x = sample(:, 1);
     y = sample(:, 2);
     z = sample(:, 3);

     %a = ((z(1)-z(2))*(y(1)-y(3)) - (z(1)-z(3))*(y(1)-y(2)))/((x(1)-x(2))*(y(1)-y(3)) - (x(1)-x(3))*(y(1)-y(2)));
     %b = ((z(1) - z(3)) - a * (x(1) - x(3)))/(y(1)-y(3));
     %c = z(1) - a * x(1) - b * y(1);
     a = (x(3)*(y(2)-z(2))-y(3)*(x(2)-z(2))-(y(2)-z(2))*z(3)+(x(2)-z(2))*z(3))/((x(1)-z(1))*(y(2)-z(2))-(y(1)-z(1))*(x(2)-z(2)));
     b = (x(3)-(x(1)-z(1))*a-z(3))/(x(2)-z(2));
     c=z(3)-a*z(1)-b*z(2);
     
     plane = [a b -1 c]

     mask=abs(plane*[data; ones(1,size(data,2))]);    %求每个数据到拟合平面的距离
     total=sum(mask<sigma);              %计算数据距离平面小于一定阈值的数据的个数

     if total>pretotal            %找到符合拟合平面数据最多的拟合平面
         pretotal=total;
         bestplane=plane;          %找到最好的拟合平面
    end  
 end
 %显示符合最佳拟合的数据
mask=abs(bestplane*[data; ones(1,size(data,2))])<sigma;    
hold on;
k = 1;
for i=1:length(mask)
    if mask(i)
        inliers(1,k) = data(1,i);
        inliers(2,k) = data(2,i);
        plot3(data(1,i),data(2,i),data(3,i),'r+');
        k = k+1;
    end
end

 %%% 绘制最佳匹配平面
 bestParameter1 = bestplane(1);
 bestParameter2 = bestplane(2);
 bestParameter3 = bestplane(4);
 xAxis = min(inliers(1,:)):max(inliers(1,:));
 yAxis = min(inliers(2,:)):max(inliers(2,:));
 [x,y] = meshgrid(xAxis, yAxis);
 z = bestParameter1 * x + bestParameter2 * y + bestParameter3;
 surf(x, y, z);
 title(['bestPlane:  z =  ',num2str(bestParameter1),'x + ',num2str(bestParameter2),'y + ',num2str(bestParameter3)]);

三、RANSAC点云多平面拟合分割

三维平面拟合(最小二乘法)
在这里插入图片描述
具体实现见下面代码:

def SVD(points):
    # 二维,三维均适用
    # 二维直线,三维平面
    pts = points.copy()
    # 奇异值分解
    c = np.mean(pts, axis=0)
    A = pts - c # shift the points
    A = A.T #3*n
    u, s, vh = np.linalg.svd(A, full_matrices=False, compute_uv=True) # A=u*s*vh
    normal = u[:,-1]

    # 法向量归一化
    nlen = np.sqrt(np.dot(normal,normal))
    normal = normal / nlen
    # normal 是主方向的方向向量 与PCA最小特征值对应的特征向量是垂直关系
    # u 每一列是一个方向
    # s 是对应的特征值
    # c >>> 点的中心
    # normal >>> 拟合的方向向量
    return u,s,c,normal


class plane_model(object):
    def __init__(self):
        平面模型参数(平面上任意一点 cx, cy, cx) + (平面法向量 nx, ny, nz)
        self.parameters = None

    def calc_inliers(self,points,dst_threshold):
        # 根据点到平面的距离计算内点和外点
        c = self.parameters[0:3]
        n = self.parameters[3:6]
        dst = abs(np.dot(points-c,n))
        ind = dst<dst_threshold
        return ind

    def estimate_parameters(self,pts):
        # 最小二乘法估算平面模型
        # 只有三个点时,可以直接计算
        num = pts.shape[0]
        if num == 3:
            c = np.mean(pts,axis=0)
            l1 = pts[1]-pts[0]
            l2 = pts[2]-pts[0]
            n = np.cross(l1,l2)
            scale = [n[i]**2 for i in range(n.shape[0])]
            #print(scale)
            n = n/np.sqrt(np.sum(scale))
        else:
            _,_,c,n = SVD(pts)

        params = np.hstack((c.reshape(1,-1),n.reshape(1,-1)))[0,:]
        self.parameters = params
        return params

    def set_parameters(self,parameters):
        self.parameters = parameters

RANSAC算法

RANSAC是一种算法,一种思想,不仅仅可以用于拟合平面,实际上还有很多用处。

这里的算法如下算法

随机挑选3个点,并计算3点形成的采样平面、
根据采样平面计算所有点到平面的距离,并根据参数(距离的阈值)将点分为内点和外点
统计内点,外点数量,更新最大迭代次数 ( )
重复以上过程直到达到停止条件(最大迭代次数,内点比例等)

# 注意这里并没有根据内点比例和模型可靠的概率动态调整最大迭代次数
def ransac_planefit(points, ransac_n, max_dst, max_trials=1000, stop_inliers_ratio=1.0, initial_inliers=None):
    # RANSAC 平面拟合
    pts = points.copy()
    num = pts.shape[0]
    cc = np.mean(pts,axis=0)
    iter_max = max_trials
    best_inliers_ratio = 0 #符合拟合模型的数据的比例
    best_plane_params = None
    best_inliers = None
    best_remains = None
    for i in range(iter_max):
        sample_index = random.sample(range(num),ransac_n)
        sample_points = pts[sample_index,:]
        plane = plane_model()
        plane_params = plane.estimate_parameters(sample_points)
        #  计算内点 外点
        index = plane.calc_inliers(points,max_dst)
        inliers_ratio = pts[index].shape[0]/num

        if inliers_ratio > best_inliers_ratio:
            best_inliers_ratio = inliers_ratio
            best_plane_params = plane_params
            bset_inliers = pts[index]
            bset_remains = pts[index==False]

        if best_inliers_ratio > stop_inliers_ratio:
            # 检查是否达到最大的比例
            print("iter: %d\n" % i)
            print("best_inliers_ratio: %f\n" % best_inliers_ratio)
            break

    return best_plane_params,bset_inliers,bset_remains

多平面拟合
针对一个三维点云,进行多平面拟合的基本思想是:首先使用RANSAC平面拟合算法从点云中提取出一个平面,对于剩下的外点,继续循环RANSAC平面拟合算法提取平面,直到提取出所有的平面,循环时注意循环停止条件的设置

具体实现见下面代码:

def ransac_plane_detection(points, ransac_n, max_dst, max_trials=1000, stop_inliers_ratio=1.0, initial_inliers=None, out_layer_inliers_threshold=230, out_layer_remains_threshold=230):
    # out_layer_inliers_threshold --> 每一次RANSAC提取出来的平面最少含有的内点数量
    # out_layer_remains_threshold --> 每一次RANSAC提取平面后剩余外点的最少数量,少于这个值,则停止循环
    inliers_num = out_layer_inliers_threshold + 1
    remains_num = out_layer_remains_threshold + 1

    plane_set = []
    plane_inliers_set = []
    plane_inliers_num_set = []

    data_remains = np.copy(points)

    i = 0

    while inliers_num>out_layer_inliers_threshold and remains_num>out_layer_remains_threshold:
        # robustly fit line only using inlier data with RANSAC algorithm
        best_plane_params,pts_inliers,pts_outliers = ransac_planefit(data_remains, ransac_n, max_dst, max_trials=max_trials, stop_inliers_ratio=stop_inliers_ratio)

        inliers_num = pts_inliers.shape[0]
        remains_num = pts_outliers.shape[0]

        if inliers_num>out_layer_inliers_threshold:
            plane_set.append(best_plane_params)
            plane_inliers_set.append(pts_inliers)
            plane_inliers_num_set.append(inliers_num)
            i = i+1
            print('------------> %d <--------------' % i)
            print(best_plane_params)

        data_remains = pts_outliers

    # sorting
    plane_set = [x for _, x in sorted(zip(plane_inliers_num_set,plane_set), key=lambda s: s[0], reverse=True)]
    plane_inliers_set = [x for _, x in sorted(zip(plane_inliers_num_set,plane_inliers_set), key=lambda s: s[0], reverse=True)]

    return plane_set, plane_inliers_set, data_remains

这里给出一个立方体的三维点云,如下图所示
在这里插入图片描述
全部代码
包含一些读取txt和绘制三维点的代码,如下:

import os
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

import math
import random


def strlist2num(dl):
    #将字符串列表转化为浮点型列表
    data = []
    for i in range(len(dl)):
        if dl[i]=='nan'or dl[i]=='NaN':
            raise ValueError('data is nan')
        data.append(float(dl[i]))
    return np.array(data)


def read_txt(path,row_skip=0,split_char=',',num_range=None, verbose=False):
    """
    read txt file into a np.ndarray.

    Input:
    ------
    path: file path
    row_skip: skip the first rows to read data
    split_char: spliting character
    num_range: data range of each number
    Output:
    ------
    data: data read. data is np.array([]) when reading error happened
                     data is np.array([]) when nan or NaN appears
                     data is np.array([]) when any number is out of range
    """

    try:
        f = open(path,'r',encoding='utf-8')
        line_list = f.readlines()
        read_lines_num = len(line_list)

        for i in range(read_lines_num):
            line_list[i] = line_list[i].rstrip()

        i = row_skip # 从第三行开始读取
        data = []
        while i <= read_lines_num-1:
            data_str = line_list[i].split(split_char)
            data.append(strlist2num(data_str))
            i = i + 1
        f.close()
    except:
        if verbose:
            print("type data of [{}] is wrong...".format(path))
        data = np.array([])
        f.close()
    data = np.array(data)
    if num_range is not None:
        if np.any(data<num_range[0]) or np.any(data>num_range[1]):
            data = np.array([])
            if verbose:
                print("data of [{}] is out of range...".format(path))
    return data


def SVD(points):
    # 二维,三维均适用
    # 二维直线,三维平面
    pts = points.copy()
    # 奇异值分解
    c = np.mean(pts, axis=0)
    A = pts - c # shift the points
    A = A.T #3*n
    u, s, vh = np.linalg.svd(A, full_matrices=False, compute_uv=True) # A=u*s*vh
    normal = u[:,-1]

    # 法向量归一化
    nlen = np.sqrt(np.dot(normal,normal))
    normal = normal / nlen
    # normal 是主方向的方向向量 与PCA最小特征值对应的特征向量是垂直关系
    # u 每一列是一个方向
    # s 是对应的特征值
    # c >>> 点的中心
    # normal >>> 拟合的方向向量
    return u,s,c,normal


class plane_model(object):
    def __init__(self):
        self.parameters = None

    def calc_inliers(self,points,dst_threshold):
        c = self.parameters[0:3]
        n = self.parameters[3:6]
        dst = abs(np.dot(points-c,n))
        ind = dst<dst_threshold
        return ind

    def estimate_parameters(self,pts):
        num = pts.shape[0]
        if num == 3:
            c = np.mean(pts,axis=0)
            l1 = pts[1]-pts[0]
            l2 = pts[2]-pts[0]
            n = np.cross(l1,l2)
            scale = [n[i]**2 for i in range(n.shape[0])]
            #print(scale)
            n = n/np.sqrt(np.sum(scale))
        else:
            _,_,c,n = SVD(pts)

        params = np.hstack((c.reshape(1,-1),n.reshape(1,-1)))[0,:]
        self.parameters = params
        return params

    def set_parameters(self,parameters):
        self.parameters = parameters


def ransac_planefit(points, ransac_n, max_dst, max_trials=1000, stop_inliers_ratio=1.0, initial_inliers=None):
    # RANSAC 平面拟合
    pts = points.copy()
    num = pts.shape[0]
    cc = np.mean(pts,axis=0)
    iter_max = max_trials
    best_inliers_ratio = 0 #符合拟合模型的数据的比例
    best_plane_params = None
    best_inliers = None
    best_remains = None
    for i in range(iter_max):
        sample_index = random.sample(range(num),ransac_n)
        sample_points = pts[sample_index,:]
        plane = plane_model()
        plane_params = plane.estimate_parameters(sample_points)
        #  计算内点 外点
        index = plane.calc_inliers(points,max_dst)
        inliers_ratio = pts[index].shape[0]/num

        if inliers_ratio > best_inliers_ratio:
            best_inliers_ratio = inliers_ratio
            best_plane_params = plane_params
            bset_inliers = pts[index]
            bset_remains = pts[index==False]

        if best_inliers_ratio > stop_inliers_ratio:
            # 检查是否达到最大的比例
            print("iter: %d\n" % i)
            print("best_inliers_ratio: %f\n" % best_inliers_ratio)
            break

    return best_plane_params,bset_inliers,bset_remains


def ransac_plane_detection(points, ransac_n, max_dst, max_trials=1000, stop_inliers_ratio=1.0, initial_inliers=None, out_layer_inliers_threshold=230, out_layer_remains_threshold=230):
    inliers_num = out_layer_inliers_threshold + 1
    remains_num = out_layer_remains_threshold + 1

    plane_set = []
    plane_inliers_set = []
    plane_inliers_num_set = []

    data_remains = np.copy(points)

    i = 0

    while inliers_num>out_layer_inliers_threshold and remains_num>out_layer_remains_threshold:
        # robustly fit line only using inlier data with RANSAC algorithm
        best_plane_params,pts_inliers,pts_outliers = ransac_planefit(data_remains, ransac_n, max_dst, max_trials=max_trials, stop_inliers_ratio=stop_inliers_ratio)

        inliers_num = pts_inliers.shape[0]
        remains_num = pts_outliers.shape[0]

        if inliers_num>out_layer_inliers_threshold:
            plane_set.append(best_plane_params)
            plane_inliers_set.append(pts_inliers)
            plane_inliers_num_set.append(inliers_num)
            i = i+1
            print('------------> %d <--------------' % i)
            print(best_plane_params)

        data_remains = pts_outliers

    # sorting
    plane_set = [x for _, x in sorted(zip(plane_inliers_num_set,plane_set), key=lambda s: s[0], reverse=True)]
    plane_inliers_set = [x for _, x in sorted(zip(plane_inliers_num_set,plane_inliers_set), key=lambda s: s[0], reverse=True)]

    return plane_set, plane_inliers_set, data_remains


def show_3dpoints(pointcluster,s=None,colors=None,quiver=None,q_length=10,tri_face_index=None):
    # pointcluster should be a list of numpy ndarray
    # This functions would show a list of pint cloud in different colors
    n = len(pointcluster)
    if colors is None:
        colors = ['r','g','b','c','m','y','k','tomato','gold']
        if n < 10:
            colors = np.array(colors[0:n])
        else: 
            colors = np.random.rand(n,3)

    fig = plt.figure()
    ax = fig.add_subplot(projection='3d')

    if s is None:
        s = np.ones(n)*2

    for i in range(n):
        ax.scatter(pointcluster[i][:,0],pointcluster[i][:,1],pointcluster[i][:,2],s=s[i],c=[colors[i]],alpha=0.6)

    if not (quiver is None):
        c1 = [random.random() for _ in range(len(quiver))]
        c2 = [random.random() for _ in range(len(quiver))]
        c3 = [random.random() for _ in range(len(quiver))]
        c = []
        for i in range(len(quiver)):
            c.append((c1[i],c2[i],c3[i]))
        cp = []
        for i in range(len(quiver)):
            cp.append(c[i])
            cp.append(c[i])
        c = c + cp
        ax.quiver(quiver[:,0],quiver[:,1],quiver[:,2],quiver[:,3],quiver[:,4],quiver[:,5],length=q_length,arrow_length_ratio=.2,pivot='tail',normalize=False,color=c)

    if not (tri_face_index is None):
        for i in range(len(tri_face_index)):
            for j in range(tri_face_index[i].shape[0]):
                index = tri_face_index[i][j].tolist()
                index = index + [index[0]]
                ax.plot(*zip(*pointcluster[i][index]))

    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')

    #ax.set_ylim([-20,60])

    plt.show()

    return 0


if __name__ == "__main__":
    path = "./files/multiplane_detection/cubic15.txt"
    pcd = read_txt(path, row_skip=1, split_char=' ')
    pcd = pcd[:,:3]
    plane_set, plane_inliers_set, data_remains = ransac_plane_detection(pcd, 3, 5, max_trials=1000, stop_inliers_ratio=1.0, initial_inliers=None,
                                    out_layer_inliers_threshold=230, out_layer_remains_threshold=230)
    plane_set = np.array(plane_set)

    print("================= 平面参数 ====================")
    print(plane_set)
    # 绘图
    show_3dpoints(plane_inliers_set)
    print("over!!!")

6个平面
红色边框是6个平面的参数,即 (cx,cy,cz,nx,ny,nz)
在这里插入图片描述
绘图显示
不同颜色代表不同的平面
在这里插入图片描述
在这里插入图片描述
参考文献:RANSAC拟合平面

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RANSACRandom Sample Consensus)是一种用于拟合模型的迭代算法,常用于处理含有噪声和异常值的数据。它可以用来拟合各种模型,包括直线平面、圆等。 在C语言中实现RANSAC拟合直线的步骤如下: 1. 随机选择一定数量的数据点作为初始样本集。 2. 根据选定的样本集,计算拟合直线的参数。常用的方法是最小二乘法。 3. 对于剩余的数据点,计算它们到拟合直线的距离,并将距离小于一定阈值的点加入内点集。 4. 如果内点集的数量大于预设的阈值,重新使用内点集进行拟合直线的参数计算。 5. 重复步骤3和步骤4,直到达到预设的迭代次数或者满足停止条件。 6. 最后,使用所有内点集进行最终的拟合直线参数计算。 以下是一个简单的示例代码,用于在C语言中实现RANSAC拟合直线: ```c #include <stdio.h> #include <stdlib.h> #include <math.h> #define MAX_POINTS 100 #define THRESHOLD 0.1 #define MAX_ITERATIONS 1000 typedef struct { double x; double y; } Point; typedef struct { double slope; double intercept; } Line; double distance(Point p, Line line) { return fabs(line.slope * p.x - p.y + line.intercept) / sqrt(pow(line.slope, 2) + 1); } Line ransac(Point points[], int numPoints) { Line bestLine; int bestCount = 0; for (int i = 0; i < MAX_ITERATIONS; i++) { // Randomly select two points int index1 = rand() % numPoints; int index2 = rand() % numPoints; // Calculate the slope and intercept of the line passing through the two points double slope = (points[index2].y - points[index1].y) / (points[index2].x - points[index1].x); double intercept = points[index1].y - slope * points[index1].x; // Count the number of inliers int count = 0; for (int j = 0; j < numPoints; j++) { if (distance(points[j], (Line){slope, intercept}) < THRESHOLD) { count++; } } // Update the best line if necessary if (count > bestCount) { bestCount = count; bestLine.slope = slope; bestLine.intercept = intercept; } } return bestLine; } int main() { Point points[MAX_POINTS] = {{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}}; int numPoints = 5; Line line = ransac(points, numPoints); printf("Fitted line: y = %.2fx + %.2f\n", line.slope, line.intercept); return 0; } ``` 这段代码演示了如何使用RANSAC算法拟合一条直线。你可以根据自己的需求修改数据点的坐标和数量,以及阈值和迭代次数等参数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值