增加线的读取

This commit is contained in:
yiyi 2026-02-22 16:54:45 +08:00
parent 8c8dd5a5a2
commit 550ed280f9
5 changed files with 297 additions and 102 deletions

View File

@ -54,6 +54,11 @@ public:
std::vector<std::vector<SVzNL3DPosition>>& scanLines,
size_t* validPointCount = nullptr);
// 从文件加载Poly折线数据带颜色
// polyLines: 每条折线的点坐标列表polyLines[i] 是第i条折线的所有点含RGB颜色
int LoadPolySegments(const std::string& fileName,
std::vector<std::vector<SVzNLPointXYZRGBA>>& polyLines);
// 获取最后的错误信息
std::string GetLastError() const { return m_lastError; }

View File

@ -102,6 +102,21 @@ int LaserDataLoader::LoadLaserScanData(const std::string& fileName,
nLaserPointIdx = 0;
bFindLineNum = false;
} else if (line.find("Poly_") == 0) {
// 跳过 Poly 折线头及后续点坐标数据
size_t lastUnderscore = line.rfind('_');
int polyNum = 0;
if (lastUnderscore != std::string::npos && lastUnderscore + 1 < line.size()) {
polyNum = std::atoi(line.c_str() + lastUnderscore + 1);
}
for (int skip = 0; skip < polyNum; ) {
if (!std::getline(inputFile, line)) break;
TrimCarriageReturn(line);
if (!line.empty()) {
++skip;
}
}
} else if (line.find("{") == 0) {
// 使用正则表达式判断是XYZ还是RGBD格式
// XYZ格式: {x,y,z}-{leftX,leftY}-{rightX,rightY}
@ -278,6 +293,141 @@ int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector<std::vecto
}
}
int LaserDataLoader::LoadPolySegments(const std::string& fileName,
std::vector<std::vector<SVzNLPointXYZRGBA>>& polyLines)
{
LOG_INFO("Loading poly segments from file: %s\n", fileName.c_str());
polyLines.clear();
std::ifstream inputFile(fileName);
if (!inputFile.is_open()) {
m_lastError = "Cannot open file: " + fileName;
LOG_ERROR("Cannot open file: %s\n", fileName.c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
std::string line;
while (std::getline(inputFile, line)) {
TrimCarriageReturn(line);
// 跳过空行和注释
if (line.empty() || line[0] == '#') {
continue;
}
// 查找 Poly_index_num 头
if (line.find("Poly_") != 0) {
continue;
}
// 解析点数量(最后一个下划线后的数字)
size_t lastUnderscore = line.rfind('_');
if (lastUnderscore == std::string::npos || lastUnderscore + 1 >= line.size()) {
LOG_WARN("[LaserDataLoader] Invalid Poly header: %s\n", line.c_str());
continue;
}
int num = std::atoi(line.c_str() + lastUnderscore + 1);
if (num < 2) {
LOG_WARN("[LaserDataLoader] Invalid point count in Poly header: %s\n", line.c_str());
continue;
}
// 读取 num 个点坐标
std::vector<SVzNLPointXYZRGBA> points;
for (int i = 0; i < num; ) {
if (!std::getline(inputFile, line)) break;
TrimCarriageReturn(line);
if (line.empty()) {
continue; // 跳过空行,不计数
}
SVzNLPointXYZRGBA pt;
memset(&pt, 0, sizeof(pt));
pt.nRGB = 0x00FFFFFF; // 默认白色
bool parsed = false;
// 尝试花括号格式
if (line.find('{') != std::string::npos) {
float fx, fy, fz;
int R = 255, G = 255, B = 255, A = 255;
// 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A}
int cnt = sscanf(line.c_str(),
" { %f , %f , %f } - { %*f , %*f } - { %*f , %*f } - { %d , %d , %d , %d }",
&fx, &fy, &fz, &R, &G, &B, &A);
if (cnt >= 3) {
pt.x = fx;
pt.y = fy;
pt.z = fz;
if (cnt >= 7) {
// 解析到了颜色 R,G,B,A0-255
int nRGB = B; nRGB <<= 8; nRGB += G; nRGB <<= 8; nRGB += R;
pt.nRGB = nRGB;
}
parsed = true;
}
// 旧RGBD格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry}
if (!parsed) {
float r, g, b;
if (sscanf(line.c_str(), " { %f , %f , %f , %f , %f , %f }",
&fx, &fy, &fz, &r, &g, &b) == 6) {
pt.x = fx;
pt.y = fy;
pt.z = fz;
int nRGB = (int)(b * 255); nRGB <<= 8;
nRGB += (int)(g * 255); nRGB <<= 8;
nRGB += (int)(r * 255);
pt.nRGB = nRGB;
parsed = true;
}
}
// XYZ格式: {x,y,z}-{...}-{...}
if (!parsed) {
if (sscanf(line.c_str(), " { %f , %f , %f }", &fx, &fy, &fz) == 3) {
pt.x = fx;
pt.y = fy;
pt.z = fz;
parsed = true;
}
}
}
// 尝试纯数字格式: x y z空格/Tab/逗号分隔)
if (!parsed) {
float fx, fy, fz;
std::string lineCopy = line;
for (char& c : lineCopy) {
if (c == ',' || c == '\t') c = ' ';
}
if (sscanf(lineCopy.c_str(), "%f %f %f", &fx, &fy, &fz) == 3) {
pt.x = fx;
pt.y = fy;
pt.z = fz;
parsed = true;
}
}
if (parsed) {
points.push_back(pt);
}
++i;
}
if (points.size() >= 2) {
polyLines.push_back(std::move(points));
}
}
inputFile.close();
LOG_INFO("Loaded %zu poly lines from file: %s\n", polyLines.size(), fileName.c_str());
return SUCCESS;
}
void LaserDataLoader::FreeLaserScanData(std::vector<std::pair<EVzResultDataType, SVzLaserLineData>>& laserLines)
{
LOG_DEBUG("Freeing unified laser scan data, line count: %zu\n", laserLines.size());
@ -487,10 +637,39 @@ int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNL3DPositi
int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNLPointXYZRGBA& sData, SVzNL2DLRPoint& s2DData)
{
float X, Y, Z;
float r, g, b;
float leftX, leftY;
float rightX, rightY;
sscanf(data.c_str(), "{%f,%f,%f,%f,%f,%f}-{%f,%f}-{%f,%f}", &X, &Y, &Z, &r, &g, &b, &leftX, &leftY, &rightX, &rightY);
// 尝试新格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A}
int R, G, B, A;
int parsed = sscanf(data.c_str(),
" { %f , %f , %f } - { %f , %f } - { %f , %f } - { %d , %d , %d , %d }",
&X, &Y, &Z, &leftX, &leftY, &rightX, &rightY, &R, &G, &B, &A);
if (parsed >= 11) {
sData.x = X;
sData.y = Y;
sData.z = Z;
// R,G,B 是 0-255 整数值
int nRGB = B;
nRGB <<= 8;
nRGB += G;
nRGB <<= 8;
nRGB += R;
sData.nRGB = nRGB;
s2DData.sLeft.x = leftX;
s2DData.sLeft.y = leftY;
s2DData.sRight.x = rightX;
s2DData.sRight.y = rightY;
return SUCCESS;
}
// 旧格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} r,g,b 为 0~1 浮点值
float r, g, b;
sscanf(data.c_str(),
" { %f , %f , %f , %f , %f , %f } - { %f , %f } - { %f , %f }",
&X, &Y, &Z, &r, &g, &b, &leftX, &leftY, &rightX, &rightY);
sData.x = X;
sData.y = Y;
sData.z = Z;
@ -524,27 +703,38 @@ int LaserDataLoader::_GetLaserType(const std::string& fileName, EVzResultDataTyp
bool bFind = false;
while (std::getline(inputFile, linedata)) {
// 去除行末的 \r 字符(处理 Windows 格式的 CR LF 换行符)
TrimCarriageReturn(linedata);
if (linedata.find("{") == 0) {
// 修复正则表达式以匹配实际数据格式
// XYZ格式: {x,y,z}-{leftX,leftY}-{rightX,rightY}
// RGBD格式: {x,y,z,r,g,b}-{leftX,leftY}-{rightX,rightY}
// 更宽松的正则表达式,允许更多的空格变化
std::regex xyzPattern(R"(\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\}\s*-\s*\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\}\s*-\s*\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\})");
std::regex rgbdPattern(R"(\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\}\s*-\s*\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\}\s*-\s*\{\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*,\s*[+-]?(?:\d+\.?\d*|\.\d+)\s*\})");
// 统计 { 的数量判断格式
int braceCount = 0;
for (char c : linedata) {
if (c == '{') braceCount++;
}
// 先尝试匹配RGBD格式6个数字
if (std::regex_match(linedata, rgbdPattern)) {
if (braceCount == 4) {
// 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A}
eDataType = keResultDataType_PointXYZRGBA;
bFind = true;
} else if (braceCount == 3) {
// 检查第一组的逗号数量区分XYZ和旧RGBD
size_t firstClose = linedata.find('}');
if (firstClose != std::string::npos) {
int commaCount = 0;
for (size_t i = 0; i < firstClose; ++i) {
if (linedata[i] == ',') commaCount++;
}
// 再尝试匹配XYZ格式3个数字
else if (std::regex_match(linedata, xyzPattern)) {
if (commaCount >= 5) {
// 旧RGBD格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry}
eDataType = keResultDataType_PointXYZRGBA;
bFind = true;
} else if (commaCount >= 2) {
// XYZ格式: {x,y,z}-{lx,ly}-{rx,ry}
eDataType = keResultDataType_Position;
bFind = true;
}
}
}
break;
}
}

View File

@ -44,11 +44,6 @@ private slots:
*/
void onOpenFile();
/**
* @brief 线
*/
void onOpenSegmentFile();
/**
* @brief 姿
*/
@ -225,6 +220,18 @@ private:
*/
void updateLinePointsDialog();
/**
* @brief
* @return
*/
bool loadPointCloudFile(const QString& fileName);
/**
* @brief 线Poly格式
* @return 线
*/
bool loadSegmentFile(const QString& fileName);
// 点云显示控件
PointCloudGLWidget* m_glWidget;
@ -233,7 +240,6 @@ private:
// 文件操作控件
QPushButton* m_btnOpenFile;
QPushButton* m_btnOpenSegment;
QPushButton* m_btnOpenPose;
QPushButton* m_btnClearAll;

View File

@ -13,6 +13,7 @@
#include <QGridLayout>
#include <cmath>
#include "VrLog.h"
#include "LaserDataLoader.h"
CloudViewMainWindow::CloudViewMainWindow(QWidget* parent)
: QMainWindow(parent)
@ -125,18 +126,12 @@ QGroupBox* CloudViewMainWindow::createFileGroup()
layout->setSpacing(3);
layout->setContentsMargins(5, 5, 5, 5);
m_btnOpenFile = new QPushButton("打开点云", group);
m_btnOpenFile = new QPushButton("打开文件", group);
m_btnOpenFile->setMinimumHeight(24);
m_btnOpenFile->setMaximumHeight(24);
connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile);
layout->addWidget(m_btnOpenFile);
m_btnOpenSegment = new QPushButton("打开线段 {x,y,z}-{x,y,z}", group);
m_btnOpenSegment->setMinimumHeight(24);
m_btnOpenSegment->setMaximumHeight(24);
connect(m_btnOpenSegment, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenSegmentFile);
layout->addWidget(m_btnOpenSegment);
m_btnOpenPose = new QPushButton("打开姿态点 {x,y,z}-{r,p,y}", group);
m_btnOpenPose->setMinimumHeight(24);
m_btnOpenPose->setMaximumHeight(24);
@ -523,6 +518,7 @@ QGroupBox* CloudViewMainWindow::createLineGroup()
m_btnSelectByNumber->setMaximumHeight(24);
m_btnSelectByNumber->setMaximumWidth(50);
connect(m_btnSelectByNumber, &QPushButton::clicked, this, &CloudViewMainWindow::onSelectLineByNumber);
connect(m_lineNumberInput, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onSelectLineByNumber);
inputLayout->addWidget(m_lineNumberInput, 1);
inputLayout->addWidget(m_btnSelectByNumber);
layout->addLayout(inputLayout);
@ -579,17 +575,42 @@ QGroupBox* CloudViewMainWindow::createCloudListGroup()
void CloudViewMainWindow::onOpenFile()
{
// 如果已有打开的文件,提示是否清除
if (m_cloudList->count() > 0) {
auto ret = QMessageBox::question(this, "提示",
"当前已有打开的文件,是否清除?",
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (ret == QMessageBox::Cancel) {
return;
}
if (ret == QMessageBox::Yes) {
onClearAll();
}
}
QString fileName = QFileDialog::getOpenFileName(
this,
"打开点云文件",
"打开文件",
QString(),
"点云文件 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)"
"所有支持格式 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
// 同一文件中可能同时包含点云和线段,两者都尝试加载
bool cloudOk = loadPointCloudFile(fileName);
bool segmentOk = loadSegmentFile(fileName);
if (!cloudOk && !segmentOk) {
QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据");
statusBar()->showMessage("加载失败");
}
}
bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName)
{
statusBar()->showMessage("正在加载点云...");
QFileInfo fileInfo(fileName);
@ -601,9 +622,8 @@ void CloudViewMainWindow::onOpenFile()
int result = m_converter->loadFromFile(fileName.toStdString(), rgbCloud);
if (result != 0) {
QMessageBox::critical(this, "错误", QString("加载点云失败: %1").arg(QString::fromStdString(m_converter->getLastError())));
statusBar()->showMessage("加载失败");
return;
LOG_INFO("[CloudView] Load point cloud failed: %s\n", m_converter->getLastError().c_str());
return false;
}
// 保存原始完整点云 XYZ用于旋转/线上点等功能)
@ -632,11 +652,9 @@ void CloudViewMainWindow::onOpenFile()
}
if (hadColor) {
// 有颜色数据:使用 addPointCloud(PointCloudXYZRGB) 显示原始颜色
m_glWidget->addPointCloud(rgbCloud, cloudName);
LOG_INFO("[CloudView] Loaded with original color, points: %zu\n", rgbCloud.size());
} else {
// 无颜色数据:使用 addPointCloud(PointCloudXYZ) 显示(颜色表轮换)
m_glWidget->addPointCloud(m_originalCloud, cloudName);
LOG_INFO("[CloudView] Loaded without color (color table), points: %zu\n", m_originalCloud.size());
}
@ -664,86 +682,60 @@ void CloudViewMainWindow::onOpenFile()
m_cloudList->addItem(itemText);
statusBar()->showMessage(QString("已加载 %1 个点%2").arg(m_converter->getLoadedPointCount()).arg(hadColor ? " (彩色)" : ""));
return true;
}
void CloudViewMainWindow::onOpenSegmentFile()
bool CloudViewMainWindow::loadSegmentFile(const QString& fileName)
{
QString fileName = QFileDialog::getOpenFileName(
this,
"打开线段文件",
QString(),
"文本文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
statusBar()->showMessage("正在加载线段...");
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName));
statusBar()->showMessage("加载失败");
return;
LaserDataLoader loader;
std::vector<std::vector<SVzNLPointXYZRGBA>> polyLines;
int result = loader.LoadPolySegments(fileName.toStdString(), polyLines);
if (result != 0) {
LOG_INFO("[CloudView] Load segments failed: %s\n", loader.GetLastError().c_str());
return false;
}
if (polyLines.empty()) {
return false;
}
// 将折线点转换为线段(使用点自带的颜色)
QVector<LineSegment> segments;
QTextStream in(&file);
int lineNum = 0;
int validCount = 0;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
lineNum++;
// 跳过空行和注释
if (line.isEmpty() || line.startsWith('#')) {
continue;
for (const auto& polyLine : polyLines) {
for (size_t i = 0; i + 1 < polyLine.size(); ++i) {
const auto& p1 = polyLine[i];
const auto& p2 = polyLine[i + 1];
// 取起点颜色作为线段颜色nRGB 为 BGR 打包格式
float r = ((p1.nRGB >> 0) & 0xFF) / 255.0f;
float g = ((p1.nRGB >> 8) & 0xFF) / 255.0f;
float b = ((p1.nRGB >> 16) & 0xFF) / 255.0f;
segments.append(LineSegment(
p1.x, p1.y, p1.z,
p2.x, p2.y, p2.z,
r, g, b));
}
// 解析格式:{x,y,z}-{x,y,z}
QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}");
if (regex.indexIn(line) == -1) {
LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{x,y,z}\n", lineNum);
continue;
}
QString point1Str = regex.cap(1);
QString point2Str = regex.cap(2);
QStringList p1 = point1Str.split(',');
QStringList p2 = point2Str.split(',');
if (p1.size() != 3 || p2.size() != 3) {
LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum);
continue;
}
bool ok = true;
float x1 = p1[0].toFloat(&ok); if (!ok) continue;
float y1 = p1[1].toFloat(&ok); if (!ok) continue;
float z1 = p1[2].toFloat(&ok); if (!ok) continue;
float x2 = p2[0].toFloat(&ok); if (!ok) continue;
float y2 = p2[1].toFloat(&ok); if (!ok) continue;
float z2 = p2[2].toFloat(&ok); if (!ok) continue;
// 默认白色
segments.append(LineSegment(x1, y1, z1, x2, y2, z2, 1.0f, 1.0f, 1.0f));
validCount++;
}
file.close();
if (segments.isEmpty()) {
QMessageBox::warning(this, "警告", "文件中没有有效的线段数据");
statusBar()->showMessage("加载失败");
return;
return false;
}
m_glWidget->addLineSegments(segments);
statusBar()->showMessage(QString("已加载 %1 条线段").arg(validCount));
LOG_INFO("[CloudView] Loaded %d line segments from %s\n", validCount, fileName.toStdString().c_str());
// 添加到列表
QFileInfo fileInfo(fileName);
int totalPolyCount = static_cast<int>(polyLines.size());
QString itemName = QString("Segments (%1)").arg(fileInfo.fileName());
m_cloudList->addItem(QString("%1 - %2 条折线, %3 条线段")
.arg(itemName).arg(totalPolyCount).arg(segments.size()));
statusBar()->showMessage(QString("已加载 %1 条折线, %2 条线段").arg(totalPolyCount).arg(segments.size()));
LOG_INFO("[CloudView] Loaded %d polylines, %d segments from %s\n",
totalPolyCount, segments.size(), fileName.toStdString().c_str());
return true;
}
void CloudViewMainWindow::onOpenPoseFile()
@ -831,6 +823,8 @@ void CloudViewMainWindow::onOpenPoseFile()
void CloudViewMainWindow::onClearAll()
{
m_glWidget->clearPointClouds();
m_glWidget->clearLineSegments();
m_glWidget->clearPosePoints();
m_cloudList->clear();
m_cloudCount = 0;
m_currentLineNum = 0;

View File

@ -3,7 +3,7 @@
#include <QIcon>
#include "CloudViewMainWindow.h"
#define APP_VERSION "1.1.0"
#define APP_VERSION "1.1.1"
int main(int argc, char* argv[])
{