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, 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<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;
|
||||
size_t m_loadedPointCount;
|
||||
int m_loadedLineCount;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<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 lineNum, int linePtNum, int& newLineNum, int& newLinePtNum)
|
||||
{
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include <QIcon>
|
||||
#include "CloudViewMainWindow.h"
|
||||
|
||||
#define APP_VERSION "1.1.3"
|
||||
#define APP_VERSION "1.1.4"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user