无人机表演点云路径规划(Python版)

摘要:本文介绍了一个无人机表演路径规划系统,支持加载两个TXT格式的点云数据文件进行路径规划。系统采用PyQt5框架开发3D可视化界面,包含点云加载、路径优化、动画控制等功能模块。核心算法通过KD树实现点云匹配和路径优化,支持多种插值方式生成平滑动画。系统可调整无人机数量、大小、颜色等参数,提供3D可视化视图和轨迹显示功能,适用于大型无人机编队表演的路径规划需求。

python 复制代码
"""
无人机表演路径规划系统
支持加载两个TXT点云数据文件进行路径规划
"""

import sys
import numpy as np
from scipy.spatial import KDTree
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                             QHBoxLayout, QPushButton, QLabel, QSlider, QGroupBox,
                             QSpinBox, QDoubleSpinBox, QGridLayout, QProgressBar,
                             QComboBox, QCheckBox, QFileDialog, QMessageBox, QLineEdit)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread
from PyQt5.QtGui import QFont, QColor, QPalette
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from pyqtgraph.opengl import GLViewWidget, GLScatterPlotItem, GLLinePlotItem, GLGridItem, GLAxisItem


class TXTPointCloudLoader:
    """TXT点云数据加载器"""
    
    @staticmethod
    def load_txt(file_path):
        """从TXT文件加载点云数据"""
        points = []
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith('#'):
                        continue
                    parts = line.split()
                    if len(parts) >= 3:
                        try:
                            x, y, z = float(parts[0]), float(parts[1]), float(parts[2])
                            points.append([x, y, z])
                        except ValueError:
                            continue
            
            if len(points) == 0:
                print(f"警告: 文件 {file_path} 中没有找到有效的点数据")
                return None
                
            points = np.array(points)
            print(f"成功加载 {len(points)} 个点")
            
            # 数据清洗:移除异常值
            mean = np.mean(points, axis=0)
            std = np.std(points, axis=0)
            points = points[np.all(np.abs(points - mean) < 3 * std, axis=1)]
            print(f"清洗后剩余 {len(points)} 个点")
            
            return points
        except Exception as e:
            print(f"加载点云失败: {e}")
            return None
    
    @staticmethod
    def preprocess_pointcloud(points, target_count=None):
        """预处理点云:下采样/上采样到目标数量"""
        if target_count is None:
            return points
        
        current_count = len(points)
        if current_count == target_count:
            return points
        
        if current_count > target_count:
            # 下采样
            indices = np.random.choice(current_count, target_count, replace=False)
            return points[indices]
        else:
            # 上采样:随机复制并添加噪声
            indices = np.random.choice(current_count, target_count - current_count, replace=True)
            new_points = points[indices].copy()
            # 添加微小噪声
            noise = np.random.normal(0, 0.005, new_points.shape)
            new_points += noise
            return np.vstack([points, new_points])
    
    @staticmethod
    def normalize_pointcloud(points):
        """归一化点云到标准范围"""
        # 计算中心
        center = np.mean(points, axis=0)
        # 移动到原点
        centered = points - center
        # 缩放以适应[-2, 2]范围
        max_dist = np.max(np.abs(centered))
        if max_dist == 0:
            max_dist = 1
        scale = 2.0 / max_dist
        return centered * scale, center, scale


class DronePathPlanner:
    """无人机路径规划器"""
    
    def __init__(self):
        self.source_positions = None
        self.target_positions = None
        self.num_drones = 800
        
    def load_pointcloud_from_txt(self, file_path, target_count=None):
        """从TXT文件加载点云"""
        points = TXTPointCloudLoader.load_txt(file_path)
        if points is None:
            return None
        
        if target_count is not None:
            points = TXTPointCloudLoader.preprocess_pointcloud(points, target_count)
        
        # 归一化
        points, center, scale = TXTPointCloudLoader.normalize_pointcloud(points)
        
        print(f"点云加载完成: {len(points)} 点")
        return points
    
    def optimize_paths(self, start_points, end_points):
        """优化路径规划,避免交叉碰撞"""
        n_points = len(start_points)
        
        # 使用KD树找到最近点对应
        kdtree_start = KDTree(start_points)
        kdtree_end = KDTree(end_points)
        
        # 双向匹配算法
        _, indices_start = kdtree_start.query(end_points)
        _, indices_end = kdtree_end.query(start_points)
        
        # 创建优化映射
        mapping = np.zeros(n_points, dtype=int)
        used_end = set()
        used_start = set()
        
        # 第一阶段:匹配双向最优的点对
        for i in range(n_points):
            if indices_end[indices_start[i]] == i and i not in used_end and indices_start[i] not in used_start:
                mapping[indices_start[i]] = i
                used_end.add(i)
                used_start.add(indices_start[i])
        
        # 第二阶段:处理剩余点
        remaining_start = [i for i in range(n_points) if i not in used_start]
        remaining_end = [i for i in range(n_points) if i not in used_end]
        
        if remaining_start and remaining_end:
            remaining_start_arr = np.array(remaining_start)
            remaining_end_arr = np.array(remaining_end)
            
            # 计算剩余点的距离矩阵
            dist_matrix = np.linalg.norm(
                start_points[remaining_start_arr][:, np.newaxis] - end_points[remaining_end_arr][np.newaxis, :],
                axis=2
            )
            
            # 贪心匹配
            for _ in range(min(len(remaining_start), len(remaining_end))):
                min_idx = np.unravel_index(np.argmin(dist_matrix), dist_matrix.shape)
                start_idx = remaining_start[min_idx[0]]
                end_idx = remaining_end[min_idx[1]]
                
                mapping[start_idx] = end_idx
                dist_matrix[min_idx[0], :] = np.inf
                dist_matrix[:, min_idx[1]] = np.inf
        
        # 重新排列终点
        return end_points[mapping]
    
    def interpolate_positions(self, start_pos, end_pos, progress, method='smooth'):
        """插值计算中间位置"""
        if method == 'linear':
            return start_pos + (end_pos - start_pos) * progress
        elif method == 'smooth':
            # 五次缓动函数
            t = progress
            smooth_t = t * t * t * (t * (6 * t - 15) + 10)
            return start_pos + (end_pos - start_pos) * smooth_t
        elif method == 'elastic':
            t = progress
            if t == 0 or t == 1:
                return start_pos + (end_pos - start_pos) * t
            c4 = (2 * np.pi) / 3
            elastic_t = -np.power(2, 10 * t - 10) * np.sin((t * 10 - 10.75) * c4)
            return start_pos + (end_pos - start_pos) * elastic_t
        elif method == 'bounce':
            t = progress
            n1 = 7.5625
            d1 = 2.75
            if t < 1 / d1:
                bounce_t = n1 * t * t
            elif t < 2 / d1:
                t = t - 1.5 / d1
                bounce_t = n1 * t * t + 0.75
            elif t < 2.5 / d1:
                t = t - 2.25 / d1
                bounce_t = n1 * t * t + 0.9375
            else:
                t = t - 2.625 / d1
                bounce_t = n1 * t * t + 0.984375
            return start_pos + (end_pos - start_pos) * bounce_t
        else:
            return start_pos + (end_pos - start_pos) * progress


class AnimationThread(QThread):
    """动画线程"""
    frame_ready = pyqtSignal(np.ndarray, float)
    finished_signal = pyqtSignal()
    
    def __init__(self, planner, start_pos, end_pos, duration=5.0, fps=30, method='smooth'):
        super().__init__()
        self.planner = planner
        self.start_pos = start_pos
        self.end_pos = end_pos
        self.duration = duration
        self.fps = fps
        self.method = method
        self.running = True
        self.paused = False
        
    def run(self):
        total_frames = int(self.duration * self.fps)
        for frame in range(total_frames):
            if not self.running:
                break
            while self.paused:
                if not self.running:
                    break
                self.msleep(100)
            if not self.running:
                break
            progress = frame / total_frames
            current_pos = self.planner.interpolate_positions(
                self.start_pos, self.end_pos, progress, self.method
            )
            self.frame_ready.emit(current_pos, progress)
            self.msleep(int(1000 / self.fps))
        self.finished_signal.emit()
    
    def stop(self):
        self.running = False
        self.paused = False
    
    def pause(self):
        self.paused = True
    
    def resume(self):
        self.paused = False


class DroneShowMainWindow(QMainWindow):
    """主窗口"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("🚁 无人机表演路径规划系统")
        self.setGeometry(100, 100, 1600, 1000)
        
        self.planner = DronePathPlanner()
        self.source_positions = None
        self.target_positions = None
        self.current_positions = None
        self.animation_thread = None
        self.trail_items = []
        
        self.source_file_path = ""
        self.target_file_path = ""
        
        self.init_ui()
        
    def init_ui(self):
        """初始化UI"""
        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QHBoxLayout(central)
        main_layout.setSpacing(10)
        
        # 左侧控制面板
        control_panel = self._create_control_panel()
        main_layout.addWidget(control_panel, 1)
        
        # 右侧3D视图
        right_panel = self._create_right_panel()
        main_layout.addWidget(right_panel, 3)
        
        self._apply_styles()
        
    def _create_control_panel(self):
        """创建控制面板"""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setSpacing(10)
        
        # 标题
        title = QLabel("🚁 无人机路径规划")
        title.setFont(QFont("Microsoft YaHei", 16, QFont.Bold))
        title.setStyleSheet("color: #ff6b6b;")
        title.setAlignment(Qt.AlignCenter)
        layout.addWidget(title)
        
        # 文件加载区域
        file_group = QGroupBox("📁 点云数据加载")
        file_layout = QGridLayout()
        
        # 源点云文件
        file_layout.addWidget(QLabel("源点云 (起点):"), 0, 0)
        self.source_path_edit = QLineEdit()
        self.source_path_edit.setPlaceholderText("选择源点云TXT文件...")
        file_layout.addWidget(self.source_path_edit, 0, 1)
        source_btn = QPushButton("浏览...")
        source_btn.clicked.connect(lambda: self.load_file('source'))
        file_layout.addWidget(source_btn, 0, 2)
        
        # 目标点云文件
        file_layout.addWidget(QLabel("目标点云 (终点):"), 1, 0)
        self.target_path_edit = QLineEdit()
        self.target_path_edit.setPlaceholderText("选择目标点云TXT文件...")
        file_layout.addWidget(self.target_path_edit, 1, 1)
        target_btn = QPushButton("浏览...")
        target_btn.clicked.connect(lambda: self.load_file('target'))
        file_layout.addWidget(target_btn, 1, 2)
        
        file_group.setLayout(file_layout)
        layout.addWidget(file_group)
        
        # 无人机设置
        drone_group = QGroupBox("⚙️ 无人机设置")
        drone_layout = QGridLayout()
        
        drone_layout.addWidget(QLabel("无人机数量:"), 0, 0)
        self.drone_count_spin = QSpinBox()
        self.drone_count_spin.setRange(100, 3000)
        self.drone_count_spin.setValue(800)
        self.drone_count_spin.setSingleStep(100)
        drone_layout.addWidget(self.drone_count_spin, 0, 1)
        
        drone_layout.addWidget(QLabel("无人机大小:"), 1, 0)
        self.drone_size_spin = QDoubleSpinBox()
        self.drone_size_spin.setRange(0.01, 0.3)
        self.drone_size_spin.setValue(0.05)
        self.drone_size_spin.setSingleStep(0.01)
        drone_layout.addWidget(self.drone_size_spin, 1, 1)
        
        drone_layout.addWidget(QLabel("颜色模式:"), 2, 0)
        self.color_combo = QComboBox()
        self.color_combo.addItems(["高度渐变", "彩虹色", "单色"])
        drone_layout.addWidget(self.color_combo, 2, 1)
        
        drone_group.setLayout(drone_layout)
        layout.addWidget(drone_group)
        
        # 动画设置
        anim_group = QGroupBox("🎬 动画设置")
        anim_layout = QGridLayout()
        
        anim_layout.addWidget(QLabel("动画时长(秒):"), 0, 0)
        self.duration_spin = QDoubleSpinBox()
        self.duration_spin.setRange(1, 30)
        self.duration_spin.setValue(5)
        self.duration_spin.setSingleStep(0.5)
        drone_layout.addWidget(self.duration_spin, 0, 1)
        
        anim_layout.addWidget(QLabel("帧率(FPS):"), 1, 0)
        self.fps_spin = QSpinBox()
        self.fps_spin.setRange(10, 120)
        self.fps_spin.setValue(30)
        anim_layout.addWidget(self.fps_spin, 1, 1)
        
        anim_layout.addWidget(QLabel("插值方式:"), 2, 0)
        self.interp_combo = QComboBox()
        self.interp_combo.addItems(["平滑过渡", "弹性效果", "弹跳效果", "线性过渡"])
        anim_layout.addWidget(self.interp_combo, 2, 1)
        
        anim_group.setLayout(anim_layout)
        layout.addWidget(anim_group)
        
        # 视图控制
        view_group = QGroupBox("👁️ 视图控制")
        view_layout = QVBoxLayout()
        
        self.show_source_btn = QPushButton("📍 显示源点云")
        self.show_source_btn.clicked.connect(self.show_source)
        self.show_source_btn.setEnabled(False)
        view_layout.addWidget(self.show_source_btn)
        
        self.show_target_btn = QPushButton("🎯 显示目标点云")
        self.show_target_btn.clicked.connect(self.show_target)
        self.show_target_btn.setEnabled(False)
        view_layout.addWidget(self.show_target_btn)
        
        self.toggle_rotation_btn = QPushButton("🔄 自动旋转")
        self.toggle_rotation_btn.setCheckable(True)
        self.toggle_rotation_btn.clicked.connect(self.toggle_rotation)
        view_layout.addWidget(self.toggle_rotation_btn)
        
        self.reset_view_btn = QPushButton("🔃 重置视图")
        self.reset_view_btn.clicked.connect(self.reset_view)
        view_layout.addWidget(self.reset_view_btn)
        
        view_group.setLayout(view_layout)
        layout.addWidget(view_group)
        
        # 动画控制
        play_group = QGroupBox("▶️ 动画控制")
        play_layout = QVBoxLayout()
        
        self.play_btn = QPushButton("▶️ 开始变换")
        self.play_btn.clicked.connect(self.start_animation)
        self.play_btn.setStyleSheet("background-color: #ff6b6b;")
        self.play_btn.setEnabled(False)
        play_layout.addWidget(self.play_btn)
        
        btn_layout = QHBoxLayout()
        self.pause_btn = QPushButton("⏸️ 暂停")
        self.pause_btn.clicked.connect(self.pause_animation)
        self.pause_btn.setEnabled(False)
        btn_layout.addWidget(self.pause_btn)
        
        self.stop_btn = QPushButton("⏹️ 停止")
        self.stop_btn.clicked.connect(self.stop_animation)
        self.stop_btn.setEnabled(False)
        btn_layout.addWidget(self.stop_btn)
        play_layout.addLayout(btn_layout)
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        play_layout.addWidget(self.progress_bar)
        
        # 进度滑块
        self.progress_slider = QSlider(Qt.Horizontal)
        self.progress_slider.setRange(0, 100)
        self.progress_slider.setValue(0)
        self.progress_slider.valueChanged.connect(self.on_slider_changed)
        play_layout.addWidget(self.progress_slider)
        
        play_group.setLayout(play_layout)
        layout.addWidget(play_group)
        
        # 显示选项
        display_group = QGroupBox("📊 显示选项")
        display_layout = QVBoxLayout()
        
        self.show_grid_check = QCheckBox("显示网格")
        self.show_grid_check.setChecked(True)
        self.show_grid_check.stateChanged.connect(self.toggle_grid)
        display_layout.addWidget(self.show_grid_check)
        
        self.show_axes_check = QCheckBox("显示坐标轴")
        self.show_axes_check.setChecked(True)
        self.show_axes_check.stateChanged.connect(self.toggle_axes)
        display_layout.addWidget(self.show_axes_check)
        
        self.show_trails_check = QCheckBox("显示轨迹")
        self.show_trails_check.setChecked(False)
        display_layout.addWidget(self.show_trails_check)
        
        display_group.setLayout(display_layout)
        layout.addWidget(display_group)
        
        # 状态信息
        status_group = QGroupBox("📈 状态信息")
        status_layout = QVBoxLayout()
        
        self.status_label = QLabel("请加载两个点云文件")
        self.status_label.setAlignment(Qt.AlignCenter)
        status_layout.addWidget(self.status_label)
        
        self.source_info_label = QLabel("源点云: 未加载")
        status_layout.addWidget(self.source_info_label)
        
        self.target_info_label = QLabel("目标点云: 未加载")
        status_layout.addWidget(self.target_info_label)
        
        self.drone_info_label = QLabel("无人机数量: 0")
        status_layout.addWidget(self.drone_info_label)
        
        status_group.setLayout(status_layout)
        layout.addWidget(status_group)
        
        layout.addStretch()
        return panel
    
    def _create_right_panel(self):
        """创建右侧3D视图面板"""
        panel = QWidget()
        layout = QVBoxLayout(panel)
        layout.setContentsMargins(0, 0, 0, 0)
        
        # 3D视图
        self.view = GLViewWidget()
        self.view.setCameraPosition(distance=12, elevation=30, azimuth=45)
        self.view.setBackgroundColor('#0a0a1a')
        
        # 添加网格
        self.grid = GLGridItem()
        self.grid.setSize(20, 20)
        self.grid.setSpacing(1, 1)
        self.grid.setColor((0.2, 0.2, 0.3, 0.5))
        self.view.addItem(self.grid)
        
        # 添加坐标轴
        self.axis = GLAxisItem()
        self.axis.setSize(4, 4, 4)
        self.view.addItem(self.axis)
        
        # 散点图
        self.scatter = GLScatterPlotItem()
        self.view.addItem(self.scatter)
        
        layout.addWidget(self.view)
        
        # 创建自动旋转定时器
        self.rotation_timer = QTimer()
        self.rotation_timer.timeout.connect(self.auto_rotate)
        self.rotation_angle = 45
        
        return panel
    
    def _apply_styles(self):
        """应用样式"""
        self.setStyleSheet("""
            QMainWindow {
                background-color: #1a1a2e;
            }
            QGroupBox {
                background-color: #16213e;
                color: #e6e6e6;
                border: 2px solid #0f3460;
                border-radius: 8px;
                margin-top: 10px;
                padding-top: 15px;
                font-size: 12px;
                font-weight: bold;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 15px;
                padding: 0 8px;
                color: #ff6b6b;
            }
            QPushButton {
                background-color: #0f3460;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 6px;
                font-weight: bold;
                font-size: 12px;
            }
            QPushButton:hover {
                background-color: #e94560;
            }
            QPushButton:pressed {
                background-color: #c13651;
            }
            QPushButton:disabled {
                background-color: #444;
                color: #888;
            }
            QLabel {
                color: #e6e6e6;
                font-size: 12px;
            }
            QLineEdit {
                background-color: #0f3460;
                color: white;
                border: 1px solid #1a4a7a;
                padding: 5px;
                border-radius: 4px;
            }
            QSlider::groove:horizontal {
                height: 8px;
                background: #0f3460;
                border-radius: 4px;
            }
            QSlider::handle:horizontal {
                background: #ff6b6b;
                width: 18px;
                margin: -5px 0;
                border-radius: 9px;
            }
            QProgressBar {
                border: 2px solid #0f3460;
                border-radius: 5px;
                text-align: center;
                color: white;
            }
            QProgressBar::chunk {
                background-color: #ff6b6b;
                border-radius: 3px;
            }
            QSpinBox, QDoubleSpinBox, QComboBox {
                background-color: #0f3460;
                color: white;
                border: none;
                padding: 5px;
                border-radius: 4px;
            }
            QCheckBox {
                color: #e6e6e6;
            }
            QCheckBox::indicator {
                width: 18px;
                height: 18px;
            }
        """)
    
    def load_file(self, file_type):
        """加载点云文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, 
            f"选择{'源' if file_type == 'source' else '目标'}点云文件",
            "",
            "TXT Files (*.txt);;All Files (*)"
        )
        
        if not file_path:
            return
        
        target_count = self.drone_count_spin.value()
        points = self.planner.load_pointcloud_from_txt(file_path, target_count)
        
        if points is None:
            QMessageBox.warning(self, "加载失败", f"无法加载文件:\n{file_path}")
            return
        
        if file_type == 'source':
            self.source_positions = points
            self.source_file_path = file_path
            self.source_path_edit.setText(file_path)
            self.source_info_label.setText(f"源点云: {len(points)} 点")
            self.show_source_btn.setEnabled(True)
        else:
            self.target_positions = points
            self.target_file_path = file_path
            self.target_path_edit.setText(file_path)
            self.target_info_label.setText(f"目标点云: {len(points)} 点")
            self.show_target_btn.setEnabled(True)
        
        # 更新无人机数量
        self.drone_count_spin.setValue(len(points))
        self.drone_info_label.setText(f"无人机数量: {len(points)}")
        
        # 检查是否可以开始动画
        if self.source_positions is not None and self.target_positions is not None:
            # 确保两个点云数量一致
            if len(self.source_positions) != len(self.target_positions):
                # 重新采样到相同数量
                count = max(len(self.source_positions), len(self.target_positions))
                self.source_positions = TXTPointCloudLoader.preprocess_pointcloud(
                    self.source_positions, count
                )
                self.target_positions = TXTPointCloudLoader.preprocess_pointcloud(
                    self.target_positions, count
                )
                self.drone_count_spin.setValue(count)
                self.drone_info_label.setText(f"无人机数量: {count}")
            
            # 优化路径
            self.target_positions = self.planner.optimize_paths(
                self.source_positions, self.target_positions
            )
            
            self.play_btn.setEnabled(True)
            self.status_label.setText("✅ 准备就绪,可以开始动画")
            self.show_source()
        else:
            self.status_label.setText(f"✅ {'源' if file_type == 'source' else '目标'}点云已加载,请加载另一个")
    
    def show_source(self):
        """显示源点云"""
        if self.source_positions is not None:
            self.current_positions = self.source_positions.copy()
            self.update_scatter()
            self.clear_trails()
            self.progress_slider.setValue(0)
            self.progress_bar.setValue(0)
            self.status_label.setText("📍 显示: 源点云")
    
    def show_target(self):
        """显示目标点云"""
        if self.target_positions is not None:
            self.current_positions = self.target_positions.copy()
            self.update_scatter()
            self.clear_trails()
            self.progress_slider.setValue(100)
            self.progress_bar.setValue(100)
            self.status_label.setText("🎯 显示: 目标点云")
    
    def update_scatter(self):
        """更新散点显示"""
        if self.current_positions is None:
            return
        
        pos = self.current_positions
        size = self.drone_size_spin.value()
        
        # 根据颜色模式设置颜色
        color_mode = self.color_combo.currentIndex()
        colors = []
        
        if color_mode == 0:  # 高度渐变
            for p in pos:
                y = p[1]
                r = min(1.0, max(0.0, (y + 2) / 4))
                b = min(1.0, max(0.0, (2 - y) / 4))
                g = 0.3
                colors.append([r, g, b, 1.0])
        elif color_mode == 1:  # 彩虹色
            for i, p in enumerate(pos):
                hue = (i / len(pos)) * 360
                r, g, b = self.hsv_to_rgb(hue, 0.8, 1.0)
                colors.append([r, g, b, 1.0])
        else:  # 单色
            colors = [[0.2, 0.8, 1.0, 1.0]] * len(pos)
        
        colors = np.array(colors)
        
        self.scatter.setData(
            pos=pos,
            size=size,
            color=colors,
            pxMode=False
        )
    
    def hsv_to_rgb(self, h, s, v):
        """HSV转RGB"""
        h = h / 360.0
        i = int(h * 6)
        f = h * 6 - i
        p = v * (1 - s)
        q = v * (1 - f * s)
        t = v * (1 - (1 - f) * s)
        
        i = i % 6
        if i == 0: return v, t, p
        if i == 1: return q, v, p
        if i == 2: return p, v, t
        if i == 3: return p, q, v
        if i == 4: return t, p, v
        return v, p, q
    
    def clear_trails(self):
        """清除轨迹"""
        for item in self.trail_items:
            self.view.removeItem(item)
        self.trail_items = []
    
    def auto_rotate(self):
        """自动旋转视图"""
        self.rotation_angle += 0.5
        self.view.setCameraPosition(azimuth=self.rotation_angle)
    
    def toggle_rotation(self):
        """切换自动旋转"""
        if self.toggle_rotation_btn.isChecked():
            self.rotation_timer.start(30)
        else:
            self.rotation_timer.stop()
    
    def reset_view(self):
        """重置视图"""
        self.rotation_angle = 45
        self.view.setCameraPosition(distance=12, elevation=30, azimuth=45)
    
    def toggle_grid(self):
        """切换网格显示"""
        self.grid.setVisible(self.show_grid_check.isChecked())
    
    def toggle_axes(self):
        """切换坐标轴显示"""
        self.axis.setVisible(self.show_axes_check.isChecked())
    
    def start_animation(self):
        """开始动画"""
        if self.animation_thread and self.animation_thread.isRunning():
            return
        
        self.clear_trails()
        self.status_label.setText("🚀 动画播放中...")
        self.play_btn.setEnabled(False)
        self.pause_btn.setEnabled(True)
        self.stop_btn.setEnabled(True)
        
        # 获取动画参数
        interpolation_map = {
            "平滑过渡": 'smooth',
            "弹性效果": 'elastic',
            "弹跳效果": 'bounce',
            "线性过渡": 'linear'
        }
        method = interpolation_map.get(self.interp_combo.currentText(), 'smooth')
        
        self.animation_thread = AnimationThread(
            self.planner,
            self.source_positions,
            self.target_positions,
            self.duration_spin.value(),
            self.fps_spin.value(),
            method
        )
        self.animation_thread.frame_ready.connect(self.on_frame_ready)
        self.animation_thread.finished_signal.connect(self.on_animation_finished)
        self.animation_thread.start()
    
    def pause_animation(self):
        """暂停/继续动画"""
        if self.animation_thread and self.animation_thread.isRunning():
            if self.animation_thread.paused:
                self.animation_thread.resume()
                self.pause_btn.setText("⏸️ 暂停")
                self.status_label.setText("🚀 动画播放中...")
            else:
                self.animation_thread.pause()
                self.pause_btn.setText("▶️ 继续")
                self.status_label.setText("⏸️ 动画已暂停")
    
    def stop_animation(self):
        """停止动画"""
        if self.animation_thread:
            self.animation_thread.stop()
            self.animation_thread.wait()
        self.show_source()
        self.play_btn.setEnabled(True)
        self.pause_btn.setEnabled(False)
        self.stop_btn.setEnabled(False)
        self.pause_btn.setText("⏸️ 暂停")
        self.status_label.setText("⏹️ 动画已停止")
    
    def on_frame_ready(self, positions, progress):
        """帧就绪回调"""
        self.current_positions = positions
        self.update_scatter()
        
        progress_percent = int(progress * 100)
        self.progress_bar.setValue(progress_percent)
        self.progress_slider.setValue(progress_percent)
        
        # 显示轨迹
        if self.show_trails_check.isChecked() and progress > 0:
            self._add_trail_segment(positions, progress)
    
    def _add_trail_segment(self, positions, progress):
        """添加轨迹段"""
        if len(self.trail_items) > 20:  # 限制轨迹数量
            old_item = self.trail_items.pop(0)
            self.view.removeItem(old_item)
        
        # 采样部分点显示轨迹
        sample_indices = np.random.choice(len(positions), min(50, len(positions)), replace=False)
        trail_points = positions[sample_indices]
        
        alpha = 0.3 + progress * 0.4
        color = (0.9, 0.5, 0.1, alpha)
        
        # 创建小点表示轨迹
        trail_scatter = GLScatterPlotItem(
            pos=trail_points,
            size=0.02,
            color=[color] * len(trail_points),
            pxMode=False
        )
        self.view.addItem(trail_scatter)
        self.trail_items.append(trail_scatter)
    
    def on_animation_finished(self):
        """动画完成回调"""
        self.play_btn.setEnabled(True)
        self.pause_btn.setEnabled(False)
        self.stop_btn.setEnabled(False)
        self.pause_btn.setText("⏸️ 暂停")
        self.status_label.setText("✅ 动画完成")
    
    def on_slider_changed(self, value):
        """滑块变化回调"""
        if self.animation_thread and self.animation_thread.isRunning():
            return
        
        if self.source_positions is None or self.target_positions is None:
            return
        
        progress = value / 100.0
        self.current_positions = self.planner.interpolate_positions(
            self.source_positions,
            self.target_positions,
            progress,
            'smooth'
        )
        self.update_scatter()
        self.progress_bar.setValue(value)
    
    def closeEvent(self, event):
        """关闭事件"""
        if self.animation_thread:
            self.animation_thread.stop()
            self.animation_thread.wait()
        event.accept()


def main():
    app = QApplication(sys.argv)
    
    # 设置应用样式
    app.setStyle('Fusion')
    palette = QPalette()
    palette.setColor(QPalette.Window, QColor(26, 26, 46))
    palette.setColor(QPalette.WindowText, Qt.white)
    app.setPalette(palette)
    
    window = DroneShowMainWindow()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

点云推荐下载地址

https://gitcode.com/open-source-toolkit/d98ff

相关推荐
廋到被风吹走1 小时前
持续学习方向:云原生深度(Kubernetes Operator、Service Mesh、Dapr)
java·开发语言·学习
程序喵大人1 小时前
源码剖析:iostream 的缓冲区设计
开发语言·c++·iostream
mr_LuoWei20091 小时前
自定义的中文脚本解释器来实现对excel自动化处理(一)
python·自动化·excel
泯仲1 小时前
RabbitMQ的延迟消息在项目中的运用及实现剖析
开发语言·后端·rabbitmq
wapicn991 小时前
技术实战:基于Python的企业信息四要素核验API调用示例
开发语言·python
xyq20241 小时前
Scala 正则表达式
开发语言
sg_knight1 小时前
外观模式(Facade)
开发语言·python·外观模式·facade
老师好,我是刘同学1 小时前
Python字典完整用法指南
python
深蓝电商API1 小时前
百度百科词条关联关系爬取
爬虫·python