2026-05-17 00:45:12 +08:00
|
|
|
|
#include <vtkAxesActor.h>
|
|
|
|
|
|
#include <vtkOrientationMarkerWidget.h>
|
|
|
|
|
|
#include <vtkRenderWindow.h>
|
|
|
|
|
|
#include <vtkRenderWindowInteractor.h>
|
|
|
|
|
|
#include <vtkTextActor.h>
|
|
|
|
|
|
#include <vtkTextProperty.h>
|
|
|
|
|
|
#include <vtkCoordinate.h>
|
|
|
|
|
|
#include <vtkRendererCollection.h>
|
|
|
|
|
|
|
|
|
|
|
|
#include "CloudShow.h"
|
2026-06-13 13:24:31 +08:00
|
|
|
|
#include "LaserDataLoader.h"
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
#include <pcl/io/pcd_io.h>
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
#include <chrono>
|
2026-05-17 00:45:12 +08:00
|
|
|
|
#include <cmath>
|
2026-06-13 13:24:31 +08:00
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
#include <ctime>
|
|
|
|
|
|
#include <exception>
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
#include <iostream>
|
|
|
|
|
|
#include <new>
|
|
|
|
|
|
#include <sstream>
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 工厂方法
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
std::unique_ptr<ICloudShow> ICloudShow::Create()
|
|
|
|
|
|
{
|
|
|
|
|
|
return std::make_unique<CCloudShow>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
CCloudShow::CCloudShow() = default;
|
|
|
|
|
|
|
|
|
|
|
|
CCloudShow::~CCloudShow()
|
|
|
|
|
|
{
|
|
|
|
|
|
Stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// 内部:根据数据类型返回单点字节数
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
static size_t pointSize(EVzResultDataType t)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 内部:CloudData → PCL(仅处理 PointXYZI 类型,其余跳过)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
static pcl::PointCloud<pcl::PointXYZI>::Ptr toPcl(const ICloudShow::CloudData& cloudData)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
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<pcl::PointCloud<pcl::PointXYZI>>();
|
|
|
|
|
|
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<const SVzNLPointXYZI*>(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<uint32_t>(idx);
|
|
|
|
|
|
cloud->height = 1;
|
|
|
|
|
|
return cloud;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Show — 单次显示,阻塞直到关闭窗口
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
int CCloudShow::Show(const CloudData& cloudData,
|
|
|
|
|
|
const CloudFrameInfo& info,
|
|
|
|
|
|
const std::string& windowName)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
auto pclCloud = toPcl(cloudData);
|
|
|
|
|
|
if (!pclCloud)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
|
|
pcl::visualization::PCLVisualizer viewer(windowName);
|
|
|
|
|
|
viewer.setSize(670, 450);
|
|
|
|
|
|
viewer.setBackgroundColor(0.05, 0.05, 0.05);
|
2026-06-13 13:24:31 +08:00
|
|
|
|
viewer.addPointCloud<pcl::PointXYZI>(pclCloud, windowName);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
viewer.setPointCloudRenderingProperties(
|
|
|
|
|
|
pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2.0, windowName);
|
|
|
|
|
|
viewer.initCameraParameters();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
viewer.setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 右下角坐标轴
|
|
|
|
|
|
vtkAxesActor* axes = vtkAxesActor::New();
|
|
|
|
|
|
vtkOrientationMarkerWidget* widget = vtkOrientationMarkerWidget::New();
|
|
|
|
|
|
widget->SetOrientationMarker(axes);
|
|
|
|
|
|
vtkRenderWindowInteractor* iren = viewer.getRenderWindow()->GetInteractor();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (!iren)
|
|
|
|
|
|
{
|
|
|
|
|
|
iren = vtkRenderWindowInteractor::New();
|
|
|
|
|
|
viewer.getRenderWindow()->SetInteractor(iren);
|
|
|
|
|
|
iren->Delete();
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
widget->SetInteractor(iren);
|
|
|
|
|
|
widget->SetViewport(0.85, 0.0, 1.0, 0.15);
|
|
|
|
|
|
widget->SetEnabled(1);
|
|
|
|
|
|
widget->InteractiveOff();
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// SAVE 按钮
|
2026-05-17 00:45:12 +08:00
|
|
|
|
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();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (renderer)
|
|
|
|
|
|
renderer->AddActor2D(textActor);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
textActor->Delete();
|
|
|
|
|
|
|
|
|
|
|
|
viewer.registerMouseCallback([&viewer, this](const pcl::visualization::MouseEvent& event) {
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (event.getType() != pcl::visualization::MouseEvent::MouseButtonPress)
|
|
|
|
|
|
return;
|
|
|
|
|
|
if (event.getButton() != pcl::visualization::MouseEvent::LeftButton)
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
int* sz = viewer.getRenderWindow()->GetSize();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (sz[0] <= 0 || sz[1] <= 0)
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// Start — 主线程创建 viewer,注册键盘/鼠标回调
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
int CCloudShow::Start(const std::string& windowName, void* hwnd)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (m_bRunning)
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
m_windowName = windowName;
|
|
|
|
|
|
m_bFirstCloud = true;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_bQuitRequested = false;
|
|
|
|
|
|
m_bSaveRequested = false;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
m_viewer = std::make_shared<pcl::visualization::PCLVisualizer>(windowName);
|
|
|
|
|
|
m_viewer->setBackgroundColor(0.05, 0.05, 0.05);
|
|
|
|
|
|
m_viewer->setSize(670, 450);
|
|
|
|
|
|
m_viewer->initCameraParameters();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
m_viewer->setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_viewer->registerKeyboardCallback(&CCloudShow::keyboardCallback, this);
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
if (hwnd)
|
|
|
|
|
|
m_viewer->getRenderWindow()->SetParentId(hwnd);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// 右下角坐标轴
|
|
|
|
|
|
vtkAxesActor* axes = vtkAxesActor::New();
|
|
|
|
|
|
m_axesWidget = vtkOrientationMarkerWidget::New();
|
|
|
|
|
|
m_axesWidget->SetOrientationMarker(axes);
|
|
|
|
|
|
vtkRenderWindowInteractor* iren = m_viewer->getRenderWindow()->GetInteractor();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (!iren)
|
|
|
|
|
|
{
|
|
|
|
|
|
iren = vtkRenderWindowInteractor::New();
|
|
|
|
|
|
m_viewer->getRenderWindow()->SetInteractor(iren);
|
|
|
|
|
|
iren->Delete();
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
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();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
m_viewer->registerMouseCallback([this](const pcl::visualization::MouseEvent& event) {
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (event.getType() != pcl::visualization::MouseEvent::MouseButtonPress)
|
|
|
|
|
|
return;
|
|
|
|
|
|
if (event.getButton() != pcl::visualization::MouseEvent::LeftButton)
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
int* sz = m_viewer->getRenderWindow()->GetSize();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (sz[0] <= 0 || sz[1] <= 0)
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
float rx = (float)event.getX() / sz[0];
|
|
|
|
|
|
float ry = (float)event.getY() / sz[1];
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// SAVE 按钮区域
|
|
|
|
|
|
if (rx >= 0.83f && rx <= 0.94f && ry >= 0.40f && ry <= 0.50f)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_bSaveRequested = true;
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// START/STOP 按钮区域
|
|
|
|
|
|
else if (rx >= 0.83f && rx <= 0.94f && ry >= 0.30f && ry <= 0.40f)
|
|
|
|
|
|
m_bStartStopRequested = true;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (const std::exception& e)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cerr << "[CloudShow] 创建 PCL 窗口失败: " << e.what() << std::endl;
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (...)
|
|
|
|
|
|
{
|
|
|
|
|
|
std::cerr << "[CloudShow] 创建 PCL 窗口失败: 未知异常" << std::endl;
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// 预分配复用缓冲区,避免热路径 realloc 导致卡顿
|
|
|
|
|
|
{
|
|
|
|
|
|
// 显示缓冲:~500k 点 × 16B = 8MB
|
|
|
|
|
|
m_renderPool = std::make_shared<pcl::PointCloud<pcl::PointXYZI>>();
|
|
|
|
|
|
m_renderPool->points.reserve(500000);
|
|
|
|
|
|
|
|
|
|
|
|
// S 键缓存缓冲:600 线 × 1500 点 × 32B ≈ 29MB
|
|
|
|
|
|
m_lastCloudBuffers.resize(600);
|
|
|
|
|
|
for (auto& buf : m_lastCloudBuffers)
|
|
|
|
|
|
buf.reserve(1500 * sizeof(SVzNLPointXYZI));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_bRunning = true;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// PushFrame — 更新显示 + 入保存队列
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
void CCloudShow::PushFrame(const CloudData& cloudData, const CloudFrameInfo& info)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (!m_bRunning)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// 保存进行中(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<std::vector<uint8_t>> oldBuffers;
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> 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<size_t>(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<std::mutex> 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<pcl::PointCloud<pcl::PointXYZI>>();
|
|
|
|
|
|
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<const SVzNLPointXYZI*>(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<uint32_t>(idx);
|
|
|
|
|
|
cloud.height = 1;
|
|
|
|
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_cloudMutex);
|
|
|
|
|
|
m_displayCloud.swap(m_renderPool); // 旧 displayCloud 变成新的 renderPool
|
|
|
|
|
|
m_bUpdated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// 保存:按帧间隔缓存(默认每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<std::mutex> 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&)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
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);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// Stop — 关闭窗口,停止保存线程
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
int CCloudShow::Stop()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_bRunning)
|
|
|
|
|
|
return 0;
|
2026-06-13 13:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
2026-05-17 00:45:12 +08:00
|
|
|
|
}
|
2026-06-13 13:24:31 +08:00
|
|
|
|
m_saveThreads.clear();
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// 释放缓冲池 + 保存队列残余内存
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_taskPoolMutex);
|
|
|
|
|
|
m_taskPool.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_saveMutex);
|
|
|
|
|
|
m_saveQueue.clear();
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
if (m_axesWidget)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_axesWidget->Delete();
|
|
|
|
|
|
m_axesWidget = nullptr;
|
|
|
|
|
|
}
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (m_pStartStopBtnActor)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pStartStopBtnActor->Delete();
|
|
|
|
|
|
m_pStartStopBtnActor = nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (m_pProgressActor)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_pProgressActor->Delete();
|
|
|
|
|
|
m_pProgressActor = nullptr;
|
|
|
|
|
|
}
|
2026-05-17 00:45:12 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
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<std::mutex> lk(m_saveMutex);
|
|
|
|
|
|
m_saveTotal = m_saveCompleted.load()
|
|
|
|
|
|
+ static_cast<int>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<int>(task.info.height), 0.0f, 0, 0);
|
|
|
|
|
|
auto t1 = std::chrono::steady_clock::now();
|
|
|
|
|
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(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<std::mutex> 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<std::mutex> 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<size_t>(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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// keyboardCallback — PCL 键盘事件 → 原子标志
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
void CCloudShow::keyboardCallback(
|
|
|
|
|
|
const pcl::visualization::KeyboardEvent& event, void* cookie)
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (!event.keyDown())
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
auto* self = static_cast<CCloudShow*>(cookie);
|
2026-06-13 13:24:31 +08:00
|
|
|
|
if (!self)
|
|
|
|
|
|
return;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
|
|
|
|
|
|
std::string key = event.getKeySym();
|
|
|
|
|
|
if (key == "q" || key == "Q")
|
|
|
|
|
|
self->m_bQuitRequested = true;
|
|
|
|
|
|
else if (key == "s" || key == "S")
|
|
|
|
|
|
self->m_bSaveRequested = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// SpinOnce — 消费点云 + 驱动渲染 + 保存状态维护
|
2026-05-17 00:45:12 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
int CCloudShow::SpinOnce(int ms)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_bRunning || !m_viewer)
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
|
|
if (m_viewer->wasStopped())
|
|
|
|
|
|
{
|
|
|
|
|
|
m_bQuitRequested = true;
|
|
|
|
|
|
Stop();
|
|
|
|
|
|
return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 13:24:31 +08:00
|
|
|
|
// START/STOP 按钮处理
|
|
|
|
|
|
if (m_bStartStopRequested.exchange(false))
|
|
|
|
|
|
handleStartStopToggle();
|
|
|
|
|
|
|
|
|
|
|
|
// S 键单帧保存(需持有 m_lastCloudMutex 与 PushFrame 互斥)
|
|
|
|
|
|
if (m_bSaveRequested.exchange(false))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 锁内:取走数据所有权(swap,极快),避免持锁做文件 IO
|
|
|
|
|
|
CloudData localData;
|
|
|
|
|
|
std::vector<std::vector<uint8_t>> localBuffers;
|
|
|
|
|
|
CloudFrameInfo localInfo{};
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> 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<int>(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<std::mutex> lk(m_taskPoolMutex);
|
|
|
|
|
|
m_taskPool.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_saveMutex);
|
|
|
|
|
|
m_saveQueue.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
std::lock_guard<std::mutex> lk(m_lastCloudMutex);
|
|
|
|
|
|
m_lastCloudData.clear();
|
|
|
|
|
|
m_lastCloudBuffers.clear();
|
|
|
|
|
|
m_lastCloudBuffers.shrink_to_fit();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染最新点云
|
|
|
|
|
|
pcl::PointCloud<pcl::PointXYZI>::Ptr cloud;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
std::lock_guard<std::mutex> lk(m_cloudMutex);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
if (m_bUpdated)
|
|
|
|
|
|
{
|
2026-06-13 13:24:31 +08:00
|
|
|
|
cloud = m_displayCloud;
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_bUpdated = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cloud && !cloud->points.empty())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_bFirstCloud)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_viewer->addPointCloud<pcl::PointXYZI>(cloud, m_windowName);
|
|
|
|
|
|
m_viewer->setPointCloudRenderingProperties(
|
|
|
|
|
|
pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2.0, m_windowName);
|
|
|
|
|
|
m_viewer->resetCamera();
|
2026-06-13 13:24:31 +08:00
|
|
|
|
m_viewer->setCameraPosition(0, -100000, -50000, 0, 0, 50000, 0, -1, 0);
|
2026-05-17 00:45:12 +08:00
|
|
|
|
m_bFirstCloud = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
m_viewer->updatePointCloud<pcl::PointXYZI>(cloud, m_windowName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_viewer->spinOnce(ms);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|