#include #include #include #include #include #include #include #include #include "CloudShow.h" #include "LaserDataLoader.h" #include #include #include #include #include #include #include #include #include #include // ============================================================ // 工厂方法 // ============================================================ std::unique_ptr ICloudShow::Create() { return std::make_unique(); } CCloudShow::CCloudShow() = default; CCloudShow::~CCloudShow() { Stop(); } // ============================================================ // 内部:根据数据类型返回单点字节数 // ============================================================ static size_t pointSize(EVzResultDataType t) { switch (t) { case keResultDataType_PointXYZI: return sizeof(SVzNLPointXYZI); case keResultDataType_PointXYZRGBA: return sizeof(SVzNLPointXYZRGBA); case keResultDataType_PointF: return sizeof(SVzNL3DPointF); case keResultDataType_Position: return sizeof(SVzNL3DPosition); default: return 0; } } // ============================================================ // 内部:CloudData → PCL(仅处理 PointXYZI 类型,其余跳过) // ============================================================ static pcl::PointCloud::Ptr toPcl(const ICloudShow::CloudData& cloudData) { size_t total = 0; for (const auto& p : cloudData) { if (p.first == keResultDataType_PointXYZI && p.second.p3DPoint) total += p.second.nPointCount; } if (total == 0) return nullptr; auto cloud = std::make_shared>(); cloud->is_dense = true; cloud->points.resize(total); // 一次分配,下标赋值,避免 40 万次 push_back size_t idx = 0; for (const auto& p : cloudData) { if (p.first != keResultDataType_PointXYZI || !p.second.p3DPoint) continue; const auto* src = static_cast(p.second.p3DPoint); for (int i = 0; i < p.second.nPointCount; ++i) { cloud->points[idx].x = src[i].fData[0]; cloud->points[idx].y = src[i].fData[1]; cloud->points[idx].z = src[i].fData[2]; cloud->points[idx].intensity = src[i].fData_c[0]; ++idx; } } cloud->width = static_cast(idx); cloud->height = 1; return cloud; } // ============================================================ // Show — 单次显示,阻塞直到关闭窗口 // ============================================================ int CCloudShow::Show(const CloudData& cloudData, const CloudFrameInfo& info, const std::string& windowName) { auto pclCloud = toPcl(cloudData); if (!pclCloud) return -1; pcl::visualization::PCLVisualizer viewer(windowName); viewer.setSize(670, 450); viewer.setBackgroundColor(0.05, 0.05, 0.05); viewer.addPointCloud(pclCloud, windowName); viewer.setPointCloudRenderingProperties( pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2.0, windowName); viewer.initCameraParameters(); viewer.setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0); // 右下角坐标轴 vtkAxesActor* axes = vtkAxesActor::New(); vtkOrientationMarkerWidget* widget = vtkOrientationMarkerWidget::New(); widget->SetOrientationMarker(axes); vtkRenderWindowInteractor* iren = viewer.getRenderWindow()->GetInteractor(); if (!iren) { iren = vtkRenderWindowInteractor::New(); viewer.getRenderWindow()->SetInteractor(iren); iren->Delete(); } widget->SetInteractor(iren); widget->SetViewport(0.85, 0.0, 1.0, 0.15); widget->SetEnabled(1); widget->InteractiveOff(); // SAVE 按钮 vtkTextActor* textActor = vtkTextActor::New(); textActor->SetInput("SAVE"); textActor->GetTextProperty()->SetColor(0.2, 0.8, 0.2); textActor->GetTextProperty()->SetFontSize(20); textActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); textActor->SetPosition(0.88, 0.48); vtkRenderer* renderer = viewer.getRendererCollection()->GetFirstRenderer(); if (renderer) renderer->AddActor2D(textActor); textActor->Delete(); viewer.registerMouseCallback([&viewer, this](const pcl::visualization::MouseEvent& event) { if (event.getType() != pcl::visualization::MouseEvent::MouseButtonPress) return; if (event.getButton() != pcl::visualization::MouseEvent::LeftButton) return; int* sz = viewer.getRenderWindow()->GetSize(); if (sz[0] <= 0 || sz[1] <= 0) return; float rx = (float)event.getX() / sz[0]; float ry = (float)event.getY() / sz[1]; if (rx >= 0.83f && rx <= 0.94f && ry >= 0.43f && ry <= 0.53f) m_bSaveRequested = true; }); viewer.spin(); widget->Delete(); axes->Delete(); return 0; } // ============================================================ // Start — 主线程创建 viewer,注册键盘/鼠标回调 // ============================================================ int CCloudShow::Start(const std::string& windowName, void* hwnd) { if (m_bRunning) return 0; m_windowName = windowName; m_bFirstCloud = true; m_bQuitRequested = false; m_bSaveRequested = false; try { m_viewer = std::make_shared(windowName); m_viewer->setBackgroundColor(0.05, 0.05, 0.05); m_viewer->setSize(670, 450); m_viewer->initCameraParameters(); m_viewer->setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0); m_viewer->registerKeyboardCallback(&CCloudShow::keyboardCallback, this); #ifdef _WIN32 if (hwnd) m_viewer->getRenderWindow()->SetParentId(hwnd); #endif // 右下角坐标轴 vtkAxesActor* axes = vtkAxesActor::New(); m_axesWidget = vtkOrientationMarkerWidget::New(); m_axesWidget->SetOrientationMarker(axes); vtkRenderWindowInteractor* iren = m_viewer->getRenderWindow()->GetInteractor(); if (!iren) { iren = vtkRenderWindowInteractor::New(); m_viewer->getRenderWindow()->SetInteractor(iren); iren->Delete(); } m_axesWidget->SetInteractor(iren); m_axesWidget->SetViewport(0.85, 0.0, 1.0, 0.15); m_axesWidget->SetEnabled(1); m_axesWidget->InteractiveOff(); axes->Delete(); vtkRenderer* renderer = m_viewer->getRendererCollection()->GetFirstRenderer(); // SAVE 按钮 vtkTextActor* saveActor = vtkTextActor::New(); saveActor->SetInput("SAVE"); saveActor->GetTextProperty()->SetColor(0.2, 0.8, 0.2); saveActor->GetTextProperty()->SetFontSize(20); saveActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); saveActor->SetPosition(0.88, 0.45); if (renderer) renderer->AddActor2D(saveActor); saveActor->Delete(); // START/STOP 按钮 m_pStartStopBtnActor = vtkTextActor::New(); m_pStartStopBtnActor->SetInput("START"); m_pStartStopBtnActor->GetTextProperty()->SetColor(0.2, 0.8, 0.2); m_pStartStopBtnActor->GetTextProperty()->SetFontSize(20); m_pStartStopBtnActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); m_pStartStopBtnActor->SetPosition(0.87, 0.35); if (renderer) renderer->AddActor2D(m_pStartStopBtnActor); // 保存进度文字 m_pProgressActor = vtkTextActor::New(); m_pProgressActor->SetInput(""); m_pProgressActor->GetTextProperty()->SetColor(0.8, 0.8, 0.2); m_pProgressActor->GetTextProperty()->SetFontSize(14); m_pProgressActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedViewport(); m_pProgressActor->SetPosition(0.85, 0.25); if (renderer) renderer->AddActor2D(m_pProgressActor); m_viewer->registerMouseCallback([this](const pcl::visualization::MouseEvent& event) { if (event.getType() != pcl::visualization::MouseEvent::MouseButtonPress) return; if (event.getButton() != pcl::visualization::MouseEvent::LeftButton) return; int* sz = m_viewer->getRenderWindow()->GetSize(); if (sz[0] <= 0 || sz[1] <= 0) return; float rx = (float)event.getX() / sz[0]; float ry = (float)event.getY() / sz[1]; // SAVE 按钮区域 if (rx >= 0.83f && rx <= 0.94f && ry >= 0.40f && ry <= 0.50f) m_bSaveRequested = true; // START/STOP 按钮区域 else if (rx >= 0.83f && rx <= 0.94f && ry >= 0.30f && ry <= 0.40f) m_bStartStopRequested = true; }); } catch (const std::exception& e) { std::cerr << "[CloudShow] 创建 PCL 窗口失败: " << e.what() << std::endl; return -1; } catch (...) { std::cerr << "[CloudShow] 创建 PCL 窗口失败: 未知异常" << std::endl; return -1; } // 预分配复用缓冲区,避免热路径 realloc 导致卡顿 { // 显示缓冲:~500k 点 × 16B = 8MB m_renderPool = std::make_shared>(); m_renderPool->points.reserve(500000); // S 键缓存缓冲:600 线 × 1500 点 × 32B ≈ 29MB m_lastCloudBuffers.resize(600); for (auto& buf : m_lastCloudBuffers) buf.reserve(1500 * sizeof(SVzNLPointXYZI)); } m_bRunning = true; return 0; } // ============================================================ // PushFrame — 更新显示 + 入保存队列 // ============================================================ void CCloudShow::PushFrame(const CloudData& cloudData, const CloudFrameInfo& info) { if (!m_bRunning) return; try { // 保存进行中(m_saveTotal > 0 表示 STOP 已点击,saveLoop 正在写盘) // 此时跳过显示更新和 S 键缓存,避免与写盘争抢内存/IO 导致 UI 卡死 const bool bSaving = (m_saveTotal.load() > 0); if (!bSaving) { // 仅非连续保存时缓存最近一帧 CloudData 的深拷贝(供 S 键单帧保存) if (!m_bSaveActive.load()) { // 1. 锁内:取出旧数据(swap 只交换指针,极快) CloudData oldData; std::vector> oldBuffers; { std::lock_guard lk(m_lastCloudMutex); m_lastFrameInfo = info; oldData.swap(m_lastCloudData); oldBuffers.swap(m_lastCloudBuffers); } // 2. 锁外:复用 oldBuffers,resize + memcpy(零分配) oldBuffers.resize(cloudData.size()); oldData.clear(); oldData.reserve(cloudData.size()); for (size_t i = 0; i < cloudData.size(); ++i) { const auto& src = cloudData[i]; oldData.push_back(src); auto& ld = oldData.back().second; ld.p2DPoint = nullptr; if (ld.p3DPoint && ld.nPointCount > 0) { size_t sz = pointSize(src.first) * static_cast(ld.nPointCount); if (sz > 0) { // 加 4KB margin,避免点数的帧间微小波动导致反复 realloc if (oldBuffers[i].capacity() < sz) oldBuffers[i].reserve(sz + 4096); oldBuffers[i].resize(sz); std::memcpy(oldBuffers[i].data(), ld.p3DPoint, sz); ld.p3DPoint = oldBuffers[i].data(); } else { ld.p3DPoint = nullptr; } } } // 3. 锁内:写入新数据(swap 只交换指针,极快) { std::lock_guard lk(m_lastCloudMutex); m_lastCloudData.swap(oldData); m_lastCloudBuffers.swap(oldBuffers); } } // 显示:复用 m_renderPool,resize + 下标赋值(零分配) { size_t total = 0; for (const auto& p : cloudData) if (p.first == keResultDataType_PointXYZI && p.second.p3DPoint) total += p.second.nPointCount; if (total > 0) { if (!m_renderPool) m_renderPool = std::make_shared>(); auto& cloud = *m_renderPool; cloud.is_dense = true; if (cloud.points.capacity() < total) cloud.points.reserve(total + 2048); cloud.points.resize(total); size_t idx = 0; for (const auto& p : cloudData) { if (p.first != keResultDataType_PointXYZI || !p.second.p3DPoint) continue; const auto* src = static_cast(p.second.p3DPoint); for (int i = 0; i < p.second.nPointCount; ++i) { cloud.points[idx].x = src[i].fData[0]; cloud.points[idx].y = src[i].fData[1]; cloud.points[idx].z = src[i].fData[2]; cloud.points[idx].intensity = src[i].fData_c[0]; ++idx; } } cloud.width = static_cast(idx); cloud.height = 1; std::lock_guard lk(m_cloudMutex); m_displayCloud.swap(m_renderPool); // 旧 displayCloud 变成新的 renderPool m_bUpdated = true; } } } // 保存:按帧间隔缓存(默认每10帧存1帧) // 注意:m_nPushFrameCount 仅回调线程独占写入,无需原子操作 m_nPushFrameCount++; if (m_bSaveActive.load() && (m_nPushFrameCount % m_nSaveFrameInterval == 0)) { SaveTask task = acquireTask(); // 从池取预分配 task packIntoTask(task, cloudData, info); // memcpy 到预分配 buffer { std::lock_guard lk(m_saveMutex); if (m_bSaveActive.load()) // 双重检查:STOP 可能在上一步期间触发 { if (!task.cloudData.empty()) m_saveQueue.emplace_back(std::move(task)); else releaseTask(task); m_saveCv.notify_one(); } else { releaseTask(task); // STOP 已点击,丢弃此帧 } } } } catch (const std::bad_alloc&) { m_nSaveDropped.fetch_add(1, std::memory_order_relaxed); } catch (const std::exception&) { m_nSaveDropped.fetch_add(1, std::memory_order_relaxed); } catch (...) { m_nSaveDropped.fetch_add(1, std::memory_order_relaxed); } } // ============================================================ // Stop — 关闭窗口,停止保存线程 // ============================================================ int CCloudShow::Stop() { if (!m_bRunning) return 0; m_bRunning = false; m_bSaveActive = false; m_bFlushAndSave = false; m_saveExit = true; m_saveCv.notify_all(); for (auto& t : m_saveThreads) { if (t.joinable()) t.join(); } m_saveThreads.clear(); // 释放缓冲池 + 保存队列残余内存 { std::lock_guard lk(m_taskPoolMutex); m_taskPool.clear(); } { std::lock_guard lk(m_saveMutex); m_saveQueue.clear(); } if (m_axesWidget) { m_axesWidget->Delete(); m_axesWidget = nullptr; } if (m_pStartStopBtnActor) { m_pStartStopBtnActor->Delete(); m_pStartStopBtnActor = nullptr; } if (m_pProgressActor) { m_pProgressActor->Delete(); m_pProgressActor = nullptr; } if (m_viewer) { m_viewer->close(); m_viewer.reset(); } return 0; } bool CCloudShow::IsRunning() const { return m_bRunning; } bool CCloudShow::IsQuitRequested() const { return m_bQuitRequested; } bool CCloudShow::IsSaveRequested() const { return m_bSaveRequested; } void CCloudShow::ClearSaveRequest() { m_bSaveRequested = false; } bool CCloudShow::IsSaveActive() const { return m_bSaveActive.load(); } void CCloudShow::SetSaveBaseDir(const std::string& dir) { m_saveBaseDir = dir; // 扫描已有 LaserData_*.txt,从最大序号 +1 开始 int maxSeq = 0; std::error_code ec; for (const auto& entry : std::filesystem::directory_iterator(m_saveBaseDir, ec)) { if (ec) break; std::string name = entry.path().filename().string(); int n = 0; if (sscanf(name.c_str(), "LaserData_%d.txt", &n) == 1) { if (n > maxSeq) maxSeq = n; } } m_nSingleSaveSeq = maxSeq; m_nSaveSeq = maxSeq; std::cout << "[CloudShow] 保存基目录: " << m_saveBaseDir << " 已有最大序号: " << maxSeq << std::endl; } void CCloudShow::SetSaveFrameInterval(int interval) { if (interval < 1) interval = 1; m_nSaveFrameInterval = interval; std::cout << "[CloudShow] 保存帧间隔: " << interval << std::endl; } // ============================================================ // 内部:START/STOP 切换 // ============================================================ void CCloudShow::handleStartStopToggle() { if (m_saveTotal.load() > 0 && !m_bSaveActive.load()) return; // 刷新中,忽略 if (!m_bSaveActive.load()) { // === START:开始缓存数据到内存(不写盘) === m_nSaveSeq = 0; // 每次会话从 1 开始 m_saveCompleted = 0; m_saveTotal = 0; m_bFlushAndSave = false; // 保存线程等待,不写盘 auto now = std::time(nullptr); std::ostringstream dirOss; dirOss << m_saveBaseDir << "/" << std::put_time(std::localtime(&now), "%Y%m%d_%H%M%S"); m_currentSessionDir = dirOss.str(); std::error_code ec; std::filesystem::create_directories(m_currentSessionDir, ec); if (ec) { std::cerr << "[CloudShow] 创建目录失败: " << m_currentSessionDir << std::endl; return; } if (m_saveThreads.empty()) { m_saveExit = false; for (int i = 0; i < SAVE_THREAD_COUNT; ++i) m_saveThreads.emplace_back(&CCloudShow::saveLoop, this); } m_bSaveActive = true; updateStartStopButton(); updateProgressText(); std::cout << "[CloudShow] 开始缓存 -> " << m_currentSessionDir << std::endl; } else { // === STOP:停止缓存,2线程写盘 === m_bSaveActive = false; { std::lock_guard lk(m_saveMutex); m_saveTotal = m_saveCompleted.load() + static_cast(m_saveQueue.size()); } m_bFlushAndSave = true; // 通知保存线程开始写盘 m_saveCv.notify_all(); // 唤醒 2 个保存线程 updateStartStopButton(); std::cout << "[CloudShow] 停止缓存,写盘 " << m_saveTotal.load() << " 帧..." << std::endl; } } void CCloudShow::updateStartStopButton() { if (!m_pStartStopBtnActor) return; std::lock_guard lk(m_textMutex); if (m_bSaveActive.load()) { m_pStartStopBtnActor->SetInput("STOP"); m_pStartStopBtnActor->GetTextProperty()->SetColor(1.0, 0.3, 0.3); } else { m_pStartStopBtnActor->SetInput("START"); m_pStartStopBtnActor->GetTextProperty()->SetColor(0.2, 0.8, 0.2); } m_pStartStopBtnActor->Modified(); } void CCloudShow::updateProgressText() { if (!m_pProgressActor) return; std::lock_guard lk(m_textMutex); int done = m_saveCompleted.load(); int total = m_saveTotal.load(); int dropped = m_nSaveDropped.load(); std::ostringstream oss; if (total > 0) oss << "Saved: " << done << "/" << total; else if (done > 0) oss << "Saved: " << done; { std::lock_guard lk2(m_saveMutex); if (!m_saveQueue.empty()) oss << " Q:" << m_saveQueue.size(); } if (dropped > 0) oss << " drop:" << dropped; m_pProgressActor->SetInput(oss.str().c_str()); m_pProgressActor->Modified(); } // ============================================================ // 内部:保存线程(多线程共享队列) // ============================================================ void CCloudShow::saveLoop() { while (!m_saveExit.load()) { SaveTask task; // 在 try 外声明,catch 块可访问 bool haveTask = false; try { { std::unique_lock lk(m_saveMutex); // 等待条件:退出 或 (STOP已点击 且 队列非空) m_saveCv.wait(lk, [this] { return m_saveExit.load() || (m_bFlushAndSave.load() && !m_saveQueue.empty()); }); if (m_saveQueue.empty() || !m_bFlushAndSave.load()) { if (m_saveExit.load()) return; continue; // 虚假唤醒或 START 期间,继续等待 } task = std::move(m_saveQueue.front()); m_saveQueue.pop_front(); haveTask = true; } const int seq = ++m_nSaveSeq; std::ostringstream ofn; ofn << m_currentSessionDir << "/LaserData_" << seq << ".txt"; // 统计总点数 size_t totalPts = 0; for (const auto& p : task.cloudData) totalPts += p.second.nPointCount; auto t0 = std::chrono::steady_clock::now(); LaserDataLoader loader; loader.SaveLaserScanData(ofn.str(), task.cloudData, static_cast(task.info.height), 0.0f, 0, 0); auto t1 = std::chrono::steady_clock::now(); auto ms = std::chrono::duration_cast(t1 - t0).count(); std::cout << "[CloudShow] 保存 " << ofn.str() << " 行=" << task.cloudData.size() << " 点=" << totalPts << " 耗时=" << ms << "ms" << " Q=" << m_saveQueue.size() << std::endl; m_saveCompleted.fetch_add(1, std::memory_order_relaxed); // 归还 task 到缓冲池 releaseTask(task); haveTask = false; } catch (const std::exception& e) { std::cerr << "[CloudShow] 保存异常: " << e.what() << " Q=" << m_saveQueue.size() << std::endl; m_nSaveDropped.fetch_add(1, std::memory_order_relaxed); if (haveTask) releaseTask(task); } catch (...) { std::cerr << "[CloudShow] 保存未知异常 Q=" << m_saveQueue.size() << std::endl; m_nSaveDropped.fetch_add(1, std::memory_order_relaxed); if (haveTask) releaseTask(task); } } } // ============================================================ // SaveTask 缓冲池 — 消除回调线程的堆分配 // ============================================================ CCloudShow::SaveTask CCloudShow::acquireTask() { std::lock_guard lk(m_taskPoolMutex); if (!m_taskPool.empty()) { SaveTask task = std::move(m_taskPool.back()); m_taskPool.pop_back(); return task; } return SaveTask{}; // 池空:首次/扩容时分配(稀有路径) } void CCloudShow::releaseTask(SaveTask& task) { // 清空元数据但保留 buffers 的 capacity 供下次复用 task.cloudData.clear(); task.info = CloudFrameInfo{}; std::lock_guard lk(m_taskPoolMutex); m_taskPool.push_back(std::move(task)); } void CCloudShow::packIntoTask(SaveTask& task, const ICloudShow::CloudData& cloudData, const CloudFrameInfo& info) { task.info = info; task.cloudData.clear(); task.cloudData.reserve(cloudData.size()); // 确保 buffers 足够(仅首次或帧结构变化时 resize) if (task.buffers.size() < cloudData.size()) task.buffers.resize(cloudData.size()); for (size_t i = 0; i < cloudData.size(); ++i) { const auto& src = cloudData[i]; task.cloudData.push_back(src); auto& ld = task.cloudData.back().second; ld.p2DPoint = nullptr; if (ld.p3DPoint && ld.nPointCount > 0) { size_t sz = pointSize(src.first) * static_cast(ld.nPointCount); if (sz > 0) { if (task.buffers[i].capacity() < sz) task.buffers[i].reserve(sz + 4096); task.buffers[i].resize(sz); std::memcpy(task.buffers[i].data(), ld.p3DPoint, sz); ld.p3DPoint = task.buffers[i].data(); } else { ld.p3DPoint = nullptr; } } } task.cloudData.resize(cloudData.size()); } // ============================================================ // keyboardCallback — PCL 键盘事件 → 原子标志 // ============================================================ void CCloudShow::keyboardCallback( const pcl::visualization::KeyboardEvent& event, void* cookie) { if (!event.keyDown()) return; auto* self = static_cast(cookie); if (!self) return; std::string key = event.getKeySym(); if (key == "q" || key == "Q") self->m_bQuitRequested = true; else if (key == "s" || key == "S") self->m_bSaveRequested = true; } // ============================================================ // SpinOnce — 消费点云 + 驱动渲染 + 保存状态维护 // ============================================================ int CCloudShow::SpinOnce(int ms) { if (!m_bRunning || !m_viewer) return -1; if (m_viewer->wasStopped()) { m_bQuitRequested = true; Stop(); return -1; } // START/STOP 按钮处理 if (m_bStartStopRequested.exchange(false)) handleStartStopToggle(); // S 键单帧保存(需持有 m_lastCloudMutex 与 PushFrame 互斥) if (m_bSaveRequested.exchange(false)) { // 锁内:取走数据所有权(swap,极快),避免持锁做文件 IO CloudData localData; std::vector> localBuffers; CloudFrameInfo localInfo{}; { std::lock_guard lk(m_lastCloudMutex); if (!m_lastCloudData.empty()) { localData.swap(m_lastCloudData); localBuffers.swap(m_lastCloudBuffers); localInfo = m_lastFrameInfo; } } // 锁外:执行文件写入(不阻塞 PushFrame) if (!localData.empty()) { const int seq = ++m_nSingleSaveSeq; std::ostringstream oss; oss << m_saveBaseDir << "/LaserData_" << seq << ".txt"; LaserDataLoader loader; loader.SaveLaserScanData(oss.str(), localData, static_cast(localInfo.height), 0.0f, 0, 0); std::cout << "[CloudShow] 单帧保存: " << oss.str() << std::endl; } } // 实时进度更新 if (m_bSaveActive.load() && m_saveCompleted.load() > 0) { updateProgressText(); } else if (!m_bSaveActive.load() && m_saveTotal.load() > 0) { int done = m_saveCompleted.load(); int total = m_saveTotal.load(); updateProgressText(); if (done >= total) { std::cout << "[CloudShow] 保存完成: " << m_currentSessionDir << " | 共 " << total << " 帧" << std::endl; m_saveTotal = 0; m_saveCompleted = 0; m_bFlushAndSave = false; // 写盘结束,保存线程回到等待 updateProgressText(); // 释放全部内存:缓冲池 + 保存队列 + S键缓存 { std::lock_guard lk(m_taskPoolMutex); m_taskPool.clear(); } { std::lock_guard lk(m_saveMutex); m_saveQueue.clear(); } { std::lock_guard lk(m_lastCloudMutex); m_lastCloudData.clear(); m_lastCloudBuffers.clear(); m_lastCloudBuffers.shrink_to_fit(); } } } // 渲染最新点云 pcl::PointCloud::Ptr cloud; { std::lock_guard lk(m_cloudMutex); if (m_bUpdated) { cloud = m_displayCloud; m_bUpdated = false; } } if (cloud && !cloud->points.empty()) { if (m_bFirstCloud) { m_viewer->addPointCloud(cloud, m_windowName); m_viewer->setPointCloudRenderingProperties( pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2.0, m_windowName); m_viewer->resetCamera(); m_viewer->setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0); m_bFirstCloud = false; } else { m_viewer->updatePointCloud(cloud, m_windowName); } } m_viewer->spinOnce(ms); return 0; }