feat(点云加载): 添加PLY文件格式支持并更新版本号至1.1.4
实现PLY文件加载功能,支持ASCII和二进制格式(大端/小端) 扩展文件对话框过滤器包含PLY格式 统一PLY和PCD文件的颜色处理逻辑
This commit is contained in:
parent
c98aa7b5d6
commit
7e887aaeee
@ -74,6 +74,12 @@ public:
|
|||||||
int loadFromPcd(const std::string& fileName, PointCloudXYZ& cloud);
|
int loadFromPcd(const std::string& fileName, PointCloudXYZ& cloud);
|
||||||
int loadFromPcd(const std::string& fileName, PointCloudXYZRGB& cloud);
|
int loadFromPcd(const std::string& fileName, PointCloudXYZRGB& cloud);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从 ply 文件加载点云(支持 ASCII / Binary Little-Endian / Binary Big-Endian)
|
||||||
|
*/
|
||||||
|
int loadFromPly(const std::string& fileName, PointCloudXYZ& cloud);
|
||||||
|
int loadFromPly(const std::string& fileName, PointCloudXYZRGB& cloud);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 根据文件扩展名自动选择加载方式
|
* @brief 根据文件扩展名自动选择加载方式
|
||||||
*/
|
*/
|
||||||
@ -141,6 +147,48 @@ private:
|
|||||||
|
|
||||||
bool parsePcdHeader(std::ifstream& file, PcdHeader& header);
|
bool parsePcdHeader(std::ifstream& file, PcdHeader& header);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PLY 属性描述
|
||||||
|
*/
|
||||||
|
struct PlyProperty
|
||||||
|
{
|
||||||
|
std::string name; // 属性名 (x, y, z, red, green, blue, alpha, nx, ny, nz ...)
|
||||||
|
std::string type; // 数据类型 (float, double, uchar, int, short ...)
|
||||||
|
int byteSize = 0; // 字节数
|
||||||
|
bool isList = false; // 是否是 list 类型(如 face 的 vertex_indices)
|
||||||
|
std::string listCountType; // list 的计数类型
|
||||||
|
std::string listValueType; // list 的值类型
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PLY 元素描述
|
||||||
|
*/
|
||||||
|
struct PlyElement
|
||||||
|
{
|
||||||
|
std::string name; // 元素名 (vertex, face ...)
|
||||||
|
int count = 0; // 元素数量
|
||||||
|
std::vector<PlyProperty> properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PLY 文件头
|
||||||
|
*/
|
||||||
|
struct PlyHeader
|
||||||
|
{
|
||||||
|
enum Format { ASCII, BINARY_LE, BINARY_BE };
|
||||||
|
Format format = ASCII;
|
||||||
|
std::vector<PlyElement> elements;
|
||||||
|
int vertexElementIndex = -1; // vertex 元素在 elements 中的索引
|
||||||
|
};
|
||||||
|
|
||||||
|
bool parsePlyHeader(std::ifstream& file, PlyHeader& header);
|
||||||
|
int getPlyTypeByteSize(const std::string& type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算 PLY 元素中一个非 list 记录的总字节数
|
||||||
|
*/
|
||||||
|
int calcPlyElementStride(const PlyElement& element);
|
||||||
|
|
||||||
std::string m_lastError;
|
std::string m_lastError;
|
||||||
size_t m_loadedPointCount;
|
size_t m_loadedPointCount;
|
||||||
int m_loadedLineCount;
|
int m_loadedLineCount;
|
||||||
|
|||||||
@ -759,7 +759,7 @@ void CloudViewMainWindow::onOpenFile()
|
|||||||
this,
|
this,
|
||||||
"打开文件",
|
"打开文件",
|
||||||
QString(),
|
QString(),
|
||||||
"所有支持格式 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)"
|
"所有支持格式 (*.pcd *.ply *.txt);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;所有文件 (*.*)"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fileName.isEmpty()) {
|
if (fileName.isEmpty()) {
|
||||||
@ -806,8 +806,8 @@ bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName)
|
|||||||
// 根据是否有颜色选择显示方式
|
// 根据是否有颜色选择显示方式
|
||||||
bool hadColor = m_converter->lastLoadHadColor();
|
bool hadColor = m_converter->lastLoadHadColor();
|
||||||
|
|
||||||
if (ext == "pcd") {
|
if (ext == "pcd" || ext == "ply") {
|
||||||
// PCD 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等
|
// PCD/PLY 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等
|
||||||
if (!hadColor) {
|
if (!hadColor) {
|
||||||
// 无 rgb 字段:默认灰色显示
|
// 无 rgb 字段:默认灰色显示
|
||||||
for (size_t i = 0; i < rgbCloud.points.size(); ++i) {
|
for (size_t i = 0; i < rgbCloud.points.size(); ++i) {
|
||||||
@ -817,7 +817,7 @@ bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_glWidget->addPointCloud(rgbCloud, cloudName);
|
m_glWidget->addPointCloud(rgbCloud, cloudName);
|
||||||
LOG_INFO("[CloudView] PCD loaded with XYZRGB path (hasRgbField=%d), points: %zu\n",
|
LOG_INFO("[CloudView] PCD/PLY loaded with XYZRGB path (hasRgbField=%d), points: %zu\n",
|
||||||
m_converter->lastLoadHadColor(), rgbCloud.size());
|
m_converter->lastLoadHadColor(), rgbCloud.size());
|
||||||
} else if (hadColor) {
|
} else if (hadColor) {
|
||||||
m_glWidget->addPointCloud(rgbCloud, cloudName);
|
m_glWidget->addPointCloud(rgbCloud, cloudName);
|
||||||
|
|||||||
@ -445,6 +445,8 @@ int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZ
|
|||||||
return loadFromPcd(fileName, cloud);
|
return loadFromPcd(fileName, cloud);
|
||||||
} else if (ext == "txt") {
|
} else if (ext == "txt") {
|
||||||
return loadFromTxt(fileName, cloud);
|
return loadFromTxt(fileName, cloud);
|
||||||
|
} else if (ext == "ply") {
|
||||||
|
return loadFromPly(fileName, cloud);
|
||||||
} else {
|
} else {
|
||||||
m_lastError = "不支持的文件格式: " + ext;
|
m_lastError = "不支持的文件格式: " + ext;
|
||||||
return -1;
|
return -1;
|
||||||
@ -459,6 +461,8 @@ int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZ
|
|||||||
return loadFromPcd(fileName, cloud);
|
return loadFromPcd(fileName, cloud);
|
||||||
} else if (ext == "txt") {
|
} else if (ext == "txt") {
|
||||||
return loadFromTxt(fileName, cloud);
|
return loadFromTxt(fileName, cloud);
|
||||||
|
} else if (ext == "ply") {
|
||||||
|
return loadFromPly(fileName, cloud);
|
||||||
} else {
|
} else {
|
||||||
m_lastError = "不支持的文件格式: " + ext;
|
m_lastError = "不支持的文件格式: " + ext;
|
||||||
return -1;
|
return -1;
|
||||||
@ -513,6 +517,507 @@ int PointCloudConverter::saveToTxt(const std::string& fileName, const PointCloud
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PointCloudConverter::getPlyTypeByteSize(const std::string& type)
|
||||||
|
{
|
||||||
|
if (type == "char" || type == "int8") return 1;
|
||||||
|
if (type == "uchar" || type == "uint8") return 1;
|
||||||
|
if (type == "short" || type == "int16") return 2;
|
||||||
|
if (type == "ushort" || type == "uint16") return 2;
|
||||||
|
if (type == "int" || type == "int32") return 4;
|
||||||
|
if (type == "uint" || type == "uint32") return 4;
|
||||||
|
if (type == "float" || type == "float32") return 4;
|
||||||
|
if (type == "double" || type == "float64") return 8;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PointCloudConverter::calcPlyElementStride(const PlyElement& element)
|
||||||
|
{
|
||||||
|
int stride = 0;
|
||||||
|
for (const auto& prop : element.properties) {
|
||||||
|
if (prop.isList) return -1; // list 类型无法计算固定 stride
|
||||||
|
stride += prop.byteSize;
|
||||||
|
}
|
||||||
|
return stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PointCloudConverter::parsePlyHeader(std::ifstream& file, PlyHeader& header)
|
||||||
|
{
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
// 第一行必须是 "ply"
|
||||||
|
if (!std::getline(file, line)) return false;
|
||||||
|
// 去除行尾可能的 \r
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
if (line != "ply") return false;
|
||||||
|
|
||||||
|
PlyElement* currentElement = nullptr;
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
// 去除行尾 \r
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
if (line.empty()) continue;
|
||||||
|
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::string keyword;
|
||||||
|
iss >> keyword;
|
||||||
|
|
||||||
|
if (keyword == "format") {
|
||||||
|
std::string fmt;
|
||||||
|
iss >> fmt;
|
||||||
|
if (fmt == "ascii") header.format = PlyHeader::ASCII;
|
||||||
|
else if (fmt == "binary_little_endian") header.format = PlyHeader::BINARY_LE;
|
||||||
|
else if (fmt == "binary_big_endian") header.format = PlyHeader::BINARY_BE;
|
||||||
|
else return false;
|
||||||
|
} else if (keyword == "comment" || keyword == "obj_info") {
|
||||||
|
// 跳过注释
|
||||||
|
} else if (keyword == "element") {
|
||||||
|
PlyElement elem;
|
||||||
|
iss >> elem.name >> elem.count;
|
||||||
|
header.elements.push_back(elem);
|
||||||
|
currentElement = &header.elements.back();
|
||||||
|
if (elem.name == "vertex") {
|
||||||
|
header.vertexElementIndex = static_cast<int>(header.elements.size()) - 1;
|
||||||
|
}
|
||||||
|
} else if (keyword == "property") {
|
||||||
|
if (!currentElement) return false;
|
||||||
|
std::string secondToken;
|
||||||
|
iss >> secondToken;
|
||||||
|
|
||||||
|
PlyProperty prop;
|
||||||
|
if (secondToken == "list") {
|
||||||
|
// property list <count_type> <value_type> <name>
|
||||||
|
prop.isList = true;
|
||||||
|
iss >> prop.listCountType >> prop.listValueType >> prop.name;
|
||||||
|
prop.byteSize = 0; // list 类型没有固定大小
|
||||||
|
} else {
|
||||||
|
// property <type> <name>
|
||||||
|
prop.type = secondToken;
|
||||||
|
iss >> prop.name;
|
||||||
|
prop.byteSize = getPlyTypeByteSize(prop.type);
|
||||||
|
prop.isList = false;
|
||||||
|
}
|
||||||
|
currentElement->properties.push_back(prop);
|
||||||
|
} else if (keyword == "end_header") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从二进制缓冲区读取指定 PLY 类型的值并转为 float
|
||||||
|
*/
|
||||||
|
static float readPlyValueAsFloat(const char* data, const std::string& type, bool swapEndian)
|
||||||
|
{
|
||||||
|
if (type == "float" || type == "float32") {
|
||||||
|
float v;
|
||||||
|
memcpy(&v, data, sizeof(float));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[3]);
|
||||||
|
std::swap(p[1], p[2]);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
} else if (type == "double" || type == "float64") {
|
||||||
|
double v;
|
||||||
|
memcpy(&v, data, sizeof(double));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[7]);
|
||||||
|
std::swap(p[1], p[6]);
|
||||||
|
std::swap(p[2], p[5]);
|
||||||
|
std::swap(p[3], p[4]);
|
||||||
|
}
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "int" || type == "int32") {
|
||||||
|
int32_t v;
|
||||||
|
memcpy(&v, data, sizeof(int32_t));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[3]);
|
||||||
|
std::swap(p[1], p[2]);
|
||||||
|
}
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "uint" || type == "uint32") {
|
||||||
|
uint32_t v;
|
||||||
|
memcpy(&v, data, sizeof(uint32_t));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[3]);
|
||||||
|
std::swap(p[1], p[2]);
|
||||||
|
}
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "short" || type == "int16") {
|
||||||
|
int16_t v;
|
||||||
|
memcpy(&v, data, sizeof(int16_t));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[1]);
|
||||||
|
}
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "ushort" || type == "uint16") {
|
||||||
|
uint16_t v;
|
||||||
|
memcpy(&v, data, sizeof(uint16_t));
|
||||||
|
if (swapEndian) {
|
||||||
|
char* p = reinterpret_cast<char*>(&v);
|
||||||
|
std::swap(p[0], p[1]);
|
||||||
|
}
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "uchar" || type == "uint8") {
|
||||||
|
uint8_t v;
|
||||||
|
memcpy(&v, data, sizeof(uint8_t));
|
||||||
|
return static_cast<float>(v);
|
||||||
|
} else if (type == "char" || type == "int8") {
|
||||||
|
int8_t v;
|
||||||
|
memcpy(&v, data, sizeof(int8_t));
|
||||||
|
return static_cast<float>(v);
|
||||||
|
}
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从二进制缓冲区读取指定 PLY 类型的值并转为 uint8_t(用于颜色)
|
||||||
|
*/
|
||||||
|
static uint8_t readPlyValueAsUint8(const char* data, const std::string& type, bool swapEndian)
|
||||||
|
{
|
||||||
|
float v = readPlyValueAsFloat(data, type, swapEndian);
|
||||||
|
// 如果是 float/double 类型的颜色(0~1范围),需要映射到 0~255
|
||||||
|
if (type == "float" || type == "float32" || type == "double" || type == "float64") {
|
||||||
|
if (v >= 0.0f && v <= 1.0f) {
|
||||||
|
return static_cast<uint8_t>(v * 255.0f + 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 截断到 0~255
|
||||||
|
if (v < 0.0f) return 0;
|
||||||
|
if (v > 255.0f) return 255;
|
||||||
|
return static_cast<uint8_t>(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
int PointCloudConverter::loadFromPly(const std::string& fileName, PointCloudXYZ& cloud)
|
||||||
|
{
|
||||||
|
std::ifstream file(fileName, std::ios::binary);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
m_lastError = "无法打开文件: " + fileName;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlyHeader header;
|
||||||
|
if (!parsePlyHeader(file, header)) {
|
||||||
|
m_lastError = "无法解析 PLY 文件头";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.vertexElementIndex < 0) {
|
||||||
|
m_lastError = "PLY 文件中未找到 vertex 元素";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlyElement& vertexElem = header.elements[header.vertexElementIndex];
|
||||||
|
int numPoints = vertexElem.count;
|
||||||
|
|
||||||
|
// 查找 x, y, z 属性索引
|
||||||
|
int xIdx = -1, yIdx = -1, zIdx = -1;
|
||||||
|
for (size_t i = 0; i < vertexElem.properties.size(); ++i) {
|
||||||
|
const std::string& name = vertexElem.properties[i].name;
|
||||||
|
if (name == "x") xIdx = static_cast<int>(i);
|
||||||
|
else if (name == "y") yIdx = static_cast<int>(i);
|
||||||
|
else if (name == "z") zIdx = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xIdx < 0 || yIdx < 0 || zIdx < 0) {
|
||||||
|
m_lastError = "PLY 文件缺少 x, y, z 属性";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[CloudView] PLY header: vertex count=%d, format=%d\n", numPoints, header.format);
|
||||||
|
|
||||||
|
cloud.clear();
|
||||||
|
cloud.reserve(numPoints);
|
||||||
|
|
||||||
|
bool swapEndian = (header.format == PlyHeader::BINARY_BE);
|
||||||
|
|
||||||
|
if (header.format == PlyHeader::ASCII) {
|
||||||
|
// 跳过 vertex 之前的元素
|
||||||
|
for (int ei = 0; ei < header.vertexElementIndex; ++ei) {
|
||||||
|
const PlyElement& elem = header.elements[ei];
|
||||||
|
for (int j = 0; j < elem.count; ++j) {
|
||||||
|
std::string skipLine;
|
||||||
|
if (!std::getline(file, skipLine)) {
|
||||||
|
m_lastError = "PLY 文件数据不完整(跳过元素时)";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 vertex 数据
|
||||||
|
std::string line;
|
||||||
|
for (int i = 0; i < numPoints && std::getline(file, line); ++i) {
|
||||||
|
// 去除 \r
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::vector<float> values;
|
||||||
|
float val;
|
||||||
|
while (iss >> val) {
|
||||||
|
values.push_back(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<size_t>(xIdx) < values.size() &&
|
||||||
|
static_cast<size_t>(yIdx) < values.size() &&
|
||||||
|
static_cast<size_t>(zIdx) < values.size()) {
|
||||||
|
Point3D pt(values[xIdx], values[yIdx], values[zIdx]);
|
||||||
|
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
|
||||||
|
cloud.push_back(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Binary 格式(Little-Endian 或 Big-Endian)
|
||||||
|
// 跳过 vertex 之前的元素
|
||||||
|
for (int ei = 0; ei < header.vertexElementIndex; ++ei) {
|
||||||
|
const PlyElement& elem = header.elements[ei];
|
||||||
|
int stride = calcPlyElementStride(elem);
|
||||||
|
if (stride > 0) {
|
||||||
|
// 固定大小元素,直接跳过
|
||||||
|
file.seekg(static_cast<std::streamoff>(stride) * elem.count, std::ios::cur);
|
||||||
|
} else {
|
||||||
|
// 含 list 类型,需要逐条跳过
|
||||||
|
for (int j = 0; j < elem.count; ++j) {
|
||||||
|
for (const auto& prop : elem.properties) {
|
||||||
|
if (prop.isList) {
|
||||||
|
int countSize = getPlyTypeByteSize(prop.listCountType);
|
||||||
|
char countBuf[8] = {};
|
||||||
|
file.read(countBuf, countSize);
|
||||||
|
// 读取 list 计数值
|
||||||
|
uint32_t listCount = 0;
|
||||||
|
if (countSize == 1) listCount = static_cast<uint8_t>(countBuf[0]);
|
||||||
|
else if (countSize == 2) { uint16_t v; memcpy(&v, countBuf, 2); listCount = v; }
|
||||||
|
else if (countSize == 4) { uint32_t v; memcpy(&v, countBuf, 4); listCount = v; }
|
||||||
|
int valueSize = getPlyTypeByteSize(prop.listValueType);
|
||||||
|
file.seekg(static_cast<std::streamoff>(valueSize) * listCount, std::ios::cur);
|
||||||
|
} else {
|
||||||
|
file.seekg(prop.byteSize, std::ios::cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 vertex 的 stride
|
||||||
|
int vertexStride = calcPlyElementStride(vertexElem);
|
||||||
|
if (vertexStride <= 0) {
|
||||||
|
m_lastError = "PLY vertex 元素包含 list 属性,不支持";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算各属性在 vertex 记录中的偏移
|
||||||
|
std::vector<int> offsets(vertexElem.properties.size(), 0);
|
||||||
|
int offset = 0;
|
||||||
|
for (size_t i = 0; i < vertexElem.properties.size(); ++i) {
|
||||||
|
offsets[i] = offset;
|
||||||
|
offset += vertexElem.properties[i].byteSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取所有 vertex 数据
|
||||||
|
std::vector<char> buffer(static_cast<size_t>(vertexStride) * numPoints);
|
||||||
|
file.read(buffer.data(), buffer.size());
|
||||||
|
if (!file) {
|
||||||
|
m_lastError = "PLY 文件数据不完整";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < numPoints; ++i) {
|
||||||
|
const char* record = buffer.data() + static_cast<size_t>(i) * vertexStride;
|
||||||
|
float x = readPlyValueAsFloat(record + offsets[xIdx], vertexElem.properties[xIdx].type, swapEndian);
|
||||||
|
float y = readPlyValueAsFloat(record + offsets[yIdx], vertexElem.properties[yIdx].type, swapEndian);
|
||||||
|
float z = readPlyValueAsFloat(record + offsets[zIdx], vertexElem.properties[zIdx].type, swapEndian);
|
||||||
|
|
||||||
|
if (std::isfinite(x) && std::isfinite(y) && std::isfinite(z)) {
|
||||||
|
cloud.push_back(Point3D(x, y, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[CloudView] Loaded %zu points from PLY\n", cloud.size());
|
||||||
|
m_loadedPointCount = cloud.size();
|
||||||
|
m_loadedLineCount = 0; // PLY 文件没有线信息
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PointCloudConverter::loadFromPly(const std::string& fileName, PointCloudXYZRGB& cloud)
|
||||||
|
{
|
||||||
|
std::ifstream file(fileName, std::ios::binary);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
m_lastError = "无法打开文件: " + fileName;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlyHeader header;
|
||||||
|
if (!parsePlyHeader(file, header)) {
|
||||||
|
m_lastError = "无法解析 PLY 文件头";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.vertexElementIndex < 0) {
|
||||||
|
m_lastError = "PLY 文件中未找到 vertex 元素";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlyElement& vertexElem = header.elements[header.vertexElementIndex];
|
||||||
|
int numPoints = vertexElem.count;
|
||||||
|
|
||||||
|
// 查找属性索引
|
||||||
|
int xIdx = -1, yIdx = -1, zIdx = -1;
|
||||||
|
int rIdx = -1, gIdx = -1, bIdx = -1;
|
||||||
|
for (size_t i = 0; i < vertexElem.properties.size(); ++i) {
|
||||||
|
const std::string& name = vertexElem.properties[i].name;
|
||||||
|
if (name == "x") xIdx = static_cast<int>(i);
|
||||||
|
else if (name == "y") yIdx = static_cast<int>(i);
|
||||||
|
else if (name == "z") zIdx = static_cast<int>(i);
|
||||||
|
else if (name == "red" || name == "r") rIdx = static_cast<int>(i);
|
||||||
|
else if (name == "green" || name == "g") gIdx = static_cast<int>(i);
|
||||||
|
else if (name == "blue" || name == "b") bIdx = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xIdx < 0 || yIdx < 0 || zIdx < 0) {
|
||||||
|
m_lastError = "PLY 文件缺少 x, y, z 属性";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasColor = (rIdx >= 0 && gIdx >= 0 && bIdx >= 0);
|
||||||
|
|
||||||
|
LOG_INFO("[CloudView] PLY header(XYZRGB): vertex count=%d, format=%d, hasColor=%d\n",
|
||||||
|
numPoints, header.format, hasColor);
|
||||||
|
|
||||||
|
cloud.clear();
|
||||||
|
cloud.reserve(numPoints);
|
||||||
|
|
||||||
|
bool swapEndian = (header.format == PlyHeader::BINARY_BE);
|
||||||
|
|
||||||
|
if (header.format == PlyHeader::ASCII) {
|
||||||
|
// 跳过 vertex 之前的元素
|
||||||
|
for (int ei = 0; ei < header.vertexElementIndex; ++ei) {
|
||||||
|
const PlyElement& elem = header.elements[ei];
|
||||||
|
for (int j = 0; j < elem.count; ++j) {
|
||||||
|
std::string skipLine;
|
||||||
|
if (!std::getline(file, skipLine)) {
|
||||||
|
m_lastError = "PLY 文件数据不完整(跳过元素时)";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 vertex 数据
|
||||||
|
std::string line;
|
||||||
|
for (int i = 0; i < numPoints && std::getline(file, line); ++i) {
|
||||||
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::vector<float> values;
|
||||||
|
float val;
|
||||||
|
while (iss >> val) {
|
||||||
|
values.push_back(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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 (hasColor &&
|
||||||
|
static_cast<size_t>(rIdx) < values.size() &&
|
||||||
|
static_cast<size_t>(gIdx) < values.size() &&
|
||||||
|
static_cast<size_t>(bIdx) < values.size()) {
|
||||||
|
pt.r = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, values[rIdx])));
|
||||||
|
pt.g = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, values[gIdx])));
|
||||||
|
pt.b = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, values[bIdx])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
|
||||||
|
cloud.push_back(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Binary 格式
|
||||||
|
// 跳过 vertex 之前的元素
|
||||||
|
for (int ei = 0; ei < header.vertexElementIndex; ++ei) {
|
||||||
|
const PlyElement& elem = header.elements[ei];
|
||||||
|
int stride = calcPlyElementStride(elem);
|
||||||
|
if (stride > 0) {
|
||||||
|
file.seekg(static_cast<std::streamoff>(stride) * elem.count, std::ios::cur);
|
||||||
|
} else {
|
||||||
|
for (int j = 0; j < elem.count; ++j) {
|
||||||
|
for (const auto& prop : elem.properties) {
|
||||||
|
if (prop.isList) {
|
||||||
|
int countSize = getPlyTypeByteSize(prop.listCountType);
|
||||||
|
char countBuf[8] = {};
|
||||||
|
file.read(countBuf, countSize);
|
||||||
|
uint32_t listCount = 0;
|
||||||
|
if (countSize == 1) listCount = static_cast<uint8_t>(countBuf[0]);
|
||||||
|
else if (countSize == 2) { uint16_t v; memcpy(&v, countBuf, 2); listCount = v; }
|
||||||
|
else if (countSize == 4) { uint32_t v; memcpy(&v, countBuf, 4); listCount = v; }
|
||||||
|
int valueSize = getPlyTypeByteSize(prop.listValueType);
|
||||||
|
file.seekg(static_cast<std::streamoff>(valueSize) * listCount, std::ios::cur);
|
||||||
|
} else {
|
||||||
|
file.seekg(prop.byteSize, std::ios::cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 vertex 的 stride
|
||||||
|
int vertexStride = calcPlyElementStride(vertexElem);
|
||||||
|
if (vertexStride <= 0) {
|
||||||
|
m_lastError = "PLY vertex 元素包含 list 属性,不支持";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算各属性偏移
|
||||||
|
std::vector<int> offsets(vertexElem.properties.size(), 0);
|
||||||
|
int offset = 0;
|
||||||
|
for (size_t i = 0; i < vertexElem.properties.size(); ++i) {
|
||||||
|
offsets[i] = offset;
|
||||||
|
offset += vertexElem.properties[i].byteSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取所有 vertex 数据
|
||||||
|
std::vector<char> buffer(static_cast<size_t>(vertexStride) * numPoints);
|
||||||
|
file.read(buffer.data(), buffer.size());
|
||||||
|
if (!file) {
|
||||||
|
m_lastError = "PLY 文件数据不完整";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < numPoints; ++i) {
|
||||||
|
const char* record = buffer.data() + static_cast<size_t>(i) * vertexStride;
|
||||||
|
|
||||||
|
Point3DRGB pt;
|
||||||
|
pt.x = readPlyValueAsFloat(record + offsets[xIdx], vertexElem.properties[xIdx].type, swapEndian);
|
||||||
|
pt.y = readPlyValueAsFloat(record + offsets[yIdx], vertexElem.properties[yIdx].type, swapEndian);
|
||||||
|
pt.z = readPlyValueAsFloat(record + offsets[zIdx], vertexElem.properties[zIdx].type, swapEndian);
|
||||||
|
|
||||||
|
if (hasColor) {
|
||||||
|
pt.r = readPlyValueAsUint8(record + offsets[rIdx], vertexElem.properties[rIdx].type, swapEndian);
|
||||||
|
pt.g = readPlyValueAsUint8(record + offsets[gIdx], vertexElem.properties[gIdx].type, swapEndian);
|
||||||
|
pt.b = readPlyValueAsUint8(record + offsets[bIdx], vertexElem.properties[bIdx].type, swapEndian);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::isfinite(pt.x) && std::isfinite(pt.y) && std::isfinite(pt.z)) {
|
||||||
|
cloud.push_back(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[CloudView] Loaded %zu points from PLY (hasColor=%d)\n", cloud.size(), hasColor);
|
||||||
|
m_loadedPointCount = cloud.size();
|
||||||
|
m_loadedLineCount = 0; // PLY 文件没有线信息
|
||||||
|
m_lastLoadHadColor = hasColor;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int PointCloudConverter::rotateCloud(const PointCloudXYZ& cloud, PointCloudXYZ& rotatedCloud,
|
int PointCloudConverter::rotateCloud(const PointCloudXYZ& cloud, PointCloudXYZ& rotatedCloud,
|
||||||
int lineNum, int linePtNum, int& newLineNum, int& newLinePtNum)
|
int lineNum, int linePtNum, int& newLineNum, int& newLinePtNum)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include "CloudViewMainWindow.h"
|
#include "CloudViewMainWindow.h"
|
||||||
|
|
||||||
#define APP_VERSION "1.1.3"
|
#define APP_VERSION "1.1.4"
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user