当前位置:网站首页>CloudCompare源码分析:读取ply文件
CloudCompare源码分析:读取ply文件
2022-06-11 22:33:00 【高精度计算机视觉】
CloudCompare源码分析_读取ply文件
写这些博客的原因,是因为打算好好研究一下点云的各种库的源码,其中比较知名的是PCL(point cloud library)和CC(CloudCompare)。
读源码的时候也没有什么头绪,所以看到哪里就写到哪里,算是随兴之作吧!因为不追求严格的逻辑的代码回溯,所以错误不足之处在所难免。回想起当年学网络编程,也是先一头扎进去,总结一番再说。至于总结得比较到位的结论,往往要等到对框架有一个大体的了解熟悉之后才行。
从哪里开始呢?这里从最基本最简单的PLY文件读取开始吧。
PLY是Polygon的意思,是以多边形的方式保存了空间点位信息。Mesh的格式非常多,如果想了解的话,可以参考这里,
Data Formats: 3D, Audio, Image
具体到PLY格式,可以参考这里,
下面我们言归正传。
首先,我随便找一个3D建模软件,画了一个圆柱体,然后保存为ply格式,用CC打开读取其内容,形成mesh网格。
因为格式对应的功能较多,所以作者定义了一系列的读取函数,函数格式定义如下,
/* ----------------------------------------------------------------------
* Input/output driver
*
* Depending on file mode, different functions are used to read/write
* property fields. The drivers make it transparent to read/write in ascii,
* big endian or little endian cases.
* ---------------------------------------------------------------------- */
typedef int (*p_ply_ihandler)(p_ply ply, double *value);
typedef int (*p_ply_ichunk)(p_ply ply, void *anydata, size_t size);
typedef struct t_ply_idriver_ {
p_ply_ihandler ihandler[16];
p_ply_ichunk ichunk;
const char *name;
} t_ply_idriver;
typedef t_ply_idriver *p_ply_idriver;
typedef int (*p_ply_ohandler)(p_ply ply, double value);
typedef int (*p_ply_ochunk)(p_ply ply, void *anydata, size_t size);
typedef struct t_ply_odriver_ {
p_ply_ohandler ohandler[16];
p_ply_ochunk ochunk;
const char *name;
} t_ply_odriver;
typedef t_ply_odriver *p_ply_odriver;其中i表示读入,o表示读出。
比如说,读取一个格式是32位的float(所以这里取名float32),函数定义是这样的,
static int ibinary_float32(p_ply ply, double *value) {
float float32;
if (!ply->idriver->ichunk(ply, &float32, sizeof(float32))) return 0;
*value = float32;
return 1;
}然后,在driver中把该函数的指针传递进去,如下所示,其中ply_idriver_ascii表示读取ascii字符,ply_idriver_binary表示读取二进制码,
/* ----------------------------------------------------------------------
* Constants
* ---------------------------------------------------------------------- */
static t_ply_idriver ply_idriver_ascii = {
{ iascii_int8, iascii_uint8, iascii_int16, iascii_uint16,
iascii_int32, iascii_uint32, iascii_float32, iascii_float64,
iascii_int8, iascii_uint8, iascii_int16, iascii_uint16,
iascii_int32, iascii_uint32, iascii_float32, iascii_float64
}, /* order matches e_ply_type enum */
NULL,
"ascii input"
};
static t_ply_idriver ply_idriver_binary = {
{ ibinary_int8, ibinary_uint8, ibinary_int16, ibinary_uint16,
ibinary_int32, ibinary_uint32, ibinary_float32, ibinary_float64,
ibinary_int8, ibinary_uint8, ibinary_int16, ibinary_uint16,
ibinary_int32, ibinary_uint32, ibinary_float32, ibinary_float64
}, /* order matches e_ply_type enum */
ply_read_chunk,
"binary input"
};注意这里的16个p_ply_ihandler分别对应了16种相关格式,如下,
/* ply data type */
typedef enum e_ply_type {
PLY_INT8, PLY_UINT8, PLY_INT16, PLY_UINT16,
PLY_INT32, PLY_UIN32, PLY_FLOAT32, PLY_FLOAT64,
PLY_CHAR, PLY_UCHAR, PLY_SHORT, PLY_USHORT,
PLY_INT, PLY_UINT, PLY_FLOAT, PLY_DOUBLE,
PLY_LIST /* has to be the last in enum */
} e_ply_type; /* order matches ply_type_list */所以在后面的ply_read_scalar_property等函数中,只需要取相应的序号即可得到相应的处理函数,
p_ply_ihandler handler = driver[property->type];这样准备好后,后面就可以读取文件了。
以我的圆柱体文件为例,内容是这样的,
ply
format binary_little_endian 1.0
comment SOLIDWORKS generated,length unit = 毫米
element vertex 80
property float x
property float y
property float z
element face 152
property uchar red
property uchar green
property uchar blue
property uchar alpha
property list uchar int vertex_indices
end_header读取文件的主体函数是loadFile,这个函数比较长,全面负责管理读取工作。
下面我们来慢慢介绍。
(一)文件读取的过程
读取文件的过程是这样的,
第一步,通过ply_open这个函数,打开文件流,打开后,文件指针是
ply->fp = fp;第二步,通过ply_read_header函数,读取文件头。
首先,ply_read_header读取前面的文件头的信息,一直读取到“end_header”字符串。在ply_read_header读取完之后, LoadFile分析读取的文件头信息,设置ply的相关参数,比如little-endian,是否是PLY_LIST/PLY_FLOAT格式,有没有face等。
同时,根据文件头的信息,这里会设置一些回调函数。例如,在ply_read_header中,通过 ply_read_header_format设置了相关的回调函数。例如我这里是binary格式,little-endian,其中的处理函数就是前面定义的
ply->idriver = &ply_idriver_binary;
如果是big-endian的话,处理函数就是
ply_idriver_binary_reverse
第三步,弹出读取框。
根据前面的文件,弹出对话框,

第四步,读取Normals相关信息,并设置回调函数。
因为我的这个文件里并没有保存normals信息,所以这里直接跳过。
第五步,读取faces相关信息,并设置回调函数。
这里,通过ply_set_read_cb设置了读取回调函数 face_cb。
第六步,读取顶点(vertices)和面(faces)的数据值。
读取函数是ply_read,
//ref. PlyFilter.cpp, LoadFile
success = ply_read(ply);
//ref. rply.c, ply_read
int ply_read(p_ply ply) {
long i;
p_ply_argument argument;
assert(ply && ply->fp && ply->io_mode == PLY_READ);
argument = &ply->argument;
/* for each element type */
for (i = 0; i < ply->nelements; i++) {
p_ply_element element = &ply->element[i];
argument->element = element;
if (!ply_read_element(ply, element, argument))
return 0;
}
return 1;
}函数会根据element的类型来进行回调,比如是vertices,就调用前面传入的vertices_cb;如果是faces,就调用face_cb。
对于顶点信息,比如我这里有80个vertex,在ply_read_element中会逐个循环读取。
读取的线路是这样的,如果是PLY_FLOAT,
ply_read_element ----> ply_read_property ----> ply_read_scalar_property ----> ibinary_float32
如果是PLY_LIST,
ply_read_element ----> ply_read_property ----> ply_read_list_property ----> ibinary_float32
在发现有X,Y, Z坐标时,会设置相应的顶点读取回调函数,
ply_set_read_cb(ply, pointElements[pp.elemIndex].elementName, pp.propName, vertex_cb, cloud, flags);具体顶点的读取,可以参考后面的说明。
第七步,读取面的数据
对于面的数据,非常类似,也是通过ply_read_element这个函数实现循环读取的,
static int ply_read_element(p_ply ply, p_ply_element element,
...在读取的时候,先通过
ply_read_element ----> ply_read_property ----> ply_read_scalar_property ----> ibinary_int32
读取下面的5个参数,占5个字节,
property uchar red
property uchar green
property uchar blue
property uchar alpha
property list uchar int vertex_indices然后,对于后面的PLY_LIST格式,通过
ply_read_element ----> ply_read_property ----> ply_read_list_property ----> read_cb(face_cb), ibinary_int32,etc.
一次读取4个字节,逐个数据地读取;读取后通过
//ref. PlyFilter.cpp, face_cb
mesh->addTriangle(s_tri[0], s_tri[1], s_tri[2]);添加到ccMesh结构中去。如下图所示,对于前5个字节,ca d1 ee 00 03,分别表示前面的5个属性参数,最后的3表示后续有3个32位的数据;后面的3个32位的数据(2, 1, 0)表示三个顶点的序号。依次循环读取faces的信息。

读取的时候,不管是何种方式何种数据,最后都通过ply_read_chunk实现
static int ply_read_chunk(p_ply ply, void *anybuffer, size_t size) {
char *buffer = (char *) anybuffer;
size_t i = 0;
assert(ply && ply->fp && ply->io_mode == PLY_READ);
assert(ply->buffer_first <= ply->buffer_last);
while (i < size) {
if (ply->buffer_first < ply->buffer_last) {
buffer[i] = ply->buffer[ply->buffer_first];
ply->buffer_first++;
i++;
} else {
ply->buffer_first = 0;
ply->buffer_last = fread(ply->buffer, 1, BUFFERSIZE, ply->fp);
if (ply->buffer_last <= 0) return 0;
}
}
return 1;
}读取的时候都是逐个字节的,感觉这是作者为了统一读取方式故意这么做的。
最后读取完毕,数据根据种类(vertices,faces, etc.)的不同,数据的地址在ply->ply_element->ply_property->pdata指针保存,如果是vertices,这个数据的管理类就是ccPointCloud;如果是faces,这个数据的管理类就是ccMesh。
cloud这些顶点信息,最后也会加入到mesh里,
//ref. PlyFilter.cpp, LoadFile
mesh->addChild(cloud);mesh本身也是一个ccHObject,所以实际上就是放在m_children中,
class QCC_DB_LIB_API ccHObject : public ccObject, public ccDrawableObject
{
public: //construction
......
//! Standard instances container (for children, etc.)
using Container = std::vector<ccHObject *>;
......
//! Children
Container m_children;
......
}最后, 当一切结束的时候,这个mesh被加入到container中,
// ref. PlyFilter.cpp, loadFile
CC_FILE_ERROR PlyFilter::loadFile(const QString& filename, const QString& inputTextureFilename, ccHObject& container, LoadParameters& parameters)
{
......
container.addChild(mesh);
......
}第八步,关闭文件。
读取完后, 最后通过ply_close(ply)关闭文件。
(二)顶点(Vertices)数据保存在哪里
顶点数据保存在一个叫做ccPointCloud的结构体里。在PlyFilter.cpp中,当loadFile在读文件的时候,创建了这个结构体,
/*************************/
/*** Callbacks setup ***/
/*************************/
//Main point cloud
ccPointCloud* cloud = new ccPointCloud("unnamed - Cloud");随即这个顶点通过传递给property->pdata指针,
ply_set_read_cb(ply, pointElements[pp.elemIndex].elementName, pp.propName, vertex_cb, cloud, flags);\
//ref.rply.c
long ply_set_read_cb(p_ply ply, const char *element_name,
const char* property_name, p_ply_read_cb read_cb,
void *pdata, long idata) {
p_ply_element element = NULL;
p_ply_property property = NULL;
assert(ply && element_name && property_name);
element = ply_find_element(ply, element_name);
if (!element) return 0;
property = ply_find_property(element, property_name);
if (!property) return 0;
property->read_cb = read_cb;
property->pdata = pdata;
property->idata = idata;
return (int) element->ninstances;
}在读取数据的进修,这个指针被ply_read_element又传递给了argument->pdata,
//ref. rply.c
static int ply_read_element(p_ply ply, p_ply_element element,
p_ply_argument argument) {
long j, k;
/* for each element of this type */
for (j = 0; j < element->ninstances; j++) {
argument->instance_index = j;
/* for each property */
for (k = 0; k < element->nproperties; k++) {
p_ply_property property = &element->property[k];
argument->property = property;
argument->pdata = property->pdata;
argument->idata = property->idata;
if (!ply_read_property(ply, element, property, argument))
return 0;
}
}
return 1;
}经过几个设置函数,最后由前面传递进去的read_cb函数,也就是PlyFilter.cpp中的vertex_cb函数将读取到的点加入cloud中,
//ref. PlyFilter.cpp
static int vertex_cb(p_ply_argument argument)
{
ccPointCloud* cloud;
ply_get_argument_user_data(argument, (void**)(&cloud), &flags);
......
if (flags & ELEM_EOL)
{
......
cloud->addPoint((s_Point + s_Pshift).toPC());
++s_PointCount;
......
}
return 1;
}这个addPoint函数,最终把点加入到一个vector向量中,
// ref. CCTypes.h
using PointCoordinateType = float;
// ref. CCGeom.h
using CCVector3 = Vector3Tpl<PointCoordinateType>;
// ref. PointCloudTpl.h
std::vector<CCVector3> m_points;这个模板CCVector3最重要的定义是下面这个Union,他定义了一个XYZ的位置坐标。
//! 3-Tuple structure (templated version)
template <class Type> class Tuple3Tpl
{
public:
// The 3 tuple values as a union (array/separate values)
union
{
struct
{
Type x, y, z;
};
Type u[3];
};
......
};
template <typename Type> class Vector3Tpl : public Tuple3Tpl<Type>
{
public:
......
};(三)面(faces)的数据保存在哪里
类似的,ccMesh是一个专门保存面数据信息的类。
//ref. PlyFilter.cpp, LoadFile
mesh = new ccMesh(cloud);我这里,圆柱体有152个面(numberOfFacets==152)。注意到前面顶点信息其实是保存在向量里,和顶点信息类似,面的信息也是通过一个向量保存的;不同的是,这里显示通过下面这个函数来获取内存空间,
// ref. PlyFilter.cpp, LoadFile
mesh->reserve(numberOfFacets)
// ref. ccMesh.cpp
bool ccMesh::reserve(size_t n)
{
if (m_triNormalIndexes)
if (!m_triNormalIndexes->reserveSafe(n))
return false;
if (m_triMtlIndexes)
if (!m_triMtlIndexes->reserveSafe(n))
return false;
if (m_texCoordIndexes)
if (!m_texCoordIndexes->reserveSafe(n))
return false;
return m_triVertIndexes->reserveSafe(n);
}最终使用到的,是下面这样的一个结构体,
//ref. ccMesh.h
//! Container of per-triangle vertices indexes (3)
using triangleIndexesContainer = ccArray<CCCoreLib::VerticesIndexes, 3, unsigned>;
//! Triangles' vertices indexes (3 per triangle)
triangleIndexesContainer* m_triVertIndexes;根据定义,
ccArray<CCCoreLib::VerticesIndexes, 3, unsigned>可以知道每一个元素只保存了3个unsigned数据。
更具体一些,其数据类型的定义如下,
//ref. GenericIndexedMesh.h
struct VerticesIndexes
{
union
{
struct
{
unsigned i1, i2, i3;
};
unsigned i[3];
};
....
};其相关定义ccArray如下,
ccArrary.h
//! Shareable array that can be properly inserted in the DB tree
template <class Type, int N, class ComponentType>
class ccArray :
public std::vector<Type>,
public CCShareable, public ccHObject
{
public:
...
}其核心就是
std::vector<Type>也就是
std::vector<CCCoreLib::VerticesIndexes>在读取时,几乎和顶点一模一样的操作,先设置face_cb回调函数,
ply_set_read_cb(ply, meshElements[pp.elemIndex].elementName, pp.propName, face_cb, mesh, 0);然后逐个读取face的数据点,通过face_cb添加到mesh
//ref. PlyFilter.cpp, face_cb
mesh->addTriangle(s_tri[0], s_tri[1], s_tri[2]);本文结束。
边栏推荐
- Message queue MySQL table that stores message data
- STM32开发笔记112:ADS1258驱动设计——读寄存器
- 移动端——swipe特效之图片时间轴
- C language implements eight sorts of sort merge sort
- R7-1 sum of numeric elements of a list or tuple
- Inner join execution plan changed
- Computer forced shutdown Oracle login failed
- 分类统计字符个数 (15 分)
- 玩家必读|Starfish NFT进阶攻略
- Stack栈的实现
猜你喜欢

NLP - fastText

电脑强制关机 oracle登录不上

The device is in use when win10 ejects USB

Inner join execution plan changed

leetcode 257. Binary tree paths all paths to a binary tree (simple)

Regular execution of shell scripts in crontab

How to adjust the font blur of win10

Optimization of quick sort

Explain asynchronous tasks in detail: the task of function calculation triggers de duplication

Bit operation in leetcode
随机推荐
Leetcode stack topic summary
Number of classified statistical characters (15 points)
利用SecureCRTPortable脚本功能完成网络设备的数据读取
【Uniapp 原生插件】商米钱箱插件
Tkinter学习笔记(二)
Submit task to thread pool
leetcode 257. Binary Tree Paths 二叉树的所有路径(简单)
Point cloud read / write (2): read / write TXT point cloud (space separated | comma separated)
STM32 development note 113:ads1258 drive design - reading temperature value
Regular execution of shell scripts in crontab
使用VBScript读取网络的日志数据进行处理
C language implements eight sorts (3)
每日一题-1317. 将整数转换为两个无零整数的和
Exercise 9-1 time conversion (15 points)
R7-1 sum of numeric elements of a list or tuple
Exercise 8-8 moving letters (10 points)
Php+mysql library management system (course design)
Determine whether the linked list is palindrome structure
剑指offer数组题型总结篇
Basic operation and question type summary of binary tree