diff --git a/doc/tutorials/3d/point_cloud/point_cloud.markdown b/doc/tutorials/3d/point_cloud/point_cloud.markdown index fe5d52ef43..453a18e084 100644 --- a/doc/tutorials/3d/point_cloud/point_cloud.markdown +++ b/doc/tutorials/3d/point_cloud/point_cloud.markdown @@ -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) diff --git a/modules/3d/include/opencv2/3d.hpp b/modules/3d/include/opencv2/3d.hpp index ceef7f615b..422f0b8d80 100644 --- a/modules/3d/include/opencv2/3d.hpp +++ b/modules/3d/include/opencv2/3d.hpp @@ -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 diff --git a/modules/3d/src/pointcloud/io_ply.cpp b/modules/3d/src/pointcloud/io_ply.cpp index 0e8447edf5..8d29691f56 100644 --- a/modules/3d/src/pointcloud/io_ply.cpp +++ b/modules/3d/src/pointcloud/io_ply.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include namespace cv { @@ -16,15 +18,16 @@ void PlyDecoder::readData(std::vector &points, std::vector &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 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 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 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 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 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 @@ -142,42 +327,185 @@ T readNext(std::ifstream &file, DataFormat format) return val; } -void PlyDecoder::parseBody(std::ifstream &file, std::vector &points, std::vector &normals, std::vector> &rgb) +template <> +uchar readNext(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 &points, std::vector &normals, std::vector> &rgb, + std::vector> &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 bytes; + VertexFields vf; + }; + + // to avoid string matching at file loading + std::vector 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(file, m_inputDataFormat); - vertex.y = readNext(file, m_inputDataFormat); - vertex.z = readNext(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(file, m_inputDataFormat); + break; + case CV_16U: case CV_16S: + ival = readNext(file, m_inputDataFormat); + break; + case CV_32S: case CV_32U: + ival = readNext(file, m_inputDataFormat); + break; + case CV_32F: + fval = readNext(file, m_inputDataFormat); + break; + case CV_64F: + fval = (float)readNext(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_ colour; - colour.x = readNext(file, m_inputDataFormat) & 0xff; - colour.y = readNext(file, m_inputDataFormat) & 0xff; - colour.z = readNext(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(file, m_inputDataFormat); - normal.y = readNext(file, m_inputDataFormat); - normal.z = readNext(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(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(file, m_inputDataFormat); + int vert2 = readNext(file, m_inputDataFormat); + for (size_t j = 2; j < nVerts; j++) + { + int vert3 = readNext(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(file, m_inputDataFormat); + break; + case CV_16U: case CV_16S: + readNext(file, m_inputDataFormat); + break; + case CV_32S: case CV_32U: + readNext(file, m_inputDataFormat); + break; + case CV_32F: + readNext(file, m_inputDataFormat); + break; + case CV_64F: + readNext(file, m_inputDataFormat); + break; + default: + break; + } + } } } } void PlyEncoder::writeData(const std::vector &points, const std::vector &normals, const std::vector> &rgb, const std::vector> &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 &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 &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(rgb[i].x) << " " << static_cast(rgb[i].y) << " " << static_cast(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; } diff --git a/modules/3d/src/pointcloud/io_ply.hpp b/modules/3d/src/pointcloud/io_ply.hpp index d8fc44e353..1553f109aa 100644 --- a/modules/3d/src/pointcloud/io_ply.hpp +++ b/modules/3d/src/pointcloud/io_ply.hpp @@ -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 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 &points, std::vector &normals, std::vector> &rgb); + void parseBody(std::ifstream &file, std::vector &points, std::vector &normals, std::vector> &rgb, + std::vector> &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 diff --git a/modules/3d/src/pointcloud/load_point_cloud.cpp b/modules/3d/src/pointcloud/load_point_cloud.cpp index f2a052c43b..a7c9e0ac70 100644 --- a/modules/3d/src/pointcloud/load_point_cloud.cpp +++ b/modules/3d/src/pointcloud/load_point_cloud.cpp @@ -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(vec_vertices.size()), CV_32FC3, vec_vertices.data()).copyTo(vertices); } - if (!vec_normals.empty()) { + if (normals.needed() && !vec_normals.empty()) + { Mat(1, static_cast(vec_normals.size()), CV_32FC3, vec_normals.data()).copyTo(normals); } - if (!vec_indices.empty()) { + if (colors.needed() && !vec_rgb.empty()) + { + Mat(1, static_cast(vec_rgb.size()), CV_8UC3, vec_rgb.data()).convertTo(colors, CV_32F, (1.0/255.0)); + } + + if (!vec_indices.empty()) + { std::vector>& vec = *(std::vector>*)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 vec_vertices(vertices.getMat()); std::vector vec_normals; std::vector> 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_indices; indices.getMatVector(mat_indices); std::vector> 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 diff --git a/modules/3d/test/test_pointcloud.cpp b/modules/3d/test/test_pointcloud.cpp index 13453bab1f..466d3f7270 100644 --- a/modules/3d/test/test_pointcloud.cpp +++ b/modules/3d/test/test_pointcloud.cpp @@ -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 points_gold; std::vector normals_gold; std::vector> 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 PlyTest; + +TEST_P(PlyTest, LoadSaveMesh) { - std::vector points; - std::vector normals; - std::vector> indices; + std::string fname = GetParam(); + + std::vector points_gold, normals_gold, colors_gold; + std::vector> 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 points, normals, colors; + std::vector> 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 points; diff --git a/samples/python/point_cloud.py b/samples/python/point_cloud.py index 2cdcfc4550..e7cadf2c3c 100644 --- a/samples/python/point_cloud.py +++ b/samples/python/point_cloud.py @@ -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)