【Python】圆柱体内部3D点云仿真及ply文件生成,圆形3D点云检测及拟合算法

【Python】圆柱体内部3D点云仿真及ply文件生成,圆形3D点云检测及拟合算法

更新以gitee为准:
管道内管壁ply点云生成及管道检测

文章目录

ply点云显示

通过plyfile库可以完成点云的读取或写入

同时 引入color map 使其与不同的深度值进行对应

ply读取函数如下:

python 复制代码
color_map = [
    (128, 0, 0),     (132, 0, 0),     (136, 0, 0),     (140, 0, 0),
    (144, 0, 0),     (148, 0, 0),     (152, 0, 0),     (156, 0, 0),
    (160, 0, 0),     (164, 0, 0),     (168, 0, 0),     (172, 0, 0),
    (176, 0, 0),     (180, 0, 0),     (184, 0, 0),     (188, 0, 0),
    (192, 0, 0),     (196, 0, 0),     (200, 0, 0),     (204, 0, 0),
    (208, 0, 0),     (212, 0, 0),     (216, 0, 0),     (220, 0, 0),
    (224, 0, 0),     (228, 0, 0),     (232, 0, 0),     (236, 0, 0),
    (240, 0, 0),     (244, 0, 0),     (248, 0, 0),     (252, 0, 0),
    (255, 0, 0),     (255, 4, 0),     (255, 8, 0),     (255, 12, 0),
    (255, 16, 0),    (255, 20, 0),    (255, 24, 0),    (255, 28, 0),
    (255, 32, 0),    (255, 36, 0),    (255, 40, 0),    (255, 44, 0),
    (255, 48, 0),    (255, 52, 0),    (255, 56, 0),    (255, 60, 0),
    (255, 64, 0),    (255, 68, 0),    (255, 72, 0),    (255, 76, 0),
    (255, 80, 0),    (255, 84, 0),    (255, 88, 0),    (255, 92, 0),
    (255, 96, 0),    (255, 100, 0),   (255, 104, 0),   (255, 108, 0),
    (255, 112, 0),   (255, 116, 0),   (255, 120, 0),   (255, 124, 0),
    (255, 128, 0),   (255, 132, 0),   (255, 136, 0),   (255, 140, 0),
    (255, 144, 0),   (255, 148, 0),   (255, 152, 0),   (255, 156, 0),
    (255, 160, 0),   (255, 164, 0),   (255, 168, 0),   (255, 172, 0),
    (255, 176, 0),   (255, 180, 0),   (255, 184, 0),   (255, 188, 0),
    (255, 192, 0),   (255, 196, 0),   (255, 200, 0),   (255, 204, 0),
    (255, 208, 0),   (255, 212, 0),   (255, 216, 0),   (255, 220, 0),
    (255, 224, 0),   (255, 228, 0),   (255, 232, 0),   (255, 236, 0),
    (255, 240, 0),   (255, 244, 0),   (255, 248, 0),   (255, 252, 0),
    (254, 255, 1),   (250, 255, 6),   (246, 255, 10),  (242, 255, 14),
    (238, 255, 18),  (234, 255, 22),  (230, 255, 26),  (226, 255, 30),
    (222, 255, 34),  (218, 255, 38),  (214, 255, 42),  (210, 255, 46),
    (206, 255, 50),  (202, 255, 54),  (198, 255, 58),  (194, 255, 62),
    (190, 255, 66),  (186, 255, 70),  (182, 255, 74),  (178, 255, 78),
    (174, 255, 82),  (170, 255, 86),  (166, 255, 90),  (162, 255, 94),
    (158, 255, 98),  (154, 255, 102), (150, 255, 106), (146, 255, 110),
    (142, 255, 114), (138, 255, 118), (134, 255, 122), (130, 255, 126),
    (126, 255, 130), (122, 255, 134), (118, 255, 138), (114, 255, 142),
    (110, 255, 146), (106, 255, 150), (102, 255, 154), (98, 255, 158),
    (94, 255, 162),  (90, 255, 166),  (86, 255, 170),  (82, 255, 174),
    (78, 255, 178),  (74, 255, 182),  (70, 255, 186),  (66, 255, 190),
    (62, 255, 194),  (58, 255, 198),  (54, 255, 202),  (50, 255, 206),
    (46, 255, 210),  (42, 255, 214),  (38, 255, 218),  (34, 255, 222),
    (30, 255, 226),  (26, 255, 230),  (22, 255, 234),  (18, 255, 238),
    (14, 255, 242),  (10, 255, 246),  (6, 255, 250),   (2, 255, 254),
    (0, 252, 255),   (0, 248, 255),   (0, 244, 255),   (0, 240, 255),
    (0, 236, 255),   (0, 232, 255),   (0, 228, 255),   (0, 224, 255),
    (0, 220, 255),   (0, 216, 255),   (0, 212, 255),   (0, 208, 255),
    (0, 204, 255),   (0, 200, 255),   (0, 196, 255),   (0, 192, 255),
    (0, 188, 255),   (0, 184, 255),   (0, 180, 255),   (0, 176, 255),
    (0, 172, 255),   (0, 168, 255),   (0, 164, 255),   (0, 160, 255),
    (0, 156, 255),   (0, 152, 255),   (0, 148, 255),   (0, 144, 255),
    (0, 140, 255),   (0, 136, 255),   (0, 132, 255),   (0, 128, 255),
    (0, 124, 255),   (0, 120, 255),   (0, 116, 255),   (0, 112, 255),
    (0, 108, 255),   (0, 104, 255),   (0, 100, 255),   (0, 96, 255),
    (0, 92, 255),    (0, 88, 255),    (0, 84, 255),    (0, 80, 255),
    (0, 76, 255),    (0, 72, 255),    (0, 68, 255),    (0, 64, 255),
    (0, 60, 255),    (0, 56, 255),    (0, 52, 255),    (0, 48, 255),
    (0, 44, 255),    (0, 40, 255),    (0, 36, 255),    (0, 32, 255),
    (0, 28, 255),    (0, 24, 255),    (0, 20, 255),    (0, 16, 255),
    (0, 12, 255),    (0, 8, 255),     (0, 4, 255),     (0, 0, 255),
    (0, 0, 252),     (0, 0, 248),     (0, 0, 244),     (0, 0, 240),
    (0, 0, 236),     (0, 0, 232),     (0, 0, 228),     (0, 0, 224),
    (0, 0, 220),     (0, 0, 216),     (0, 0, 212),     (0, 0, 208),
    (0, 0, 204),     (0, 0, 200),     (0, 0, 196),     (0, 0, 192),
    (0, 0, 188),     (0, 0, 184),     (0, 0, 180),     (0, 0, 176),
    (0, 0, 172),     (0, 0, 168),     (0, 0, 164),     (0, 0, 160),
    (0, 0, 156),     (0, 0, 152),     (0, 0, 148),     (0, 0, 144),
    (0, 0, 140),     (0, 0, 136),     (0, 0, 132),     (0, 0, 128)
]

def visualize_ply(file_path):
    # 读取PLY文件
    ply_data = PlyData.read(file_path)
    vertices = ply_data['vertex']
    
    # 提取坐标数据
    x = vertices['x']
    y = vertices['y']
    z = vertices['z']
    
    # 计算深度范围
    min_depth = np.min(z)
    max_depth = np.max(z)
    depth_range = max_depth - min_depth
    
    # 生成颜色映射
    colors = []
    for depth in z:
        # 将深度映射到颜色索引 (0-255)
        color_idx = int(((depth - min_depth) / depth_range) * (len(color_map)-1))
        colors.append(color_map[color_idx])
    
    # 转换为0-1范围的RGB值
    colors = np.array(colors) / 255.0

    # 创建3D可视化
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # 绘制点云
    ax.scatter(x, y, z, c=colors, s=1, marker='o')
    
    # 设置坐标轴标签
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    
    # 启用交互式旋转
    plt.show(block=True)  # 添加block=True参数保持窗口显示

点云生成

建立一个tkinter界面 用于生成

其中 fov角度的引入表示 某一传感器防止在圆柱体内部中心位置 延伸z轴的正向延长方向上做检测 传感器测量范围有限 那么最近的一个点距离S与fov的关系为:

sin(fov/2)=R/S

python 复制代码
 min_range = radius / math.sin(math.radians(fov_deg / 2))

即最大fov角度时 测到的管壁的第一个点 其必须小于测量范围 否则就完全测不到

对应的z轴最小范围为:

python 复制代码
min_distance = radius / math.tan(math.radians(fov_deg / 2))

当测得最远的一个点时 其斜边为最大测量范围 那么此时对应的角度a与半径关系为:

sin(a)=R/max_range

求得的a要小于fov/2才行

根据几何关系如果第一个点能被测得到 那么距离最大的一个点肯定也能被侧得到

但也可加上条件判断

python 复制代码
    a_rad = math.asin(radius / max_range)
    a_deg = math.degrees(a_rad)
    if a_deg > fov_deg / 2:
        return None

此时 距离最大值是斜边 半径为对边 z轴最大距离s为临边

那么z轴最大距离即为:

python 复制代码
max_distance = math.sqrt(max_range**2 - radius**2)

最后再加上点的数量、噪音、以及近距离分布多 远距离分布少的系数得到

生成函数为:

python 复制代码
def generate_cylinder(radius, fov_deg, max_range, points=5000,noise=5,coefficient=4):
    if radius <= 0 or fov_deg <= 0 or max_range <= 0:
        return None

    if fov_deg > 180:
        return None

    if radius >= max_range:
        return None

    if points <= 0:
        return None

    min_range = radius / math.sin(math.radians(fov_deg / 2))
    if min_range > max_range:
        return None
    
    max_distance = math.sqrt(max_range**2 - radius**2)
    min_distance = radius / math.tan(math.radians(fov_deg / 2))

    if max_distance <= min_distance or max_distance <= 0 or min_distance <= 0:
        return None

    a_rad = math.asin(radius / max_range)
    a_deg = math.degrees(a_rad)
    if a_deg > fov_deg / 2:
        return None

    # 生成圆柱面点云
    theta = np.random.uniform(0, 2*math.pi, points)

    u = np.random.rand(points)
    z = max_distance - (max_distance - min_distance) * np.power(u,1/coefficient)
    # z = np.random.uniform(min_distance, max_distance, points)

    # 转换为笛卡尔坐标(管壁表面)
    radius_noise = np.random.normal(0, noise, points)
    radius += radius_noise
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)

    x_noise = np.random.normal(0, noise, points)
    y_noise = np.random.normal(0, noise, points) 

    x +=  x_noise
    y +=  y_noise

    return x, y, z

保存ply并显示:

python 复制代码
# 创建PLY结构并保存(保留原有实现)
            vertices = np.zeros(len(x), dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')])
            vertices['x'], vertices['y'], vertices['z'] = x, y, z
            
            ply = PlyData([PlyElement.describe(vertices, 'vertex')], text=True)
            ply.write('tube.ply')
            
            # 调用可视化
            visualize_ply('tube.ply')

点云生成完整代码

默认半径300 fov60 测量距离2000 得到的最后效果:

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from plyfile import PlyData, PlyElement
import tkinter as tk
from tkinter import ttk
import math
from tkinter import messagebox 

color_map = [
    (128, 0, 0),     (132, 0, 0),     (136, 0, 0),     (140, 0, 0),
    (144, 0, 0),     (148, 0, 0),     (152, 0, 0),     (156, 0, 0),
    (160, 0, 0),     (164, 0, 0),     (168, 0, 0),     (172, 0, 0),
    (176, 0, 0),     (180, 0, 0),     (184, 0, 0),     (188, 0, 0),
    (192, 0, 0),     (196, 0, 0),     (200, 0, 0),     (204, 0, 0),
    (208, 0, 0),     (212, 0, 0),     (216, 0, 0),     (220, 0, 0),
    (224, 0, 0),     (228, 0, 0),     (232, 0, 0),     (236, 0, 0),
    (240, 0, 0),     (244, 0, 0),     (248, 0, 0),     (252, 0, 0),
    (255, 0, 0),     (255, 4, 0),     (255, 8, 0),     (255, 12, 0),
    (255, 16, 0),    (255, 20, 0),    (255, 24, 0),    (255, 28, 0),
    (255, 32, 0),    (255, 36, 0),    (255, 40, 0),    (255, 44, 0),
    (255, 48, 0),    (255, 52, 0),    (255, 56, 0),    (255, 60, 0),
    (255, 64, 0),    (255, 68, 0),    (255, 72, 0),    (255, 76, 0),
    (255, 80, 0),    (255, 84, 0),    (255, 88, 0),    (255, 92, 0),
    (255, 96, 0),    (255, 100, 0),   (255, 104, 0),   (255, 108, 0),
    (255, 112, 0),   (255, 116, 0),   (255, 120, 0),   (255, 124, 0),
    (255, 128, 0),   (255, 132, 0),   (255, 136, 0),   (255, 140, 0),
    (255, 144, 0),   (255, 148, 0),   (255, 152, 0),   (255, 156, 0),
    (255, 160, 0),   (255, 164, 0),   (255, 168, 0),   (255, 172, 0),
    (255, 176, 0),   (255, 180, 0),   (255, 184, 0),   (255, 188, 0),
    (255, 192, 0),   (255, 196, 0),   (255, 200, 0),   (255, 204, 0),
    (255, 208, 0),   (255, 212, 0),   (255, 216, 0),   (255, 220, 0),
    (255, 224, 0),   (255, 228, 0),   (255, 232, 0),   (255, 236, 0),
    (255, 240, 0),   (255, 244, 0),   (255, 248, 0),   (255, 252, 0),
    (254, 255, 1),   (250, 255, 6),   (246, 255, 10),  (242, 255, 14),
    (238, 255, 18),  (234, 255, 22),  (230, 255, 26),  (226, 255, 30),
    (222, 255, 34),  (218, 255, 38),  (214, 255, 42),  (210, 255, 46),
    (206, 255, 50),  (202, 255, 54),  (198, 255, 58),  (194, 255, 62),
    (190, 255, 66),  (186, 255, 70),  (182, 255, 74),  (178, 255, 78),
    (174, 255, 82),  (170, 255, 86),  (166, 255, 90),  (162, 255, 94),
    (158, 255, 98),  (154, 255, 102), (150, 255, 106), (146, 255, 110),
    (142, 255, 114), (138, 255, 118), (134, 255, 122), (130, 255, 126),
    (126, 255, 130), (122, 255, 134), (118, 255, 138), (114, 255, 142),
    (110, 255, 146), (106, 255, 150), (102, 255, 154), (98, 255, 158),
    (94, 255, 162),  (90, 255, 166),  (86, 255, 170),  (82, 255, 174),
    (78, 255, 178),  (74, 255, 182),  (70, 255, 186),  (66, 255, 190),
    (62, 255, 194),  (58, 255, 198),  (54, 255, 202),  (50, 255, 206),
    (46, 255, 210),  (42, 255, 214),  (38, 255, 218),  (34, 255, 222),
    (30, 255, 226),  (26, 255, 230),  (22, 255, 234),  (18, 255, 238),
    (14, 255, 242),  (10, 255, 246),  (6, 255, 250),   (2, 255, 254),
    (0, 252, 255),   (0, 248, 255),   (0, 244, 255),   (0, 240, 255),
    (0, 236, 255),   (0, 232, 255),   (0, 228, 255),   (0, 224, 255),
    (0, 220, 255),   (0, 216, 255),   (0, 212, 255),   (0, 208, 255),
    (0, 204, 255),   (0, 200, 255),   (0, 196, 255),   (0, 192, 255),
    (0, 188, 255),   (0, 184, 255),   (0, 180, 255),   (0, 176, 255),
    (0, 172, 255),   (0, 168, 255),   (0, 164, 255),   (0, 160, 255),
    (0, 156, 255),   (0, 152, 255),   (0, 148, 255),   (0, 144, 255),
    (0, 140, 255),   (0, 136, 255),   (0, 132, 255),   (0, 128, 255),
    (0, 124, 255),   (0, 120, 255),   (0, 116, 255),   (0, 112, 255),
    (0, 108, 255),   (0, 104, 255),   (0, 100, 255),   (0, 96, 255),
    (0, 92, 255),    (0, 88, 255),    (0, 84, 255),    (0, 80, 255),
    (0, 76, 255),    (0, 72, 255),    (0, 68, 255),    (0, 64, 255),
    (0, 60, 255),    (0, 56, 255),    (0, 52, 255),    (0, 48, 255),
    (0, 44, 255),    (0, 40, 255),    (0, 36, 255),    (0, 32, 255),
    (0, 28, 255),    (0, 24, 255),    (0, 20, 255),    (0, 16, 255),
    (0, 12, 255),    (0, 8, 255),     (0, 4, 255),     (0, 0, 255),
    (0, 0, 252),     (0, 0, 248),     (0, 0, 244),     (0, 0, 240),
    (0, 0, 236),     (0, 0, 232),     (0, 0, 228),     (0, 0, 224),
    (0, 0, 220),     (0, 0, 216),     (0, 0, 212),     (0, 0, 208),
    (0, 0, 204),     (0, 0, 200),     (0, 0, 196),     (0, 0, 192),
    (0, 0, 188),     (0, 0, 184),     (0, 0, 180),     (0, 0, 176),
    (0, 0, 172),     (0, 0, 168),     (0, 0, 164),     (0, 0, 160),
    (0, 0, 156),     (0, 0, 152),     (0, 0, 148),     (0, 0, 144),
    (0, 0, 140),     (0, 0, 136),     (0, 0, 132),     (0, 0, 128)
]

def visualize_ply(file_path):
    # 读取PLY文件
    ply_data = PlyData.read(file_path)
    vertices = ply_data['vertex']
    
    # 提取坐标数据
    x = vertices['x']
    y = vertices['y']
    z = vertices['z']
    
    # 计算深度范围
    min_depth = np.min(z)
    max_depth = np.max(z)
    depth_range = max_depth - min_depth
    
    # 生成颜色映射
    colors = []
    for depth in z:
        # 将深度映射到颜色索引 (0-255)
        color_idx = int(((depth - min_depth) / depth_range) * (len(color_map)-1))
        colors.append(color_map[color_idx])
    
    # 转换为0-1范围的RGB值
    colors = np.array(colors) / 255.0

    # 创建3D可视化
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # 绘制点云
    ax.scatter(x, y, z, c=colors, s=1, marker='o')
    
    # 设置坐标轴标签
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    
    # 启用交互式旋转
    plt.show(block=True)  # 添加block=True参数保持窗口显示

# 在文件末尾添加以下新功能
def generate_cylinder(radius, fov_deg, max_range, points=5000,noise=5,coefficient=4):
    if radius <= 0 or fov_deg <= 0 or max_range <= 0:
        return None

    if fov_deg > 180:
        return None

    if radius >= max_range:
        return None

    if points <= 0:
        return None

    min_range = radius / math.sin(math.radians(fov_deg / 2))
    if min_range > max_range:
        return None
    
    max_distance = math.sqrt(max_range**2 - radius**2)
    min_distance = radius / math.tan(math.radians(fov_deg / 2))

    if max_distance <= min_distance or max_distance <= 0 or min_distance <= 0:
        return None

    a_rad = math.asin(radius / max_range)
    a_deg = math.degrees(a_rad)
    if a_deg > fov_deg / 2:
        return None

    # 生成圆柱面点云
    theta = np.random.uniform(0, 2*math.pi, points)

    u = np.random.rand(points)
    z = max_distance - (max_distance - min_distance) * np.power(u,1/coefficient)
    # z = np.random.uniform(min_distance, max_distance, points)

    # 转换为笛卡尔坐标(管壁表面)
    radius_noise = np.random.normal(0, noise, points)
    radius += radius_noise
    x = radius * np.cos(theta)
    y = radius * np.sin(theta)

    x_noise = np.random.normal(0, noise, points)
    y_noise = np.random.normal(0, noise, points) 

    x +=  x_noise
    y +=  y_noise

    return x, y, z

class CylinderGUI:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('圆柱体内部3D点云生成器')
        
        ttk.Label(self.root, text='管道半径:').grid(row=0, column=0)
        self.radius_entry = ttk.Entry(self.root)
        self.radius_entry.insert(0, '300')
        self.radius_entry.grid(row=0, column=1)

        # 更新输入控件
        ttk.Label(self.root, text='FOV角度:').grid(row=1, column=0)
        self.fov_entry = ttk.Entry(self.root)
        self.fov_entry.insert(0, '60')
        self.fov_entry.grid(row=1, column=1)
        
        ttk.Label(self.root, text='测量范围:').grid(row=2, column=0)
        self.range_entry = ttk.Entry(self.root)
        self.range_entry.insert(0, '2000')
        self.range_entry.grid(row=2, column=1)
        
        # 生成按钮
        self.generate_btn = ttk.Button(self.root, text='生成', command=self.generate)
        self.generate_btn.grid(row=3, columnspan=2)

    def generate(self):
        try:
            fov = float(self.fov_entry.get())
            max_range = float(self.range_entry.get())
            radius = float(self.radius_entry.get())

            # 调用新的生成函数
            li = generate_cylinder(
                radius=radius,
                fov_deg=fov,
                max_range=max_range
            )

            if li is None:
                messagebox.showwarning("警告", "未生成有效点云,请调整检测参数")
                return
            
            x, y, z = li

            # 创建PLY结构并保存(保留原有实现)
            vertices = np.zeros(len(x), dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')])
            vertices['x'], vertices['y'], vertices['z'] = x, y, z
            
            ply = PlyData([PlyElement.describe(vertices, 'vertex')], text=True)
            ply.write('tube.ply')
            
            # 调用可视化
            visualize_ply('tube.ply')
            
        except ValueError as e:
            print(f"参数错误: {e}")

if __name__ == "__main__":
    gui = CylinderGUI()
    gui.root.mainloop()

圆形3D点云检测及拟合算法

获取最密集部分点云

获取到的点云图 其在xy平面是一个圆 所以可以根据几何特性来进行拟合

由于生成的点云近处密集 远处较疏 所以先筛选出一段范围内密集的点

python 复制代码
# 读取PLY文件
    ply_data = PlyData.read(ply_file)
    vertices = ply_data['vertex']
    
    # 提取坐标数据
    x = vertices['x']
    y = vertices['y']
    z = vertices['z']

    # 计算深度范围
    min_depth = np.min(z)
    max_depth = np.max(z)
    depth_range = max_depth - min_depth

    # 筛选深度在指定范围内的点
    x_list = []
    y_list = []
    z_list = []
    
    # 动态计算bins数量(每个区间宽度为choose_depth)
    num_bins = int(np.ceil(depth_range / choose_depth))
    
    # 生成直方图
    hist, bin_edges = np.histogram(z, bins=num_bins, range=(min_depth, max_depth))
    max_bin_index = np.argmax(hist)
    z_min = bin_edges[max_bin_index]
    z_max = bin_edges[max_bin_index + 1]

    x_list = []
    y_list = []
    z_list = []

    # 筛选最密集区间内的点
    for i in range(len(z)):
        if z[i] >= z_min and z[i] <= z_max:
            x_list.append(x[i])
            y_list.append(y[i])
            z_list.append(z[i])

    # 在筛选点之后添加以下代码
    points = np.column_stack((x_list, y_list, z_list))

而后再进行圆形拟合算法

随机取三点

在这部分随机取三点 进行组合

这里的点不重复

python 复制代码
def detect_circle_radom_3_points(points,retry_num=3):
    circle_points=[]
    circle_r=[]

    for j in range(retry_num):
        # 打乱点集顺序
        shuffled_points = np.random.permutation(points)
        
        i = 0
        while i < len(shuffled_points) - 2:
            # 取连续三个不重复的点
            triplet = shuffled_points[i:i+3]
            
            # 计算圆心和半径
            center, radius = calculate_circle(*triplet)
            
            if center is not None and radius is not None:
                circle_points.append(center)

                circle_r.append(radius)
            
            # 跳过已处理的点
            i += 3

    return circle_points, circle_r

同时 通过retry_num循环进行随机重复取值

求三点共圆进行拟合

假定圆的平面在X Y平面上 第三个点求其两个点的投影所在共圆面上 即可得到多个圆

python 复制代码
def calculate_circle(p1, p2, p3):
    # 转换为二维坐标(假设在XY平面)
    p1_2d = p1[:2]
    p2_2d = p2[:2]
    p3_2d = p3[:2]
    
    # 检查共线性
    if np.linalg.matrix_rank([p2_2d - p1_2d, p3_2d - p1_2d]) < 2:
        return None, None
    
    # 构建2x2系数矩阵
    A = np.array([
        [2*(p2_2d[0] - p1_2d[0]), 2*(p2_2d[1] - p1_2d[1])],
        [2*(p3_2d[0] - p2_2d[0]), 2*(p3_2d[1] - p2_2d[1])]
    ])
    
    # 构建常数项
    b = np.array([
        p2_2d[0]**2 - p1_2d[0]**2 + p2_2d[1]**2 - p1_2d[1]**2,
        p3_2d[0]**2 - p2_2d[0]**2 + p3_2d[1]**2 - p2_2d[1]**2
    ])
    
    try:
        center_2d = np.linalg.solve(A, b)
        radius = np.linalg.norm(center_2d - p1_2d)
        # 恢复z坐标为三点平均值
        z_center = (p1[2] + p2[2] + p3[2]) / 3
        return np.array([center_2d[0], center_2d[1], z_center]), radius
    except np.linalg.LinAlgError:
        return None, None

每次结果都是随机的

最终圆的拟合

通过过滤掉异常半径值 并进行加权平均计算 得到最后的圆心和半径:

python 复制代码
def detect_circle(circle_points,circle_r):
    if len(circle_points) > 0:
        # 过滤异常半径(超过1.5倍IQR)
        radii = np.array(circle_r)
        q75, q25 = np.percentile(radii, [75, 25])
        iqr = q75 - q25
        mask = (radii > q25 - 1.5*iqr) & (radii < q75 + 1.5*iqr)
        
        # 计算加权平均值(根据半径密度加权)
        filtered_points = np.array(circle_points)[mask]
        filtered_radii = radii[mask]
        
        # 使用核密度估计找到最密集区域
        kde = gaussian_kde(filtered_radii)
        xvals = np.linspace(min(filtered_radii), max(filtered_radii), 100)
        kde_vals = kde(xvals)
        best_radius = xvals[np.argmax(kde_vals)]
        
        # 找到对应半径的所有圆心
        candidate_centers = filtered_points[np.abs(filtered_radii - best_radius) < iqr*0.5]
        
        # 取三维坐标中位数作为最终圆心
        final_center = np.median(candidate_centers, axis=0)
        final_radius = np.median(filtered_radii)
           
    return final_center,final_radius


如果加上刚刚的三次循环读取 则增加圆的数量:

最终结果和代码


当然 每次随机取值得到的圆也不一样 所以每次的结果都有略微偏差 可以对最后的结果进行滤波再整合

python 复制代码
from tube_generate import *
import numpy as np
import matplotlib.pyplot as plt
from plyfile import PlyData, PlyElement
import math
from scipy.stats import gaussian_kde

def calculate_circle(p1, p2, p3):
    # 转换为二维坐标(假设在XY平面)
    p1_2d = p1[:2]
    p2_2d = p2[:2]
    p3_2d = p3[:2]
    
    # 检查共线性
    if np.linalg.matrix_rank([p2_2d - p1_2d, p3_2d - p1_2d]) < 2:
        return None, None
    
    # 构建2x2系数矩阵
    A = np.array([
        [2*(p2_2d[0] - p1_2d[0]), 2*(p2_2d[1] - p1_2d[1])],
        [2*(p3_2d[0] - p2_2d[0]), 2*(p3_2d[1] - p2_2d[1])]
    ])
    
    # 构建常数项
    b = np.array([
        p2_2d[0]**2 - p1_2d[0]**2 + p2_2d[1]**2 - p1_2d[1]**2,
        p3_2d[0]**2 - p2_2d[0]**2 + p3_2d[1]**2 - p2_2d[1]**2
    ])
    
    try:
        center_2d = np.linalg.solve(A, b)
        radius = np.linalg.norm(center_2d - p1_2d)
        # 恢复z坐标为三点平均值
        z_center = (p1[2] + p2[2] + p3[2]) / 3
        return np.array([center_2d[0], center_2d[1], z_center]), radius
    except np.linalg.LinAlgError:
        return None, None

def detect_circle_radom_3_points(points,retry_num=3):
    circle_points=[]
    circle_r=[]

    for j in range(retry_num):
        # 打乱点集顺序
        shuffled_points = np.random.permutation(points)
        
        i = 0
        while i < len(shuffled_points) - 2:
            # 取连续三个不重复的点
            triplet = shuffled_points[i:i+3]
            
            # 计算圆心和半径
            center, radius = calculate_circle(*triplet)
            
            if center is not None and radius is not None:
                circle_points.append(center)

                circle_r.append(radius)
            
            # 跳过已处理的点
            i += 3

    return circle_points, circle_r

def detect_circle(circle_points,circle_r):
    if len(circle_points) > 0:
        # 过滤异常半径(超过1.5倍IQR)
        radii = np.array(circle_r)
        q75, q25 = np.percentile(radii, [75, 25])
        iqr = q75 - q25
        mask = (radii > q25 - 1.5*iqr) & (radii < q75 + 1.5*iqr)
        
        # 计算加权平均值(根据半径密度加权)
        filtered_points = np.array(circle_points)[mask]
        filtered_radii = radii[mask]
        
        # 使用核密度估计找到最密集区域
        kde = gaussian_kde(filtered_radii)
        xvals = np.linspace(min(filtered_radii), max(filtered_radii), 100)
        kde_vals = kde(xvals)
        best_radius = xvals[np.argmax(kde_vals)]
        
        # 找到对应半径的所有圆心
        candidate_centers = filtered_points[np.abs(filtered_radii - best_radius) < iqr*0.5]
        
        # 取三维坐标中位数作为最终圆心
        final_center = np.median(candidate_centers, axis=0)
        final_radius = np.median(filtered_radii)
           
    return final_center,final_radius

def detect_tube(ply_file,choose_depth=10):
    # 读取PLY文件
    ply_data = PlyData.read(ply_file)
    vertices = ply_data['vertex']
    
    # 提取坐标数据
    x = vertices['x']
    y = vertices['y']
    z = vertices['z']

    # 计算深度范围
    min_depth = np.min(z)
    max_depth = np.max(z)
    depth_range = max_depth - min_depth

    # 筛选深度在指定范围内的点
    x_list = []
    y_list = []
    z_list = []
    
    # 动态计算bins数量(每个区间宽度为choose_depth)
    num_bins = int(np.ceil(depth_range / choose_depth))
    
    # 生成直方图
    hist, bin_edges = np.histogram(z, bins=num_bins, range=(min_depth, max_depth))
    max_bin_index = np.argmax(hist)
    z_min = bin_edges[max_bin_index]
    z_max = bin_edges[max_bin_index + 1]

    x_list = []
    y_list = []
    z_list = []

    # 筛选最密集区间内的点
    for i in range(len(z)):
        if z[i] >= z_min and z[i] <= z_max:
            x_list.append(x[i])
            y_list.append(y[i])
            z_list.append(z[i])

    # 在筛选点之后添加以下代码
    points = np.column_stack((x_list, y_list, z_list))
    circle_points = []
    circle_r = []

    circle_points,circle_r = detect_circle_radom_3_points(points)

    circle = detect_circle(circle_points,circle_r)  

    colors = []
    for depth in z:
        # 将深度映射到颜色索引 (0-255)
        color_idx = int(((depth - min_depth) / depth_range) * (len(color_map)-1))
        colors.append(color_map[color_idx])
    
    # 转换为0-1范围的RGB值
    colors = np.array(colors) / 255.0

    # 创建3D可视化
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # 绘制原始点云
    ax.scatter(x, y, z, c=colors, s=1, marker='o', label='原始点云')
    
    # 绘制筛选后的点云(红色突出显示)
    # ax.scatter(x_list, y_list, z_list, c="black", s=1, marker='o', label='筛选点')

    # 绘制筛选后的圆心和半径
    # if len(circle_points) > 0:
    #     for center, radius in zip(circle_points, circle_r):
    #         # 在XY平面生成圆环
    #         theta = np.linspace(0, 2*np.pi, 100)
    #         x_circle = center[0] + radius * np.cos(theta)
    #         y_circle = center[1] + radius * np.sin(theta)
            
    #         # 使用圆心Z坐标的平均值作为高度
    #         z_circle = np.full_like(x_circle, center[2])
            
    #         ax.plot(x_circle, y_circle, z_circle, color='red', linewidth=2, 
    #                 label='检测圆环' if center is circle_points[0] else "")
    
    print(circle)
    if len(circle[0]) and circle[1]:
        # 生成更密集的点用于最终圆环
        theta_final = np.linspace(0, 2*np.pi, 500)
        x_final = circle[0][0] + circle[1] * np.cos(theta_final)
        y_final = circle[0][1] + circle[1] * np.sin(theta_final)
        z_final = np.full_like(theta_final, circle[0][2])
        
        # 使用蓝色粗线突出显示
        ax.plot(x_final, y_final, z_final, color='blue', linewidth=4, 
               linestyle='--', label='最终检测圆环')
        
        # 添加半径标注
        ax.text(circle[0][0], circle[0][1], circle[0][2], 
               f'R={circle[1]:.2f}mm', color='blue', fontsize=12)
        
    # 设置坐标轴标签
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    
    plt.show(block=True)  # 添加block=True参数保持窗口显示

if __name__ == "__main__":
    detect_tube('tube.ply')

优化方案

对于算法优化 可以从1/2、1/4个点中来进行取值和拟合 以减小算法开销

另外 也可以用几何规律方法、最小二乘法来进行拟合

标准差计算更新

增加了最终结果的标准差计算,使其与原始值相比较

设定为大于50则不认定拟合成功 并画出黑色的圆

python 复制代码
radius_std = np.std(filtered_radii - final_radius)

如果拟合成功 则还是蓝色的圆:

附录:列表的赋值类型和py打包

列表赋值

BUG复现

闲来无事写了个小程序 代码如下:

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 19 19:47:01 2021

@author: 16016
"""

a_list = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15']
#print(len(a_list))
#b_list = ['','','','','','','','','','','','','','','','']
c_list = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]]
#for i in range(16):
if len(a_list):
    for j in range(16):
        a_list[j]=str(a_list[j])+'_'+str(j)
        print("序号:",j)
        print('a_list:\n',a_list)
        
        
        c_list[j]=a_list
        print('c_list[0]:\n',c_list[0])
        print('\n')
#        b_list[j]=a_list[7],a_list[8]
#        print(b_list[j])
        # 写入到Excel:
#print(c_list,'\n')    

我在程序中 做了一个16次的for循环 把列表a的每个值后面依次加上"_"和循环序号

比如循环第x次 就是把第x位加上_x 这一位变成x_x 我在输出测试中 列表a的每一次输出也是对的

循环16次后列表a应该变成['0_0', '1_1', '2_2', '3_3', '4_4', '5_5', '6_6', '7_7', '8_8', '9_9', '10_10', '11_11', '12_12', '13_13', '14_14', '15_15'] 这也是对的

同时 我将每一次循环时列表a的值 写入到空列表c中 比如第x次循环 就是把更改以后的列表a的值 写入到列表c的第x位

第0次循环后 c[0]的值应该是['0_0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'] 这也是对的

但是在第1次循环以后 c[0]的值就一直在变 变成了c[x]的值

相当于把c_list[0]变成了c_list[1]...以此类推 最后得出的列表c的值也是每一项完全一样

我不明白这是怎么回事

我的c[0]只在第0次循环时被赋值了 但是后面它的值跟着在改变

如图:

第一次老出bug 赋值以后 每次循环都改变c[0]的值 搞了半天都没搞出来

无论是用appen函数添加 还是用二维数组定义 或者增加第三个空数组来过渡 都无法解决

代码改进

后来在我华科同学的指导下 突然想到赋值可以赋的是个地址 地址里面的值一直变化 导致赋值也一直变化 于是用第二张图的循环套循环深度复制实现了

代码如下:

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 19 19:47:01 2021

@author: 16016
"""

a_list = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15']
#print(len(a_list))
#b_list = ['','','','','','','','','','','','','','','','']
c_list = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]]
#for i in range(16):
if len(a_list):
    for j in range(16):
        a_list[j]=str(a_list[j])+'_'+str(j)
        print("序号:",j)
        print('a_list:\n',a_list)
        
        
        for i in range(16):
            c_list[j].append(a_list[i])
        print('c_list[0]:\n',c_list[0])
        print('\n')
#        b_list[j]=a_list[7],a_list[8]
#        print(b_list[j])
        # 写入到Excel:
print(c_list,'\n')    

解决了问题

优化

第三次是请教了老师 用copy函数来赋真值

代码如下:

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Fri Nov 19 19:47:01 2021

@author: 16016
"""

a_list = ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15']
#print(len(a_list))
#b_list = ['','','','','','','','','','','','','','','','']
c_list = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]]
#for i in range(16):
if len(a_list):
    for j in range(16):
        a_list[j]=str(a_list[j])+'_'+str(j)
        print("序号:",j)
        print('a_list:\n',a_list)
        
        
        c_list[j]=a_list.copy()
        print('c_list[0]:\n',c_list[0])
        print('\n')
#        b_list[j]=a_list[7],a_list[8]
#        print(b_list[j])
        # 写入到Excel:
#print(c_list,'\n')    

同样能解决问题

最后得出问题 就是指针惹的祸!

a_list指向的是个地址 而不是值 a_list[i]指向的才是单个的值 copy()函数也是复制值而不是地址

如果这个用C语言来写 就直观一些了 难怪C语言是基础 光学Python不学C 遇到这样的问题就解决不了

C语言yyds Python是什么垃圾弱智语言

总结

由于Python无法单独定义一个值为指针或者独立的值 所以只能用列表来传送

只要赋值是指向一个列表整体的 那么就是指向的一个指针内存地址 解决方法只有一个 那就是将每个值深度复制赋值(子列表内的元素提取出来重新依次连接) 或者用copy函数单独赋值

如图测试:




部分代码:

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Sat Nov 20 16:45:48 2021

@author: 16016
"""

def text1():
    A=[1,2,3]
    B=[[],[],[]]
    for i in range(len(A)):
        A[i]=A[i]+i
        B[i]=A
        print(B)

def text2():
    A=[1,2,3]
    B=[[],[],[]]
    
    A[0]=A[0]+0
    B[0]=A
    print(B)
    A[1]=A[1]+1
    B[1]=A
    print(B)
    A[2]=A[2]+2
    B[2]=A
    print(B)
    
if __name__ == '__main__':
    text1()
    print('\n')
    text2()

py打包

Pyinstaller打包exe(包括打包资源文件 绝不出错版)

依赖包及其对应的版本号

PyQt5 5.10.1

PyQt5-Qt5 5.15.2

PyQt5-sip 12.9.0

pyinstaller 4.5.1

pyinstaller-hooks-contrib 2021.3

Pyinstaller -F setup.py 打包exe

Pyinstaller -F -w setup.py 不带控制台的打包

Pyinstaller -F -i xx.ico setup.py 打包指定exe图标打包

打包exe参数说明:

-F:打包后只生成单个exe格式文件;

-D:默认选项,创建一个目录,包含exe文件以及大量依赖文件;

-c:默认选项,使用控制台(就是类似cmd的黑框);

-w:不使用控制台;

-p:添加搜索路径,让其找到对应的库;

-i:改变生成程序的icon图标。

如果要打包资源文件

则需要对代码中的路径进行转换处理

另外要注意的是 如果要打包资源文件 则py程序里面的路径要从./xxx/yy换成xxx/yy 并且进行路径转换

但如果不打包资源文件的话 最好路径还是用作./xxx/yy 并且不进行路径转换

python 复制代码
def get_resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

而后再spec文件中的datas部分加入目录

如:

python 复制代码
a = Analysis(['cxk.py'],
             pathex=['D:\\Python Test\\cxk'],
             binaries=[],
             datas=[('root','root')],
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

而后直接Pyinstaller -F setup.spec即可

如果打包的文件过大则更改spec文件中的excludes 把不需要的库写进去(但是已经在环境中安装了的)就行

这些不要了的库在上一次编译时的shell里面输出

比如:

然后用pyinstaller --clean -F 某某.spec

相关推荐
艾莉丝努力练剑2 分钟前
【C语言16天强化训练】从基础入门到进阶:Day 6
c语言·数据结构·学习·算法
淘晶驰AK36 分钟前
51单片机与stm32单片机,先学习哪一个?
stm32·单片机·51单片机
快去睡觉~42 分钟前
力扣1005:k次取反后最大化的数组和
数据结构·算法·leetcode
smilejingwei1 小时前
数据分析编程第二步: 最简单的数据分析尝试
数据库·算法·数据分析·esprocspl
Moonnnn.2 小时前
【51单片机学习】AT24C02(I2C)、DS18B20(单总线)、LCD1602(液晶显示屏)
笔记·单片机·学习·51单片机
草莓熊Lotso2 小时前
【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day10
c语言·开发语言·经验分享·算法·强化
张同学的IT技术日记2 小时前
详细实例说明+典型案例实现 对迭代法进行全面分析 | C++
算法
Coovally AI模型快速验证2 小时前
全景式综述|多模态目标跟踪全面解析:方法、数据、挑战与未来
人工智能·深度学习·算法·机器学习·计算机视觉·目标跟踪·无人机
TDengine (老段)3 小时前
TDengine IDMP 应用场景:微电网监控
大数据·数据库·物联网·ai·时序数据库·tdengine·涛思数据
AAA修煤气灶刘哥3 小时前
物联网-智能设备数据交互核心技术解析
物联网·华为