From 6699ca1a408af83f713ea36b1f7304fcc8a48061 Mon Sep 17 00:00:00 2001
From: Rostislav Vasilikhin <savuor@gmail.com>
Date: Tue, 16 Apr 2024 09:20:44 +0200
Subject: [PATCH] Merge pull request #25382 from savuor:rv/fix_mesh_load

Fix mesh loading for texture coordinates and face indices #25382

### This PR changes

* Texture coordinates were stored incorrectly (3-channel array is read as if there were 2 channels), fixed
* Faces were pushed back to the output array instead of indexed writing which produced a lot of empty faces, fixed
* A set of ground truth tests were added to cover these issues
* `std::vector<cv::Mat>` support added for `saveMesh()` which is required for Python bindings
* More command line args were added to rasterization test data generator

### 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
---
 modules/3d/misc/python/test/test_iomesh.py    |   2 +-
 modules/3d/perf/perf_rendering.cpp            |   6 +-
 .../3d/src/pointcloud/load_point_cloud.cpp    |  55 ++---
 modules/3d/test/test_pointcloud.cpp           | 181 ++++++++++-----
 modules/3d/test/test_rendering.cpp            |   8 +-
 samples/opengl/opengl_testdata_generator.cpp  | 219 ++++++++++++------
 6 files changed, 314 insertions(+), 157 deletions(-)

diff --git a/modules/3d/misc/python/test/test_iomesh.py b/modules/3d/misc/python/test/test_iomesh.py
index 20f301e8d2..647eb06337 100644
--- a/modules/3d/misc/python/test/test_iomesh.py
+++ b/modules/3d/misc/python/test/test_iomesh.py
@@ -32,7 +32,7 @@ class raster_test(NewOpenCVTests):
             if a.shape not in goodShapes:
                 self.fail(errorMsg % s)
 
-        if texCoords.shape not in [(1, 18, 2), (18, 1, 2)]:
+        if texCoords.shape not in [(1, 18, 2), (18, 1, 2), (18, 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)]:
diff --git a/modules/3d/perf/perf_rendering.cpp b/modules/3d/perf/perf_rendering.cpp
index 48e6bb5739..c565709300 100644
--- a/modules/3d/perf/perf_rendering.cpp
+++ b/modules/3d/perf/perf_rendering.cpp
@@ -285,10 +285,8 @@ PERF_TEST_P(RenderingTest, rasterizeTriangles, ::testing::Combine(
     if (shadingType != RASTERIZE_SHADING_WHITE)
     {
         // let vertices be in BGR format to avoid later color conversions
-        // cvtColor does not work with 1d Mats
-        std::vector<Mat> xyz;
-        cv::split(colors, xyz);
-        cv::merge(std::vector<Mat>{xyz[2], xyz[1], xyz[0]}, colors);
+        // mixChannels does not support in-place operation
+        cv::mixChannels(Mat(colors).clone(), colors, {0, 2, 1, 1, 2, 0});
     }
 
     double zNear = 0.1, zFar = 50.0;
diff --git a/modules/3d/src/pointcloud/load_point_cloud.cpp b/modules/3d/src/pointcloud/load_point_cloud.cpp
index c99f4d2c24..63f6f828f6 100644
--- a/modules/3d/src/pointcloud/load_point_cloud.cpp
+++ b/modules/3d/src/pointcloud/load_point_cloud.cpp
@@ -140,7 +140,7 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays
     std::vector<std::vector<int32_t>> vec_indices;
 
     std::vector<Point3f> vec_texCoords;
-    int nTexCoords;
+    int nTexCoords = 0;
 
     decoder->readData(vec_vertices, vec_normals, vec_rgb, vec_texCoords, nTexCoords, vec_indices, 0);
 
@@ -210,32 +210,30 @@ void loadMesh(const String &filename, OutputArray vertices, OutputArrayOfArrays
 
     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
+        if (nTexCoords)
         {
-            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)
+            CV_Assert(!texCoords.fixedType() || (texCoords.type() == CV_MAKE_TYPE(CV_32F, nTexCoords)));
+
+            Mat tex3(vec_texCoords);
+
+            if (nTexCoords == 3)
             {
-                std::vector<Mat> marr = { varr[0], varr[1], Mat::zeros(varr[0].size(), CV_32F) };
-                cv::merge(marr, newTexMat);
+                tex3.copyTo(texCoords);
             }
-            else
+            else if (nTexCoords == 2)
             {
-                newTexMat = texMat;
+                // if texCoords is empty then channels() can be any number
+                bool has3ch = texCoords.channels() == 3;
+                int ch = has3ch ? 3 : 2;
+                std::vector<int> permut = has3ch ? std::vector<int>{ 0, 0, 1, 1, -1, 2 } : std::vector<int>{ 0, 0, 1, 1 };
+                texCoords.createSameSize(vec_texCoords, CV_MAKE_TYPE(CV_32F, ch));
+                Mat out = texCoords.getMat();
+                cv::mixChannels(tex3, out, permut);
             }
-            newTexMat.copyTo(texCoords);
+        }
+        else
+        {
+            texCoords.clear();
         }
     }
 
@@ -280,7 +278,8 @@ void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays in
 
     std::vector<std::vector<int32_t>> vec_indices;
     CV_Assert(indices.depth() == CV_32S);
-    if (indices.kind() == _InputArray::KindFlag::STD_VECTOR_VECTOR)
+    if (indices.kind() == _InputArray::KindFlag::STD_VECTOR_VECTOR ||
+        indices.kind() == _InputArray::KindFlag::STD_VECTOR_MAT)
     {
         std::vector<Mat> mat_indices;
         indices.getMatVector(mat_indices);
@@ -312,13 +311,9 @@ void saveMesh(const String &filename, InputArray vertices, InputArrayOfArrays in
     }
     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});
-        }
+        // extend by 3rd zero channel
+        vec_texCoords.resize(texCoords.total());
+        cv::mixChannels(texCoords, vec_texCoords, {0, 0, 1, 1, -1, 2});
     }
     if (nTexCoords == 3)
     {
diff --git a/modules/3d/test/test_pointcloud.cpp b/modules/3d/test/test_pointcloud.cpp
index ba5a34e385..e18c54d66e 100644
--- a/modules/3d/test/test_pointcloud.cpp
+++ b/modules/3d/test/test_pointcloud.cpp
@@ -11,67 +11,135 @@
 
 namespace opencv_test { namespace {
 
-TEST(PointCloud, LoadPointCloudObj)
+struct OriginalObjGoldValues
 {
-    std::vector<cv::Point3f> 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<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::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}
-    };
+    OriginalObjGoldValues()
+    {
+        std::array<float, 6> vals = { -5.93915f, -0.13257f, 2.55837f, 1.86743f, -1.16339f, 0.399941f };
+        points =
+        {
+            { vals[0], vals[1], vals[2] },
+            { vals[0], vals[3], vals[2] },
+            { vals[0], vals[1], vals[4] },
+            { vals[0], vals[3], vals[4] },
+            { vals[5], vals[1], vals[2] },
+            { vals[5], vals[3], vals[2] },
+            { vals[5], vals[1], vals[4] },
+            { vals[5], vals[3], vals[4] },
+        };
+
+        normals =
+        {
+            {-1.0f,  0.0f,  0.0f},
+            { 0.0f,  0.0f, -1.0f},
+            { 1.0f,  0.0f,  0.0f},
+            { 0.0f,  0.0f,  1.0f},
+            { 0.0f, -1.0f,  0.0f},
+            { 0.0f,  1.0f,  0.0f}
+        };
+
+        rgb =
+        {
+            {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<std::pair<int, int>> tcvals =
+        {
+            { 3, 0 },
+            { 5, 0 },
+            { 5, 2 },
+            { 3, 2 },
+            { 5, 4 },
+            { 3, 4 },
+            { 5, 6 },
+            { 3, 6 },
+            { 5, 8 },
+            { 3, 8 },
+            { 1, 4 },
+            { 1, 6 },
+            { 7, 4 },
+            { 7, 6 },
+        };
+
+        for (const auto& p : tcvals)
+        {
+            texCoords.push_back({p.first * 0.125f, p.second * 0.125f});
+        }
+
+        // mesh data is duplicated for each face
+        std::vector<std::array<int, 9>> fileIndices =
+        {
+            { 1,  1,  1, /**/ 2,  2,  1, /**/ 4,  3,  1 },
+            { 3,  4,  2, /**/ 4,  3,  2, /**/ 8,  5,  2 },
+            { 7,  6,  3, /**/ 8,  5,  3, /**/ 6,  7,  3 },
+            { 5,  8,  4, /**/ 6,  7,  4, /**/ 2,  9,  4 },
+            { 3, 11,  5, /**/ 7,  6,  5, /**/ 5,  8,  5 },
+            { 8,  5,  6, /**/ 4, 13,  6, /**/ 2, 14,  6 },
+        };
+
+        for (const auto& fi : fileIndices)
+        {
+            pointsMesh.push_back(points.at(fi[0] - 1));
+            pointsMesh.push_back(points.at(fi[3] - 1));
+            pointsMesh.push_back(points.at(fi[6] - 1));
+            rgbMesh.push_back(rgb.at(fi[0] - 1));
+            rgbMesh.push_back(rgb.at(fi[3] - 1));
+            rgbMesh.push_back(rgb.at(fi[6] - 1));
+
+            texCoordsMesh.push_back(texCoords.at(fi[1] - 1));
+            texCoordsMesh.push_back(texCoords.at(fi[4] - 1));
+            texCoordsMesh.push_back(texCoords.at(fi[7] - 1));
+
+            normalsMesh.push_back(normals.at(fi[2] - 1));
+            normalsMesh.push_back(normals.at(fi[5] - 1));
+            normalsMesh.push_back(normals.at(fi[8] - 1));
+        }
+
+        indices =
+        {
+            { 0,  1,  2},
+            { 3,  4,  5},
+            { 6,  7,  8},
+            { 9, 10, 11},
+            {12, 13, 14},
+            {15, 16, 17},
+        };
+    }
+
+    std::vector<Point3f> points, pointsMesh, normals, normalsMesh, rgb, rgbMesh;
+    std::vector<Point2f> texCoords, texCoordsMesh;
+    std::vector<std::vector<int32_t>> indices;
+};
 
+OriginalObjGoldValues origGold;
+
+TEST(PointCloud, LoadPointCloudObj)
+{
     std::vector<cv::Point3f> points, normals, rgb;
 
     auto folder = cvtest::TS::ptr()->get_data_path();
     cv::loadPointCloud(folder + "pointcloudio/orig.obj", points, normals, rgb);
 
-    EXPECT_EQ(points_gold, points);
-    EXPECT_EQ(rgb_gold, rgb);
-    EXPECT_EQ(normals_gold, normals);
+    EXPECT_EQ(origGold.points, points);
+    EXPECT_EQ(origGold.rgb, rgb);
+    EXPECT_EQ(origGold.normals, normals);
 }
 
 TEST(PointCloud, LoadObjNoNormals)
 {
-    std::vector<cv::Point3f> 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<cv::Point3f> points, 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_EQ(origGold.points, points);
     EXPECT_TRUE(normals.empty());
 }
 
@@ -125,11 +193,11 @@ TEST(PointCloud, LoadSaveMeshObj)
     std::string new_path = tempfile("new_mesh.obj");
 
     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());
+    EXPECT_EQ(origGold.pointsMesh, points);
+    EXPECT_EQ(origGold.indices, indices);
+    EXPECT_EQ(origGold.normalsMesh, normals);
+    EXPECT_EQ(origGold.rgbMesh, colors);
+    EXPECT_EQ(origGold.texCoordsMesh, texCoords);
     cv::saveMesh(new_path, points, indices, normals, colors, texCoords);
 
     std::vector<cv::Point3f> points_gold, normals_gold, colors_gold;
@@ -164,8 +232,17 @@ TEST_P(PlyTest, LoadSaveMesh)
     std::string new_path = tempfile("new_mesh.ply");
 
     cv::loadMesh(folder + fname, points_gold, indices_gold, normals_gold, colors_gold);
-    EXPECT_FALSE(points_gold.empty());
-    EXPECT_FALSE(indices_gold.empty());
+    size_t truePts, trueFaces;
+    if (fname.find("/dragon.ply") != fname.npos)
+    {
+        truePts = 50000; trueFaces = 100000;
+    }
+    else
+    {
+        truePts = 8; trueFaces = 12;
+    }
+    EXPECT_EQ(points_gold.size(), truePts);
+    EXPECT_EQ(indices_gold.size(), trueFaces);
 
     cv::saveMesh(new_path, points_gold, indices_gold, normals_gold, colors_gold);
 
diff --git a/modules/3d/test/test_rendering.cpp b/modules/3d/test/test_rendering.cpp
index da1be90cac..02e73fee26 100644
--- a/modules/3d/test/test_rendering.cpp
+++ b/modules/3d/test/test_rendering.cpp
@@ -516,13 +516,11 @@ protected:
 
         if (shadingType != RASTERIZE_SHADING_WHITE)
         {
+            // let vertices be in BGR format to avoid later color conversions
+            // mixChannels() does not support in-place operation
             colors = Mat(modelData.colors);
             colors.convertTo(colors, ftype);
-            // let vertices be in BGR format to avoid later color conversions
-            // cvtColor does not work with 1d Mats
-            std::vector<Mat> xyz;
-            cv::split(colors, xyz);
-            cv::merge(std::vector<Mat>{xyz[2], xyz[1], xyz[0]}, colors);
+            cv::mixChannels(colors.clone(), colors, {0, 2, 1, 1, 2, 0});
         }
 
         indices = Mat(modelData.indices);
diff --git a/samples/opengl/opengl_testdata_generator.cpp b/samples/opengl/opengl_testdata_generator.cpp
index 0f6bf6e42b..22e8bb19fa 100644
--- a/samples/opengl/opengl_testdata_generator.cpp
+++ b/samples/opengl/opengl_testdata_generator.cpp
@@ -1,4 +1,5 @@
 #include <iostream>
+#include <map>
 
 #ifdef _WIN32
 #define WIN32_LEAN_AND_MEAN 1
@@ -78,6 +79,9 @@ public:
             upVector = Vec3d(0.0, 1.0, 0.0);
 
             fovy = 45.0;
+            zNear = 0.1;
+            zFar = 50;
+            scaleCoeff = 1000.0;
 
             vertices = std::vector<Vec3f>(4, {2.0f, 0, -2.0f});
             colors   = std::vector<Vec3f>(4, {0, 0, 1.0f});
@@ -91,6 +95,9 @@ public:
             upVector = Vec3d( 0.0, 1.0, 0.0);
 
             fovy = 45.0;
+            zNear = 0.1;
+            zFar = 50;
+            scaleCoeff = 1000.0;
 
             objectPath = objPath;
             std::vector<vector<int>> indvec;
@@ -119,6 +126,9 @@ public:
             upVector = Vec3d(0.0, 1.0, 0.0);
 
             fovy = 45.0;
+            zNear = 0.1;
+            zFar = 50;
+            scaleCoeff = 1000.0;
 
             vertices =
             {
@@ -152,6 +162,9 @@ public:
             upVector = Vec3d(0.0, 1.0, 0.0);
 
             fovy = 45.0;
+            zNear = 0.1;
+            zFar = 50;
+            scaleCoeff = 1000.0;
 
             vertices =
             {
@@ -181,6 +194,9 @@ public:
             upVector = Vec3d(0.0, 1.0, 0.0);
 
             fovy = 60.0;
+            zNear = 0.1;
+            zFar = 50;
+            scaleCoeff = 1000.0;
 
             vertices =
             {
@@ -208,11 +224,35 @@ public:
         }
     }
 
+    ModelData(std::string modelPath, double fov, double near, double far, double scale, Vec3d pos, Vec3d center, Vec3d up)
+    {
+        objectPath = modelPath;
+        position = pos;
+        lookat   = center;
+        upVector = up;
+        fovy = fov;
+        zNear = near;
+        zFar = far;
+        scaleCoeff = scale;
+
+        std::vector<vector<int>> indvec;
+
+        loadMesh(objectPath, vertices, indvec, noArray(), colors);
+        if (vertices.size() != colors.size())
+        {
+            std::runtime_error("Model should contain normals for each vertex");
+        }
+        for (const auto &vec : indvec)
+        {
+            indices.push_back({vec[0], vec[1], vec[2]});
+        }
+    }
+
     Vec3d position;
     Vec3d lookat;
     Vec3d upVector;
 
-    double fovy;
+    double fovy, zNear, zFar, scaleCoeff;
 
     std::vector<Vec3f> vertices;
     std::vector<Vec3i> indices;
@@ -239,13 +279,11 @@ void draw(void* userdata)
 }
 
 static void generateImage(cv::Size imgSz, TriangleShadingType shadingType, TriangleCullingMode cullingMode,
-                          ModelType modelType, std::string modelPath, cv::Mat& colorImage, cv::Mat& depthImage)
+                          const ModelData& modelData, cv::Mat& colorImage, cv::Mat& depthImage)
 {
     namedWindow("OpenGL", WINDOW_OPENGL);
     resizeWindow("OpenGL", imgSz.width, imgSz.height);
 
-    ModelData modelData(modelType, modelPath);
-
     DrawData data;
 
     std::vector<Vec3f> vertices;
@@ -293,10 +331,9 @@ static void generateImage(cv::Size imgSz, TriangleShadingType shadingType, Trian
     data.arr.setColorArray(colors4f);
     data.indices.copyFrom(idxLinear);
 
-    double zNear = 0.1, zFar = 50;
     glMatrixMode(GL_PROJECTION);
     glLoadIdentity();
-    gluPerspective(modelData.fovy, (double)imgSz.width / imgSz.height, zNear, zFar);
+    gluPerspective(modelData.fovy, (double)imgSz.width / imgSz.height, modelData.zNear, modelData.zFar);
 
     glMatrixMode(GL_MODELVIEW);
     glLoadIdentity();
@@ -343,10 +380,10 @@ static void generateImage(cv::Size imgSz, TriangleShadingType shadingType, Trian
         // map from [0, 1] to [zNear, zFar]
         for (auto it = depthImage.begin<float>(); it != depthImage.end<float>(); ++it)
         {
-            *it = (float)(zNear * zFar / (double(*it) * (zNear - zFar) + zFar));
+            *it = (float)(modelData.zNear * modelData.zFar / (double(*it) * (modelData.zNear - modelData.zFar) + modelData.zFar));
         }
         cv::flip(depthImage, depthImage, 0);
-        depthImage.convertTo(depthImage, CV_16U, 1000.0);
+        depthImage.convertTo(depthImage, CV_16U, modelData.scaleCoeff);
 
         char key = (char)waitKey(40);
         if (key == 27)
@@ -364,6 +401,26 @@ int main(int argc, char* argv[])
             "{ help h usage ? |      | show this message }"
             "{ outPath        |      | output path for generated images }"
             "{ modelPath      |      | path to 3d model to render }"
+            "{ custom         |      | pass it to use custom camera parameters instead of iterating through test parameters }"
+            "{ fov            | 45.0 | (if custom parameters are used) field of view }"
+            "{ posx           | 1.0  | (if custom parameters are used) camera position x }"
+            "{ posy           | 1.0  | (if custom parameters are used) camera position y }"
+            "{ posz           | 1.0  | (if custom parameters are used) camera position z }"
+            "{ lookatx        | 0.0  | (if custom parameters are used) lookup camera direction x }"
+            "{ lookaty        | 0.0  | (if custom parameters are used) lookup camera direction y }"
+            "{ lookatz        | 0.0  | (if custom parameters are used) lookup camera direction z }"
+            "{ upx            | 0.0  | (if custom parameters are used) up camera direction x }"
+            "{ upy            | 1.0  | (if custom parameters are used) up camera direction y }"
+            "{ upz            | 0.0  | (if custom parameters are used) up camera direction z }"
+            "{ resx           | 640  | (if custom parameters are used) camera resolution x }"
+            "{ resy           | 480  | (if custom parameters are used) camera resolution y }"
+            "{ zNear          | 0.1  | (if custom parameters are used) near z clipping plane }"
+            "{ zFar           |  50  | (if custom parameters are used) far z clipping plane }"
+            "{ scaleCoeff     | 1000 | (if custom parameters are used) scale coefficient for saving depth }"
+            "{ shading        |      | (if custom parameters are used) shading type: white/flat/shaded }"
+            "{ culling        |      | (if custom parameters are used) culling type: none/cw/ccw }"
+            "{ colorPath      |      | (if custom parameters are used) output path for color image }"
+            "{ depthPath      |      | (if custom parameters are used) output path for depth image }"
     );
     parser.about("This app is used to generate test data for triangleRasterize() function");
 
@@ -380,77 +437,109 @@ int main(int argc, char* argv[])
         return -1;
     }
 
-    std::string outPath = parser.get<std::string>("outPath");
-    if (outPath.empty())
+    if (parser.has("custom"))
     {
-        std::cout << "No output path given" << std::endl;
-        return -1;
+        double fov = parser.get<double>("fov");
+        Vec3d position, lookat, upVector;
+        position[0] = parser.get<double>("posx");
+        position[1] = parser.get<double>("posy");
+        position[2] = parser.get<double>("posz");
+        lookat[0]   = parser.get<double>("lookatx");
+        lookat[1]   = parser.get<double>("lookaty");
+        lookat[2]   = parser.get<double>("lookatz");
+        upVector[0] = parser.get<double>("upx");
+        upVector[1] = parser.get<double>("upy");
+        upVector[2] = parser.get<double>("upz");
+        Size res;
+        res.width  = parser.get<int>("resx");
+        res.height = parser.get<int>("resy");
+        double zNear = parser.get<double>("zNear");
+        double zFar  = parser.get<double>("zFar");
+        double scaleCoeff = parser.get<double>("scaleCoeff");
+
+        std::map<std::string, cv::TriangleShadingType> shadingTxt = {
+            { "white",  RASTERIZE_SHADING_WHITE  },
+            { "flat",   RASTERIZE_SHADING_FLAT   },
+            { "shaded", RASTERIZE_SHADING_SHADED },
+        };
+        cv::TriangleShadingType shadingType = shadingTxt.at(parser.get<std::string>("shading"));
+
+        std::map<std::string, cv::TriangleCullingMode> cullingTxt = {
+            { "none", RASTERIZE_CULLING_NONE },
+            { "cw",   RASTERIZE_CULLING_CW   },
+            { "ccw",  RASTERIZE_CULLING_CCW  },
+        };
+        cv::TriangleCullingMode cullingMode = cullingTxt.at(parser.get<std::string>("culling"));
+
+        std::string colorPath = parser.get<std::string>("colorPath");
+        std::string depthPath = parser.get<std::string>("depthPath");
+
+        Mat colorImage, depthImage;
+        ModelData modelData(modelPath, fov, zNear, zFar, scaleCoeff, position, lookat, upVector);
+        generateImage(res, shadingType, cullingMode, modelData, colorImage, depthImage);
+
+        cv::imwrite(colorPath, colorImage);
+        cv::imwrite(depthPath, depthImage);
     }
-
-    std::array<cv::Size, 4> resolutions = { cv::Size {700, 700}, cv::Size {640, 480}, cv::Size(256, 256), cv::Size(320, 240) };
-    for (const auto& res : resolutions)
+    else
     {
-        for (const auto shadingType : {
-                RASTERIZE_SHADING_WHITE,
-                RASTERIZE_SHADING_FLAT,
-                RASTERIZE_SHADING_SHADED
-            })
+        std::string outPath = parser.get<std::string>("outPath");
+        if (outPath.empty())
         {
-            std::string shadingName;
-            switch (shadingType)
-            {
-            case RASTERIZE_SHADING_WHITE:  shadingName = "White";  break;
-            case RASTERIZE_SHADING_FLAT:   shadingName = "Flat";   break;
-            case RASTERIZE_SHADING_SHADED: shadingName = "Shaded"; break;
-            default:
-                break;
-            }
+            std::cout << "No output path given" << std::endl;
+            return -1;
+        }
 
-            for (const auto cullingMode : {
-                    RASTERIZE_CULLING_NONE,
-                    RASTERIZE_CULLING_CW,
-                    RASTERIZE_CULLING_CCW
-            })
+        std::array<cv::Size, 4> resolutions = {cv::Size{700, 700}, cv::Size{640, 480}, cv::Size(256, 256), cv::Size(320, 240)};
+        std::vector<std::pair<cv::TriangleShadingType, std::string>> shadingTxt = {
+            {RASTERIZE_SHADING_WHITE,  "White"},
+            {RASTERIZE_SHADING_FLAT,   "Flat"},
+            {RASTERIZE_SHADING_SHADED, "Shaded"},
+        };
+        std::vector<std::pair<cv::TriangleCullingMode, std::string>> cullingTxt = {
+            {RASTERIZE_CULLING_NONE, "None"},
+            {RASTERIZE_CULLING_CW,   "CW"},
+            {RASTERIZE_CULLING_CCW,  "CCW"},
+        };
+        std::vector<std::pair<ModelType, std::string>> modelTxt = {
+            {ModelType::File,     "File"},
+            {ModelType::Clipping, "Clipping"},
+            {ModelType::Color,    "Color"},
+            {ModelType::Centered, "Centered"},
+        };
+
+        for (const auto& res : resolutions)
+        {
+            for (const auto shadingPair : shadingTxt)
             {
-                std::string cullingName;
-                switch (cullingMode)
-                {
-                    case RASTERIZE_CULLING_NONE: cullingName = "None"; break;
-                    case RASTERIZE_CULLING_CW:   cullingName = "CW"; break;
-                    case RASTERIZE_CULLING_CCW:  cullingName = "CCW"; break;
-                    default: break;
-                }
+                cv::TriangleShadingType shadingType = shadingPair.first;
+                std::string shadingName = shadingPair.second;
 
-                for (const auto modelType : {
-                            ModelType::File,
-                            ModelType::Clipping,
-                            ModelType::Color,
-                            ModelType::Centered,
-                    })
+                for (const auto cullingPair : cullingTxt)
                 {
-                    std::string modelName;
-                    switch (modelType)
+                    cv::TriangleCullingMode cullingMode = cullingPair.first;
+                    std::string cullingName = cullingPair.second;
+
+                    for (const auto modelPair : modelTxt)
                     {
-                    case ModelType::File:     modelName = "File";     break;
-                    case ModelType::Clipping: modelName = "Clipping"; break;
-                    case ModelType::Color:    modelName = "Color";    break;
-                    case ModelType::Centered: modelName = "Centered"; break;
-                    default:
-                        break;
-                    }
+                        ModelType modelType = modelPair.first;
+                        std::string modelName = modelPair.second;
 
-                    std::string suffix = cv::format("%s_%dx%d_Cull%s", modelName.c_str(), res.width, res.height, cullingName.c_str());
+                        std::string suffix = cv::format("%s_%dx%d_Cull%s", modelName.c_str(), res.width, res.height, cullingName.c_str());
 
-                    std::cout << suffix + "_" + shadingName << "..." << std::endl;
+                        std::cout << suffix + "_" + shadingName << "..." << std::endl;
 
-                    cv::Mat colorImage, depthImage;
-                    generateImage(res, shadingType, cullingMode, modelType, modelPath, colorImage, depthImage);
+                        cv::Mat colorImage, depthImage;
 
-                    std::string gtPathColor = outPath + "/example_image_" + suffix + "_" + shadingName + ".png";
-                    std::string gtPathDepth = outPath + "/depth_image_"   + suffix + ".png";
+                        ModelData modelData(modelType, modelPath);
+                        generateImage(res, shadingType, cullingMode, modelData, colorImage, depthImage);
 
-                    cv::imwrite(gtPathColor, colorImage);
-                    cv::imwrite(gtPathDepth, depthImage);
+                        std::string gtPathColor = outPath + "/example_image_" + suffix + "_" + shadingName + ".png";
+                        std::string gtPathDepth = outPath + "/depth_image_"   + suffix + ".png";
+
+                        cv::imwrite(gtPathColor, colorImage);
+                        cv::imwrite(gtPathDepth, depthImage);
+                    }
                 }
             }
         }