mirror of
https://github.com/opencv/opencv.git
synced 2025-06-07 17:44:04 +08:00
Merge pull request #24961 from savuor:rv/ply_mesh
PLY mesh support #24961 **Warning:** The PR changes exising API. Fixes #24960 Connected PR: [#1145@extra](https://github.com/opencv/opencv_extra/pull/1145) ### Changes * Adds faces loading from and saving to PLY files * Fixes incorrect PLY loading (see issue) * Adds per-vertex color loading / saving ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
parent
466ad96b1d
commit
f96111ef05
@ -66,7 +66,7 @@ For that we use special functions to load mesh data and display it.
|
||||
Here for now only .OBJ files are supported and they should be triangulated before processing (triangulation - process of breaking faces into triangles).
|
||||
|
||||
@code{.py}
|
||||
vertices, _, indices = cv2.loadMesh("../data/teapot.obj")
|
||||
vertices, indices = cv2.loadMesh("../data/teapot.obj")
|
||||
vertices = np.squeeze(vertices, axis=1)
|
||||
|
||||
cv2.viz3d.showMesh("window", "mesh", vertices, indices)
|
||||
|
@ -2785,12 +2785,15 @@ CV_EXPORTS_W void savePointCloud(const String &filename, InputArray vertices, In
|
||||
*
|
||||
* Currently, the following file formats are supported:
|
||||
* - [Wavefront obj file *.obj](https://en.wikipedia.org/wiki/Wavefront_.obj_file) (ONLY TRIANGULATED FACES)
|
||||
* - [Polygon File Format *.ply](https://en.wikipedia.org/wiki/PLY_(file_format))
|
||||
* @param filename Name of the file.
|
||||
* @param vertices (vector of Point3f) vertex coordinates of a mesh
|
||||
* @param normals (vector of Point3f) vertex normals of a mesh
|
||||
* @param indices (vector of vectors of int) vertex normals of a mesh
|
||||
* @param normals (vector of Point3f) vertex normals of a mesh
|
||||
* @param colors (vector of Point3f) vertex colors of a mesh
|
||||
*/
|
||||
CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputArray normals, OutputArrayOfArrays indices);
|
||||
CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays indices,
|
||||
OutputArray normals = noArray(), OutputArray colors = noArray());
|
||||
|
||||
/** @brief Saves a mesh to a specified file.
|
||||
*
|
||||
@ -2799,11 +2802,12 @@ CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputA
|
||||
*
|
||||
* @param filename Name of the file.
|
||||
* @param vertices (vector of Point3f) vertex coordinates of a mesh
|
||||
* @param normals (vector of Point3f) vertex normals of a mesh
|
||||
* @param indices (vector of vectors of int) vertex normals of a mesh
|
||||
* @param normals (vector of Point3f) vertex normals of a mesh
|
||||
* @param colors (vector of Point3f) vertex colors of a mesh
|
||||
*/
|
||||
CV_EXPORTS_W void saveMesh(const String &filename, InputArray vertices, InputArray normals, InputArrayOfArrays indices);
|
||||
|
||||
CV_EXPORTS_W void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays indices,
|
||||
InputArray normals = noArray(), InputArray colors = noArray());
|
||||
|
||||
//! @} _3d
|
||||
} //end namespace cv
|
||||
|
@ -8,6 +8,8 @@
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
#include <cstddef>
|
||||
|
||||
namespace cv {
|
||||
|
||||
@ -16,15 +18,16 @@ void PlyDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
|
||||
points.clear();
|
||||
normals.clear();
|
||||
rgb.clear();
|
||||
CV_UNUSED(indices);
|
||||
indices.clear();
|
||||
|
||||
std::ifstream file(m_filename, std::ios::binary);
|
||||
if (parseHeader(file))
|
||||
{
|
||||
parseBody(file, points, normals, rgb);
|
||||
parseBody(file, points, normals, rgb, indices);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool PlyDecoder::parseHeader(std::ifstream &file)
|
||||
{
|
||||
std::string s;
|
||||
@ -36,6 +39,11 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
|
||||
}
|
||||
std::getline(file, s);
|
||||
auto splitArr = split(s, ' ');
|
||||
// "\r" symbols are not trimmed by default
|
||||
for (auto& e : splitArr)
|
||||
{
|
||||
e = trimSpaces(e);
|
||||
}
|
||||
if (splitArr[0] != "format")
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Provided file doesn't have format");
|
||||
@ -59,63 +67,240 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool onVertexRead = false;
|
||||
const std::map<std::string, int> dataTypes =
|
||||
{
|
||||
{ "char", CV_8S }, { "int8", CV_8S },
|
||||
{ "uchar", CV_8U }, { "uint8", CV_8U },
|
||||
{ "short", CV_16S }, { "int16", CV_16S },
|
||||
{ "ushort", CV_16U }, { "uint16", CV_16U },
|
||||
{ "int", CV_32S }, { "int32", CV_32S },
|
||||
{ "uint", CV_32U }, { "uint32", CV_32U },
|
||||
{ "float", CV_32F }, { "float32", CV_32F },
|
||||
{ "double", CV_64F }, { "float64", CV_64F },
|
||||
};
|
||||
|
||||
enum ReadElement
|
||||
{
|
||||
READ_OTHER = 0,
|
||||
READ_VERTEX = 1,
|
||||
READ_FACE = 2
|
||||
};
|
||||
ReadElement elemRead = READ_OTHER;
|
||||
m_vertexDescription = ElementDescription();
|
||||
m_faceDescription = ElementDescription();
|
||||
while (std::getline(file, s))
|
||||
{
|
||||
if (startsWith(s, "element"))
|
||||
{
|
||||
auto splitArrElem = split(s, ' ');
|
||||
if (splitArrElem[1] == "vertex")
|
||||
std::vector<std::string> splitArrElem = split(s, ' ');
|
||||
// "\r" symbols are not trimmed by default
|
||||
for (auto& e : splitArrElem)
|
||||
{
|
||||
onVertexRead = true;
|
||||
e = trimSpaces(e);
|
||||
}
|
||||
std::string elemName = splitArrElem.at(1);
|
||||
if (elemName == "vertex")
|
||||
{
|
||||
elemRead = READ_VERTEX;
|
||||
if(splitArrElem.size() != 3)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex element description has " << splitArrElem.size()
|
||||
<< " words instead of 3");
|
||||
return false;
|
||||
}
|
||||
std::istringstream iss(splitArrElem[2]);
|
||||
iss >> m_vertexCount;
|
||||
iss >> m_vertexDescription.amount;
|
||||
}
|
||||
else if (elemName == "face")
|
||||
{
|
||||
elemRead = READ_FACE;
|
||||
if(splitArrElem.size() != 3)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Face element description has " << splitArrElem.size()
|
||||
<< " words instead of 3");
|
||||
return false;
|
||||
}
|
||||
std::istringstream iss(splitArrElem[2]);
|
||||
iss >> m_faceDescription.amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
onVertexRead = false;
|
||||
elemRead = READ_OTHER;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (startsWith(s, "property"))
|
||||
{
|
||||
if (onVertexRead)
|
||||
Property property;
|
||||
std::string elName = (elemRead == READ_VERTEX) ? "Vertex" : "Face";
|
||||
std::vector<std::string> splitArrElem = split(s, ' ');
|
||||
// "\r" symbols are not trimmed by default
|
||||
for (auto& e : splitArrElem)
|
||||
{
|
||||
auto splitArrElem = split(s, ' ');
|
||||
if (splitArrElem[2] == "x" || splitArrElem[2] == "y" || splitArrElem[2] == "z")
|
||||
{
|
||||
if (splitArrElem[1] != "float") {
|
||||
CV_LOG_ERROR(NULL, "Provided property '" << splitArrElem[2] << "' with format '" << splitArrElem[1]
|
||||
<< "' is not supported");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (splitArrElem[2] == "red" || splitArrElem[2] == "green" || splitArrElem[2] == "blue")
|
||||
{
|
||||
if (splitArrElem[1] != "uchar") {
|
||||
CV_LOG_ERROR(NULL, "Provided property '" << splitArrElem[2] << "' with format '" << splitArrElem[1]
|
||||
<< "' is not supported");
|
||||
return false;
|
||||
}
|
||||
m_hasColour = true;
|
||||
}
|
||||
if (splitArrElem[2] == "nx")
|
||||
{
|
||||
if (splitArrElem[1] != "float") {
|
||||
CV_LOG_ERROR(NULL, "Provided property '" << splitArrElem[2] << "' with format '" << splitArrElem[1]
|
||||
<< "' is not supported");
|
||||
return false;
|
||||
}
|
||||
m_hasNormal = true;
|
||||
}
|
||||
e = trimSpaces(e);
|
||||
}
|
||||
if (splitArrElem.size() < 3)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, elName << " property has " << splitArrElem.size()
|
||||
<< " words instead of at least 3");
|
||||
return false;
|
||||
}
|
||||
std::string propType = splitArrElem[1];
|
||||
if (propType == "list")
|
||||
{
|
||||
property.isList = true;
|
||||
if (splitArrElem.size() < 5)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, elName << " property has " << splitArrElem.size()
|
||||
<< " words instead of at least 5");
|
||||
return false;
|
||||
}
|
||||
std::string amtTypeString = splitArrElem[2];
|
||||
if (dataTypes.count(amtTypeString) == 0)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Property type " << amtTypeString
|
||||
<< " is not supported");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
property.counterType = dataTypes.at(amtTypeString);
|
||||
}
|
||||
std::string idxTypeString = splitArrElem[3];
|
||||
if (dataTypes.count(idxTypeString) == 0)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Property type " << idxTypeString
|
||||
<< " is not supported");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
property.valType = dataTypes.at(idxTypeString);
|
||||
}
|
||||
|
||||
property.name = splitArrElem[4];
|
||||
}
|
||||
else
|
||||
{
|
||||
property.isList = false;
|
||||
if (dataTypes.count(propType) == 0)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Property type " << propType
|
||||
<< " is not supported");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
property.valType = dataTypes.at(propType);
|
||||
}
|
||||
property.name = splitArrElem[2];
|
||||
}
|
||||
|
||||
if (elemRead == READ_VERTEX)
|
||||
{
|
||||
m_vertexDescription.properties.push_back(property);
|
||||
}
|
||||
else if (elemRead == READ_FACE)
|
||||
{
|
||||
m_faceDescription.properties.push_back(property);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
if (startsWith(s, "end_header"))
|
||||
break;
|
||||
|
||||
}
|
||||
return true;
|
||||
|
||||
bool good = true;
|
||||
m_vertexCount = m_vertexDescription.amount;
|
||||
std::map<std::string, int> amtProps;
|
||||
for (const auto& p : m_vertexDescription.properties)
|
||||
{
|
||||
bool known = false;
|
||||
if (p.name == "x" || p.name == "y" || p.name == "z")
|
||||
{
|
||||
known = true;
|
||||
if (p.valType != CV_32F)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << p.name
|
||||
<< " should be float");
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
if (p.name == "nx" || p.name == "ny" || p.name == "nz")
|
||||
{
|
||||
known = true;
|
||||
if (p.valType != CV_32F)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << p.name
|
||||
<< " should be float");
|
||||
good = false;
|
||||
}
|
||||
m_hasNormal = true;
|
||||
}
|
||||
if (p.name == "red" || p.name == "green" || p.name == "blue")
|
||||
{
|
||||
known = true;
|
||||
if (p.valType != CV_8U)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << p.name
|
||||
<< " should be uchar");
|
||||
good = false;
|
||||
}
|
||||
m_hasColour = true;
|
||||
}
|
||||
if (p.isList)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "List properties for vertices are not supported");
|
||||
good = false;
|
||||
}
|
||||
if (known)
|
||||
{
|
||||
amtProps[p.name]++;
|
||||
}
|
||||
}
|
||||
|
||||
// check if we have no duplicates
|
||||
for (const auto& a : amtProps)
|
||||
{
|
||||
if (a.second > 1)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << a.first << " is duplicated");
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
const std::array<std::string, 3> vertKeys = {"x", "y", "z"};
|
||||
for (const std::string& c : vertKeys)
|
||||
{
|
||||
if (amtProps.count(c) == 0)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Vertex property " << c << " is not presented in the file");
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
|
||||
m_faceCount = m_faceDescription.amount;
|
||||
int amtLists = 0;
|
||||
for (const auto& p : m_faceDescription.properties)
|
||||
{
|
||||
if (p.isList)
|
||||
{
|
||||
amtLists++;
|
||||
if (!(p.counterType == CV_8U && (p.valType == CV_32S || p.valType == CV_32U)))
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "List property " << p.name
|
||||
<< " should have type uint8 for counter and uint32 for values");
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (amtLists > 1)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Only 1 list property is supported per face");
|
||||
good = false;
|
||||
}
|
||||
|
||||
return good;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@ -142,42 +327,185 @@ T readNext(std::ifstream &file, DataFormat format)
|
||||
return val;
|
||||
}
|
||||
|
||||
void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb)
|
||||
template <>
|
||||
uchar readNext<uchar>(std::ifstream &file, DataFormat format)
|
||||
{
|
||||
if (format == DataFormat::ASCII)
|
||||
{
|
||||
int val;
|
||||
file >> val;
|
||||
return (uchar)val;
|
||||
}
|
||||
uchar val;
|
||||
file.read((char *)&val, sizeof(uchar));
|
||||
// 1 byte does not have to be endian-swapped
|
||||
return val;
|
||||
}
|
||||
|
||||
void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb,
|
||||
std::vector<std::vector<int32_t>> &indices)
|
||||
{
|
||||
points.reserve(m_vertexCount);
|
||||
if (m_hasColour)
|
||||
{
|
||||
rgb.reserve(m_vertexCount);
|
||||
}
|
||||
if (m_hasNormal)
|
||||
{
|
||||
normals.reserve(m_vertexCount);
|
||||
}
|
||||
|
||||
struct VertexFields
|
||||
{
|
||||
float vx, vy, vz;
|
||||
float nx, ny, nz;
|
||||
uchar r, g, b;
|
||||
};
|
||||
|
||||
union VertexData
|
||||
{
|
||||
std::array<uchar, 27> bytes;
|
||||
VertexFields vf;
|
||||
};
|
||||
|
||||
// to avoid string matching at file loading
|
||||
std::vector<size_t> vertexOffsets(m_vertexDescription.properties.size(), (size_t)(-1));
|
||||
for (size_t j = 0; j < m_vertexDescription.properties.size(); j++)
|
||||
{
|
||||
const auto& p = m_vertexDescription.properties[j];
|
||||
size_t offset = 0;
|
||||
if (p.name == "x")
|
||||
offset = offsetof(VertexFields, vx);
|
||||
if (p.name == "y")
|
||||
offset = offsetof(VertexFields, vy);
|
||||
if (p.name == "z")
|
||||
offset = offsetof(VertexFields, vz);
|
||||
if (p.name == "nx")
|
||||
offset = offsetof(VertexFields, nx);
|
||||
if (p.name == "ny")
|
||||
offset = offsetof(VertexFields, ny);
|
||||
if (p.name == "nz")
|
||||
offset = offsetof(VertexFields, nz);
|
||||
if (p.name == "red")
|
||||
offset = offsetof(VertexFields, r);
|
||||
if (p.name == "green")
|
||||
offset = offsetof(VertexFields, g);
|
||||
if (p.name == "blue")
|
||||
offset = offsetof(VertexFields, b);
|
||||
vertexOffsets[j] = offset;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_vertexCount; i++)
|
||||
{
|
||||
Point3f vertex;
|
||||
vertex.x = readNext<float>(file, m_inputDataFormat);
|
||||
vertex.y = readNext<float>(file, m_inputDataFormat);
|
||||
vertex.z = readNext<float>(file, m_inputDataFormat);
|
||||
points.push_back(vertex);
|
||||
VertexData vertexData{ };
|
||||
for (size_t j = 0; j < m_vertexDescription.properties.size(); j++)
|
||||
{
|
||||
const auto& p = m_vertexDescription.properties[j];
|
||||
uint ival = 0; float fval = 0;
|
||||
// here signedness is not important
|
||||
switch (p.valType)
|
||||
{
|
||||
case CV_8U: case CV_8S:
|
||||
ival = readNext<uchar>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_16U: case CV_16S:
|
||||
ival = readNext<ushort>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_32S: case CV_32U:
|
||||
ival = readNext<uint>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_32F:
|
||||
fval = readNext<float>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_64F:
|
||||
fval = (float)readNext<double>(file, m_inputDataFormat);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
size_t offset = vertexOffsets[j];
|
||||
if (offset != (size_t)(-1))
|
||||
{
|
||||
switch (p.valType)
|
||||
{
|
||||
case CV_8U: case CV_8S:
|
||||
*(vertexData.bytes.data() + offset) = (uchar)ival;
|
||||
break;
|
||||
case CV_32F:
|
||||
*(float*)(vertexData.bytes.data() + offset) = fval;
|
||||
break;
|
||||
default:
|
||||
// the rest are unused
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
points.push_back({ vertexData.vf.vx, vertexData.vf.vy, vertexData.vf.vz });
|
||||
if (m_hasColour)
|
||||
{
|
||||
Point3_<uchar> colour;
|
||||
colour.x = readNext<int>(file, m_inputDataFormat) & 0xff;
|
||||
colour.y = readNext<int>(file, m_inputDataFormat) & 0xff;
|
||||
colour.z = readNext<int>(file, m_inputDataFormat) & 0xff;
|
||||
rgb.push_back(colour);
|
||||
rgb.push_back({ vertexData.vf.r, vertexData.vf.g, vertexData.vf.b });
|
||||
}
|
||||
if (m_hasNormal)
|
||||
{
|
||||
Point3f normal;
|
||||
normal.x = readNext<float>(file, m_inputDataFormat);
|
||||
normal.y = readNext<float>(file, m_inputDataFormat);
|
||||
normal.z = readNext<float>(file, m_inputDataFormat);
|
||||
normals.push_back(normal);
|
||||
normals.push_back({ vertexData.vf.nx, vertexData.vf.ny, vertexData.vf.nz });
|
||||
}
|
||||
}
|
||||
|
||||
indices.reserve(m_faceCount);
|
||||
for (size_t i = 0; i < m_faceCount; i++)
|
||||
{
|
||||
for (const auto& p : m_faceDescription.properties)
|
||||
{
|
||||
if (p.isList)
|
||||
{
|
||||
size_t nVerts = readNext<uchar>(file, m_inputDataFormat);
|
||||
if (nVerts < 3)
|
||||
{
|
||||
CV_LOG_ERROR(NULL, "Face should have at least 3 vertices but has " << nVerts);
|
||||
return;
|
||||
}
|
||||
// PLY can have faces with >3 vertices in TRIANGLE_FAN format
|
||||
// in this case we load them as separate triangles
|
||||
int vert1 = readNext<int>(file, m_inputDataFormat);
|
||||
int vert2 = readNext<int>(file, m_inputDataFormat);
|
||||
for (size_t j = 2; j < nVerts; j++)
|
||||
{
|
||||
int vert3 = readNext<int>(file, m_inputDataFormat);
|
||||
indices.push_back({vert1, vert2, vert3});
|
||||
vert2 = vert3;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// read and discard
|
||||
switch (p.valType)
|
||||
{
|
||||
case CV_8U: case CV_8S:
|
||||
readNext<uchar>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_16U: case CV_16S:
|
||||
readNext<ushort>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_32S: case CV_32U:
|
||||
readNext<uint>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_32F:
|
||||
readNext<float>(file, m_inputDataFormat);
|
||||
break;
|
||||
case CV_64F:
|
||||
readNext<double>(file, m_inputDataFormat);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlyEncoder::writeData(const std::vector<Point3f> &points, const std::vector<Point3f> &normals, const std::vector<Point3_<uchar>> &rgb, const std::vector<std::vector<int32_t>> &indices)
|
||||
{
|
||||
CV_UNUSED(indices);
|
||||
std::ofstream file(m_filename, std::ios::binary);
|
||||
if (!file) {
|
||||
CV_LOG_ERROR(NULL, "Impossible to open the file: " << m_filename);
|
||||
@ -194,7 +522,8 @@ void PlyEncoder::writeData(const std::vector<Point3f> &points, const std::vector
|
||||
file << "property float y" << std::endl;
|
||||
file << "property float z" << std::endl;
|
||||
|
||||
if(hasColor) {
|
||||
if(hasColor)
|
||||
{
|
||||
file << "property uchar red" << std::endl;
|
||||
file << "property uchar green" << std::endl;
|
||||
file << "property uchar blue" << std::endl;
|
||||
@ -207,17 +536,34 @@ void PlyEncoder::writeData(const std::vector<Point3f> &points, const std::vector
|
||||
file << "property float nz" << std::endl;
|
||||
}
|
||||
|
||||
file << "end_header" << std::endl;
|
||||
if (!indices.empty())
|
||||
{
|
||||
file << "element face " << indices.size() << std::endl;
|
||||
file << "property list uchar int vertex_indices" << std::endl;
|
||||
}
|
||||
|
||||
file << "end_header" << std::endl;
|
||||
|
||||
for (size_t i = 0; i < points.size(); i++)
|
||||
{
|
||||
file << points[i].x << " " << points[i].y << " " << points[i].z;
|
||||
if (hasColor) {
|
||||
file << std::setprecision(9) << points[i].x << " " << points[i].y << " " << points[i].z;
|
||||
if (hasColor)
|
||||
{
|
||||
file << " " << static_cast<int>(rgb[i].x) << " " << static_cast<int>(rgb[i].y) << " " << static_cast<int>(rgb[i].z);
|
||||
}
|
||||
if (hasNormals) {
|
||||
file << " " << normals[i].x << " " << normals[i].y << " " << normals[i].z;
|
||||
if (hasNormals)
|
||||
{
|
||||
file << " " << std::setprecision(9) << normals[i].x << " " << normals[i].y << " " << normals[i].z;
|
||||
}
|
||||
file << std::endl;
|
||||
}
|
||||
|
||||
for (const auto& faceIndices : indices)
|
||||
{
|
||||
file << faceIndices.size();
|
||||
for (const auto& index : faceIndices)
|
||||
{
|
||||
file << " " << index;
|
||||
}
|
||||
file << std::endl;
|
||||
}
|
||||
|
@ -18,6 +18,20 @@ enum class DataFormat
|
||||
BinaryBigEndian
|
||||
};
|
||||
|
||||
struct Property
|
||||
{
|
||||
bool isList;
|
||||
int counterType;
|
||||
int valType;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct ElementDescription
|
||||
{
|
||||
size_t amount;
|
||||
std::vector<Property> properties;
|
||||
};
|
||||
|
||||
class PlyDecoder CV_FINAL : public BasePointCloudDecoder
|
||||
{
|
||||
public:
|
||||
@ -25,12 +39,16 @@ public:
|
||||
|
||||
protected:
|
||||
bool parseHeader(std::ifstream &file);
|
||||
void parseBody(std::ifstream &file, std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb);
|
||||
void parseBody(std::ifstream &file, std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb,
|
||||
std::vector<std::vector<int32_t>> &indices);
|
||||
|
||||
DataFormat m_inputDataFormat;
|
||||
size_t m_vertexCount{0};
|
||||
size_t m_faceCount{0};
|
||||
bool m_hasColour{false};
|
||||
bool m_hasNormal{false};
|
||||
ElementDescription m_vertexDescription;
|
||||
ElementDescription m_faceDescription;
|
||||
};
|
||||
|
||||
class PlyEncoder CV_FINAL : public BasePointCloudEncoder
|
||||
|
@ -122,12 +122,15 @@ void savePointCloud(const String &filename, InputArray vertices, InputArray norm
|
||||
#endif
|
||||
}
|
||||
|
||||
void loadMesh(const String &filename, OutputArray vertices, OutputArray normals, OutputArrayOfArrays indices)
|
||||
void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays indices, OutputArray normals, OutputArray colors)
|
||||
{
|
||||
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
CV_Assert(vertices.needed());
|
||||
CV_Assert(indices.needed());
|
||||
|
||||
PointCloudDecoder decoder = findDecoder(filename);
|
||||
String file_ext = getExtension(filename);
|
||||
if (!decoder || (file_ext != "obj" && file_ext != "OBJ")) {
|
||||
if (!decoder) {
|
||||
CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported");
|
||||
return;
|
||||
}
|
||||
@ -141,15 +144,23 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArray normals,
|
||||
|
||||
decoder->readData(vec_vertices, vec_normals, vec_rgb, vec_indices);
|
||||
|
||||
if (!vec_vertices.empty()) {
|
||||
if (!vec_vertices.empty())
|
||||
{
|
||||
Mat(1, static_cast<int>(vec_vertices.size()), CV_32FC3, vec_vertices.data()).copyTo(vertices);
|
||||
}
|
||||
|
||||
if (!vec_normals.empty()) {
|
||||
if (normals.needed() && !vec_normals.empty())
|
||||
{
|
||||
Mat(1, static_cast<int>(vec_normals.size()), CV_32FC3, vec_normals.data()).copyTo(normals);
|
||||
}
|
||||
|
||||
if (!vec_indices.empty()) {
|
||||
if (colors.needed() && !vec_rgb.empty())
|
||||
{
|
||||
Mat(1, static_cast<int>(vec_rgb.size()), CV_8UC3, vec_rgb.data()).convertTo(colors, CV_32F, (1.0/255.0));
|
||||
}
|
||||
|
||||
if (!vec_indices.empty())
|
||||
{
|
||||
std::vector<std::vector<int32_t>>& vec = *(std::vector<std::vector<int32_t>>*)indices.getObj();
|
||||
vec.resize(vec_indices.size());
|
||||
for (size_t i = 0; i < vec_indices.size(); ++i) {
|
||||
@ -161,11 +172,13 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArray normals,
|
||||
CV_UNUSED(filename);
|
||||
CV_UNUSED(vertices);
|
||||
CV_UNUSED(normals);
|
||||
CV_UNUSED(colors);
|
||||
CV_UNUSED(indices);
|
||||
CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!");
|
||||
#endif
|
||||
}
|
||||
|
||||
void saveMesh(const String &filename, InputArray vertices, InputArray normals, InputArrayOfArrays indices)
|
||||
void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays indices, InputArray normals, InputArray colors)
|
||||
{
|
||||
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
if (vertices.empty()) {
|
||||
@ -175,7 +188,7 @@ void saveMesh(const String &filename, InputArray vertices, InputArray normals, I
|
||||
|
||||
auto encoder = findEncoder(filename);
|
||||
String file_ext = getExtension(filename);
|
||||
if (!encoder || (file_ext != "obj" && file_ext != "OBJ")) {
|
||||
if (!encoder) {
|
||||
CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported");
|
||||
return;
|
||||
}
|
||||
@ -185,15 +198,22 @@ void saveMesh(const String &filename, InputArray vertices, InputArray normals, I
|
||||
std::vector<Point3f> vec_vertices(vertices.getMat());
|
||||
std::vector<Point3f> vec_normals;
|
||||
std::vector<Point3_<uchar>> vec_rgb;
|
||||
if (!normals.empty()){
|
||||
if (!normals.empty())
|
||||
{
|
||||
vec_normals = normals.getMat();
|
||||
}
|
||||
|
||||
if (!colors.empty())
|
||||
{
|
||||
colors.getMat().convertTo(vec_rgb, CV_8U, 255.0);
|
||||
}
|
||||
|
||||
std::vector<Mat> mat_indices;
|
||||
indices.getMatVector(mat_indices);
|
||||
std::vector<std::vector<int32_t>> vec_indices(mat_indices.size());
|
||||
|
||||
for (size_t i = 0; i < mat_indices.size(); ++i) {
|
||||
for (size_t i = 0; i < mat_indices.size(); ++i)
|
||||
{
|
||||
mat_indices[i].copyTo(vec_indices[i]);
|
||||
}
|
||||
|
||||
@ -202,7 +222,9 @@ void saveMesh(const String &filename, InputArray vertices, InputArray normals, I
|
||||
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
|
||||
CV_UNUSED(filename);
|
||||
CV_UNUSED(vertices);
|
||||
CV_UNUSED(colors);
|
||||
CV_UNUSED(normals);
|
||||
CV_UNUSED(indices);
|
||||
CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!");
|
||||
#endif
|
||||
|
||||
|
@ -132,14 +132,14 @@ TEST(PointCloud, LoadSaveMeshObj)
|
||||
auto folder = cvtest::TS::ptr()->get_data_path();
|
||||
std::string new_path = tempfile("new_mesh.obj");
|
||||
|
||||
cv::loadMesh(folder + "pointcloudio/orig.obj", points, normals, indices);
|
||||
cv::saveMesh(new_path, points, normals, indices);
|
||||
cv::loadMesh(folder + "pointcloudio/orig.obj", points, indices, normals);
|
||||
cv::saveMesh(new_path, points, indices, normals);
|
||||
|
||||
std::vector<cv::Point3f> points_gold;
|
||||
std::vector<cv::Point3f> normals_gold;
|
||||
std::vector<std::vector<int32_t>> indices_gold;
|
||||
|
||||
cv::loadMesh(new_path, points_gold, normals_gold, indices_gold);
|
||||
cv::loadMesh(new_path, points_gold, indices_gold, normals_gold);
|
||||
|
||||
EXPECT_EQ(normals_gold, normals);
|
||||
EXPECT_EQ(points_gold, points);
|
||||
@ -148,29 +148,44 @@ TEST(PointCloud, LoadSaveMeshObj)
|
||||
std::remove(new_path.c_str());
|
||||
}
|
||||
|
||||
TEST(PointCloud, LoadSaveMeshPly)
|
||||
typedef std::string PlyTestParamsType;
|
||||
typedef testing::TestWithParam<PlyTestParamsType> PlyTest;
|
||||
|
||||
TEST_P(PlyTest, LoadSaveMesh)
|
||||
{
|
||||
std::vector<cv::Point3f> points;
|
||||
std::vector<cv::Point3f> normals;
|
||||
std::vector<std::vector<int32_t>> indices;
|
||||
std::string fname = GetParam();
|
||||
|
||||
std::vector<cv::Point3f> points_gold, normals_gold, colors_gold;
|
||||
std::vector<std::vector<int32_t>> indices_gold;
|
||||
|
||||
auto folder = cvtest::TS::ptr()->get_data_path();
|
||||
std::string new_path = tempfile("new_mesh.ply");
|
||||
|
||||
// we don't support meshes in PLY format right now but it should exit silently
|
||||
cv::loadMesh(folder + "pointcloudio/orig.ply", points, normals, indices);
|
||||
EXPECT_TRUE(points.empty());
|
||||
EXPECT_TRUE(normals.empty());
|
||||
EXPECT_TRUE(indices.empty());
|
||||
cv::loadMesh(folder + fname, points_gold, indices_gold, normals_gold, colors_gold);
|
||||
EXPECT_FALSE(points_gold.empty());
|
||||
EXPECT_FALSE(indices_gold.empty());
|
||||
|
||||
cv::saveMesh(new_path, points, normals, indices);
|
||||
EXPECT_TRUE(points.empty());
|
||||
EXPECT_TRUE(normals.empty());
|
||||
EXPECT_TRUE(indices.empty());
|
||||
cv::saveMesh(new_path, points_gold, indices_gold, normals_gold, colors_gold);
|
||||
|
||||
std::vector<cv::Point3f> points, normals, colors;
|
||||
std::vector<std::vector<int32_t>> indices;
|
||||
cv::loadMesh(new_path, points, indices, normals, colors);
|
||||
|
||||
if (!normals.empty())
|
||||
{
|
||||
EXPECT_LE(cv::norm(normals_gold, normals, NORM_INF), 0);
|
||||
}
|
||||
EXPECT_LE(cv::norm(points_gold, points, NORM_INF), 0);
|
||||
EXPECT_LE(cv::norm(colors_gold, colors, NORM_INF), 0);
|
||||
EXPECT_EQ(indices_gold, indices);
|
||||
std::remove(new_path.c_str());
|
||||
}
|
||||
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(PointCloud, PlyTest,
|
||||
::testing::Values("pointcloudio/orig.ply", "pointcloudio/orig_ascii_fidx.ply", "pointcloudio/orig_bin_fidx.ply",
|
||||
"pointcloudio/orig_ascii_vidx.ply", "pointcloudio/orig_bin.ply", "viz/dragon.ply"));
|
||||
|
||||
TEST(PointCloud, NonexistentFile)
|
||||
{
|
||||
std::vector<cv::Point3f> points;
|
||||
|
@ -15,7 +15,7 @@ cv.viz3d.setGridVisible("window", True)
|
||||
|
||||
cv.waitKey(0)
|
||||
|
||||
vertices, _, indices = cv.loadMesh("../data/teapot.obj")
|
||||
vertices, indices = cv.loadMesh("../data/teapot.obj")
|
||||
vertices = np.squeeze(vertices, axis=1)
|
||||
|
||||
cv.viz3d.showMesh("window", "mesh", vertices, indices)
|
||||
|
Loading…
Reference in New Issue
Block a user