Merge pull request #25221 from savuor:rv/bench_3d

OBJ and PLY loaders extention to support texture coordinates and difused colors #25221

### This PR changes
* Texture coordinates support added to `loadMesh()` and `saveMesh()`
* `loadMesh()` changes its behavior: all vertex attribute arrays (vertex coordinates, colors, normals, texture coordinates) now have the same size and same-index corresponce
  - This makes sense for OBJ files where vertex attribute arrays are independent from each other and are randomly accessed when defining faces
  - Looks like this behavior may also happen in some PLY files; however, it is not implemented until we encounter such files in a wild nature
  - At the same time `loadPointCloud()` keeps its behavior and loads vertex attributes as they are given in the file
* PLY loader supports synonyms for the properties: `diffuse_red`, `diffuse_green` and `diffuse_blue` along with `red`, `green` and `blue`
* `std::vector<cv::Vec3i>` supported as an index array type
* Colors are loaded as [0, 1] floats instead of uchars
  - Since colors are usually saved as floats, internal conversion to uchar at loading significantly drops accuracy
  - Performing uchar conversion does not always makes sense and can be performed by a user if they needs it
* PLY loading fixed: wrong offset ruined x coordinate
* Python tests added for `loadPointCloud` and `loadMesh`

### 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
pull/25335/head
Rostislav Vasilikhin 12 months ago committed by GitHub
parent 65074651a4
commit 82038be4cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 50
      modules/3d/include/opencv2/3d.hpp
  2. 46
      modules/3d/misc/python/test/test_iomesh.py
  3. 15
      modules/3d/src/pointcloud/io_base.cpp
  4. 24
      modules/3d/src/pointcloud/io_base.hpp
  5. 191
      modules/3d/src/pointcloud/io_obj.cpp
  6. 9
      modules/3d/src/pointcloud/io_obj.hpp
  7. 159
      modules/3d/src/pointcloud/io_ply.cpp
  8. 17
      modules/3d/src/pointcloud/io_ply.hpp
  9. 172
      modules/3d/src/pointcloud/load_point_cloud.cpp
  10. 102
      modules/3d/test/test_pointcloud.cpp
  11. 3
      modules/3d/test/test_ptcloud_utils.cpp
  12. 34
      modules/3d/test/test_rendering.cpp

@ -2836,16 +2836,19 @@ protected:
/** @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
* If the cloud cannot be read, throws an error.
* Vertex coordinates, normals and colors are returned as they are saved in the file
* even if these arrays have different sizes and their elements do not correspond to each other
* (which is typical for OBJ files for example)
*
* 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
* @param rgb (vector of Point3_<uchar>) Point RGB color of a point cloud
* @param filename Name of the file
* @param vertices vertex coordinates, each value contains 3 floats
* @param normals per-vertex normals, each value contains 3 floats
* @param rgb per-vertex colors, each value contains 3 floats
*/
CV_EXPORTS_W void loadPointCloud(const String &filename, OutputArray vertices, OutputArray normals = noArray(), OutputArray rgb = noArray());
@ -2854,10 +2857,10 @@ CV_EXPORTS_W void loadPointCloud(const String &filename, OutputArray vertices, O
* 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
* @param rgb (vector of Point3_<uchar>) Point RGB color of a point cloud
* @param filename Name of the file
* @param vertices vertex coordinates, each value contains 3 floats
* @param normals per-vertex normals, each value contains 3 floats
* @param rgb per-vertex colors, each value contains 3 floats
*/
CV_EXPORTS_W void savePointCloud(const String &filename, InputArray vertices, InputArray normals = noArray(), InputArray rgb = noArray());
@ -2865,18 +2868,24 @@ CV_EXPORTS_W void savePointCloud(const String &filename, InputArray vertices, In
*
* The function loads mesh from the specified file and returns it.
* If the mesh cannot be read, throws an error
* Vertex attributes (i.e. space and texture coodinates, normals and colors) are returned in same-sized
* arrays with corresponding elements having the same indices.
* This means that if a face uses a vertex with a normal or a texture coordinate with different indices
* (which is typical for OBJ files for example), this vertex will be duplicated for each face it uses.
*
* 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 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
* @param filename Name of the file
* @param vertices vertex coordinates, each value contains 3 floats
* @param indices per-face list of vertices, each value is a vector of ints
* @param normals per-vertex normals, each value contains 3 floats
* @param colors per-vertex colors, each value contains 3 floats
* @param texCoords per-vertex texture coordinates, each value contains 2 or 3 floats
*/
CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays indices,
OutputArray normals = noArray(), OutputArray colors = noArray());
OutputArray normals = noArray(), OutputArray colors = noArray(),
OutputArray texCoords = noArray());
/** @brief Saves a mesh to a specified file.
*
@ -2884,13 +2893,14 @@ CV_EXPORTS_W void loadMesh(const String &filename, OutputArray vertices, OutputA
* 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 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
* @param vertices vertex coordinates, each value contains 3 floats
* @param indices per-face list of vertices, each value is a vector of ints
* @param normals per-vertex normals, each value contains 3 floats
* @param colors per-vertex colors, each value contains 3 floats
* @param texCoords per-vertex texture coordinates, each value contains 2 or 3 floats
*/
CV_EXPORTS_W void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays indices,
InputArray normals = noArray(), InputArray colors = noArray());
InputArray normals = noArray(), InputArray colors = noArray(), InputArray texCoords = noArray());
//! Triangle fill settings

@ -0,0 +1,46 @@
#!/usr/bin/env python
# Python 2/3 compatibility
from __future__ import print_function
import os, numpy
import math
import unittest
import cv2 as cv
from tests_common import NewOpenCVTests
class raster_test(NewOpenCVTests):
def test_loadCloud(self):
fin = self.find_file("pointcloudio/orig.obj")
points, normals, colors = cv.loadPointCloud(fin)
if points.shape != (8, 1, 3):
self.fail('point array should be 8x1x3')
if normals.shape != (6, 1, 3):
self.fail('normals array should be 6x1x3')
if colors.shape != (8, 1, 3):
self.fail('colors array should be 8x1x3')
def test_loadMesh(self):
fin = self.find_file("pointcloudio/orig.obj")
points, indices, normals, colors, texCoords = cv.loadMesh(fin)
goodShapes = [(1, 18, 3), (18, 1, 3)]
errorMsg = "%s array should be 18x1x3 or 1x18x3"
for a, s in [(points, 'points'), (normals, 'normals'), (colors, 'colors')]:
if a.shape not in goodShapes:
self.fail(errorMsg % s)
if texCoords.shape not in [(1, 18, 2), (18, 1, 2)]:
self.fail('texture coordinates array should be 1x18x2 or 18x1x2')
if isinstance(indices, numpy.ndarray):
if indices.shape not in [(1, 6, 3), (6, 1, 3)]:
self.fail('indices array should be 1x6x3 or 6x1x3')
elif isinstance(indices, list) or isinstance(indices, tuple):
for i in indices:
if len(indices) != 6 or i.shape != (1, 3):
self.fail('indices array should be 1x6x3 or 6x1x3')
if __name__ == '__main__':
NewOpenCVTests.bootstrap()

@ -7,26 +7,29 @@
namespace cv {
void BasePointCloudDecoder::setSource(const std::string &filename) noexcept
void BasePointCloudDecoder::setSource(const std::string& filename) noexcept
{
m_filename = filename;
}
void BasePointCloudDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb)
void BasePointCloudDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3f> &rgb)
{
std::vector<std::vector<int32_t>> indices;
readData(points, normals, rgb, indices);
std::vector<Point3f> texCoords;
int nTexCoords;
readData(points, normals, rgb, texCoords, nTexCoords, indices, READ_AS_IS_FLAG);
}
void BasePointCloudEncoder::setDestination(const std::string &filename) noexcept
void BasePointCloudEncoder::setDestination(const std::string& filename) noexcept
{
m_filename = filename;
}
void BasePointCloudEncoder::writeData(const std::vector<Point3f> &points, const std::vector<Point3f> &normals, const std::vector<Point3_<uchar>> &rgb)
void BasePointCloudEncoder::writeData(const std::vector<Point3f> &points, const std::vector<Point3f> &normals, const std::vector<Point3f> &rgb)
{
std::vector<std::vector<int32_t>> indices;
writeData(points, normals, rgb, indices);
std::vector<Point3f> texCoords;
writeData(points, normals, rgb, texCoords, 0, indices);
}
} /* namespace cv */

@ -18,18 +18,24 @@ class BasePointCloudEncoder;
using PointCloudDecoder = std::unique_ptr<BasePointCloudDecoder>;
using PointCloudEncoder = std::unique_ptr<BasePointCloudEncoder>;
// for OBJ files: to read vertices, normals and texture coords as they are given in the file
// or duplicate them according to faces
const int READ_AS_IS_FLAG = 1;
///////////////////////////////// base class for decoders ////////////////////////
class BasePointCloudDecoder
{
public:
virtual ~BasePointCloudDecoder() = default;
virtual void setSource(const String &filename) noexcept;
virtual void readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb);
virtual void readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb, std::vector<std::vector<int32_t>> &indices) = 0;
virtual void setSource(const std::string& filename) noexcept;
virtual void readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb);
virtual void readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords, int& nTexCoords,
std::vector<std::vector<int32_t>>& indices, int flags) = 0;
protected:
String m_filename;
std::string m_filename;
};
///////////////////////////////// base class for encoders ////////////////////////
@ -38,12 +44,14 @@ class BasePointCloudEncoder
public:
virtual ~BasePointCloudEncoder() = default;
virtual void setDestination(const String &filename) noexcept;
virtual void writeData(const std::vector<Point3f> &points, const std::vector<Point3f> &normals, const std::vector<Point3_<uchar>> &rgb);
virtual void 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) = 0;
virtual void setDestination(const std::string& filename) noexcept;
virtual void writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb);
virtual void writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb,
const std::vector<Point3f>& texCoords, int nTexCoords,
const std::vector<std::vector<int32_t>>& indices) = 0;
protected:
String m_filename;
std::string m_filename;
};
} /* namespace cv */

@ -12,12 +12,24 @@ namespace cv {
std::unordered_set<std::string> ObjDecoder::m_unsupportedKeys;
void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb, std::vector<std::vector<int32_t>> &indices)
void ObjDecoder::readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb)
{
points.clear();
normals.clear();
rgb.clear();
indices.clear();
std::vector<Point3f> texCoords;
int nTexCoords;
std::vector<std::vector<int32_t>> indices;
this->readData(points, normals, rgb, texCoords, nTexCoords, indices, READ_AS_IS_FLAG);
}
void ObjDecoder::readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords, int& nTexCoords,
std::vector<std::vector<int32_t>>& indices, int flags)
{
std::vector<Point3f> ptsList, nrmList, texCoordList, rgbList;
std::vector<std::vector<int32_t>> idxList, texIdxList, normalIdxList;
nTexCoords = 0;
bool duplicateVertices = false;
std::ifstream file(m_filename, std::ios::binary);
if (!file)
@ -30,6 +42,8 @@ void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
while (!file.eof())
{
std::getline(file, s);
// "\r" symbols are not trimmed by default
s = trimSpaces(s);
if (s.empty())
continue;
std::stringstream ss(s);
@ -49,7 +63,7 @@ void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
}
Point3f vertex;
ss >> vertex.x >> vertex.y >> vertex.z;
points.push_back(vertex);
ptsList.push_back(vertex);
if (splitArr.size() == 5 || splitArr.size() == 8)
{
float w;
@ -59,13 +73,10 @@ void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
if (splitArr.size() >= 7)
{
Point3f color;
if (ss.rdbuf()->in_avail() != 0) {
Point3_<uchar> uc_color;
if (ss.rdbuf()->in_avail() != 0)
{
ss >> color.x >> color.y >> color.z;
uc_color.x = static_cast<uchar>(std::round(255.f * color.x));
uc_color.y = static_cast<uchar>(std::round(255.f * color.y));
uc_color.z = static_cast<uchar>(std::round(255.f * color.z));
rgb.push_back(uc_color);
rgbList.push_back(color);
}
}
}
@ -73,21 +84,92 @@ void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
{
Point3f normal;
ss >> normal.x >> normal.y >> normal.z;
normals.push_back(normal);
nrmList.push_back(normal);
}
else if (key == "f")
{
std::vector<int> vertexInd;
// format: "f v0 / t0 / n0 v1 / t1 / n1 v2/t2/n2 ..."
std::vector<int> vertexInd, normInd, texInd;
vertexInd.reserve(3); normInd.reserve(3); texInd.reserve(3);
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);
std::array<int, 3> idx = { -1, -1, -1 };
for (int j = 0; j < (int)vertexinfo.size(); j++)
{
std::string sj = vertexinfo[j];
// trimming spaces; as a result s can become empty - this is not an error
auto si = std::find_if(sj.begin(), sj.end(), [](char c) { return (c >= '0' && c <= '9'); });
auto ei = std::find_if(sj.rbegin(), sj.rend(), [](char c) { return (c >= '0' && c <= '9'); });
if (si != sj.end() && ei != sj.rend())
{
auto first = std::distance(si, sj.begin());
auto last = std::distance(ei, sj.rend());
sj = sj.substr(first, last - first + 1);
try
{
idx[j] = std::stoi(sj);
}
// std::invalid_exception, std::out_of_range
catch(const std::exception&)
{
CV_LOG_ERROR(NULL, "Failed to parse face index: " + sj);
return;
}
}
}
int vertexIndex = idx[0];
int texCoordIndex = idx[1];
int normalIndex = idx[2];
if (vertexIndex <= 0)
{
CV_LOG_ERROR(NULL, "Vertex index is not present or incorrect");
return;
}
if ((vertexIndex != texCoordIndex && texCoordIndex >= 0) ||
(vertexIndex != normalIndex && normalIndex >= 0))
{
duplicateVertices = !(flags & READ_AS_IS_FLAG);
}
vertexInd.push_back(vertexIndex - 1);
normInd.push_back(normalIndex - 1);
texInd.push_back(texCoordIndex - 1);
}
idxList.push_back(vertexInd);
texIdxList.push_back(texInd);
normalIdxList.push_back(normInd);
}
else if (key == "vt")
{
// (u, [v, [w]])
auto splitArr = split(s, ' ');
int ncoords = (int)splitArr.size() - 1;
if (!nTexCoords)
{
nTexCoords = ncoords;
if (nTexCoords < 1 || nTexCoords > 3)
{
CV_LOG_ERROR(NULL, "The amount of texture coordinates should be between 1 and 3");
return;
}
}
indices.push_back(vertexInd);
if (ncoords != nTexCoords)
{
CV_LOG_ERROR(NULL, "All points should have the same number of texture coordinates");
return;
}
Vec3f tc;
for (int i = 0; i < nTexCoords; i++)
{
ss >> tc[i];
}
texCoordList.push_back(tc);
}
else
{
@ -98,10 +180,53 @@ void ObjDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &no
}
}
if (duplicateVertices)
{
points.clear();
normals.clear();
rgb.clear();
texCoords.clear();
indices.clear();
for (int tri = 0; tri < (int)idxList.size(); tri++)
{
auto vi = idxList[tri];
auto ti = texIdxList[tri];
auto ni = normalIdxList[tri];
std::vector<int32_t> newvi;
newvi.reserve(3);
for (int i = 0; i < (int)vi.size(); i++)
{
newvi.push_back((int)points.size());
points.push_back(ptsList.at(vi[i]));
if (!rgbList.empty())
{
rgb.push_back(rgbList.at(vi[i]));
}
texCoords.push_back(texCoordList.at(ti[i]));
normals.push_back(nrmList.at(ni[i]));
}
indices.push_back(newvi);
}
}
else
{
points = std::move(ptsList);
normals = std::move(nrmList);
rgb = std::move(rgbList);
texCoords = std::move(texCoordList);
indices = std::move(idxList);
}
file.close();
}
void ObjEncoder::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)
void ObjEncoder::writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb,
const std::vector<Point3f>& texCoords, int nTexCoords,
const std::vector<std::vector<int32_t>>& indices)
{
std::ofstream file(m_filename, std::ios::binary);
if (!file) {
@ -114,15 +239,21 @@ void ObjEncoder::writeData(const std::vector<Point3f> &points, const std::vector
return;
}
if (texCoords.empty() && nTexCoords > 0)
{
CV_LOG_ERROR(NULL, "No texture coordinates provided while having nTexCoord > 0");
return;
}
file << "# OBJ file writer" << std::endl;
file << "o Point_Cloud" << std::endl;
for (size_t i = 0; i < points.size(); ++i)
{
file << "v " << points[i].x << " " << points[i].y << " " << points[i].z;
if (!rgb.empty()) {
file << " " << static_cast<float>(rgb[i].x) / 255.f << " " <<
static_cast<float>(rgb[i].y) / 255.f << " " << static_cast<float>(rgb[i].z) / 255.f;
if (!rgb.empty())
{
file << " " << rgb[i].x << " " << rgb[i].y << " " << rgb[i].z;
}
file << std::endl;
}
@ -132,6 +263,20 @@ void ObjEncoder::writeData(const std::vector<Point3f> &points, const std::vector
file << "vn " << normal.x << " " << normal.y << " " << normal.z << std::endl;
}
for (const auto& tc : texCoords)
{
file << "vt " << tc.x;
if (nTexCoords > 1)
{
file << " " << tc.y;
}
if (nTexCoords > 2)
{
file << " " << tc.z;
}
file << std::endl;
}
for (const auto& faceIndices : indices)
{
file << "f ";

@ -13,7 +13,10 @@ namespace cv {
class ObjDecoder CV_FINAL : public BasePointCloudDecoder
{
public:
void readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb, std::vector<std::vector<int32_t>> &indices) CV_OVERRIDE;
void readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb) CV_OVERRIDE;
void readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords, int& nTexCoords,
std::vector<std::vector<int32_t>>& indices, int flags) CV_OVERRIDE;
protected:
static std::unordered_set<std::string> m_unsupportedKeys;
@ -22,7 +25,9 @@ protected:
class ObjEncoder CV_FINAL : public BasePointCloudEncoder
{
public:
void 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_OVERRIDE;
void writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb,
const std::vector<Point3f>& texCoords, int nTexCoords,
const std::vector<std::vector<int32_t>>& indices) CV_OVERRIDE;
};

@ -13,22 +13,28 @@
namespace cv {
void PlyDecoder::readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb, std::vector<std::vector<int32_t>> &indices)
static const std::set<std::string> colorKeys = { "red", "diffuse_red", "green", "diffuse_green", "blue", "diffuse_blue" };
void PlyDecoder::readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords, int& nTexCoords,
std::vector<std::vector<int32_t>>& indices, int /*flags*/)
{
points.clear();
normals.clear();
rgb.clear();
texCoords.clear();
indices.clear();
nTexCoords = 0;
std::ifstream file(m_filename, std::ios::binary);
if (parseHeader(file))
if (parseHeader(file, nTexCoords))
{
parseBody(file, points, normals, rgb, indices);
parseBody(file, points, normals, rgb, texCoords, indices);
}
}
bool PlyDecoder::parseHeader(std::ifstream &file)
bool PlyDecoder::parseHeader(std::ifstream &file, int& nTexCoords)
{
std::string s;
std::getline(file, s);
@ -211,6 +217,8 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
break;
}
static const std::set<std::string> texCoordKeys = { "texture_u", "s", "texture_v", "t", "texture_w" };
bool good = true;
m_vertexCount = m_vertexDescription.amount;
std::map<std::string, int> amtProps;
@ -238,7 +246,7 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
}
m_hasNormal = true;
}
if (p.name == "red" || p.name == "green" || p.name == "blue")
if (colorKeys.count(p.name) > 0)
{
known = true;
if (p.valType != CV_8U)
@ -249,6 +257,17 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
}
m_hasColour = true;
}
if (texCoordKeys.count(p.name) > 0)
{
known = true;
if (p.valType != CV_32F)
{
CV_LOG_ERROR(NULL, "Vertex property " << p.name
<< " should be float");
good = false;
}
m_hasTexCoord = true;
}
if (p.isList)
{
CV_LOG_ERROR(NULL, "List properties for vertices are not supported");
@ -279,6 +298,50 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
}
}
// check for synonyms
std::vector<std::pair<size_t, size_t>> propCounts;
std::vector<std::pair<std::string, std::string>> synonyms = {
{"red", "diffuse_red"},
{"green", "diffuse_green"},
{"blue", "diffuse_blue"},
{"texture_u", "s"},
{"texture_v", "t"},
};
for (const auto& p : synonyms)
{
std::string a, b;
a = p.first; b = p.second;
size_t ca = amtProps.count(a), cb = amtProps.count(b);
propCounts.push_back({ca, cb});
if (ca + cb > 1)
{
CV_LOG_ERROR(NULL, "Vertex property " << a << " should not go with its synonym " << b);
good = false;
}
}
// check for color conventions
bool shortColorConv = propCounts[0].first || propCounts[1].first || propCounts[2].first;
bool diffuseColorConv = propCounts[0].second || propCounts[1].second || propCounts[2].second;
if (shortColorConv && diffuseColorConv)
{
CV_LOG_ERROR(NULL, "Vertex color properties should not be diffuse and not diffuse at the same time");
good = false;
}
// check for texture conventions
bool shortTexConv = propCounts[3].second || propCounts[4].second;
bool longTexConv = propCounts[3].first || propCounts[4].first;
if (shortTexConv && longTexConv)
{
CV_LOG_ERROR(NULL, "Vertex texture coordinates properties should not be in a short and in a long form at the same time");
good = false;
}
nTexCoords = 0;
for (const auto& k : texCoordKeys)
{
nTexCoords += (int)(amtProps.count(k));
}
m_faceCount = m_faceDescription.amount;
int amtLists = 0;
for (const auto& p : m_faceDescription.properties)
@ -292,6 +355,12 @@ bool PlyDecoder::parseHeader(std::ifstream &file)
<< " should have type uint8 for counter and uint32 for values");
good = false;
}
if (!(p.name == "vertex_index" || p.name == "vertex_indices"))
{
CV_LOG_ERROR(NULL, "List property should be vertex_index or vertex_indices, "
<< p.name << " is not supported");
good = false;
}
}
}
if (amtLists > 1)
@ -342,7 +411,9 @@ uchar readNext<uchar>(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,
void PlyDecoder::parseBody(std::ifstream &file,
std::vector<Point3f>& points, std::vector<Point3f>& normals,
std::vector<Point3f>& rgb, std::vector<Point3f>& texCoords,
std::vector<std::vector<int32_t>> &indices)
{
points.reserve(m_vertexCount);
@ -359,12 +430,13 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
{
float vx, vy, vz;
float nx, ny, nz;
uchar r, g, b;
float u, v, w;
float r, g, b;
};
union VertexData
{
std::array<uchar, 27> bytes;
std::array<uchar, sizeof(VertexFields)> bytes;
VertexFields vf;
};
@ -373,7 +445,7 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
for (size_t j = 0; j < m_vertexDescription.properties.size(); j++)
{
const auto& p = m_vertexDescription.properties[j];
size_t offset = 0;
size_t offset = (size_t)(-1);
if (p.name == "x")
offset = offsetof(VertexFields, vx);
if (p.name == "y")
@ -386,11 +458,17 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
offset = offsetof(VertexFields, ny);
if (p.name == "nz")
offset = offsetof(VertexFields, nz);
if (p.name == "red")
if (p.name == "texture_u" || p.name == "s")
offset = offsetof(VertexFields, u);
if (p.name == "texture_v" || p.name == "t")
offset = offsetof(VertexFields, v);
if (p.name == "texture_w")
offset = offsetof(VertexFields, w);
if (p.name == "red" || p.name == "diffuse_red")
offset = offsetof(VertexFields, r);
if (p.name == "green")
if (p.name == "green" || p.name == "diffuse_green")
offset = offsetof(VertexFields, g);
if (p.name == "blue")
if (p.name == "blue" || p.name == "diffuse_blue")
offset = offsetof(VertexFields, b);
vertexOffsets[j] = offset;
}
@ -426,18 +504,12 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
size_t offset = vertexOffsets[j];
if (offset != (size_t)(-1))
{
switch (p.valType)
if (colorKeys.count(p.name) > 0 && p.valType == CV_8U)
{
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;
fval = ival / 255.f;
}
*(float*)(vertexData.bytes.data() + offset) = fval;
}
}
@ -450,6 +522,10 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
{
normals.push_back({ vertexData.vf.nx, vertexData.vf.ny, vertexData.vf.nz });
}
if (m_hasTexCoord)
{
texCoords.push_back({ vertexData.vf.u, vertexData.vf.v, vertexData.vf.w });
}
}
indices.reserve(m_faceCount);
@ -504,14 +580,22 @@ void PlyDecoder::parseBody(std::ifstream &file, std::vector<Point3f> &points, st
}
}
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)
void PlyEncoder::writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb,
const std::vector<Point3f>& texCoords, int nTexCoords,
const std::vector<std::vector<int32_t>>& indices)
{
std::ofstream file(m_filename, std::ios::binary);
if (!file) {
if (!file)
{
CV_LOG_ERROR(NULL, "Impossible to open the file: " << m_filename);
return;
}
bool hasNormals = !normals.empty(), hasColor = !rgb.empty();
if (texCoords.empty() && nTexCoords > 0)
{
CV_LOG_ERROR(NULL, "No texture coordinates provided while having nTexCoord > 0");
return;
}
file << "ply" << std::endl;
file << "format ascii 1.0" << std::endl;
@ -536,6 +620,19 @@ void PlyEncoder::writeData(const std::vector<Point3f> &points, const std::vector
file << "property float nz" << std::endl;
}
if (nTexCoords > 0)
{
file << "property float texture_u" << std::endl;
}
if (nTexCoords > 1)
{
file << "property float texture_v" << std::endl;
}
if (nTexCoords > 2)
{
file << "property float texture_w" << std::endl;
}
if (!indices.empty())
{
file << "element face " << indices.size() << std::endl;
@ -549,12 +646,24 @@ void PlyEncoder::writeData(const std::vector<Point3f> &points, const std::vector
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);
file << " " << static_cast<int>(rgb[i].x * 255.f) << " " << static_cast<int>(rgb[i].y * 255.f) << " " << static_cast<int>(rgb[i].z * 255.f);
}
if (hasNormals)
{
file << " " << std::setprecision(9) << normals[i].x << " " << normals[i].y << " " << normals[i].z;
}
if (nTexCoords > 0)
{
file << " " << std::setprecision(9) << texCoords[i].x;
}
if (nTexCoords > 1)
{
file << " " << std::setprecision(9) << texCoords[i].y;
}
if (nTexCoords > 2)
{
file << " " << std::setprecision(9) << texCoords[i].z;
}
file << std::endl;
}

@ -35,18 +35,23 @@ struct ElementDescription
class PlyDecoder CV_FINAL : public BasePointCloudDecoder
{
public:
void readData(std::vector<Point3f> &points, std::vector<Point3f> &normals, std::vector<Point3_<uchar>> &rgb, std::vector<std::vector<int32_t>> &indices) CV_OVERRIDE;
void readData(std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords, int& nTexCoords,
std::vector<std::vector<int32_t>>& indices, int flags) CV_OVERRIDE;
protected:
bool parseHeader(std::ifstream &file);
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);
bool parseHeader(std::ifstream &file, int& nTexCoords);
void parseBody(std::ifstream &file,
std::vector<Point3f>& points, std::vector<Point3f>& normals, std::vector<Point3f>& rgb,
std::vector<Point3f>& texCoords,
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};
bool m_hasTexCoord{false};
ElementDescription m_vertexDescription;
ElementDescription m_faceDescription;
};
@ -54,7 +59,9 @@ protected:
class PlyEncoder CV_FINAL : public BasePointCloudEncoder
{
public:
void 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_OVERRIDE;
void writeData(const std::vector<Point3f>& points, const std::vector<Point3f>& normals, const std::vector<Point3f>& rgb,
const std::vector<Point3f>& texCoords, int nTexCoords,
const std::vector<std::vector<int32_t>>& indices) CV_OVERRIDE;
};

@ -61,20 +61,18 @@ void loadPointCloud(const String &filename, OutputArray vertices, OutputArray no
decoder->setSource(filename);
std::vector<Point3f> vec_vertices;
std::vector<Point3f> vec_normals;
std::vector<Point3_<uchar>> vec_rgb;
std::vector<Point3f> vec_vertices, vec_normals, vec_rgb;
decoder->readData(vec_vertices, vec_normals, vec_rgb);
if (!vec_vertices.empty())
Mat(static_cast<int>(vec_vertices.size()), 1, CV_32FC3, &vec_vertices[0]).copyTo(vertices);
Mat(static_cast<int>(vec_vertices.size()), 1, CV_32FC3, vec_vertices.data()).copyTo(vertices);
if (!vec_normals.empty() && normals.needed())
Mat(static_cast<int>(vec_normals.size()), 1, CV_32FC3, &vec_normals[0]).copyTo(normals);
Mat(static_cast<int>(vec_normals.size()), 1, CV_32FC3, vec_normals.data()).copyTo(normals);
if (!vec_rgb.empty() && rgb.needed())
Mat(static_cast<int>(vec_rgb.size()), 1, CV_8UC3, &vec_rgb[0]).copyTo(rgb);
Mat(static_cast<int>(vec_rgb.size()), 1, CV_32FC3, vec_rgb.data()).copyTo(rgb);
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
CV_UNUSED(filename);
@ -101,15 +99,15 @@ void savePointCloud(const String &filename, InputArray vertices, InputArray norm
encoder->setDestination(filename);
std::vector<Point3f> vec_vertices(vertices.getMat());
std::vector<Point3f> vec_normals;
std::vector<Point3_<uchar>> vec_rgb;
std::vector<Point3f> vec_vertices(vertices.getMat()), vec_normals, vec_rgb;
if (!normals.empty()){
if (!normals.empty())
{
vec_normals = normals.getMat();
}
if (!rgb.empty()){
if (!rgb.empty())
{
vec_rgb = rgb.getMat();
}
encoder->writeData(vec_vertices, vec_normals, vec_rgb);
@ -122,7 +120,8 @@ void savePointCloud(const String &filename, InputArray vertices, InputArray norm
#endif
}
void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays indices, OutputArray normals, OutputArray colors)
void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays indices,
OutputArray normals, OutputArray colors, OutputArray texCoords)
{
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
CV_Assert(vertices.needed());
@ -137,12 +136,13 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays
decoder->setSource(filename);
std::vector<Point3f> vec_vertices;
std::vector<Point3f> vec_normals;
std::vector<Point3_<uchar>> vec_rgb;
std::vector<Point3f> vec_vertices, vec_normals, vec_rgb;
std::vector<std::vector<int32_t>> vec_indices;
decoder->readData(vec_vertices, vec_normals, vec_rgb, vec_indices);
std::vector<Point3f> vec_texCoords;
int nTexCoords;
decoder->readData(vec_vertices, vec_normals, vec_rgb, vec_texCoords, nTexCoords, vec_indices, 0);
if (!vec_vertices.empty())
{
@ -156,15 +156,86 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays
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));
Mat(1, static_cast<int>(vec_rgb.size()), CV_32FC3, vec_rgb.data()).copyTo(colors);
}
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) {
Mat(1, static_cast<int>(vec_indices[i].size()), CV_32SC1, vec_indices[i].data()).copyTo(vec[i]);
_InputArray::KindFlag kind = indices.kind();
int vecsz = (int)vec_indices.size();
if (kind == _InputArray::KindFlag::STD_VECTOR_VECTOR)
{
CV_Assert(indices.depth() == CV_32S);
std::vector<std::vector<int32_t>>& vec = *(std::vector<std::vector<int32_t>>*)indices.getObj();
vec.resize(vecsz);
for (int i = 0; i < vecsz; ++i)
{
Mat(1, static_cast<int>(vec_indices[i].size()), CV_32SC1, vec_indices[i].data()).copyTo(vec[i]);
}
}
// std::array<Mat> has fixed size, unsupported
else if (kind == _InputArray::KindFlag::STD_VECTOR_MAT)
{
indices.create(vecsz, 1, CV_32S);
for (int i = 0; i < vecsz; i++)
{
std::vector<int> vi = vec_indices[i];
indices.create(1, (int)vi.size(), CV_32S, i);
Mat(vi).copyTo(indices.getMat(i));
}
}
else
{
indices.create(1, (int)vec_indices.size(), CV_32SC3);
std::vector<Vec3i>& vec = *(std::vector<Vec3i>*)indices.getObj();
for (int i = 0; i < vecsz; ++i)
{
Vec3i tri;
size_t sz = vec_indices[i].size();
if (sz != 3)
{
CV_Error(Error::StsBadArg, "Face contains " + std::to_string(sz) + " vertices, can not put it into 3-channel indices array");
}
else
{
for (int j = 0; j < 3; j++)
{
tri[j] = vec_indices[i][j];
}
}
vec[i] = tri;
}
}
}
if (texCoords.needed())
{
int ch = texCoords.empty() ? 0 : texCoords.channels();
Mat texMat = Mat(1, static_cast<int>(vec_texCoords.size()), CV_MAKETYPE(CV_32F, nTexCoords), vec_texCoords.data());
if (ch == nTexCoords)
{
texMat.copyTo(texCoords);
}
else
{
Mat newTexMat;
std::vector<Mat> varr;
cv::split(texMat, varr);
if (ch == 2 && nTexCoords == 3)
{
std::vector<Mat> marr = { varr[0], varr[1] };
cv::merge(marr, newTexMat);
}
else if (ch == 3 && nTexCoords == 2)
{
std::vector<Mat> marr = { varr[0], varr[1], Mat::zeros(varr[0].size(), CV_32F) };
cv::merge(marr, newTexMat);
}
else
{
newTexMat = texMat;
}
newTexMat.copyTo(texCoords);
}
}
@ -178,7 +249,8 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays
#endif
}
void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays indices, InputArray normals, InputArray colors)
void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays indices,
InputArray normals, InputArray colors, InputArray texCoords)
{
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
if (vertices.empty()) {
@ -195,9 +267,7 @@ void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays in
encoder->setDestination(filename);
std::vector<Point3f> vec_vertices(vertices.getMat());
std::vector<Point3f> vec_normals;
std::vector<Point3_<uchar>> vec_rgb;
std::vector<Point3f> vec_vertices(vertices.getMat()), vec_normals, vec_rgb;
if (!normals.empty())
{
vec_normals = normals.getMat();
@ -205,19 +275,57 @@ void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays in
if (!colors.empty())
{
colors.getMat().convertTo(vec_rgb, CV_8U, 255.0);
vec_rgb = colors.getMat();
}
std::vector<Mat> mat_indices;
indices.getMatVector(mat_indices);
std::vector<std::vector<int32_t>> vec_indices(mat_indices.size());
std::vector<std::vector<int32_t>> vec_indices;
CV_Assert(indices.depth() == CV_32S);
if (indices.kind() == _InputArray::KindFlag::STD_VECTOR_VECTOR)
{
std::vector<Mat> mat_indices;
indices.getMatVector(mat_indices);
vec_indices.resize(mat_indices.size());
for (size_t i = 0; i < mat_indices.size(); ++i)
{
mat_indices[i].copyTo(vec_indices[i]);
}
}
else
{
CV_Assert(indices.channels() == 3);
std::vector<Vec3i>& vec = *(std::vector<Vec3i>*)indices.getObj();
vec_indices.resize(vec.size());
for (size_t i = 0; i < vec.size(); ++i)
{
for (int j = 0; j < 3; j++)
{
vec_indices[i].push_back(vec[i][j]);
}
}
}
for (size_t i = 0; i < mat_indices.size(); ++i)
std::vector<Point3f> vec_texCoords;
int nTexCoords = 0;
if (!texCoords.empty())
{
nTexCoords = texCoords.channels();
}
if (nTexCoords == 2)
{
std::vector<Point2f> vec2_texCoords;
texCoords.copyTo(vec2_texCoords);
for (size_t i = 0; i < vec2_texCoords.size(); i++)
{
Point2f p = vec2_texCoords[i];
vec_texCoords.push_back({p.x, p.y, 0});
}
}
if (nTexCoords == 3)
{
mat_indices[i].copyTo(vec_indices[i]);
texCoords.copyTo(vec_texCoords);
}
encoder->writeData(vec_vertices, vec_normals, vec_rgb, vec_indices);
encoder->writeData(vec_vertices, vec_normals, vec_rgb, vec_texCoords, nTexCoords, vec_indices);
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
CV_UNUSED(filename);

@ -11,39 +11,40 @@
namespace opencv_test { namespace {
TEST(PointCloud, LoadObj)
TEST(PointCloud, LoadPointCloudObj)
{
std::vector<cv::Point3f> points_gold = {
{-5.93915f, -0.13257f, 2.55837f},
{-5.93915f, 1.86743f, 2.55837f},
{-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},
{-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}};
{0.399941f, 1.86743f, -1.16339f}
};
std::vector<cv::Point3f> 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<cv::Point3_<uchar>> rgb_gold = {
{19, 144, 149},
{219, 28, 216},
{218, 157, 101},
{11, 161, 78},
{248, 183, 214},
{63, 196, 6},
{165, 190, 153},
{89, 203, 11}};
std::vector<cv::Point3f> points;
std::vector<cv::Point3f> normals;
std::vector<cv::Point3_<uchar>> rgb;
{-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<cv::Point3f> rgb_gold = {
{0.0756f, 0.5651f, 0.5829f},
{0.8596f, 0.1105f, 0.8455f},
{0.8534f, 0.6143f, 0.3950f},
{0.0438f, 0.6308f, 0.3065f},
{0.9716f, 0.7170f, 0.8378f},
{0.2472f, 0.7701f, 0.0234f},
{0.6472f, 0.7467f, 0.5981f},
{0.3502f, 0.7954f, 0.0443f}
};
std::vector<cv::Point3f> points, normals, rgb;
auto folder = cvtest::TS::ptr()->get_data_path();
cv::loadPointCloud(folder + "pointcloudio/orig.obj", points, normals, rgb);
@ -65,8 +66,7 @@ TEST(PointCloud, LoadObjNoNormals)
{0.399941f, -0.13257f, -1.16339f},
{0.399941f, 1.86743f, -1.16339f}};
std::vector<cv::Point3f> points;
std::vector<cv::Point3f> normals;
std::vector<cv::Point3f> points, normals;
auto folder = cvtest::TS::ptr()->get_data_path();
cv::loadPointCloud(folder + "pointcloudio/orig_no_norms.obj", points, normals);
@ -77,9 +77,7 @@ TEST(PointCloud, LoadObjNoNormals)
TEST(PointCloud, SaveObj)
{
std::vector<cv::Point3f> points_gold;
std::vector<cv::Point3f> normals_gold;
std::vector<cv::Point3_<uchar>> rgb_gold;
std::vector<cv::Point3f> points_gold, normals_gold, rgb_gold;
auto folder = cvtest::TS::ptr()->get_data_path();
auto new_path = tempfile("new.obj");
@ -87,9 +85,7 @@ TEST(PointCloud, SaveObj)
cv::loadPointCloud(folder + "pointcloudio/orig.obj", points_gold, normals_gold, rgb_gold);
cv::savePointCloud(new_path, points_gold, normals_gold, rgb_gold);
std::vector<cv::Point3f> points;
std::vector<cv::Point3f> normals;
std::vector<cv::Point3_<uchar>> rgb;
std::vector<cv::Point3f> points, normals, rgb;
cv::loadPointCloud(new_path, points, normals, rgb);
@ -101,9 +97,7 @@ TEST(PointCloud, SaveObj)
TEST(PointCloud, LoadSavePly)
{
std::vector<cv::Point3f> points;
std::vector<cv::Point3f> normals;
std::vector<cv::Point3_<uchar>> rgb;
std::vector<cv::Point3f> points, normals, rgb;
auto folder = cvtest::TS::ptr()->get_data_path();
std::string new_path = tempfile("new.ply");
@ -111,9 +105,7 @@ TEST(PointCloud, LoadSavePly)
cv::loadPointCloud(folder + "pointcloudio/orig.ply", points, normals, rgb);
cv::savePointCloud(new_path, points, normals, rgb);
std::vector<cv::Point3f> points_gold;
std::vector<cv::Point3f> normals_gold;
std::vector<cv::Point3_<uchar>> rgb_gold;
std::vector<cv::Point3f> points_gold, normals_gold, rgb_gold;
cv::loadPointCloud(new_path, points_gold, normals_gold, rgb_gold);
@ -125,21 +117,31 @@ TEST(PointCloud, LoadSavePly)
TEST(PointCloud, LoadSaveMeshObj)
{
std::vector<cv::Point3f> points;
std::vector<cv::Point3f> normals;
std::vector<cv::Point3f> points, normals, colors;
std::vector<cv::Point2f> texCoords;
std::vector<std::vector<int32_t>> indices;
auto folder = cvtest::TS::ptr()->get_data_path();
std::string new_path = tempfile("new_mesh.obj");
cv::loadMesh(folder + "pointcloudio/orig.obj", points, indices, normals);
cv::saveMesh(new_path, points, indices, normals);
cv::loadMesh(folder + "pointcloudio/orig.obj", points, indices, normals, colors, texCoords);
EXPECT_FALSE(points.empty());
EXPECT_FALSE(indices.empty());
EXPECT_FALSE(normals.empty());
EXPECT_FALSE(colors.empty());
EXPECT_FALSE(texCoords.empty());
cv::saveMesh(new_path, points, indices, normals, colors, texCoords);
std::vector<cv::Point3f> points_gold;
std::vector<cv::Point3f> normals_gold;
std::vector<cv::Point3f> points_gold, normals_gold, colors_gold;
std::vector<cv::Point2f> texCoords_gold;
std::vector<std::vector<int32_t>> indices_gold;
cv::loadMesh(new_path, points_gold, indices_gold, normals_gold);
cv::loadMesh(new_path, points_gold, indices_gold, normals_gold, colors_gold, texCoords_gold);
EXPECT_FALSE(points_gold.empty());
EXPECT_FALSE(indices_gold.empty());
EXPECT_FALSE(normals_gold.empty());
EXPECT_FALSE(colors_gold.empty());
EXPECT_FALSE(texCoords_gold.empty());
EXPECT_EQ(normals_gold, normals);
EXPECT_EQ(points_gold, points);
@ -156,7 +158,7 @@ TEST_P(PlyTest, LoadSaveMesh)
std::string fname = GetParam();
std::vector<cv::Point3f> points_gold, normals_gold, colors_gold;
std::vector<std::vector<int32_t>> indices_gold;
std::vector<cv::Vec3i> indices_gold;
auto folder = cvtest::TS::ptr()->get_data_path();
std::string new_path = tempfile("new_mesh.ply");
@ -168,7 +170,7 @@ TEST_P(PlyTest, LoadSaveMesh)
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;
std::vector<cv::Vec3i> indices;
cv::loadMesh(new_path, points, indices, normals, colors);
if (!normals.empty())

@ -94,8 +94,9 @@ void generateSphere(OutputArray sphere_pts, const vector<float> &model, float th
// Let r change then generate thickness
float r = i < sphere_num ? model[3] : rng.uniform(model[3] - thr, model[3] + thr);
// Generate a random vector and normalize it.
// Note: these vectors are not spread uniformly across the sphere
Vec3f vec(rng.uniform(limit[0], limit[1]), rng.uniform(limit[2], limit[3]),
rng.uniform(limit[4], limit[5]));
rng.uniform(limit[4], limit[5]));
float l = sqrt(vec.dot(vec));
// Normalizes it to have a magnitude of r
vec /= l / r;

@ -649,19 +649,19 @@ TEST_P(RenderingTest, floatParams)
thr.depthInfThreshold = 1;
if (width == 320 && height == 240 && shadingType == RASTERIZE_SHADING_SHADED && cullingMode == RASTERIZE_CULLING_CCW)
{
thr.rgbInfThreshold = 0.000157;
thr.rgbL2Threshold = 6e-09;
thr.depthL2Threshold = 0.000413;
thr.rgbInfThreshold = 0.000229;
thr.rgbL2Threshold = 6.37e-09;
thr.depthL2Threshold = 0.000427;
}
else if (width == 700 && height == 700 && shadingType == RASTERIZE_SHADING_SHADED && cullingMode == RASTERIZE_CULLING_CW)
{
thr.rgbInfThreshold = 0.000303;
thr.rgbL2Threshold = 1.9e-09;
thr.depthL2Threshold = 0.00012;
thr.rgbInfThreshold = 0.000277;
thr.rgbL2Threshold = 1.8e-09;
thr.depthL2Threshold = 0.000124;
}
else if (width == 700 && height == 700 && shadingType == RASTERIZE_SHADING_WHITE && cullingMode == RASTERIZE_CULLING_NONE)
{
thr.depthL2Threshold = 0.00012;
thr.depthL2Threshold = 0.000124;
}
break;
case ModelType::Centered:
@ -803,27 +803,27 @@ TEST_P(RenderingTest, accuracy)
case ModelType::File:
if (width == 320 && height == 240 && shadingType == RASTERIZE_SHADING_SHADED && cullingMode == RASTERIZE_CULLING_CCW)
{
thr.rgbInfThreshold = 0.93;
thr.rgbL2Threshold = 2.45E-05;
thr.depthMaskThreshold = 2;
thr.rgbInfThreshold = 0.836;
thr.rgbL2Threshold = 2.08e-05;
thr.depthMaskThreshold = 1;
thr.depthInfThreshold = 99;
thr.depthL2Threshold = 0.00544;
}
else if (width == 700 && height == 700 && shadingType == RASTERIZE_SHADING_SHADED && cullingMode == RASTERIZE_CULLING_CW)
{
thr.rgbInfThreshold = 0.973;
thr.rgbL2Threshold = 4.46E-06;
thr.depthMaskThreshold = 3;
thr.rgbL2Threshold = 5.2e-06;
thr.depthMaskThreshold = 4;
thr.depthInfThreshold = 258;
thr.depthL2Threshold = 0.00142;
thr.depthL2Threshold = 0.00228;
}
else if (width == 700 && height == 700 && shadingType == RASTERIZE_SHADING_WHITE && cullingMode == RASTERIZE_CULLING_NONE)
{
thr.rgbInfThreshold = 1;
thr.rgbL2Threshold = 6.13E-06;
thr.depthMaskThreshold = 3;
thr.rgbL2Threshold = 7.07e-06;
thr.depthMaskThreshold = 4;
thr.depthInfThreshold = 258;
thr.depthL2Threshold = 0.00142;
thr.depthL2Threshold = 0.00228;
}
break;
default:
@ -914,7 +914,7 @@ TEST_P(RenderingTest, glCompatibleDepth)
}
double normL2Diff = cv::norm(depth_buf, convertedDepth, cv::NORM_L2) / (height * width);
const double normL2Threshold = 1.e-9;
const double normL2Threshold = 5.53e-10;
EXPECT_LE(normL2Diff, normL2Threshold);
// add --test_debug to output differences
if (debugLevel > 0)

Loading…
Cancel
Save