PLY格式点云文件由文件头和数据体两部分组成,文件头中定义了所有元素类型、每个元素包含的属性以及数据类型,解析时需要先完整读取文件头信息,才能正确读取后续的数据内容。

PLY文件头结构说明
PLY文件头以ply开头,接着是format声明格式类型,常见的有ascii 1.0、binary_little_endian 1.0、binary_big_endian 1.0,之后是多个element和property定义,最后以end_header结束。元素定义的基本格式如下:
element 元素名 元素数量 property 属性类型 属性名 property 属性类型 属性名 ...
常见的元素包括vertex(顶点)和face(面片),顶点元素通常包含x、y、z坐标属性,可选颜色、法向量等属性;面片元素通常包含顶点索引列表属性。
元素定义解析实现
首先定义存储元素属性信息的数据结构:
#include <string>
#include <vector>
#include <map>
// 属性类型枚举
enum class PropertyType {
INT8, UINT8, INT16, UINT16, INT32, UINT32, FLOAT32, FLOAT64, LIST
};
// 单个属性定义
struct PropertyDef {
PropertyType type;
std::string name;
// 如果是列表类型,存储列表长度的类型和元素类型
PropertyType listCountType;
PropertyType listItemType;
};
// 单个元素定义
struct ElementDef {
std::string name;
int count;
std::vector<PropertyDef> properties;
};
// 整个PLY文件头信息
struct PLYHeader {
std::string format; // ascii/binary_little_endian/binary_big_endian
std::vector<ElementDef> elements;
// 元素名到索引的映射,方便快速查找
std::map<std::string, int> elementIndexMap;
};
解析文件头核心逻辑
按行读取文件内容,逐行识别关键字:
#include <fstream>
#include <sstream>
#include <iostream>
bool parsePLYHeader(const std::string& filePath, PLYHeader& header) {
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "无法打开文件: " << filePath << std::endl;
return false;
}
std::string line;
// 读取第一行,必须是ply
if (!std::getline(file, line) || line != "ply") {
std::cerr << "不是合法的PLY文件" << std::endl;
return false;
}
ElementDef* currentElement = nullptr;
int elementIdx = 0;
while (std::getline(file, line)) {
// 去除首尾空格
line.erase(0, line.find_first_not_of(" t"));
line.erase(line.find_last_not_of(" t") + 1);
if (line == "end_header") {
break;
}
std::istringstream iss(line);
std::string keyword;
iss >> keyword;
if (keyword == "format") {
iss >> header.format;
std::string version;
iss >> version; // 读取版本号,如1.0
} else if (keyword == "element") {
std::string elemName;
int elemCount;
iss >> elemName >> elemCount;
header.elements.push_back({elemName, elemCount, {}});
header.elementIndexMap[elemName] = elementIdx++;
currentElement = &header.elements.back();
} else if (keyword == "property") {
if (currentElement == nullptr) {
std::cerr << "property定义必须在element之后" << std::endl;
return false;
}
std::string typeStr;
iss >> typeStr;
if (typeStr == "list") {
// 列表类型属性:property list 计数类型 元素类型 属性名
std::string countTypeStr, itemTypeStr, propName;
iss >> countTypeStr >> itemTypeStr >> propName;
PropertyDef prop;
prop.type = PropertyType::LIST;
prop.name = propName;
// 转换计数类型
if (countTypeStr == "uchar") prop.listCountType = PropertyType::UINT8;
else if (countTypeStr == "uint") prop.listCountType = PropertyType::UINT32;
// 其他类型可按需扩展
// 转换元素类型
if (itemTypeStr == "int") prop.listItemType = PropertyType::INT32;
else if (itemTypeStr == "float") prop.listItemType = PropertyType::FLOAT32;
// 其他类型可按需扩展
currentElement->properties.push_back(prop);
} else {
// 普通属性:property 类型 属性名
std::string propName;
iss >> propName;
PropertyDef prop;
prop.name = propName;
// 转换属性类型
if (typeStr == "char") prop.type = PropertyType::INT8;
else if (typeStr == "uchar") prop.type = PropertyType::UINT8;
else if (typeStr == "short") prop.type = PropertyType::INT16;
else if (typeStr == "ushort") prop.type = PropertyType::UINT16;
else if (typeStr == "int") prop.type = PropertyType::INT32;
else if (typeStr == "uint") prop.type = PropertyType::UINT32;
else if (typeStr == "float") prop.type = PropertyType::FLOAT32;
else if (typeStr == "double") prop.type = PropertyType::FLOAT64;
currentElement->properties.push_back(prop);
}
} else if (keyword == "comment") {
// 注释行,直接跳过
continue;
}
}
return true;
}
属性数据读取示例
解析完文件头后,就可以根据元素定义读取对应的属性数据,以ASCII格式的vertex元素为例:
struct Vertex {
float x, y, z;
uint8_t r, g, b; // 可选颜色属性
};
bool readASCIIVertices(const std::string& filePath, const PLYHeader& header, std::vector<Vertex>& vertices) {
std::ifstream file(filePath);
if (!file.is_open()) return false;
// 跳过文件头
std::string line;
while (std::getline(file, line)) {
line.erase(0, line.find_first_not_of(" t"));
line.erase(line.find_last_not_of(" t") + 1);
if (line == "end_header") break;
}
// 查找vertex元素定义
auto it = header.elementIndexMap.find("vertex");
if (it == header.elementIndexMap.end()) return false;
const ElementDef& vertexElem = header.elements[it->second];
vertices.resize(vertexElem.count);
// 读取每个顶点的属性
for (int i = 0; i < vertexElem.count; ++i) {
std::getline(file, line);
std::istringstream iss(line);
int propIdx = 0;
for (const auto& prop : vertexElem.properties) {
if (prop.name == "x") iss >> vertices[i].x;
else if (prop.name == "y") iss >> vertices[i].y;
else if (prop.name == "z") iss >> vertices[i].z;
else if (prop.name == "red") {
int val; iss >> val; vertices[i].r = static_cast<uint8_t>(val);
}
else if (prop.name == "green") {
int val; iss >> val; vertices[i].g = static_cast<uint8_t>(val);
}
else if (prop.name == "blue") {
int val; iss >> val; vertices[i].b = static_cast<uint8_t>(val);
}
// 其他属性可按需扩展
}
}
return true;
}
注意事项
- 二进制格式的PLY文件需要注意字节序,小端格式和大端格式的数值读取方式不同,需要根据
format字段判断。 - 列表类型的属性需要先读取列表长度,再读取对应数量的列表元素,解析时需要根据
listCountType和listItemType转换数据类型。 - 部分PLY文件可能包含自定义元素或属性,解析时需要做好兼容性处理,避免未定义的属性导致程序崩溃。