From a99b4071a21c7029660b6a43d112d476e967f7ab Mon Sep 17 00:00:00 2001 From: Klepikov Dmitrii Date: Fri, 13 May 2022 21:10:39 +0300 Subject: [PATCH] Merge pull request #20471 from ibvfteh:pointcloudio GSoC module to save and load point cloud * Add functionality to read point cloud data from files * address issues found on review, add tests for mesh, refactor * enable fail-safe execution and empty arrays as output * Some improvements for point cloud io module Co-authored-by: Julie Bareeva --- .../3d/point_cloud/point_cloud.markdown | 75 +++++++ doc/tutorials/3d/point_cloud/teapot_grid.jpg | Bin 0 -> 46126 bytes doc/tutorials/3d/point_cloud/teapot_mesh.jpg | Bin 0 -> 16776 bytes .../tutorial_point_cloud_teapot.jpg | Bin 0 -> 27045 bytes doc/tutorials/3d/table_of_content_3d.markdown | 5 + doc/tutorials/tutorials.markdown | 1 + modules/3d/include/opencv2/3d.hpp | 54 +++++ modules/3d/src/pointcloud/io_base.cpp | 32 +++ modules/3d/src/pointcloud/io_base.hpp | 51 +++++ modules/3d/src/pointcloud/io_obj.cpp | 112 ++++++++++ modules/3d/src/pointcloud/io_obj.hpp | 31 +++ modules/3d/src/pointcloud/io_ply.cpp | 205 ++++++++++++++++++ modules/3d/src/pointcloud/io_ply.hpp | 45 ++++ .../3d/src/pointcloud/load_point_cloud.cpp | 199 +++++++++++++++++ modules/3d/src/pointcloud/utils.cpp | 21 ++ modules/3d/src/pointcloud/utils.hpp | 65 ++++++ modules/3d/test/test_pointcloud.cpp | 186 ++++++++++++++++ samples/python/essential_mat_reconstr.py | 20 +- samples/python/point_cloud.py | 23 ++ 19 files changed, 1113 insertions(+), 12 deletions(-) create mode 100644 doc/tutorials/3d/point_cloud/point_cloud.markdown create mode 100644 doc/tutorials/3d/point_cloud/teapot_grid.jpg create mode 100644 doc/tutorials/3d/point_cloud/teapot_mesh.jpg create mode 100644 doc/tutorials/3d/point_cloud/tutorial_point_cloud_teapot.jpg create mode 100644 doc/tutorials/3d/table_of_content_3d.markdown create mode 100644 modules/3d/src/pointcloud/io_base.cpp create mode 100644 modules/3d/src/pointcloud/io_base.hpp create mode 100644 modules/3d/src/pointcloud/io_obj.cpp create mode 100644 modules/3d/src/pointcloud/io_obj.hpp create mode 100644 modules/3d/src/pointcloud/io_ply.cpp create mode 100644 modules/3d/src/pointcloud/io_ply.hpp create mode 100644 modules/3d/src/pointcloud/load_point_cloud.cpp create mode 100644 modules/3d/src/pointcloud/utils.cpp create mode 100644 modules/3d/src/pointcloud/utils.hpp create mode 100644 modules/3d/test/test_pointcloud.cpp create mode 100644 samples/python/point_cloud.py diff --git a/doc/tutorials/3d/point_cloud/point_cloud.markdown b/doc/tutorials/3d/point_cloud/point_cloud.markdown new file mode 100644 index 0000000000..fe5d52ef43 --- /dev/null +++ b/doc/tutorials/3d/point_cloud/point_cloud.markdown @@ -0,0 +1,75 @@ +Point cloud visualisation {#tutorial_point_cloud} +============================== + +| | | +| -: | :- | +| Original author | Dmitrii Klepikov | +| Compatibility | OpenCV >= 5.0 | + +Goal +---- + +In this tutorial you will: + +- Load and save point cloud data +- Visualise your data + +Requirements +------------ + +For visualisations you need to compile OpenCV library with OpenGL support. +For this you should set WITH_OPENGL flag ON in CMake while building OpenCV from source. + +Practice +------- + +Loading and saving of point cloud can be done using `cv::loadPointCloud` and `cv::savePointCloud` accordingly. + +Currently supported formats are: +- [.OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) (supported keys are v(which is responsible for point position), vn(normal coordinates) and f(faces of a mesh), other keys are ignored) +- [.PLY](https://en.wikipedia.org/wiki/PLY_(file_format)) (all encoding types(ascii and byte) are supported with limitation to only float type for data) + +@code{.py} +vertices, normals = cv2.loadPointCloud("teapot.obj") +@endcode + +Function `cv::loadPointCloud` returns vector of points of float (`cv::Point3f`) and vector of their normals(if specified in source file). +To visualize it you can use functions from viz3d module and it is needed to reinterpret data into another format + +@code{.py} +vertices = np.squeeze(vertices, axis=1) + +color = [1.0, 1.0, 0.0] +colors = np.tile(color, (vertices.shape[0], 1)) +obj_pts = np.concatenate((vertices, colors), axis=1).astype(np.float32) + +cv2.viz3d.showPoints("Window", "Points", obj_pts) + +cv2.waitKey(0) +@endcode + +In presented code sample we add a colour attribute to every point +Result will be: + +![](tutorial_point_cloud_teapot.jpg) + +For additional info grid can be added + +@code{.py} +vertices, normals = cv2.loadPointCloud("teapot.obj") +@endcode + +![](teapot_grid.jpg) + +Other possible way to draw 3d objects can be a mesh. +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 = np.squeeze(vertices, axis=1) + +cv2.viz3d.showMesh("window", "mesh", vertices, indices) +@endcode + +![](teapot_mesh.jpg) diff --git a/doc/tutorials/3d/point_cloud/teapot_grid.jpg b/doc/tutorials/3d/point_cloud/teapot_grid.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28331a3847927a41b32aefaa2dc620de4e362793 GIT binary patch literal 46126 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 p; }; + +/** @brief Loads a point cloud from a file. +* +* The function loads point cloud from the specified file and returns it. +* If the cloud cannot be read, throws an error +* +* Currently, the following file formats are supported: +* - [Wavefront obj file *.obj](https://en.wikipedia.org/wiki/Wavefront_.obj_file) +* - [Polygon File Format *.ply](https://en.wikipedia.org/wiki/PLY_(file_format)) +* +* @param filename Name of the file. +* @param vertices (vector of Point3f) Point coordinates of a point cloud +* @param normals (vector of Point3f) Point normals of a point cloud +*/ +CV_EXPORTS_W void loadPointCloud(const String &filename, OutputArray vertices, OutputArray normals = noArray()); + +/** @brief Saves a point cloud to a specified file. +* +* The function saves point cloud to the specified file. +* File format is chosen based on the filename extension. +* +* @param filename Name of the file. +* @param vertices (vector of Point3f) Point coordinates of a point cloud +* @param normals (vector of Point3f) Point normals of a point cloud +*/ +CV_EXPORTS_W void savePointCloud(const String &filename, InputArray vertices, InputArray normals = noArray()); + +/** @brief Loads a mesh from a file. +* +* The function loads mesh from the specified file and returns it. +* If the mesh cannot be read, throws an error +* +* Currently, the following file formats are supported: +* - [Wavefront obj file *.obj](https://en.wikipedia.org/wiki/Wavefront_.obj_file) (ONLY TRIANGULATED FACES) +* @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 +*/ +CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputArray normals, OutputArrayOfArrays indices); + +/** @brief Saves a mesh to a specified file. +* +* The function saves mesh to the specified file. +* File format is chosen based on the filename extension. +* +* @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 +*/ +CV_EXPORTS_W void saveMesh(const String &filename, InputArray vertices, InputArray normals, InputArrayOfArrays indices); + + //! @} _3d } //end namespace cv diff --git a/modules/3d/src/pointcloud/io_base.cpp b/modules/3d/src/pointcloud/io_base.cpp new file mode 100644 index 0000000000..e87700555c --- /dev/null +++ b/modules/3d/src/pointcloud/io_base.cpp @@ -0,0 +1,32 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" +#include "io_base.hpp" + +namespace cv { + +void BasePointCloudDecoder::setSource(const std::string &filename) noexcept +{ + m_filename = filename; +} + +void BasePointCloudDecoder::readData(std::vector &points, std::vector &normals) +{ + std::vector> indices; + readData(points, normals, indices); +} + +void BasePointCloudEncoder::setDestination(const std::string &filename) noexcept +{ + m_filename = filename; +} + +void BasePointCloudEncoder::writeData(const std::vector &points, const std::vector &normals) +{ + std::vector> indices; + writeData(points, normals, indices); +} + +} /* namespace cv */ diff --git a/modules/3d/src/pointcloud/io_base.hpp b/modules/3d/src/pointcloud/io_base.hpp new file mode 100644 index 0000000000..b5fe4bae89 --- /dev/null +++ b/modules/3d/src/pointcloud/io_base.hpp @@ -0,0 +1,51 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef _CODERS_BASE_H_ +#define _CODERS_BASE_H_ + +#include +#include +#include + +#include + +namespace cv { + +class BasePointCloudDecoder; +class BasePointCloudEncoder; +using PointCloudDecoder = std::unique_ptr; +using PointCloudEncoder = std::unique_ptr; + +///////////////////////////////// base class for decoders //////////////////////// +class BasePointCloudDecoder +{ +public: + virtual ~BasePointCloudDecoder() = default; + + virtual void setSource(const String &filename) noexcept; + virtual void readData(std::vector &points, std::vector &normals); + virtual void readData(std::vector &points, std::vector &normals, std::vector> &indices) = 0; + +protected: + String m_filename; +}; + +///////////////////////////////// base class for encoders //////////////////////// +class BasePointCloudEncoder +{ +public: + virtual ~BasePointCloudEncoder() = default; + + virtual void setDestination(const String &filename) noexcept; + virtual void writeData(const std::vector &points, const std::vector &normals); + virtual void writeData(const std::vector &points, const std::vector &normals, const std::vector> &indices) = 0; + +protected: + String m_filename; +}; + +} /* namespace cv */ + +#endif diff --git a/modules/3d/src/pointcloud/io_obj.cpp b/modules/3d/src/pointcloud/io_obj.cpp new file mode 100644 index 0000000000..2c7afa4e4c --- /dev/null +++ b/modules/3d/src/pointcloud/io_obj.cpp @@ -0,0 +1,112 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" +#include "io_obj.hpp" +#include +#include +#include "utils.hpp" + +namespace cv { + +std::unordered_set ObjDecoder::m_unsupportedKeys; + +void ObjDecoder::readData(std::vector &points, std::vector &normals, std::vector> &indices) +{ + points.clear(); + normals.clear(); + indices.clear(); + + std::ifstream file(m_filename, std::ios::binary); + if (!file) + { + CV_LOG_ERROR(NULL, "Impossible to open the file: " << m_filename); + return; + } + std::string s; + + while (!file.eof()) + { + std::getline(file, s); + if (s.empty()) + continue; + std::stringstream ss(s); + std::string key; + ss >> key; + + if (key == "#") + continue; + else if (key == "v") + { + Point3f vertex; + ss >> vertex.x >> vertex.y >> vertex.z; + points.push_back(vertex); + } + else if (key == "vn") + { + Point3f normal; + ss >> normal.x >> normal.y >> normal.z; + normals.push_back(normal); + } + else if (key == "f") + { + std::vector vertexInd; + auto tokens = split(s, ' '); + for (size_t i = 1; i < tokens.size(); i++) + { + auto vertexinfo = split(tokens[i], '/'); + int index; + std::stringstream vs(vertexinfo[0]); + vs >> index; + vertexInd.push_back(index - 1); + } + indices.push_back(vertexInd); + } + else + { + if (m_unsupportedKeys.find(key) == m_unsupportedKeys.end()) { + m_unsupportedKeys.insert(key); + CV_LOG_WARNING(NULL, "Key " << key << " not supported"); + } + } + } + + file.close(); +} + +void ObjEncoder::writeData(const std::vector &points, const std::vector &normals, const std::vector> &indices) +{ + std::ofstream file(m_filename, std::ios::binary); + if (!file) { + CV_LOG_ERROR(NULL, "Impossible to open the file: " << m_filename); + return; + } + + file << "# OBJ file writer" << std::endl; + file << "o Point_Cloud" << std::endl; + + for (const auto& point : points) + { + file << "v " << point.x << " " << point.y << " " << point.z << std::endl; + } + + for (const auto& normal : normals) + { + file << "vn " << normal.x << " " << normal.y << " " << normal.z << std::endl; + } + + for (const auto& faceIndices : indices) + { + file << "f "; + for (const auto& index : faceIndices) + { + file << index + 1 << " "; + } + file << std::endl; + } + + file.close(); +} + +} /* namespace cv */ diff --git a/modules/3d/src/pointcloud/io_obj.hpp b/modules/3d/src/pointcloud/io_obj.hpp new file mode 100644 index 0000000000..924827fa2c --- /dev/null +++ b/modules/3d/src/pointcloud/io_obj.hpp @@ -0,0 +1,31 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef _CODERS_OBJ_H_ +#define _CODERS_OBJ_H_ + +#include "io_base.hpp" +#include + +namespace cv { + +class ObjDecoder CV_FINAL : public BasePointCloudDecoder +{ +public: + void readData(std::vector &points, std::vector &normals, std::vector> &indices) CV_OVERRIDE; + +protected: + static std::unordered_set m_unsupportedKeys; +}; + +class ObjEncoder CV_FINAL : public BasePointCloudEncoder +{ +public: + void writeData(const std::vector &points, const std::vector &normals, const std::vector> &indices) CV_OVERRIDE; + +}; + +} /* namespace cv */ + +#endif diff --git a/modules/3d/src/pointcloud/io_ply.cpp b/modules/3d/src/pointcloud/io_ply.cpp new file mode 100644 index 0000000000..d33b027777 --- /dev/null +++ b/modules/3d/src/pointcloud/io_ply.cpp @@ -0,0 +1,205 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" +#include "io_ply.hpp" +#include "utils.hpp" +#include +#include +#include + +namespace cv { + +void PlyDecoder::readData(std::vector &points, std::vector &normals, std::vector> &indices) +{ + points.clear(); + normals.clear(); + CV_UNUSED(indices); + + std::ifstream file(m_filename, std::ios::binary); + if (parseHeader(file)) + { + parseBody(file, points, normals); + } +} + +bool PlyDecoder::parseHeader(std::ifstream &file) +{ + std::string s; + std::getline(file, s); + if (trimSpaces(s) != "ply") + { + CV_LOG_ERROR(NULL, "Provided file is not in PLY format"); + return false; + } + std::getline(file, s); + auto splitArr = split(s, ' '); + if (splitArr[0] != "format") + { + CV_LOG_ERROR(NULL, "Provided file doesn't have format"); + return false; + } + if (splitArr[1] == "ascii") + { + m_inputDataFormat = DataFormat::ASCII; + } + else if (splitArr[1] == "binary_little_endian") + { + m_inputDataFormat = DataFormat::BinaryLittleEndian; + } + else if (splitArr[1] == "binary_big_endian") + { + m_inputDataFormat = DataFormat::BinaryBigEndian; + } + else + { + CV_LOG_ERROR(NULL, "Provided PLY file format is not supported"); + return false; + } + + bool onVertexRead = false; + while (std::getline(file, s)) + { + if (startsWith(s, "element")) + { + auto splitArrElem = split(s, ' '); + if (splitArrElem[1] == "vertex") + { + onVertexRead = true; + std::istringstream iss(splitArrElem[2]); + iss >> m_vertexCount; + } + else + { + onVertexRead = false; + } + continue; + } + if (startsWith(s, "property")) + { + if (onVertexRead) + { + auto splitArrElem = split(s, ' '); + if (splitArrElem[2] == "x" || splitArrElem[2] == "red" || splitArrElem[2] == "nx") + { + if (splitArrElem[1] != "float") { + CV_LOG_ERROR(NULL, "Provided PLY file format '" << splitArrElem[1] << "' is not supported"); + return false; + } + } + if (splitArrElem[2] == "red") + { + m_hasColour = true; + } + if (splitArrElem[2] == "nx") + { + m_hasNormal = true; + } + } + continue; + } + if (startsWith(s, "end_header")) + break; + + } + return true; +} + +template +T readNext(std::ifstream &file, DataFormat format) +{ + T val; + if (format == DataFormat::ASCII) + { + file >> val; + return val; + } + file.read((char *)&val, sizeof(T)); +#ifdef WORDS_BIGENDIAN + if (!(format == DataFormat::BinaryBigEndian) ) + { + swapEndian(val); + } +#else + if (format == DataFormat::BinaryBigEndian) + { + swapEndian(val); + } +#endif + return val; +} + +void PlyDecoder::parseBody(std::ifstream &file, std::vector &points, std::vector &normals) +{ + points.reserve(m_vertexCount); + if (m_hasNormal) + { + normals.reserve(m_vertexCount); + } + 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); + if (m_hasColour) + { + readNext(file, m_inputDataFormat); + readNext(file, m_inputDataFormat); + readNext(file, m_inputDataFormat); + } + 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); + } + } +} + +void PlyEncoder::writeData(const std::vector &points, const std::vector &normals, 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); + return; + } + + file << "ply" << std::endl; + file << "format ascii 1.0" << std::endl; + file << "comment created by OpenCV" << std::endl; + file << "element vertex " << points.size() << std::endl; + + file << "property float x" << std::endl; + file << "property float y" << std::endl; + file << "property float z" << std::endl; + + file << "end_header" << std::endl; + + if (normals.size() != 0) + { + file << "property float nx" << std::endl; + file << "property float ny" << std::endl; + file << "property float nz" << std::endl; + } + + bool isNormals = (normals.size() != 0); + + for (size_t i = 0; i < points.size(); i++) + { + file << points[i].x << " " << points[i].y << " " << points[i].z; + if (isNormals) + { + file << normals[i].x << " " << normals[i].y << " " << normals[i].z; + } + file << std::endl; + } + file.close(); +} + +} /* namespace cv */ diff --git a/modules/3d/src/pointcloud/io_ply.hpp b/modules/3d/src/pointcloud/io_ply.hpp new file mode 100644 index 0000000000..5fd4c39814 --- /dev/null +++ b/modules/3d/src/pointcloud/io_ply.hpp @@ -0,0 +1,45 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef _CODERS_PLY_H_ +#define _CODERS_PLY_H_ + +#include "io_base.hpp" +#include +#include + +namespace cv { + +enum class DataFormat +{ + ASCII, + BinaryLittleEndian, + BinaryBigEndian +}; + +class PlyDecoder CV_FINAL : public BasePointCloudDecoder +{ +public: + void readData(std::vector &points, std::vector &normals, std::vector> &indices) CV_OVERRIDE; + +protected: + bool parseHeader(std::ifstream &file); + void parseBody(std::ifstream &file, std::vector &points, std::vector &normals); + + DataFormat m_inputDataFormat; + size_t m_vertexCount{0}; + bool m_hasColour{false}; + bool m_hasNormal{false}; +}; + +class PlyEncoder CV_FINAL : public BasePointCloudEncoder +{ +public: + void writeData(const std::vector &points, const std::vector &normals, const std::vector> &indices) CV_OVERRIDE; + +}; + +} /* namespace cv */ + +#endif diff --git a/modules/3d/src/pointcloud/load_point_cloud.cpp b/modules/3d/src/pointcloud/load_point_cloud.cpp new file mode 100644 index 0000000000..8c9dd3d50f --- /dev/null +++ b/modules/3d/src/pointcloud/load_point_cloud.cpp @@ -0,0 +1,199 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" + +#include "io_base.hpp" +#include "io_obj.hpp" +#include "io_ply.hpp" +#include "utils.hpp" +#include "opencv2/core/utils/filesystem.private.hpp" +#include + +#include + +namespace cv { + +#if OPENCV_HAVE_FILESYSTEM_SUPPORT + +static PointCloudDecoder findDecoder(const String &filename) +{ + auto file_ext = getExtension(filename); + if (file_ext == "obj" || file_ext == "OBJ") + { + return std::unique_ptr(new ObjDecoder()); + } + if (file_ext == "ply" || file_ext == "PLY") + { + return std::unique_ptr(new PlyDecoder()); + } + + return nullptr; +} + +static PointCloudEncoder findEncoder(const String &filename) +{ + auto file_ext = getExtension(filename); + if (file_ext == "obj" || file_ext == "OBJ") + { + return std::unique_ptr(new ObjEncoder()); + } + if (file_ext == "ply" || file_ext == "PLY") + { + return std::unique_ptr(new PlyEncoder()); + } + + return nullptr; +} + +#endif + +void loadPointCloud(const String &filename, OutputArray vertices, OutputArray normals) +{ +#if OPENCV_HAVE_FILESYSTEM_SUPPORT + auto decoder = findDecoder(filename); + if (!decoder) { + String file_ext = getExtension(filename); + CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported"); + return; + } + + decoder->setSource(filename); + + std::vector vec_vertices; + std::vector vec_normals; + + decoder->readData(vec_vertices, vec_normals); + + if (!vec_vertices.empty()) + Mat(static_cast(vec_vertices.size()), 1, CV_32FC3, &vec_vertices[0]).copyTo(vertices); + + if (!vec_normals.empty() && normals.needed()) + Mat(static_cast(vec_normals.size()), 1, CV_32FC3, &vec_normals[0]).copyTo(normals); +#else // OPENCV_HAVE_FILESYSTEM_SUPPORT + CV_UNUSED(filename); + CV_UNUSED(vertices); + CV_UNUSED(normals); + CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!"); +#endif +} + +void savePointCloud(const String &filename, InputArray vertices, InputArray normals) +{ +#if OPENCV_HAVE_FILESYSTEM_SUPPORT + if (vertices.empty()) { + CV_LOG_WARNING(NULL, "Have no vertices to save"); + return; + }; + + auto encoder = findEncoder(filename); + if (!encoder) { + String file_ext = getExtension(filename); + CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported"); + return; + } + + encoder->setDestination(filename); + + std::vector vec_vertices(vertices.getMat()); + std::vector vec_normals; + if (!normals.empty()){ + vec_normals = normals.getMat(); + } + + encoder->writeData(vec_vertices, vec_normals); + +#else // OPENCV_HAVE_FILESYSTEM_SUPPORT + CV_UNUSED(filename); + CV_UNUSED(vertices); + CV_UNUSED(normals); + CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!"); +#endif +} + +void loadMesh(const String &filename, OutputArray vertices, OutputArray normals, OutputArrayOfArrays indices) +{ +#if OPENCV_HAVE_FILESYSTEM_SUPPORT + PointCloudDecoder decoder = findDecoder(filename); + String file_ext = getExtension(filename); + if (!decoder || (file_ext != "obj" && file_ext != "OBJ")) { + CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported"); + return; + } + + decoder->setSource(filename); + + std::vector vec_vertices; + std::vector vec_normals; + std::vector> vec_indices; + + decoder->readData(vec_vertices, vec_normals, vec_indices); + + if (!vec_vertices.empty()) { + Mat(1, static_cast(vec_vertices.size()), CV_32FC3, vec_vertices.data()).copyTo(vertices); + } + + if (!vec_normals.empty()) { + Mat(1, static_cast(vec_normals.size()), CV_32FC3, vec_normals.data()).copyTo(normals); + } + + 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) { + Mat(1, static_cast(vec_indices[i].size()), CV_32SC1, vec_indices[i].data()).copyTo(vec[i]); + } + } + +#else // OPENCV_HAVE_FILESYSTEM_SUPPORT + CV_UNUSED(filename); + CV_UNUSED(vertices); + CV_UNUSED(normals); + 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) +{ +#if OPENCV_HAVE_FILESYSTEM_SUPPORT + if (vertices.empty()) { + CV_LOG_WARNING(NULL, "Have no vertices to save"); + return; + } + + auto encoder = findEncoder(filename); + String file_ext = getExtension(filename); + if (!encoder || (file_ext != "obj" && file_ext != "OBJ")) { + CV_LOG_ERROR(NULL, "File extension '" << file_ext << "' is not supported"); + return; + } + + encoder->setDestination(filename); + + std::vector vec_vertices(vertices.getMat()); + std::vector vec_normals; + if (!normals.empty()){ + vec_normals = normals.getMat(); + } + + 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) { + mat_indices[i].copyTo(vec_indices[i]); + } + + encoder->writeData(vec_vertices, vec_normals, vec_indices); + +#else // OPENCV_HAVE_FILESYSTEM_SUPPORT + CV_UNUSED(filename); + CV_UNUSED(vertices); + CV_UNUSED(normals); + CV_LOG_WARNING(NULL, "File system support is disabled in this OpenCV build!"); +#endif + +} + +}/* namespace cv */ diff --git a/modules/3d/src/pointcloud/utils.cpp b/modules/3d/src/pointcloud/utils.cpp new file mode 100644 index 0000000000..ed15531c94 --- /dev/null +++ b/modules/3d/src/pointcloud/utils.cpp @@ -0,0 +1,21 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "utils.hpp" + +namespace cv { + +std::vector split(const std::string &s, char delimiter) +{ + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) + { + tokens.push_back(token); + } + return tokens; +} + +} /* namespace cv */ diff --git a/modules/3d/src/pointcloud/utils.hpp b/modules/3d/src/pointcloud/utils.hpp new file mode 100644 index 0000000000..815ec9094c --- /dev/null +++ b/modules/3d/src/pointcloud/utils.hpp @@ -0,0 +1,65 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef _CODERS_UTILS_H_ +#define _CODERS_UTILS_H_ + +#include +#include +#include +#include +#include + + +namespace cv { + +std::vector split(const std::string &s, char delimiter); + +inline bool startsWith(const std::string &s1, const std::string &s2) +{ + return s1.compare(0, s2.length(), s2) == 0; +} + +inline std::string trimSpaces(const std::string &input) +{ + size_t start = 0; + while (start < input.size() && input[start] == ' ') + { + start++; + } + size_t end = input.size(); + while (end > start && (input[end - 1] == ' ' || input[end - 1] == '\n' || input[end - 1] == '\r')) + { + end--; + } + return input.substr(start, end - start); +} + +inline std::string getExtension(const std::string& filename) +{ + auto pos = filename.find_last_of('.'); + if (pos == std::string::npos) + { + return ""; + } + return filename.substr( pos + 1); +} + +template +void swapEndian(T &val) +{ + union U + { + T val; + std::array raw; + } src, dst; + + src.val = val; + std::reverse_copy(src.raw.begin(), src.raw.end(), dst.raw.begin()); + val = dst.val; +} + +} /* namespace cv */ + +#endif diff --git a/modules/3d/test/test_pointcloud.cpp b/modules/3d/test/test_pointcloud.cpp new file mode 100644 index 0000000000..aa72765051 --- /dev/null +++ b/modules/3d/test/test_pointcloud.cpp @@ -0,0 +1,186 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include +#include +#include + +#include "test_precomp.hpp" +#include "opencv2/ts.hpp" + +namespace opencv_test { namespace { + +TEST(PointCloud, LoadObj) +{ + std::vector points_gold = { + {-5.93915f, -0.13257f, 2.55837f}, + {-5.93915f, 1.86743f, 2.55837f}, + {-5.93915f, -0.13257f, -1.16339f}, + {-5.93915f, 1.86743f, -1.16339f}, + {0.399941f, -0.13257f, 2.55837f}, + {0.399941f, 1.86743f, 2.55837f}, + {0.399941f, -0.13257f, -1.16339f}, + {0.399941f, 1.86743f, -1.16339f}}; + std::vector normals_gold = { + {-1.0000f, 0.0000f, 0.0000f}, + {0.0000f, 0.0000f, -1.0000f}, + {1.0000f, 0.0000f, 0.0000f}, + {0.0000f, 0.0000f, 1.0000f}, + {0.0000f, -1.0000f, 0.0000f}, + {0.0000f, 1.0000f, 0.0000f}}; + + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + cv::loadPointCloud(folder + "pointcloudio/orig.obj", points, normals); + + EXPECT_EQ(points_gold, points); + EXPECT_EQ(normals_gold, normals); +} + +TEST(PointCloud, LoadObjNoNormals) +{ + std::vector points_gold = { + {-5.93915f, -0.13257f, 2.55837f}, + {-5.93915f, 1.86743f, 2.55837f}, + {-5.93915f, -0.13257f, -1.16339f}, + {-5.93915f, 1.86743f, -1.16339f}, + {0.399941f, -0.13257f, 2.55837f}, + {0.399941f, 1.86743f, 2.55837f}, + {0.399941f, -0.13257f, -1.16339f}, + {0.399941f, 1.86743f, -1.16339f}}; + + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + cv::loadPointCloud(folder + "pointcloudio/orig_no_norms.obj", points, normals); + + EXPECT_EQ(points_gold, points); + EXPECT_TRUE(normals.empty()); +} + +TEST(PointCloud, SaveObj) +{ + std::vector points_gold; + std::vector normals_gold; + + auto folder = cvtest::TS::ptr()->get_data_path(); + auto new_path = tempfile("new.obj"); + + cv::loadPointCloud(folder + "pointcloudio/orig.obj", points_gold, normals_gold); + cv::savePointCloud(new_path, points_gold, normals_gold); + + std::vector points; + std::vector normals; + + cv::loadPointCloud(new_path, points, normals); + + EXPECT_EQ(normals, normals_gold); + EXPECT_EQ(points, points_gold); + std::remove(new_path.c_str()); +} + +TEST(PointCloud, LoadSavePly) +{ + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + std::string new_path = tempfile("new.ply"); + + cv::loadPointCloud(folder + "pointcloudio/orig.ply", points, normals); + cv::savePointCloud(new_path, points, normals); + + std::vector points_gold; + std::vector normals_gold; + + cv::loadPointCloud(new_path, points_gold, normals_gold); + + EXPECT_EQ(normals_gold, normals); + EXPECT_EQ(points_gold, points); + std::remove(new_path.c_str()); +} + +TEST(PointCloud, LoadSaveMeshObj) +{ + std::vector points; + std::vector normals; + std::vector> indices; + + 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); + + std::vector points_gold; + std::vector normals_gold; + std::vector> indices_gold; + + cv::loadMesh(new_path, points_gold, normals_gold, indices_gold); + + EXPECT_EQ(normals_gold, normals); + EXPECT_EQ(points_gold, points); + EXPECT_EQ(indices_gold, indices); + EXPECT_TRUE(!indices.empty()); + std::remove(new_path.c_str()); +} + +TEST(PointCloud, LoadSaveMeshPly) +{ + std::vector points; + std::vector normals; + std::vector> indices; + + 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::saveMesh(new_path, points, normals, indices); + EXPECT_TRUE(points.empty()); + EXPECT_TRUE(normals.empty()); + EXPECT_TRUE(indices.empty()); + + std::remove(new_path.c_str()); +} + +TEST(PointCloud, NonexistentFile) +{ + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + cv::loadPointCloud(folder + "pointcloudio/fake.obj", points, normals); + EXPECT_TRUE(points.empty()); + EXPECT_TRUE(normals.empty()); +} + +TEST(PointCloud, LoadBadExtension) +{ + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + cv::loadPointCloud(folder + "pointcloudio/fake.fake", points, normals); + EXPECT_TRUE(points.empty()); + EXPECT_TRUE(normals.empty()); +} + +TEST(PointCloud, SaveBadExtension) +{ + std::vector points; + std::vector normals; + + auto folder = cvtest::TS::ptr()->get_data_path(); + cv::savePointCloud(folder + "pointcloudio/fake.fake", points, normals); +} + +}} /* namespace opencv_test */ diff --git a/samples/python/essential_mat_reconstr.py b/samples/python/essential_mat_reconstr.py index cfb4fb1d0b..58c4254e43 100644 --- a/samples/python/essential_mat_reconstr.py +++ b/samples/python/essential_mat_reconstr.py @@ -1,5 +1,4 @@ -import numpy as np, cv2 as cv, matplotlib.pyplot as plt, time, sys, os -from mpl_toolkits.mplot3d import axes3d, Axes3D +import numpy as np, cv2 as cv, time, sys, os def getEpipolarError(F, pts1_, pts2_, inliers): pts1 = np.concatenate((pts1_.T, np.ones((1, pts1_.shape[0]))))[:,inliers] @@ -119,19 +118,16 @@ if __name__ == '__main__': int(np.sqrt (image1.shape[0] * new_img_size / image1.shape[1])))) # plot object points - fig = plt.figure(figsize=(13.0, 11.0)) - ax = fig.add_subplot(111, projection='3d') - ax.set_aspect('equal') - ax.scatter(obj_pts[:,0], obj_pts[:,1], obj_pts[:,2], c='r', marker='o', s=3) - ax.set_xlabel('x') - ax.set_ylabel('y') - ax.set_zlabel('depth') - ax.view_init(azim=-80, elev=110) + color = [1.0, 0.0, 0.0] + colors = np.tile(color, (obj_pts.shape[0], 1)) + obj_pts = np.concatenate((obj_pts, colors), axis=1) + obj_pts= np.float32(obj_pts) + + cv.viz3d.showPoints("window", "points", obj_pts) + cv.viz3d.setGridVisible("window", True) # save figures cv.imshow("matches", image1) cv.imwrite('matches_E.png', image1) - plt.savefig('reconstruction_3D.png') cv.waitKey(0) - plt.show() diff --git a/samples/python/point_cloud.py b/samples/python/point_cloud.py new file mode 100644 index 0000000000..2cdcfc4550 --- /dev/null +++ b/samples/python/point_cloud.py @@ -0,0 +1,23 @@ +import numpy as np +import cv2 as cv + +vertices, _ = cv.loadPointCloud("../data/teapot.obj") +vertices = np.squeeze(vertices, axis=1) +print(vertices) + +color = [1.0, 1.0, 0.0] +colors = np.tile(color, (vertices.shape[0], 1)) +obj_pts = np.concatenate((vertices, colors), axis=1) +obj_pts= np.float32(obj_pts) + +cv.viz3d.showPoints("window", "points", obj_pts) +cv.viz3d.setGridVisible("window", True) + +cv.waitKey(0) + +vertices, _, indices = cv.loadMesh("../data/teapot.obj") +vertices = np.squeeze(vertices, axis=1) + +cv.viz3d.showMesh("window", "mesh", vertices, indices) + +cv.waitKey(0)