Python 异源mesh裁剪融合实现与优化

Python 异源mesh裁剪融合实现与优化

  • 一、项目需求
  • 二、解决方案
    • 1. 代码
    • 2. 结果
    • 3. 耗时
  • 三、优化探索
    • 0. 分析
    • 1. 在体素边界处进行裁剪
    • 2. 用mesh分块进行裁剪
    • 3. 用缓冲区的思路裁剪

一、项目需求

对mesh进行裁剪,但发现若非mesh是致密的,那么裁剪边会出现锯齿状边缘,究其原因,是因为该裁剪方式没有对三角面片进行处理,而是直接处理的mesh的顶点,导致裁剪边不光滑,那么两个相邻的裁剪后mesh(尤其是异源mesh)放在一起的时候,会出现缝隙。
在这里插入图片描述
在这里插入图片描述

计划找到一种在三角面片层面对mesh进行裁剪的方案,用来解决缝隙问题。

二、解决方案

找到三个python第三方库,分别为pyvista、vedo、trimesh可以实现上述功能,下面对这个三个库进行测试比较。

1. 代码

import time
from datetime import timedelta

import numpy as np

import pyvista as pv

mvs_path = r"T:ProjectDataSDF_StudioGT_datamvs_mesh_aligned.ply"

dataset = pv.read(mvs_path)
start_time = time.time()
bounds = [195.957, 211.373, 347.767, 362.355, 270.836, 286.781]
clipped = dataset.clip_box(bounds, invert=False)
end_time = time.time()
elapsed_time = end_time - start_time
formatted_time = str(timedelta(seconds=elapsed_time))
print(f"pyvista took  {formatted_time}")

p = pv.Plotter()
p.add_mesh(clipped, label='Clipped')
p.show()

import vedo

mesh = vedo.load(mvs_path)
start_time = time.time()
clipped_mesh = mesh.cut_with_box(bounds)
end_time = time.time()
elapsed_time = end_time - start_time
formatted_time = str(timedelta(seconds=elapsed_time))
print(f"vode took  {formatted_time}")
# 显示裁剪后的mesh
vedo.show(clipped_mesh)

import trimesh

mesh = trimesh.load_mesh(mvs_path)
start_time = time.time()
min_values = [195.957,347.767,270.836]
max_values = [211.373,362.355,286.781]
# 定义六个平面的法向量和原点
planes = np.array([
    [1, 0, 0, min_values[0], 0, 0],
    [-1, 0, 0, max_values[0], 0, 0],
    [0, 1, 0, 0, min_values[1], 0],
    [0, -1, 0, 0, max_values[1], 0],
    [0, 0, 1, 0, 0, min_values[2]],
    [0, 0, -1, 0, 0, max_values[2]]])

mesh = trimesh.intersections.slice_mesh_plane(
    mesh=mesh,
    plane_normal=planes[:, :3],
    plane_origin=planes[:, 3:],
    cap=False,
)

end_time = time.time()
elapsed_time = end_time - start_time
formatted_time = str(timedelta(seconds=elapsed_time))
print(f"trimesh took  {formatted_time}")
mesh.show()

2. 结果

pyvista:
在这里插入图片描述
vedo:
在这里插入图片描述
trimesh:
在这里插入图片描述
以上都可以实现在三角面片层面对mesh进行裁剪,接下来看看其效率和效果。

3. 耗时

pyvista took  0:00:19.446367
vode took  0:00:00.642607
trimesh took  0:00:01.582946

根据测试结果来看,vedo的效率最高,接下来用vedo来做大型的mesh裁剪融合实验。但在实验结果中发现其在边角上处理的并不好,仍有空隙存在。

在这里插入图片描述
在这里插入图片描述

改用trimesh进行测试,其边界就处理得比较好,但相同数据下,trimesh耗时是vedo的两倍。目前还仅是较大的尺寸进行融合,后续当体素设置为更小尺寸时,耗时会指数级别增长,所以还需优化。

在这里插入图片描述
在这里插入图片描述

三、优化探索

在这里记录所想到和已经实验的优化思路。

0. 分析

首先简单说一下trimesh的裁剪逻辑,它没有直接利用体素bound或者box进行裁剪方式,只有一个用平面切分mesh的方法,而且只返回法线正侧的mesh,见此处。所以想要裁剪出一个体素内的mesh,只能用6个平面循环切分得到,如上述代码。
那么很容易想到,用多个体素裁剪时会有重复计算。

1. 在体素边界处进行裁剪

先在所有体素边界处进行裁剪,然后得到边界处分割后的mesh,与原始mesh合并,在正常进行裁剪融合,但实验发现融合结果始终有缝隙存在,可能跟边界的顶点不好判别在哪一个体素有关系。
这个边框的mesh还比较有意思。
在这里插入图片描述

2. 用mesh分块进行裁剪

先找到一个大于体素的mesh分块,然后用这个mesh分块进行裁剪,实验发现用mesh分块裁剪确实很快,但在加上mesh分块的过程,与之前用整个mesh进行裁剪的耗时相当了。

def small_mesh(vertices,faces,min_v ,max_v ):
	'''在大型mesh中找到小的mesh分块'''
    # 找出满足条件的顶点的索引
    inside_indices = np.where(
        (vertices[:, 0] > min_v[0]) & (vertices[:, 0] < max_v[0]) &
        (vertices[:, 1] > min_v[1]) & (vertices[:, 1] < max_v[1]) &
        (vertices[:, 2] > min_v[2]) & (vertices[:, 2] < max_v[2])
    )[0]

    # 根据这些索引找到相关的三角面片
    inside_faces = np.any(np.isin(faces, inside_indices), axis=1)

    # 创建新的 mesh
    new_vertices = vertices[inside_indices]
    new_faces=faces[inside_faces]
    index_map1 = {old: new for new, old in enumerate(inside_indices.flatten())}
    new_faces = [[index_map1[idx] for idx in face] for face in new_faces if all(idx in index_map1 for idx in face)]
    new_mesh = trimesh.Trimesh(vertices=new_vertices, faces=new_faces)
    return new_mesh

在这里插入图片描述

3. 用缓冲区的思路裁剪

之所以有缝隙存在,是因为使用顶点判别时没有顾及到三角面片,如果设定一个缓冲区,那么三角面片就可以覆盖边界从而消除缝隙。
那怎么实现这个缓冲区的思路呢,其实也简单,在遍历顶点的时候,对每个顶点在6个方向上平移一个设定值,判断其是否会落入到其他体素内就可以了。若没有落入其他体素,记得不要重复统计该顶点。

# 核心代码
voxel_dict1 = defaultdict(list)
    buffer = 0.2  # 设置缓冲区大小
    directions=[np.array([1, 0, 0]), np.array([-1, 0, 0]), np.array([0, 1, 0]), np.array([0, -1, 0]), np.array([0, 0, 1]), np.array([0, 0, -1])]
    for i, v in tqdm(enumerate(vertices1)):
        k = ((v - global_min) / voxel_size).astype(np.int32)
        voxel_dict1[tuple(k)].append(i)
        # 将顶点向六个方向移动一个缓冲值,然后检查移动后的顶点是否会落入其他体素中
        for direction in directions:
            new_v = v + direction * buffer
            new_k = tuple(((new_v - global_min) / voxel_size).astype(np.int32))
            if not np.array_equal(new_k, k):   # 检查顶点是否移动到了其他体素中
                voxel_dict1[tuple(new_k)].append(i)

以下分别是无缓冲区、缓冲值为0.1,、缓冲值为0.2的结果。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
耗时8分钟左右,且跟体素尺寸无关,不会随着体素减小耗时指数倍增长。
在这里插入图片描述
实验表明,这种方法简单高效,可以有效去除异源mesh融合时的缝隙。在这里,最朴素的思想反而是最实用的。


打完收工