计算机视觉基石--PLY文件基础与读写

参考资料

https://github.com/ddiakopoulos/tinyply
http://paulbourke.net/dataformats/ply/


PLY文件介绍

1 PLY文件基本格式

PLY是计算机图形学中一个常用的格式,主要用于保存多边形(通常是三角形),一般有ASCII保存方式和二进制保存方式两种,其中后者的读写效率更佳。下图展示了一个PLY文件中保存的多个三角形。

在PLY文件中,最主要的属性是顶点、三角面以及一些其他的附带属性(法向、颜色等等)。同时,为了保证PLY文件不至于过于复杂,PLY并不考虑材质、参数化等等特殊属性,不具备用于图形学高级渲染的功能。除此之外,PLY文件还包括文件头,其中会存储一些文件说明等等信息,后续会有更详细的介绍。
如下是一个基本PLY(仅包含顶点和三角面)的格式规范:

Header //文件头,记录文件的属性信息
Vertex List  // 顶点坐标值,通常一个顶点一行,分别是xyz
Face List // 三角面的序号以及三个对应顶点的序号
(lists of other elements)

针对上述的格式规范,以下给出一个简单的PLY文件示例

ply
format ascii 1.0           { ascii/binary, format version number }
comment made by Greg Turk  { comments keyword specified, like all lines }
comment this file is a cube
element vertex 8           { define "vertex" element, 8 of them in file }
property float x           { vertex contains float "x" coordinate }
property float y           { y coordinate is also a vertex property }
property float z           { z coordinate, too }
element face 6             { there are 6 "face" elements in the file }
property list uchar int vertex_index { "vertex_indices" is a list of ints }
end_header                 { delimits the end of the header }
0 0 0                      { start of vertex list }
0 0 1
0 1 1
0 1 0
1 0 0
1 0 1
1 1 1
1 1 0
4 0 1 2 3                  { start of face list }
4 7 6 5 4
4 0 4 5 1
4 1 5 6 2
4 2 6 7 3
4 3 7 4 0
2 PLY文件头说明

除顶点、表面等等几何属性之外,PLY文件头也是非常重要的,此处进行一些简要的注释说明。

ply // 此字符串是PLY文件第一行的固定字符,表明此文件为PLY文件,可以称之为“钥匙”
format ascii 1.0          // format用于约定PLY文件的格式,有ASCII和binary两种,后续数字为版本号
 // comment用于在PLY文件中写一些无关痛痒的说明
 // 比如谁创建的文件啊,文件大致内容啊,等等之类的,但是对文件读写没有影响
comment made by Greg Turk 
comment this file is a cube
// element 为PLY文件的核心内容头文件
// 此处为声明 8个vertex
// element下的property为vertex的属性,此处仅有 x y z三种
// property 后置属性包括 变量类型和变量名
// 例如property float x表明一个float类型的x
// 以下给出了不同属性及其字节数
//name        type        number of bytes
//---------------------------------------
//char       character                 1
//uchar      unsigned character        1
//short      short integer             2
//ushort     unsigned short integer    2
//int        integer                   4
//uint       unsigned integer          4
//float      single-precision float    4
//double     double-precision float    8
element vertex 8           { define "vertex" element, 8 of them in file }
property float x           { vertex contains float "x" coordinate }
property float y           { y coordinate is also a vertex property }
property float z           { z coordinate, too }
// 此处的element约定了6个face
element face 6             { there are 6 "face" elements in the file }
// property list表明element中一个数据具有多个属性,当然需要分别注明属性
property list uchar int vertex_index { "vertex_indices" is a list of ints }
// 文件头结尾的标志符
end_header                 { delimits the end of the header }

...
3 更加复杂的例子

基于以上的例子,以下给出一个更加复杂的例子

ply
format ascii 1.0
comment author: Greg Turk
comment object: another cube
element vertex 8
// 此处的vertex一共有 x y z r g b等6个属性
property float x
property float y
property float z
property uchar red                   { start of vertex color }
property uchar green
property uchar blue
element face 7
property list uchar int vertex_index  { number of vertices for each face }
// 此处有一个新的特性edge
element edge 5                        { five edges in object }
property int vertex1                  { index to first vertex of edge }
property int vertex2                  { index to second vertex }
property uchar red                    { start of edge color }
property uchar green
property uchar blue
end_header
0 0 0 255 0 0                         { start of vertex list }
0 0 1 255 0 0
0 1 1 255 0 0
0 1 0 255 0 0
1 0 0 0 0 255
1 0 1 0 0 255
1 1 1 0 0 255
1 1 0 0 0 255
3 0 1 2                           { start of face list, begin with a triangle }
3 0 2 3                           { another triangle }
4 7 6 5 4                         { now some quadrilaterals }
4 0 4 5 1
4 1 5 6 2
4 2 6 7 3
4 3 7 4 0
0 1 255 255 255                   { start of edge list, begin with white edge }
1 2 255 255 255
2 3 255 255 255
3 0 255 255 255
2 0 0 0 0                         { end with a single black line }

以下为上述数据可视化结果(meshlab,注意需要删掉{}的内容):
计算机视觉基石--PLY文件基础与读写_第1张图片


程序实现

PLY文件最为经典的C/C++读写库是tinyply,基于以上对PLY的介绍,不难发现其实读写就是一个简单的文件读写,所以此处并不想过多介绍tinyply的具体实现方式,仅仅简单的介绍一个读写的demo即可,代码如下:

1 基于tinyply的读写接口read_ply和write_ply
#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "tinyply.h"

inline std::vector read_file_binary(const std::string & pathToFile)
{
    std::ifstream file(pathToFile, std::ios::binary);
    std::vector fileBufferBytes;

    if (file.is_open())
    {
        file.seekg(0, std::ios::end);
        size_t sizeBytes = file.tellg();
        file.seekg(0, std::ios::beg);
        fileBufferBytes.resize(sizeBytes);
        if (file.read((char*)fileBufferBytes.data(), sizeBytes)) return fileBufferBytes;
    }
    else throw std::runtime_error("could not open binary ifstream to path " + pathToFile);
    return fileBufferBytes;
}

struct memory_buffer : public std::streambuf
{
    char * p_start {nullptr};
    char * p_end {nullptr};
    size_t size;

    memory_buffer(char const * first_elem, size_t size)
        : p_start(const_cast(first_elem)), p_end(p_start + size), size(size)
    {
        setg(p_start, p_start, p_end);
    }

    pos_type seekoff(off_type off, std::ios_base::seekdir dir, std::ios_base::openmode which) override
    {
        if (dir == std::ios_base::cur) gbump(static_cast(off));
        else setg(p_start, (dir == std::ios_base::beg ? p_start : p_end) + off, p_end);
        return gptr() - p_start;
    }

    pos_type seekpos(pos_type pos, std::ios_base::openmode which) override
    {
        return seekoff(pos, std::ios_base::beg, which);
    }
};

struct memory_stream : virtual memory_buffer, public std::istream
{
    memory_stream(char const * first_elem, size_t size)
        : memory_buffer(first_elem, size), std::istream(static_cast(this)) {}
};

class manual_timer
{
    std::chrono::high_resolution_clock::time_point t0;
    double timestamp{ 0.0 };
public:
    void start() { t0 = std::chrono::high_resolution_clock::now(); }
    void stop() { timestamp = std::chrono::duration(std::chrono::high_resolution_clock::now() - t0).count() * 1000.0; }
    const double & get() { return timestamp; }
};

struct float2 { float x, y; };
struct float3 { float x, y, z; };
struct double3 { double x, y, z; };
struct uint3 { uint32_t x, y, z; };
struct uint4 { uint32_t x, y, z, w; };

// 写PLY文件基本代码,仅保存 x y z r g b
void write_ply_file(const std::string & filename, std::vector &verts, std::vector &colos)
{
	std::filebuf fb_binary;
	fb_binary.open(filename, std::ios::out | std::ios::binary);
	std::ostream outstream_binary(&fb_binary);
	if (outstream_binary.fail()) throw std::runtime_error("failed to open " + filename);

	tinyply::PlyFile ply_file;

	ply_file.add_properties_to_element("vertex", { "x", "y", "z" },
		tinyply::Type::FLOAT32, verts.size(), reinterpret_cast(verts.data()), tinyply::Type::INVALID, 0);

	ply_file.add_properties_to_element("vertex", { "red", "green", "blue"},
		tinyply::Type::UINT8, colos.size(), reinterpret_cast(colos.data()), tinyply::Type::INVALID, 0);

	ply_file.get_comments().push_back("generated by tinyply 2.3");

	// Write a binary file
	ply_file.write(outstream_binary, true);
}

// 读PLY文件基本代码,仅读取x y z r g b
void read_ply_file(const std::string & filepath, std::vector &verts, std::vector &colos, const bool preload_into_memory = true)
{

    std::unique_ptr file_stream;
    std::vector byte_buffer;

    try
    {
        // For most files < 1gb, pre-loading the entire file upfront and wrapping it into a 
        // stream is a net win for parsing speed, about 40% faster. 
        if (preload_into_memory)
        {
            byte_buffer = read_file_binary(filepath);
            file_stream.reset(new memory_stream((char*)byte_buffer.data(), byte_buffer.size()));
        }
        else
        {
            file_stream.reset(new std::ifstream(filepath, std::ios::binary));
        }

        if (!file_stream || file_stream->fail()) throw std::runtime_error("file_stream failed to open " + filepath);

        file_stream->seekg(0, std::ios::end);
        const float size_mb = file_stream->tellg() * float(1e-6);
        file_stream->seekg(0, std::ios::beg);

        tinyply::PlyFile file;
        file.parse_header(*file_stream);

        //std::cout << "\t[ply_header] Type: " << (file.is_binary_file() ? "binary" : "ascii") << std::endl;
        //for (const auto & c : file.get_comments()) std::cout << "\t[ply_header] Comment: " << c << std::endl;
        //for (const auto & c : file.get_info()) std::cout << "\t[ply_header] Info: " << c << std::endl;

        //for (const auto & e : file.get_elements())
        //{
        //    std::cout << "\t[ply_header] element: " << e.name << " (" << e.size << ")" << std::endl;
        //    for (const auto & p : e.properties)
        //    {
        //        std::cout << "\t[ply_header] \tproperty: " << p.name << " (type=" << tinyply::PropertyTable[p.propertyType].str << ")";
        //        if (p.isList) std::cout << " (list_type=" << tinyply::PropertyTable[p.listType].str << ")";
        //        std::cout << std::endl;
        //    }
        //}

		std::shared_ptr vertices, colors;

        // The header information can be used to programmatically extract properties on elements
        // known to exist in the header prior to reading the data. For brevity of this sample, properties 
        // like vertex position are hard-coded: 
        try { vertices = file.request_properties_from_element("vertex", { "x", "y", "z" }); }
        catch (const std::exception & e) { std::cerr << "tinyply exception: " << e.what() << std::endl; }

        try { colors = file.request_properties_from_element("vertex", { "red", "green", "blue" }); }
        catch (const std::exception & e) { std::cerr << "tinyply exception: " << e.what() << std::endl; }


        manual_timer read_timer;

        read_timer.start();
        file.read(*file_stream);
        read_timer.stop();

        //const float parsing_time = static_cast(read_timer.get()) / 1000.f;
        //std::cout << "\tparsing " << size_mb << "mb in " << parsing_time << " seconds [" << (size_mb / parsing_time) << " MBps]" << std::endl;

        //if (vertices)   std::cout << "\tRead " << vertices->count  << " total vertices "<< std::endl;
        //if (colors)     std::cout << "\tRead " << colors->count << " total vertex colors " << std::endl;

		const size_t numVerticesBytes = vertices->buffer.size_bytes();
		verts.resize(vertices->count);
		std::memcpy(verts.data(), vertices->buffer.get(), numVerticesBytes);

		const size_t numColorsBytes = colors->buffer.size_bytes();
		colos.resize(colors->count);
		std::memcpy(colos.data(), colors->buffer.get(), numColorsBytes);
    }
    catch (const std::exception & e)
    {
        std::cerr << "Caught tinyply exception: " << e.what() << std::endl;
    }
}


2 demo.cpp
std::vector verts;
std::vector colos;
read_ply_file(this->working_folder_ + "/" + this->file_names_[i] + ".ply", verts, colos);
write_ply_file(out_dir + "/" + this->file_names_[i] + this->ply_ext, verts, colos);

结语

简单总结一下ply文件和基于tinyply的读写方法。

你可能感兴趣的:(计算机图形学)