菜单

MasterXu
发布于 2021-09-24 / 965 阅读 / 0 评论 / 1 点赞

根据地形的点云抽稀算法的 Python 实现

背景

什么是点云

点云是空间中一系列的点,每个点有空间中的 X、Y、Z 坐标。通过一系列的点构成的点云,描绘出地形的一种数据。在使用无人机做地形测量中,由无人机拍摄的照片经过摄影测量计算,最终输出的一种典型格式就是点云格式。

1.png

为什么要根据地形抽稀

在通过摄影测量输出的点云中,数量级是通常的地形处理软件(如 CAD)无法处理的,以国内用的比较多的 CASS 为例,处理由几万点组成的地形数据,已经达到极限。因为之前这些软件适配的都是通过人工在地形上进行采集数据点,人工一般间隔 3-5 米采集一个点。而无人机输出的点云间隔可以在 5cm 甚至更低。
在常规的处理流程中,一般先将无人机输出的点云,经过专业软件识别地形与非地形(剔除建筑,车辆,植被点云),保留地形点,再将地形点按距离平均抽稀成 0.5 米一个点左右。而此时点数可能依然在几十万级别。接下去的抽稀就要考虑地形因素,一般来说需要保证 3-5 米一个点。同时,在地形平缓的部分,可以适当稀疏,在地形起伏部分,需要适当加密。以保证在足够少的点数下,同样可以描绘出地形。确保后续处理算量的准确度。

核心思想

  • 读取点云
  • alphashape 算法,取范围点
  • 创建三角网
  • 去除三角网中内角有大于 120 度的三角形
  • 将整幅图划分 3*3 米网格(用于确保每个网格中都存在点,防止在平整地区全部判断为非关键点,被去除)
  • 遍历每个点
  • 如果点判断为范围点,直接保留,对该点所处的网格标记,继续遍历
  • 如果点判断为非范围点,找到以该点为顶点的所有三角形
  • 计算空间三角形的面的法向量
  • 判断每个空间三角形所在的面的法向量夹角
  • 如果存在某个法向量夹角大于一个阈值,即判定该点为关键点,保留,对该点所处的网格标记,继续遍历
  • 判断即非范围点,也非关键点,则去除
  • 检查是否每个网格都有至少一个点,如没有,找点放入

代码实现

读取点云

这边读取的是 dat 格式的点云文件

def readDAT(path):
    point_list = []
    with open(path) as f:
        line = f.readline()
        while line:
            str_list = line.rstrip().split(',')
            point = [float(str_list[2]), float(str_list[3]), float(str_list[4])]
            point_list.append(point)
            line = f.readline()
    return point_list

alphashape 算法,取范围点

具体可参考 https://github.com/bellockk/alphashape ,这里只做了调用
PS:其实在这个抽稀算法中,alphashape 不重要,因为后续每 3*3m 网格会包含一个点,是否取到点的范围线问题不大

def find_shape(self, alpha=0.15):
    points_2d = [(self.points[i][0], self.points[i][1]) for i in range(len(self.points))]
    # alpha_shape = alphashape.alphashape(points)
    alpha_shape = alphashape.alphashape(points_2d, alpha=alpha)

    if type(alpha_shape) == Polygon:
        x, y = alpha_shape.exterior.coords.xy
        self.shape_points = [[x[i], y[i]] for i in range(len(x))]
        return True, '查找范围成功'
    else:
        print('查找范围失败,减小 alpha 值')
        return False, '查找范围失败,减小 alpha 值'

创建三角网

参考:https://github.com/HakanSeven12/Delaunator-Python, 这里只做调用

def create_coordinates(self):
    triangles = Delaunator(self.points).triangles
    # halfedges = Delaunator(self.points).halfedges
    # hull = Delaunator(self.points).hull
    # coords = Delaunator(self.points).coords
    coordinates = []
    for i in range(0, len(triangles), 3):
        coordinates.append([
            self.points[triangles[i]],
            self.points[triangles[i + 1]],
            self.points[triangles[i + 2]]])
    self.coordinates = coordinates

去除三角网中内角有大于 120 度的三角形

遍历三角形,根据每个三角形的平面坐标计算三个内角

def clean_above_120_coordinate(self):
    new_coordinates = []
    for coordinate in self.coordinates:
        a = math.sqrt((coordinate[0][0] - coordinate[1][0]) * (coordinate[0][0] - coordinate[1][0]) +
                      (coordinate[0][1] - coordinate[1][1]) * (coordinate[0][1] - coordinate[1][1]))
        b = math.sqrt((coordinate[1][0] - coordinate[2][0]) * (coordinate[1][0] - coordinate[2][0]) +
                      (coordinate[1][1] - coordinate[2][1]) * (coordinate[1][1] - coordinate[2][1]))
        c = math.sqrt((coordinate[2][0] - coordinate[0][0]) * (coordinate[2][0] - coordinate[0][0]) +
                      (coordinate[2][1] - coordinate[0][1]) * (coordinate[2][1] - coordinate[0][1]))
        A = math.degrees(math.acos(clamp((a * a - b * b - c * c) / (-2 * b * c), -1, 1)))  # 夹角1
        B = math.degrees(math.acos(clamp((b * b - a * a - c * c) / (-2 * a * c), -1, 1)))  # 夹角2
        C = math.degrees(math.acos(clamp((c * c - a * a - b * b) / (-2 * a * b), -1, 1)))  # 夹角3

        # print(A, B, C)
        if A >= 120 or B >= 120 or C >= 120:
            continue
        new_coordinates.append(coordinate)
    self.coordinates_without120 = new_coordinates

去除 120 度三角网后的地形三角网,黑线为 AlphaShape 取出的范围

2.png

将整幅图划分 3*3 米网格

(用于确保每个网格中都存在点,防止在平整地区全部判断为非关键点,被去除)

def init_grid(self, distance=3.5):
    # self.x_y_min_max 是平面上最上下左右的范围
    distance_x = self.x_y_min_max[1] - self.x_y_min_max[0]
    distance_y = self.x_y_min_max[3] - self.x_y_min_max[2]
    num_x = distance_x // distance
    num_y = distance_y // distance
    num_x += 1
    num_y += 1
    grid = [[False for i in range(int(num_x))] for j in range(int(num_y))]
    return grid

核心流程(遍历、判断、抽稀)

def thinning_task(self, limit_angle, limit_distance, points):
    self.grid = self.init_grid(distance=limit_distance)
    new_points = []
    for point in points:
        # 判断是否为边界点
        if is_point_in_shape(point, self.shape_points):
            new_points.append(point)
            self.set_grid(point, limit_distance)
            continue
        # 找到以该点为顶点的三角形
        coordinates_with_point = search_coordinates_with_point(self.coordinates_without120, point)
        if len(coordinates_with_point) == 0:
            continue
        # 判断是否为特征点
        if is_characteristic_point(coordinates_with_point, limit_angle):
            new_points.append(point)
            self.set_grid(point, limit_distance)
    # 补齐网格点
    for point in points:
        # 判断这个点所处的格子里是否有点了
        if self.is_grid_false(point, limit_distance) is False:
            new_points.append(point)
            self.set_grid(point, limit_distance)
    return new_points
  • 如果点判断为范围点,直接保留,对该点所处的网格标记,继续遍历
def is_point_in_shape(point, shape_points):
    if shape_points is None:
        return False
    for shape_point in shape_points:
        if point[0] == shape_point[0] and point[1] == shape_point[1]:
            return True
    return False
  • 如果点判断为非范围点,找到以该点为顶点的所有三角形

原来的代码

def search_coordinates_with_point(coordinates, point):
    # 找到以该点为顶点的三角形
    coordinates_with_point = []
    for coordinate in coordinates:
        for coordinate_point in coordinate:
            if coordinate_point[0] == point[0] and coordinate_point[1] == point[1]:
                coordinates_with_point.append(coordinate)
    return coordinates_with_point

因为效率低下,后来使用 Python 内置数据库 SQLite 用于存点和找点,速度大幅提升

def search_coordinates_with_point(self, point):
    # 找到以该点为顶点的三角形
    coordinates_with_point = []
    cur = self.con.cursor()
    for i in range(1, 4):
        cur.execute('select * from test where x%s=? and y%s=?' % (i, i), (point[0], point[1]))
        results = cur.fetchall()
        for r in results:
            # print(r)
            coordinate = [[r[0], r[1], r[2]], [r[3], r[4], r[5]], [r[6], r[7], r[8]]]
            coordinates_with_point.append(coordinate)
    return coordinates_with_point
  • 判断是否为特征点

(计算该点为顶点的三角形的面的法向量,判断法向量之间的夹角)

def is_characteristic_point(coordinates_with_point, limit_angle):
    range_angle1 = limit_angle
    range_angle2 = 180 - limit_angle
    vectors = []
    for coordinate in coordinates_with_point:
        # 计算每个三角形的空间法向量
        flag, dx, dy, dz = nv.CalNormalVector(coordinate[0][0], coordinate[0][1], coordinate[0][2],
                                              coordinate[1][0], coordinate[1][1], coordinate[1][2],
                                              coordinate[2][0], coordinate[2][1], coordinate[2][2])
        if flag:
            vectors.append((dx, dy, dz))
    for i in range(len(vectors)):
        # 只要有一个任意两个空间法向量直接的夹角大于 limit_angle,即判断这个点为特征点,保留
        # break_flag = False
        for j in range(len(vectors)):
            if i == j:
                continue
            angle = nv.CalAngle(vectors[i], vectors[j])
            # print(angle)
            if range_angle1 < angle < range_angle2:
                return True
    return False
  • 检查是否每个网格都有至少一个点,如没有,找点放入
def set_grid(self, point, distance):
    index_x = int((point[0] - self.x_y_min_max[0]) // distance)
    index_y = int((point[1] - self.x_y_min_max[2]) // distance)
    self.grid[index_y][index_x] = True
    pass

def is_grid_false(self, point, distance):
    index_x = int((point[0] - self.x_y_min_max[0]) // distance)
    index_y = int((point[1] - self.x_y_min_max[2]) // distance)
    return self.grid[index_y][index_x]

数据验证

8wpoint.dat (limit_angle=8, limit_distance=3) 测试数据
抽稀前

3.png

抽稀后

4.png

test.dat (limit_angle=10, limit_distance=5) 测试数据
抽稀前

5.png

抽稀后

6.png

误差分析

7.png

8.png


评论