diff --git a/CloudView/Inc/PointCloudConverter.h b/CloudView/Inc/PointCloudConverter.h index 04b609e..6925a45 100644 --- a/CloudView/Inc/PointCloudConverter.h +++ b/CloudView/Inc/PointCloudConverter.h @@ -74,6 +74,12 @@ public: int loadFromPcd(const std::string& fileName, PointCloudXYZ& 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 根据文件扩展名自动选择加载方式 */ @@ -141,6 +147,48 @@ private: 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 properties; + }; + + /** + * @brief PLY 文件头 + */ + struct PlyHeader + { + enum Format { ASCII, BINARY_LE, BINARY_BE }; + Format format = ASCII; + std::vector 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; size_t m_loadedPointCount; int m_loadedLineCount; diff --git a/CloudView/Src/CloudViewMainWindow.cpp b/CloudView/Src/CloudViewMainWindow.cpp index bfd3786..73fe5ed 100644 --- a/CloudView/Src/CloudViewMainWindow.cpp +++ b/CloudView/Src/CloudViewMainWindow.cpp @@ -759,7 +759,7 @@ void CloudViewMainWindow::onOpenFile() this, "打开文件", QString(), - "所有支持格式 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)" + "所有支持格式 (*.pcd *.ply *.txt);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;所有文件 (*.*)" ); if (fileName.isEmpty()) { @@ -806,8 +806,8 @@ bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) // 根据是否有颜色选择显示方式 bool hadColor = m_converter->lastLoadHadColor(); - if (ext == "pcd") { - // PCD 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等 + if (ext == "pcd" || ext == "ply") { + // PCD/PLY 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等 if (!hadColor) { // 无 rgb 字段:默认灰色显示 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); - 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()); } else if (hadColor) { m_glWidget->addPointCloud(rgbCloud, cloudName); diff --git a/CloudView/Src/PointCloudConverter.cpp b/CloudView/Src/PointCloudConverter.cpp index 8659e38..ad3ab06 100644 --- a/CloudView/Src/PointCloudConverter.cpp +++ b/CloudView/Src/PointCloudConverter.cpp @@ -445,6 +445,8 @@ int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZ return loadFromPcd(fileName, cloud); } else if (ext == "txt") { return loadFromTxt(fileName, cloud); + } else if (ext == "ply") { + return loadFromPly(fileName, cloud); } else { m_lastError = "不支持的文件格式: " + ext; return -1; @@ -459,6 +461,8 @@ int PointCloudConverter::loadFromFile(const std::string& fileName, PointCloudXYZ return loadFromPcd(fileName, cloud); } else if (ext == "txt") { return loadFromTxt(fileName, cloud); + } else if (ext == "ply") { + return loadFromPly(fileName, cloud); } else { m_lastError = "不支持的文件格式: " + ext; return -1; @@ -513,6 +517,507 @@ int PointCloudConverter::saveToTxt(const std::string& fileName, const PointCloud 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(header.elements.size()) - 1; + } + } else if (keyword == "property") { + if (!currentElement) return false; + std::string secondToken; + iss >> secondToken; + + PlyProperty prop; + if (secondToken == "list") { + // property list + prop.isList = true; + iss >> prop.listCountType >> prop.listValueType >> prop.name; + prop.byteSize = 0; // list 类型没有固定大小 + } else { + // property + 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(&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(&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(v); + } else if (type == "int" || type == "int32") { + int32_t v; + memcpy(&v, data, sizeof(int32_t)); + if (swapEndian) { + char* p = reinterpret_cast(&v); + std::swap(p[0], p[3]); + std::swap(p[1], p[2]); + } + return static_cast(v); + } else if (type == "uint" || type == "uint32") { + uint32_t v; + memcpy(&v, data, sizeof(uint32_t)); + if (swapEndian) { + char* p = reinterpret_cast(&v); + std::swap(p[0], p[3]); + std::swap(p[1], p[2]); + } + return static_cast(v); + } else if (type == "short" || type == "int16") { + int16_t v; + memcpy(&v, data, sizeof(int16_t)); + if (swapEndian) { + char* p = reinterpret_cast(&v); + std::swap(p[0], p[1]); + } + return static_cast(v); + } else if (type == "ushort" || type == "uint16") { + uint16_t v; + memcpy(&v, data, sizeof(uint16_t)); + if (swapEndian) { + char* p = reinterpret_cast(&v); + std::swap(p[0], p[1]); + } + return static_cast(v); + } else if (type == "uchar" || type == "uint8") { + uint8_t v; + memcpy(&v, data, sizeof(uint8_t)); + return static_cast(v); + } else if (type == "char" || type == "int8") { + int8_t v; + memcpy(&v, data, sizeof(int8_t)); + return static_cast(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(v * 255.0f + 0.5f); + } + } + // 截断到 0~255 + if (v < 0.0f) return 0; + if (v > 255.0f) return 255; + return static_cast(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(i); + else if (name == "y") yIdx = static_cast(i); + else if (name == "z") zIdx = static_cast(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 values; + float val; + while (iss >> val) { + values.push_back(val); + } + + if (static_cast(xIdx) < values.size() && + static_cast(yIdx) < values.size() && + static_cast(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(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(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(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 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 buffer(static_cast(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(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(i); + else if (name == "y") yIdx = static_cast(i); + else if (name == "z") zIdx = static_cast(i); + else if (name == "red" || name == "r") rIdx = static_cast(i); + else if (name == "green" || name == "g") gIdx = static_cast(i); + else if (name == "blue" || name == "b") bIdx = static_cast(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 values; + float val; + while (iss >> val) { + values.push_back(val); + } + + if (static_cast(xIdx) < values.size() && + static_cast(yIdx) < values.size() && + static_cast(zIdx) < values.size()) { + Point3DRGB pt; + pt.x = values[xIdx]; + pt.y = values[yIdx]; + pt.z = values[zIdx]; + + if (hasColor && + static_cast(rIdx) < values.size() && + static_cast(gIdx) < values.size() && + static_cast(bIdx) < values.size()) { + pt.r = static_cast(std::min(255.0f, std::max(0.0f, values[rIdx]))); + pt.g = static_cast(std::min(255.0f, std::max(0.0f, values[gIdx]))); + pt.b = static_cast(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(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(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(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 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 buffer(static_cast(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(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 lineNum, int linePtNum, int& newLineNum, int& newLinePtNum) { diff --git a/CloudView/main.cpp b/CloudView/main.cpp index 1c5a757..88a7742 100644 --- a/CloudView/main.cpp +++ b/CloudView/main.cpp @@ -3,7 +3,7 @@ #include #include "CloudViewMainWindow.h" -#define APP_VERSION "1.1.3" +#define APP_VERSION "1.1.4" int main(int argc, char* argv[]) {