GrabBag/Tools/CloudView/Src/PointCloudConverter.cpp
2026-01-16 01:04:43 +08:00

484 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PointCloudConverter.h"
#include "LaserDataLoader.h"
#include "VZNL_Types.h"
#include "VrLog.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstring>
PointCloudConverter::PointCloudConverter()
: m_loadedPointCount(0)
, m_loadedLineCount(0)
{
}
PointCloudConverter::~PointCloudConverter()
{
}
std::string PointCloudConverter::getFileExtension(const std::string& fileName)
{
size_t pos = fileName.rfind('.');
if (pos == std::string::npos) {
return "";
}
std::string ext = fileName.substr(pos + 1);
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return ext;
}
int PointCloudConverter::loadFromTxt(const std::string& fileName, PointCloudXYZ& cloud)
{
LaserDataLoader loader;
// 使用 CloudUtils 加载数据
std::vector<std::pair<EVzResultDataType, SVzLaserLineData>> laserLines;
int lineNum = 0;
float scanSpeed = 0.0f;
int maxTimeStamp = 0;
int clockPerSecond = 0;
int result = loader.LoadLaserScanData(fileName, laserLines, lineNum, scanSpeed, maxTimeStamp, clockPerSecond);
if (result != 0) {
m_lastError = "加载文件失败: " + loader.GetLastError();
return result;
}
LOG_INFO("[CloudView] LoadLaserScanData success, laserLines size: %zu, lineNum: %d\n",
laserLines.size(), lineNum);
// 转换为 SVzNL3DPosition 格式
std::vector<std::vector<SVzNL3DPosition>> scanLines;
result = loader.ConvertToSVzNL3DPosition(laserLines, scanLines);
if (result != 0) {
m_lastError = "转换数据失败";
loader.FreeLaserScanData(laserLines);
return result;
}
LOG_INFO("[CloudView] ConvertToSVzNL3DPosition success, scanLines size: %zu\n", scanLines.size());
// 转换为自定义点云格式保留线索引保留所有点包括0,0,0用于旋转
cloud.clear();
size_t totalCount = 0;
int lineIndex = 0;
for (const auto& line : scanLines) {
for (const auto& pos : line) {
totalCount++;
Point3D point;
point.x = static_cast<float>(pos.pt3D.x);
point.y = static_cast<float>(pos.pt3D.y);
point.z = static_cast<float>(pos.pt3D.z);
cloud.push_back(point, lineIndex);
}
lineIndex++;
}
LOG_INFO("[CloudView] Total points: %zu, Lines: %d\n", totalCount, lineIndex);
loader.FreeLaserScanData(laserLines);
m_loadedPointCount = totalCount;
m_loadedLineCount = lineIndex;
return 0;
}
int PointCloudConverter::loadFromTxt(const std::string& fileName, PointCloudXYZRGB& cloud)
{
PointCloudXYZ xyzCloud;
int result = loadFromTxt(fileName, xyzCloud);
if (result != 0) {
return result;
}
cloud.clear();
cloud.reserve(xyzCloud.size());
for (size_t i = 0; i < xyzCloud.points.size(); ++i) {
const auto& pt = xyzCloud.points[i];
Point3DRGB point(pt.x, pt.y, pt.z, 255, 255, 255);
int lineIdx = (i < xyzCloud.lineIndices.size()) ? xyzCloud.lineIndices[i] : 0;
cloud.push_back(point, lineIdx);
}
return 0;
}
bool PointCloudConverter::parsePcdHeader(std::ifstream& file, PcdHeader& header)
{
std::string line;
while (std::getline(file, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
std::istringstream iss(line);
std::string key;
iss >> key;
if (key == "VERSION") {
// 忽略版本
} else if (key == "FIELDS") {
std::string field;
while (iss >> field) {
header.fields.push_back(field);
if (field == "rgb" || field == "rgba") {
header.hasRgb = true;
}
}
} else if (key == "SIZE") {
int size;
while (iss >> size) {
header.fieldSizes.push_back(size);
header.pointSize += size;
}
} else if (key == "TYPE") {
char type;
while (iss >> type) {
header.fieldTypes.push_back(type);
}
} else if (key == "COUNT") {
// 忽略 COUNT
} else if (key == "WIDTH") {
iss >> header.width;
} else if (key == "HEIGHT") {
iss >> header.height;
} else if (key == "VIEWPOINT") {
// 忽略 VIEWPOINT
} else if (key == "POINTS") {
iss >> header.points;
} else if (key == "DATA") {
std::string dataType;
iss >> dataType;
header.isBinary = (dataType == "binary" || dataType == "binary_compressed");
return true; // 头部解析完成
}
}
return false;
}
int PointCloudConverter::loadFromPcd(const std::string& fileName, PointCloudXYZ& cloud)
{
std::ifstream file(fileName, std::ios::binary);
if (!file.is_open()) {
m_lastError = "无法打开文件: " + fileName;
return -1;
}
PcdHeader header;
if (!parsePcdHeader(file, header)) {
m_lastError = "无法解析 PCD 文件头";
return -1;
}
int numPoints = header.points > 0 ? header.points : header.width * header.height;
LOG_INFO("[CloudView] PCD header: points=%d, width=%d, height=%d, isBinary=%d\n",
numPoints, header.width, header.height, header.isBinary);
cloud.clear();
cloud.reserve(numPoints);
// 查找 x, y, z 字段的索引
int xIdx = -1, yIdx = -1, zIdx = -1;
for (size_t i = 0; i < header.fields.size(); ++i) {
if (header.fields[i] == "x") xIdx = static_cast<int>(i);
else if (header.fields[i] == "y") yIdx = static_cast<int>(i);
else if (header.fields[i] == "z") zIdx = static_cast<int>(i);
}
if (xIdx < 0 || yIdx < 0 || zIdx < 0) {
m_lastError = "PCD 文件缺少 x, y, z 字段";
return -1;
}
if (header.isBinary) {
// 二进制格式
std::vector<char> buffer(header.pointSize);
for (int i = 0; i < numPoints; ++i) {
file.read(buffer.data(), header.pointSize);
if (!file) break;
Point3D pt;
int offset = 0;
for (size_t j = 0; j < header.fields.size(); ++j) {
if (j == static_cast<size_t>(xIdx)) {
memcpy(&pt.x, buffer.data() + offset, sizeof(float));
} else if (j == static_cast<size_t>(yIdx)) {
memcpy(&pt.y, buffer.data() + offset, sizeof(float));
} else if (j == static_cast<size_t>(zIdx)) {
memcpy(&pt.z, buffer.data() + offset, sizeof(float));
}
offset += header.fieldSizes[j];
}
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
cloud.push_back(pt);
}
}
} else {
// ASCII 格式
std::string line;
while (std::getline(file, line) && cloud.size() < static_cast<size_t>(numPoints)) {
std::istringstream iss(line);
std::vector<float> values;
float val;
while (iss >> val) {
values.push_back(val);
}
if (values.size() >= 3 &&
static_cast<size_t>(xIdx) < values.size() &&
static_cast<size_t>(yIdx) < values.size() &&
static_cast<size_t>(zIdx) < values.size()) {
Point3D pt;
pt.x = values[xIdx];
pt.y = values[yIdx];
pt.z = values[zIdx];
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
cloud.push_back(pt);
}
}
}
}
LOG_INFO("[CloudView] Loaded %zu points from PCD\n", cloud.size());
m_loadedPointCount = cloud.size();
m_loadedLineCount = 0; // PCD 文件没有线信息
return 0;
}
int PointCloudConverter::loadFromPcd(const std::string& fileName, PointCloudXYZRGB& cloud)
{
std::ifstream file(fileName, std::ios::binary);
if (!file.is_open()) {
m_lastError = "无法打开文件: " + fileName;
return -1;
}
PcdHeader header;
if (!parsePcdHeader(file, header)) {
m_lastError = "无法解析 PCD 文件头";
return -1;
}
int numPoints = header.points > 0 ? header.points : header.width * header.height;
cloud.clear();
cloud.reserve(numPoints);
// 查找字段索引
int xIdx = -1, yIdx = -1, zIdx = -1, rgbIdx = -1;
for (size_t i = 0; i < header.fields.size(); ++i) {
if (header.fields[i] == "x") xIdx = static_cast<int>(i);
else if (header.fields[i] == "y") yIdx = static_cast<int>(i);
else if (header.fields[i] == "z") zIdx = static_cast<int>(i);
else if (header.fields[i] == "rgb" || header.fields[i] == "rgba") rgbIdx = static_cast<int>(i);
}
if (xIdx < 0 || yIdx < 0 || zIdx < 0) {
m_lastError = "PCD 文件缺少 x, y, z 字段";
return -1;
}
if (header.isBinary) {
std::vector<char> buffer(header.pointSize);
for (int i = 0; i < numPoints; ++i) {
file.read(buffer.data(), header.pointSize);
if (!file) break;
Point3DRGB pt;
int offset = 0;
for (size_t j = 0; j < header.fields.size(); ++j) {
if (j == static_cast<size_t>(xIdx)) {
memcpy(&pt.x, buffer.data() + offset, sizeof(float));
} else if (j == static_cast<size_t>(yIdx)) {
memcpy(&pt.y, buffer.data() + offset, sizeof(float));
} else if (j == static_cast<size_t>(zIdx)) {
memcpy(&pt.z, buffer.data() + offset, sizeof(float));
} else if (j == static_cast<size_t>(rgbIdx)) {
// RGB 通常存储为 packed float
float rgbFloat;
memcpy(&rgbFloat, buffer.data() + offset, sizeof(float));
uint32_t rgb = *reinterpret_cast<uint32_t*>(&rgbFloat);
pt.r = (rgb >> 16) & 0xFF;
pt.g = (rgb >> 8) & 0xFF;
pt.b = rgb & 0xFF;
}
offset += header.fieldSizes[j];
}
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
cloud.push_back(pt);
}
}
} else {
std::string line;
while (std::getline(file, line) && cloud.size() < static_cast<size_t>(numPoints)) {
std::istringstream iss(line);
std::vector<float> values;
float val;
while (iss >> val) {
values.push_back(val);
}
if (values.size() >= 3 &&
static_cast<size_t>(xIdx) < values.size() &&
static_cast<size_t>(yIdx) < values.size() &&
static_cast<size_t>(zIdx) < values.size()) {
Point3DRGB pt;
pt.x = values[xIdx];
pt.y = values[yIdx];
pt.z = values[zIdx];
if (rgbIdx >= 0 && static_cast<size_t>(rgbIdx) < values.size()) {
float rgbFloat = values[rgbIdx];
uint32_t rgb = *reinterpret_cast<uint32_t*>(&rgbFloat);
pt.r = (rgb >> 16) & 0xFF;
pt.g = (rgb >> 8) & 0xFF;
pt.b = rgb & 0xFF;
}
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
cloud.push_back(pt);
}
}
}
}
m_loadedPointCount = cloud.size();
m_loadedLineCount = 0; // PCD 文件没有线信息
return 0;
}
int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZ& cloud)
{
std::string ext = getFileExtension(fileName);
if (ext == "pcd") {
return loadFromPcd(fileName, cloud);
} else if (ext == "txt") {
return loadFromTxt(fileName, cloud);
} else {
m_lastError = "不支持的文件格式: " + ext;
return -1;
}
}
int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZRGB& cloud)
{
std::string ext = getFileExtension(fileName);
if (ext == "pcd") {
return loadFromPcd(fileName, cloud);
} else if (ext == "txt") {
return loadFromTxt(fileName, cloud);
} else {
m_lastError = "不支持的文件格式: " + ext;
return -1;
}
}
int PointCloudConverter::saveToTxt(const std::string& fileName, const PointCloudXYZ& cloud, int lineNum, int linePtNum)
{
if (cloud.empty()) {
m_lastError = "点云数据为空";
return -1;
}
// 转换为 std::vector<std::vector<SVzNL3DPosition>> 格式
std::vector<std::vector<SVzNL3DPosition>> xyzData;
xyzData.resize(lineNum);
for (int line = 0; line < lineNum; ++line) {
xyzData[line].resize(linePtNum);
}
// 填充数据
size_t ptIdx = 0;
for (int line = 0; line < lineNum; ++line) {
for (int j = 0; j < linePtNum; ++j) {
if (ptIdx < cloud.points.size()) {
xyzData[line][j].pt3D.x = cloud.points[ptIdx].x;
xyzData[line][j].pt3D.y = cloud.points[ptIdx].y;
xyzData[line][j].pt3D.z = cloud.points[ptIdx].z;
xyzData[line][j].nPointIdx = j;
ptIdx++;
} else {
// 填充零点
xyzData[line][j].pt3D.x = 0;
xyzData[line][j].pt3D.y = 0;
xyzData[line][j].pt3D.z = 0;
xyzData[line][j].nPointIdx = j;
}
}
}
// 使用 LaserDataLoader 保存
LaserDataLoader loader;
int result = loader.DebugSaveLaser(fileName, xyzData);
if (result != 0) {
m_lastError = "保存文件失败: " + loader.GetLastError();
return result;
}
LOG_INFO("[CloudView] Saved %zu points to %s (lineNum=%d, linePtNum=%d)\n",
cloud.points.size(), fileName.c_str(), lineNum, linePtNum);
return 0;
}
int PointCloudConverter::rotateCloud(const PointCloudXYZ& cloud, PointCloudXYZ& rotatedCloud,
int lineNum, int linePtNum, int& newLineNum, int& newLinePtNum)
{
if (cloud.empty()) {
m_lastError = "无效的点云数据";
return -1;
}
if (lineNum <= 0 || linePtNum <= 0) {
m_lastError = "无效的线信息";
return -1;
}
// 检查点数是否匹配
size_t expectedPoints = static_cast<size_t>(lineNum) * static_cast<size_t>(linePtNum);
if (cloud.points.size() != expectedPoints) {
m_lastError = "点云数据与线信息不匹配,无法旋转";
return -1;
}
// 矩阵转置:原来的行列互换
// 原来: lineNum 条线,每条线 linePtNum 个点
// 转置后: linePtNum 条线,每条线 lineNum 个点
newLineNum = linePtNum;
newLinePtNum = lineNum;
rotatedCloud.clear();
rotatedCloud.reserve(cloud.points.size());
// 转置操作:
// 原来第 line 条线的第 col 个点 -> 新的第 col 条线的第 line 个点
// 原索引: line * linePtNum + col
// 新索引: col * lineNum + line
for (int newLine = 0; newLine < newLineNum; ++newLine) {
for (int newCol = 0; newCol < newLinePtNum; ++newCol) {
// newLine 对应原来的 col点在线内的位置
// newCol 对应原来的 line线号
int oldLine = newCol;
int oldCol = newLine;
size_t oldIdx = static_cast<size_t>(oldLine) * static_cast<size_t>(linePtNum) + static_cast<size_t>(oldCol);
const Point3D& pt = cloud.points[oldIdx];
rotatedCloud.push_back(pt, newLine);
}
}
LOG_INFO("[CloudView] Rotated cloud: %zu points, %d lines -> %d lines\n",
rotatedCloud.points.size(), lineNum, newLineNum);
return 0;
}