diff --git a/SuperBuild/CMakeLists.txt b/SuperBuild/CMakeLists.txt index 97636956..1584845f 100644 --- a/SuperBuild/CMakeLists.txt +++ b/SuperBuild/CMakeLists.txt @@ -103,7 +103,9 @@ set(custom_libs OpenGV Catkin Ecto LAStools - PDAL) + PDAL + MvsTexturing +) foreach(lib ${custom_libs}) SETUP_EXTERNAL_PROJECT_CUSTOM(${lib}) diff --git a/SuperBuild/cmake/External-MvsTexturing.cmake b/SuperBuild/cmake/External-MvsTexturing.cmake new file mode 100644 index 00000000..601948f2 --- /dev/null +++ b/SuperBuild/cmake/External-MvsTexturing.cmake @@ -0,0 +1,29 @@ +set(_proj_name mvstexuring) +set(_SB_BINARY_DIR "${SB_BINARY_DIR}/${_proj_name}") + +ExternalProject_Add(${_proj_name} + DEPENDS + PREFIX ${_SB_BINARY_DIR} + TMP_DIR ${_SB_BINARY_DIR}/tmp + STAMP_DIR ${_SB_BINARY_DIR}/stamp + #--Download step-------------- + DOWNLOAD_DIR ${SB_DOWNLOAD_DIR} + URL https://github.com/nmoehrle/mvs-texturing/archive/dab68acaa693275c183c254a958130ee6d29c3e4.zip + URL_MD5 0b0466f5d1046699594ce7fc77bdad02 + #--Update/Patch step---------- + UPDATE_COMMAND "" + #--Configure step------------- + SOURCE_DIR ${SB_SOURCE_DIR}/${_proj_name} + CMAKE_ARGS + -DRESEARCH=OFF + -DCMAKE_BUILD_TYPE:STRING=Release + -DCMAKE_INSTALL_PREFIX:PATH=${SB_INSTALL_DIR} + #--Build step----------------- + BINARY_DIR ${_SB_BINARY_DIR} + #--Install step--------------- + INSTALL_DIR ${SB_INSTALL_DIR} + #--Output logging------------- + LOG_DOWNLOAD OFF + LOG_CONFIGURE OFF + LOG_BUILD OFF +) diff --git a/ccd_defs_check.py b/ccd_defs_check.py old mode 100755 new mode 100644 diff --git a/configure.sh b/configure.sh index d46fa7fa..2223e0ab 100644 --- a/configure.sh +++ b/configure.sh @@ -26,8 +26,16 @@ sudo apt-get install build-essential \ git \ python-pip \ libgdal-dev \ + gdal-bin \ libgeotiff-dev \ pkg-config -y + +# If we have ubuntu version 14.04, the cmake version in apt-get is too low for mvs-texturing. +if [[ `lsb_release -rs` == "14.04" ]]; +then + bash upgradecmake.sh +fi + if [ $? -ne 0 ] then echo -e "\e[1;31mERROR: \e[39mWhen Installing Required Requisites\e[0m" diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 diff --git a/modules/odm_georef/CMakeLists.txt.user b/modules/odm_georef/CMakeLists.txt.user deleted file mode 100644 index 48239e25..00000000 --- a/modules/odm_georef/CMakeLists.txt.user +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - ProjectExplorer.Project.ActiveTarget - 0 - - - ProjectExplorer.Project.EditorSettings - - true - false - - Cpp - - CppGlobal - - - - QmlJS - - QmlJSGlobal - - - 2 - System - false - 4 - true - 1 - true - 0 - true - 0 - 8 - true - 1 - true - true - true - false - - - - ProjectExplorer.Project.PluginSettings - - - - ProjectExplorer.Project.Target.0 - - Desktop - - CMakeProjectManager.DefaultCMakeTarget - 0 - 0 - 0 - - /home/spotscale/odm/OpenDroneMap-texturing_orthophoto_spotscale_additions/odm_georef-build - ProjectExplorer.ToolChain.Gcc:/usr/bin/g++.x86-linux-generic-elf-32bit./usr/bin/gdb - ProjectExplorer.ToolChain.Gcc:/usr/bin/g++.x86-linux-generic-elf-32bit./usr/bin/gdb - - - - - false - Make - - CMakeProjectManager.MakeStep - - 1 - Build - - ProjectExplorer.BuildSteps.Build - - - - clean - - true - Make - - CMakeProjectManager.MakeStep - - 1 - Clean - - ProjectExplorer.BuildSteps.Clean - - 2 - false - - all - - CMakeProjectManager.CMakeBuildConfiguration - - 1 - - - 0 - Deploy - - ProjectExplorer.BuildSteps.Deploy - - 1 - No deployment - - ProjectExplorer.DefaultDeployConfiguration - - 1 - - true - true - - - false - false - false - false - false - false - false - false - true - true - 0.01 - 0.01 - 10 - 10 - true - true - 25 - 25 - - - true - true - valgrind - valgrind - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - - 2 - odm_georef - -verbose -bundleFile ../../../shared_folder/seneca/reconstruction-with-image-size-1200/pmvs/bundle.rd.out -inputFile ../../../shared_folder/seneca/reconstruction-with-image-size-1200-results/odm_texturing/odm_textured_model.obj -imagesListPath ../../../shared_folder/seneca/reconstruction-with-image-size-1200/pmvs/list.rd.txt -gcpFile ../../../shared_folder/seneca_georef_input/gcp_list.txt -imagesPath ../../../shared_folder/seneca/ -bundleResizedTo 1200 - false - - - odm_georef - - CMakeProjectManager.CMakeRunConfiguration. - 3768 - true - false - false - - 1 - - - - ProjectExplorer.Project.TargetCount - 1 - - - ProjectExplorer.Project.Updater.EnvironmentId - {785a73be-b55f-490c-9d46-e1451c235840} - - - ProjectExplorer.Project.Updater.FileVersion - 10 - - diff --git a/modules/odm_georef/src/Georef.cpp b/modules/odm_georef/src/Georef.cpp index 10fac52c..6ff34a68 100644 --- a/modules/odm_georef/src/Georef.cpp +++ b/modules/odm_georef/src/Georef.cpp @@ -6,9 +6,6 @@ #include #include -// Modified PCL -#include "modifiedPclFunctions.hpp" - // This #include "Georef.hpp" @@ -753,7 +750,7 @@ void Georef::performGeoreferencingWithGCP() log_ << "Reading mesh file " << inputObjFilename_ <<"\n"; log_ << '\n'; pcl::TextureMesh mesh; - if (pcl::io::loadOBJFile(inputObjFilename_, mesh) == -1) + if (loadObjFile(inputObjFilename_, mesh) == -1) { throw GeorefException("Error when reading model from:\n" + inputObjFilename_ + "\n"); } @@ -1077,7 +1074,7 @@ void Georef::createGeoreferencedModelFromExifData() log_ << '\n'; log_ << "Reading mesh file...\n"; pcl::TextureMesh mesh; - pcl::io::loadOBJFile(inputObjFilename_, mesh); + loadObjFile(inputObjFilename_, mesh); log_ << ".. mesh file read.\n"; // Contains the vertices of the mesh. @@ -1249,3 +1246,364 @@ void Georef::printGeorefSystem() } +bool Georef::loadObjFile(std::string inputFile, pcl::TextureMesh &mesh) +{ + int data_type; + unsigned int data_idx; + int file_version; + int offset = 0; + Eigen::Vector4f origin; + Eigen::Quaternionf orientation; + + if (!readHeader(inputFile, mesh.cloud, origin, orientation, file_version, data_type, data_idx, offset)) + { + throw GeorefException("Problem reading header in modelfile!\n"); + } + + std::ifstream fs; + + fs.open (inputFile.c_str (), std::ios::binary); + if (!fs.is_open () || fs.fail ()) + { + //PCL_ERROR ("[pcl::OBJReader::readHeader] Could not open file '%s'! Error : %s\n", file_name.c_str (), strerror(errno)); + fs.close (); + log_<<"Could not read mesh from file "; + log_ << inputFile.c_str(); + log_ <<"\n"; + + throw GeorefException("Problem reading mesh from file!\n"); + } + + // Seek at the given offset + fs.seekg (data_idx, std::ios::beg); + + // Get normal_x field indices + int normal_x_field = -1; + for (std::size_t i = 0; i < mesh.cloud.fields.size (); ++i) + { + if (mesh.cloud.fields[i].name == "normal_x") + { + normal_x_field = i; + break; + } + } + + std::size_t v_idx = 0; + std::size_t vn_idx = 0; + std::size_t vt_idx = 0; + std::size_t f_idx = 0; + std::string line; + std::vector st; + std::vector coordinates; + std::vector allTexCoords; + + std::map f2vt; + + try + { + while (!fs.eof ()) + { + getline (fs, line); + // Ignore empty lines + if (line == "") + continue; + + // Tokenize the line + std::stringstream sstream (line); + sstream.imbue (std::locale::classic ()); + line = sstream.str (); + boost::trim (line); + boost::split (st, line, boost::is_any_of ("\t\r "), boost::token_compress_on); + + // Ignore comments + if (st[0] == "#") + continue; + // Vertex + if (st[0] == "v") + { + try + { + for (int i = 1, f = 0; i < 4; ++i, ++f) + { + float value = boost::lexical_cast (st[i]); + memcpy (&mesh.cloud.data[v_idx * mesh.cloud.point_step + mesh.cloud.fields[f].offset], &value, sizeof (float)); + } + + ++v_idx; + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex coordinates!\n"; + throw GeorefException("Unable to convert %s to vertex coordinates!"); + } + continue; + } + // Vertex normal + if (st[0] == "vn") + { + try + { + for (int i = 1, f = normal_x_field; i < 4; ++i, ++f) + { + float value = boost::lexical_cast (st[i]); + memcpy (&mesh.cloud.data[vn_idx * mesh.cloud.point_step + mesh.cloud.fields[f].offset], + &value, + sizeof (float)); + } + ++vn_idx; + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex normal!\n"; + throw GeorefException("Unable to convert %s to vertex normal!"); + } + continue; + } + // Texture coordinates + if (st[0] == "vt") + { + try + { + Eigen::Vector3f c (0, 0, 0); + for (std::size_t i = 1; i < st.size (); ++i) + c[i-1] = boost::lexical_cast (st[i]); + + if (c[2] == 0) + coordinates.push_back (Eigen::Vector2f (c[0], c[1])); + else + coordinates.push_back (Eigen::Vector2f (c[0]/c[2], c[1]/c[2])); + ++vt_idx; + + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex texture coordinates!\n"; + throw GeorefException("Unable to convert %s to vertex texture coordinates!"); + } + continue; + } + // Material + if (st[0] == "usemtl") + { + mesh.tex_polygons.push_back (std::vector ()); + mesh.tex_materials.push_back (pcl::TexMaterial ()); + for (std::size_t i = 0; i < companions_.size (); ++i) + { + std::vector::const_iterator mat_it = companions_[i].getMaterial (st[1]); + if (mat_it != companions_[i].materials_.end ()) + { + mesh.tex_materials.back () = *mat_it; + break; + } + } + // We didn't find the appropriate material so we create it here with name only. + if (mesh.tex_materials.back ().tex_name == "") + mesh.tex_materials.back ().tex_name = st[1]; + mesh.tex_coordinates.push_back (coordinates); + coordinates.clear (); + continue; + } + // Face + if (st[0] == "f") + { + //We only care for vertices indices + pcl::Vertices face_v; face_v.vertices.resize (st.size () - 1); + for (std::size_t i = 1; i < st.size (); ++i) + { + int v; + sscanf (st[i].c_str (), "%d", &v); + v = (v < 0) ? v_idx + v : v - 1; + face_v.vertices[i-1] = v; + + int v2, vt, vn; + sscanf (st[i].c_str (), "%d/%d/%d", &v2, &vt, &vn); + f2vt[3*(f_idx) + i-1] = vt-1; + } + mesh.tex_polygons.back ().push_back (face_v); + ++f_idx; + continue; + } + } + } + catch (const char *exception) + { + fs.close (); + log_<<"Unable to read file!\n"; + throw GeorefException("Unable to read file!"); + } + + if (vt_idx != v_idx) + { + std::vector texcoordinates = std::vector(0); + + for (size_t faceIndex = 0; faceIndex < f_idx; ++faceIndex) + { + for(size_t i = 0; i < 3; ++i) + { + Eigen::Vector2f vt = mesh.tex_coordinates[0][f2vt[3*faceIndex+i]]; + texcoordinates.push_back(vt); + } + } + + mesh.tex_coordinates.clear(); + mesh.tex_coordinates.push_back(texcoordinates); + } + + fs.close(); + return (0); +} + +bool Georef::readHeader (const std::string &file_name, pcl::PCLPointCloud2 &cloud, + Eigen::Vector4f &origin, Eigen::Quaternionf &orientation, + int &file_version, int &data_type, unsigned int &data_idx, + const int offset) +{ + origin = Eigen::Vector4f::Zero (); + orientation = Eigen::Quaternionf::Identity (); + file_version = 0; + cloud.width = cloud.height = cloud.point_step = cloud.row_step = 0; + cloud.data.clear (); + data_type = 0; + data_idx = offset; + + std::ifstream fs; + std::string line; + + if (file_name == "" || !boost::filesystem::exists (file_name)) + { + return false; + } + + // Open file in binary mode to avoid problem of + // std::getline() corrupting the result of ifstream::tellg() + fs.open (file_name.c_str (), std::ios::binary); + if (!fs.is_open () || fs.fail ()) + { + fs.close (); + return false; + } + + // Seek at the given offset + fs.seekg (offset, std::ios::beg); + + // Read the header and fill it in with wonderful values + bool vertex_normal_found = false; + bool vertex_texture_found = false; + // Material library, skip for now! + // bool material_found = false; + std::vector material_files; + std::size_t nr_point = 0; + std::vector st; + + try + { + while (!fs.eof ()) + { + getline (fs, line); + // Ignore empty lines + if (line == "") + continue; + + // Tokenize the line + std::stringstream sstream (line); + sstream.imbue (std::locale::classic ()); + line = sstream.str (); + boost::trim (line); + boost::split (st, line, boost::is_any_of ("\t\r "), boost::token_compress_on); + // Ignore comments + if (st.at (0) == "#") + continue; + + // Vertex + if (st.at (0) == "v") + { + ++nr_point; + continue; + } + + // Vertex texture + if ((st.at (0) == "vt") && !vertex_texture_found) + { + vertex_texture_found = true; + continue; + } + + // Vertex normal + if ((st.at (0) == "vn") && !vertex_normal_found) + { + vertex_normal_found = true; + continue; + } + + // Material library, skip for now! + if (st.at (0) == "mtllib") + { + material_files.push_back (st.at (1)); + continue; + } + } + } + catch (const char *exception) + { + fs.close (); + return false; + } + + if (!nr_point) + { + fs.close (); + return false; + } + + int field_offset = 0; + for (int i = 0; i < 3; ++i, field_offset += 4) + { + cloud.fields.push_back (pcl::PCLPointField ()); + cloud.fields[i].offset = field_offset; + cloud.fields[i].datatype = pcl::PCLPointField::FLOAT32; + cloud.fields[i].count = 1; + } + + cloud.fields[0].name = "x"; + cloud.fields[1].name = "y"; + cloud.fields[2].name = "z"; + + if (vertex_normal_found) + { + std::string normals_names[3] = { "normal_x", "normal_y", "normal_z" }; + for (int i = 0; i < 3; ++i, field_offset += 4) + { + cloud.fields.push_back (pcl::PCLPointField ()); + pcl::PCLPointField& last = cloud.fields.back (); + last.name = normals_names[i]; + last.offset = field_offset; + last.datatype = pcl::PCLPointField::FLOAT32; + last.count = 1; + } + } + + if (material_files.size () > 0) + { + for (std::size_t i = 0; i < material_files.size (); ++i) + { + pcl::MTLReader companion; + + if (companion.read (file_name, material_files[i])) + { + log_<<"Problem reading material file."; + } + + companions_.push_back (companion); + } + } + + cloud.point_step = field_offset; + cloud.width = nr_point; + cloud.height = 1; + cloud.row_step = cloud.point_step * cloud.width; + cloud.is_dense = true; + cloud.data.resize (cloud.point_step * nr_point); + fs.close (); + return true; +} + diff --git a/modules/odm_georef/src/Georef.hpp b/modules/odm_georef/src/Georef.hpp index ad89506c..2cdb884c 100644 --- a/modules/odm_georef/src/Georef.hpp +++ b/modules/odm_georef/src/Georef.hpp @@ -8,6 +8,8 @@ // PCL #include #include +// Modified PCL +#include "modifiedPclFunctions.hpp" // Logger #include "Logger.hpp" @@ -204,6 +206,24 @@ private: **/ void printGeorefSystem(); + /*! + * \brief Loads a model from an .obj file (replacement for the pcl obj loader). + * + * \param inputFile Path to the .obj file. + * \param mesh The model. + * \return True if model was loaded successfully. + */ + bool loadObjFile(std::string inputFile, pcl::TextureMesh &mesh); + + /*! + * \brief Function is compied straight from the function in the pcl::io module. + */ + bool readHeader (const std::string &file_name, pcl::PCLPointCloud2 &cloud, + Eigen::Vector4f &origin, Eigen::Quaternionf &orientation, + int &file_version, int &data_type, unsigned int &data_idx, + const int offset); + + Logger log_; /**< Logging object. */ std::string logFile_; /**< The path to the output log file. */ @@ -230,6 +250,10 @@ private: std::vector imageList_; /**< A vector containing the names of the corresponding cameras. **/ GeorefSystem georefSystem_; /**< Contains the georeference system. **/ + + bool multiMaterial_; /**< True if the mesh has multiple materials. **/ + + std::vector companions_; /**< Materials (used by loadOBJFile). **/ }; /*! diff --git a/modules/odm_georef/src/modifiedPclFunctions.hpp b/modules/odm_georef/src/modifiedPclFunctions.hpp index 5739df14..0e6a77dd 100644 --- a/modules/odm_georef/src/modifiedPclFunctions.hpp +++ b/modules/odm_georef/src/modifiedPclFunctions.hpp @@ -8,6 +8,7 @@ #include #include #include +#include int saveOBJFile(const std::string &file_name, const pcl::TextureMesh &tex_mesh, unsigned precision); diff --git a/modules/odm_orthophoto/CMakeLists.txt.user b/modules/odm_orthophoto/CMakeLists.txt.user deleted file mode 100644 index 05ffbfd9..00000000 --- a/modules/odm_orthophoto/CMakeLists.txt.user +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - ProjectExplorer.Project.ActiveTarget - 0 - - - ProjectExplorer.Project.EditorSettings - - true - false - - Cpp - - CppGlobal - - - - QmlJS - - QmlJSGlobal - - - 2 - System - false - 4 - true - 1 - true - 0 - true - 0 - 8 - true - 1 - true - true - true - false - - - - ProjectExplorer.Project.PluginSettings - - - - ProjectExplorer.Project.Target.0 - - Desktop - - CMakeProjectManager.DefaultCMakeTarget - 0 - 0 - 0 - - /home/spotscale/odm/OpenDroneMap-texturing_orthophoto_spotscale_additions/odm_orthophoto-build - ProjectExplorer.ToolChain.Gcc:/usr/bin/g++.x86-linux-generic-elf-32bit./usr/bin/gdb - ProjectExplorer.ToolChain.Gcc:/usr/bin/g++.x86-linux-generic-elf-32bit./usr/bin/gdb - - - - - false - Make - - CMakeProjectManager.MakeStep - - 1 - Build - - ProjectExplorer.BuildSteps.Build - - - - clean - - true - Make - - CMakeProjectManager.MakeStep - - 1 - Clean - - ProjectExplorer.BuildSteps.Clean - - 2 - false - - all - - CMakeProjectManager.CMakeBuildConfiguration - - 1 - - - 0 - Deploy - - ProjectExplorer.BuildSteps.Deploy - - 1 - No deployment - - ProjectExplorer.DefaultDeployConfiguration - - 1 - - true - true - - - false - false - false - false - false - false - false - false - true - true - 0.01 - 0.01 - 10 - 10 - true - true - 25 - 25 - - - true - true - valgrind - valgrind - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - - - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - - 2 - odm_orthophoto - - false - - - odm_orthophoto - - CMakeProjectManager.CMakeRunConfiguration. - 3768 - true - false - false - - 1 - - - - ProjectExplorer.Project.TargetCount - 1 - - - ProjectExplorer.Project.Updater.EnvironmentId - {785a73be-b55f-490c-9d46-e1451c235840} - - - ProjectExplorer.Project.Updater.FileVersion - 10 - - diff --git a/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp b/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp index ce5056f4..ff771283 100644 --- a/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp +++ b/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp @@ -266,7 +266,7 @@ void OdmOrthoPhoto::printHelp() log_ << "Call the program with flag \"-help\", or without parameters to print this message, or check any generated log file.\n"; log_ << "Call the program with flag \"-verbose\", to print log messages in the standard output stream as well as in the log file.\n\n"; - log_ << "Parameters are specified as: \"- \", (without <>), and the following parameters are configureable:n"; + log_ << "Parameters are specified as: \"- \", (without <>), and the following parameters are configureable:\n"; log_ << "\"-inputFile \" (mandatory)\n"; log_ << "\"Input obj file that must contain a textured mesh.\n\n"; @@ -321,18 +321,20 @@ void OdmOrthoPhoto::createOrthoPhoto() log_ << "Reading mesh file...\n"; // The textureds mesh. pcl::TextureMesh mesh; - pcl::io::loadOBJFile(inputFile_, mesh); + loadObjFile(inputFile_, mesh); log_ << ".. mesh file read.\n\n"; // Does the model have more than one material? multiMaterial_ = 1 < mesh.tex_materials.size(); + bool splitModel = false; + if(multiMaterial_) { // Need to check relationship between texture coordinates and faces. if(!isModelOk(mesh)) { - throw OdmOrthoPhotoException("Could not generate ortho photo: The given mesh has multiple textures, but the number of texture coordinates is NOT equal to 3 times the number of faces."); + splitModel = true; } } @@ -386,6 +388,58 @@ void OdmOrthoPhoto::createOrthoPhoto() pcl::PointCloud::Ptr meshCloud (new pcl::PointCloud); pcl::fromPCLPointCloud2 (mesh.cloud, *meshCloud); + // Split model and make copies of vertices and texture coordinates for all faces + //if (splitModel) + if (splitModel) + { + pcl::PointCloud::Ptr meshCloudSplit (new pcl::PointCloud); + std::vector textureCoordinates = std::vector(0); + + size_t vertexIndexCount = 0; + for(size_t t = 0; t < mesh.tex_polygons.size(); ++t) + { + + for(size_t faceIndex = 0; faceIndex < mesh.tex_polygons[t].size(); ++faceIndex) + { + pcl::Vertices polygon = mesh.tex_polygons[t][faceIndex]; + + // The index to the vertices of the polygon. + size_t v1i = polygon.vertices[0]; + size_t v2i = polygon.vertices[1]; + size_t v3i = polygon.vertices[2]; + + // The polygon's points. + pcl::PointXYZ v1 = meshCloud->points[v1i]; + pcl::PointXYZ v2 = meshCloud->points[v2i]; + pcl::PointXYZ v3 = meshCloud->points[v3i]; + + Eigen::Vector2f vt1 = mesh.tex_coordinates[0][3*faceIndex]; + Eigen::Vector2f vt2 = mesh.tex_coordinates[0][3*faceIndex + 1]; + Eigen::Vector2f vt3 = mesh.tex_coordinates[0][3*faceIndex + 2]; + + meshCloudSplit->points.push_back(v1); + textureCoordinates.push_back(vt1); + mesh.tex_polygons[t][faceIndex].vertices[0] = vertexIndexCount; + ++vertexIndexCount; + + meshCloudSplit->points.push_back(v2); + textureCoordinates.push_back(vt2); + mesh.tex_polygons[t][faceIndex].vertices[1] = vertexIndexCount; + ++vertexIndexCount; + + meshCloudSplit->points.push_back(v3); + textureCoordinates.push_back(vt3); + mesh.tex_polygons[t][faceIndex].vertices[2] = vertexIndexCount; + ++vertexIndexCount; + } + } + + mesh.tex_coordinates.clear(); + mesh.tex_coordinates.push_back(textureCoordinates); + + meshCloud = meshCloudSplit; + } + // Creates a transformation which aligns the area for the ortho photo. Eigen::Transform transform = getROITransform(xMin, -yMax); @@ -401,6 +455,7 @@ void OdmOrthoPhoto::createOrthoPhoto() { uvs.insert(uvs.end(), mesh.tex_coordinates[t].begin(), mesh.tex_coordinates[t].end()); } + //cv::namedWindow("dsfs"); // The current material texture cv::Mat texture; @@ -416,7 +471,7 @@ void OdmOrthoPhoto::createOrthoPhoto() // The material of the current submesh. pcl::TexMaterial material = mesh.tex_materials[t]; texture = cv::imread(material.tex_file); - + // Check for missing files. if(texture.empty()) { @@ -659,6 +714,7 @@ void OdmOrthoPhoto::drawTexturedTriangle(const cv::Mat &texture, const pcl::Vert v1u = uvs[3*faceIndex][0]; v1v = uvs[3*faceIndex][1]; v2u = uvs[3*faceIndex+1][0]; v2v = uvs[3*faceIndex+1][1]; v3u = uvs[3*faceIndex+2][0]; v3v = uvs[3*faceIndex+2][1]; + } else { @@ -994,3 +1050,365 @@ bool OdmOrthoPhoto::isModelOk(const pcl::TextureMesh &mesh) return 3*nFaces == nTextureCoordinates; } + + +bool OdmOrthoPhoto::loadObjFile(std::string inputFile, pcl::TextureMesh &mesh) +{ + int data_type; + unsigned int data_idx; + int file_version; + int offset = 0; + Eigen::Vector4f origin; + Eigen::Quaternionf orientation; + + if (!readHeader(inputFile, mesh.cloud, origin, orientation, file_version, data_type, data_idx, offset)) + { + throw OdmOrthoPhotoException("Problem reading header in modelfile!\n"); + } + + std::ifstream fs; + + fs.open (inputFile.c_str (), std::ios::binary); + if (!fs.is_open () || fs.fail ()) + { + //PCL_ERROR ("[pcl::OBJReader::readHeader] Could not open file '%s'! Error : %s\n", file_name.c_str (), strerror(errno)); + fs.close (); + log_<<"Could not read mesh from file "; + log_ << inputFile.c_str(); + log_ <<"\n"; + + throw OdmOrthoPhotoException("Problem reading mesh from file!\n"); + } + + // Seek at the given offset + fs.seekg (data_idx, std::ios::beg); + + // Get normal_x field indices + int normal_x_field = -1; + for (std::size_t i = 0; i < mesh.cloud.fields.size (); ++i) + { + if (mesh.cloud.fields[i].name == "normal_x") + { + normal_x_field = i; + break; + } + } + + std::size_t v_idx = 0; + std::size_t vn_idx = 0; + std::size_t vt_idx = 0; + std::size_t f_idx = 0; + std::string line; + std::vector st; + std::vector coordinates; + std::vector allTexCoords; + + std::map f2vt; + + try + { + while (!fs.eof ()) + { + getline (fs, line); + // Ignore empty lines + if (line == "") + continue; + + // Tokenize the line + std::stringstream sstream (line); + sstream.imbue (std::locale::classic ()); + line = sstream.str (); + boost::trim (line); + boost::split (st, line, boost::is_any_of ("\t\r "), boost::token_compress_on); + + // Ignore comments + if (st[0] == "#") + continue; + // Vertex + if (st[0] == "v") + { + try + { + for (int i = 1, f = 0; i < 4; ++i, ++f) + { + float value = boost::lexical_cast (st[i]); + memcpy (&mesh.cloud.data[v_idx * mesh.cloud.point_step + mesh.cloud.fields[f].offset], &value, sizeof (float)); + } + + ++v_idx; + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex coordinates!\n"; + throw OdmOrthoPhotoException("Unable to convert %s to vertex coordinates!"); + } + continue; + } + // Vertex normal + if (st[0] == "vn") + { + try + { + for (int i = 1, f = normal_x_field; i < 4; ++i, ++f) + { + float value = boost::lexical_cast (st[i]); + memcpy (&mesh.cloud.data[vn_idx * mesh.cloud.point_step + mesh.cloud.fields[f].offset], + &value, + sizeof (float)); + } + ++vn_idx; + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex normal!\n"; + throw OdmOrthoPhotoException("Unable to convert %s to vertex normal!"); + } + continue; + } + // Texture coordinates + if (st[0] == "vt") + { + try + { + Eigen::Vector3f c (0, 0, 0); + for (std::size_t i = 1; i < st.size (); ++i) + c[i-1] = boost::lexical_cast (st[i]); + + if (c[2] == 0) + coordinates.push_back (Eigen::Vector2f (c[0], c[1])); + else + coordinates.push_back (Eigen::Vector2f (c[0]/c[2], c[1]/c[2])); + ++vt_idx; + + } + catch (const boost::bad_lexical_cast &e) + { + log_<<"Unable to convert %s to vertex texture coordinates!\n"; + throw OdmOrthoPhotoException("Unable to convert %s to vertex texture coordinates!"); + } + continue; + } + // Material + if (st[0] == "usemtl") + { + mesh.tex_polygons.push_back (std::vector ()); + mesh.tex_materials.push_back (pcl::TexMaterial ()); + for (std::size_t i = 0; i < companions_.size (); ++i) + { + std::vector::const_iterator mat_it = companions_[i].getMaterial (st[1]); + if (mat_it != companions_[i].materials_.end ()) + { + mesh.tex_materials.back () = *mat_it; + break; + } + } + // We didn't find the appropriate material so we create it here with name only. + if (mesh.tex_materials.back ().tex_name == "") + mesh.tex_materials.back ().tex_name = st[1]; + mesh.tex_coordinates.push_back (coordinates); + coordinates.clear (); + continue; + } + // Face + if (st[0] == "f") + { + //We only care for vertices indices + pcl::Vertices face_v; face_v.vertices.resize (st.size () - 1); + for (std::size_t i = 1; i < st.size (); ++i) + { + int v; + sscanf (st[i].c_str (), "%d", &v); + v = (v < 0) ? v_idx + v : v - 1; + face_v.vertices[i-1] = v; + + int v2, vt, vn; + sscanf (st[i].c_str (), "%d/%d/%d", &v2, &vt, &vn); + f2vt[3*(f_idx) + i-1] = vt-1; + } + mesh.tex_polygons.back ().push_back (face_v); + ++f_idx; + continue; + } + } + } + catch (const char *exception) + { + fs.close (); + log_<<"Unable to read file!\n"; + throw OdmOrthoPhotoException("Unable to read file!"); + } + + if (vt_idx != v_idx) + { + std::vector texcoordinates = std::vector(0); + + for (size_t faceIndex = 0; faceIndex < f_idx; ++faceIndex) + { + for(size_t i = 0; i < 3; ++i) + { + Eigen::Vector2f vt = mesh.tex_coordinates[0][f2vt[3*faceIndex+i]]; + texcoordinates.push_back(vt); + } + } + + mesh.tex_coordinates.clear(); + mesh.tex_coordinates.push_back(texcoordinates); + } + + fs.close(); + return (0); +} + +bool OdmOrthoPhoto::readHeader (const std::string &file_name, pcl::PCLPointCloud2 &cloud, + Eigen::Vector4f &origin, Eigen::Quaternionf &orientation, + int &file_version, int &data_type, unsigned int &data_idx, + const int offset) +{ + origin = Eigen::Vector4f::Zero (); + orientation = Eigen::Quaternionf::Identity (); + file_version = 0; + cloud.width = cloud.height = cloud.point_step = cloud.row_step = 0; + cloud.data.clear (); + data_type = 0; + data_idx = offset; + + std::ifstream fs; + std::string line; + + if (file_name == "" || !boost::filesystem::exists (file_name)) + { + return false; + } + + // Open file in binary mode to avoid problem of + // std::getline() corrupting the result of ifstream::tellg() + fs.open (file_name.c_str (), std::ios::binary); + if (!fs.is_open () || fs.fail ()) + { + fs.close (); + return false; + } + + // Seek at the given offset + fs.seekg (offset, std::ios::beg); + + // Read the header and fill it in with wonderful values + bool vertex_normal_found = false; + bool vertex_texture_found = false; + // Material library, skip for now! + // bool material_found = false; + std::vector material_files; + std::size_t nr_point = 0; + std::vector st; + + try + { + while (!fs.eof ()) + { + getline (fs, line); + // Ignore empty lines + if (line == "") + continue; + + // Tokenize the line + std::stringstream sstream (line); + sstream.imbue (std::locale::classic ()); + line = sstream.str (); + boost::trim (line); + boost::split (st, line, boost::is_any_of ("\t\r "), boost::token_compress_on); + // Ignore comments + if (st.at (0) == "#") + continue; + + // Vertex + if (st.at (0) == "v") + { + ++nr_point; + continue; + } + + // Vertex texture + if ((st.at (0) == "vt") && !vertex_texture_found) + { + vertex_texture_found = true; + continue; + } + + // Vertex normal + if ((st.at (0) == "vn") && !vertex_normal_found) + { + vertex_normal_found = true; + continue; + } + + // Material library, skip for now! + if (st.at (0) == "mtllib") + { + material_files.push_back (st.at (1)); + continue; + } + } + } + catch (const char *exception) + { + fs.close (); + return false; + } + + if (!nr_point) + { + fs.close (); + return false; + } + + int field_offset = 0; + for (int i = 0; i < 3; ++i, field_offset += 4) + { + cloud.fields.push_back (pcl::PCLPointField ()); + cloud.fields[i].offset = field_offset; + cloud.fields[i].datatype = pcl::PCLPointField::FLOAT32; + cloud.fields[i].count = 1; + } + + cloud.fields[0].name = "x"; + cloud.fields[1].name = "y"; + cloud.fields[2].name = "z"; + + if (vertex_normal_found) + { + std::string normals_names[3] = { "normal_x", "normal_y", "normal_z" }; + for (int i = 0; i < 3; ++i, field_offset += 4) + { + cloud.fields.push_back (pcl::PCLPointField ()); + pcl::PCLPointField& last = cloud.fields.back (); + last.name = normals_names[i]; + last.offset = field_offset; + last.datatype = pcl::PCLPointField::FLOAT32; + last.count = 1; + } + } + + if (material_files.size () > 0) + { + for (std::size_t i = 0; i < material_files.size (); ++i) + { + pcl::MTLReader companion; + + if (companion.read (file_name, material_files[i])) + { + log_<<"Problem reading material file."; + } + + companions_.push_back (companion); + } + } + + cloud.point_step = field_offset; + cloud.width = nr_point; + cloud.height = 1; + cloud.row_step = cloud.point_step * cloud.width; + cloud.is_dense = true; + cloud.data.resize (cloud.point_step * nr_point); + fs.close (); + return true; +} diff --git a/modules/odm_orthophoto/src/OdmOrthoPhoto.hpp b/modules/odm_orthophoto/src/OdmOrthoPhoto.hpp index de2793d0..c02a7e53 100644 --- a/modules/odm_orthophoto/src/OdmOrthoPhoto.hpp +++ b/modules/odm_orthophoto/src/OdmOrthoPhoto.hpp @@ -172,6 +172,23 @@ private: */ bool isModelOk(const pcl::TextureMesh &mesh); + /*! + * \brief Loads a model from an .obj file (replacement for the pcl obj loader). + * + * \param inputFile Path to the .obj file. + * \param mesh The model. + * \return True if model was loaded successfully. + */ + bool loadObjFile(std::string inputFile, pcl::TextureMesh &mesh); + + /*! + * \brief Function is compied straight from the function in the pcl::io module. + */ + bool readHeader (const std::string &file_name, pcl::PCLPointCloud2 &cloud, + Eigen::Vector4f &origin, Eigen::Quaternionf &orientation, + int &file_version, int &data_type, unsigned int &data_idx, + const int offset); + Logger log_; /**< Logging object. */ std::string inputFile_; /**< Path to the textured mesh as an obj-file. */ @@ -198,6 +215,8 @@ private: cv::Mat depth_; /**< The depth of the ortho photo as an OpenCV matrix, CV_32F. */ bool multiMaterial_; /**< True if the mesh has multiple materials. **/ + + std::vector companions_; /**< Materials (used by loadOBJFile). **/ }; /*! diff --git a/opendm/config.py b/opendm/config.py index b8ec0295..9c9b663c 100644 --- a/opendm/config.py +++ b/opendm/config.py @@ -3,7 +3,7 @@ import context # parse arguments processopts = ['resize', 'opensfm', 'cmvs', 'pmvs', - 'odm_meshing', 'odm_texturing', 'odm_georeferencing', + 'odm_meshing', 'mvs_texturing', 'odm_georeferencing', 'odm_orthophoto'] @@ -196,6 +196,45 @@ parser.add_argument('--odm_meshing-solverDivide', 'times slightly but helps reduce memory usage. ' 'Default: %(default)s')) +parser.add_argument('--mvs_texturing-dataTerm', + metavar='', + default='gmi', + help=('Data term: [area, gmi]. Default: %(default)s')) + +parser.add_argument('--mvs_texturing-outlierRemovalType', + metavar='', + default='none', + help=('Type of photometric outlier removal method: ' + '[none, gauss_damping, gauss_clamping]. Default: ' + '%(default)s')) + +parser.add_argument('--mvs_texturing-skipGeometricVisibilityTest', + metavar='', + default="false", + help=('Skip geometric visibility test. Default: %(default)s')) + +parser.add_argument('--mvs_texturing-skipGlobalSeamLeveling', + metavar='', + default="false", + help=('Skip geometric visibility test. Default: %(default)s')) + +parser.add_argument('--mvs_texturing-skipLocalSeamLeveling', + metavar='', + default="false", + help=('Skip local seam blending. Default: %(default)s')) + +parser.add_argument('--mvs_texturing-skipHoleFilling', + metavar='', + default="false", + help=('Skip filling of holes in the mesh. Default: %(default)s')) + +parser.add_argument('--mvs_texturing-keepUnseenFaces', + metavar='', + default="false", + help=('Keep faces in the mesh that are not seen in any camera. ' + 'Default: %(default)s')) + +# Old odm_texturing arguments parser.add_argument('--odm_texturing-textureResolution', metavar='', default=4096, @@ -209,6 +248,7 @@ parser.add_argument('--odm_texturing-textureWithSize', type=int, help=('The resolution to rescale the images performing ' 'the texturing. Default: %(default)s')) +# End of old odm_texturing arguments parser.add_argument('--odm_georeferencing-gcpFile', metavar='', diff --git a/opendm/context.py b/opendm/context.py index a682d1f4..8850db27 100644 --- a/opendm/context.py +++ b/opendm/context.py @@ -23,6 +23,9 @@ cmvs_path = os.path.join(superbuild_path, "install/bin/cmvs") cmvs_opts_path = os.path.join(superbuild_path, "install/bin/genOption") pmvs2_path = os.path.join(superbuild_path, "install/bin/pmvs2") +# define mvstex path +mvstex_path = os.path.join(superbuild_path, "install/bin/texrecon") + # define txt2las path txt2las_path = os.path.join(superbuild_path, 'src/las-tools/bin') pdal_path = os.path.join(superbuild_path, 'build/pdal/bin') diff --git a/opendm/tasks.py b/opendm/tasks.py index aa13b664..0d413d14 100644 --- a/opendm/tasks.py +++ b/opendm/tasks.py @@ -13,7 +13,8 @@ tasks_dict = {'1': 'resize', '3': 'cmvs', '4': 'pmvs', '5': 'odm_meshing', - '6': 'odm_texturing', +# '6': 'odm_texturing', + '6': 'mvs_texturing', '7': 'odm_georeferencing', '8': 'odm_orthophoto', '9': 'zip_results'} @@ -68,7 +69,7 @@ class ODMTaskManager(object): command = None inputs = {} - elif task_name == 'odm_texturing': + elif task_name == 'mvs_texturing': # setup this task command = None inputs = {} diff --git a/opendm/types.py b/opendm/types.py index 2df3c9b2..ef6fcf27 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -346,11 +346,12 @@ class ODM_Tree(object): self.odm_mesh = io.join_paths(self.odm_meshing, 'odm_mesh.ply') self.odm_meshing_log = io.join_paths(self.odm_meshing, 'odm_meshing_log.txt') - # odm_texturing + # texturing self.odm_textured_model_obj = io.join_paths( self.odm_texturing, 'odm_textured_model.obj') self.odm_textured_model_mtl = io.join_paths( self.odm_texturing, 'odm_textured_model.mtl') +# Log is only used by old odm_texturing self.odm_texuring_log = io.join_paths( self.odm_texturing, 'odm_texturing_log.txt') diff --git a/scripts/mvstex.py b/scripts/mvstex.py new file mode 100644 index 00000000..94e81ded --- /dev/null +++ b/scripts/mvstex.py @@ -0,0 +1,111 @@ +import ecto + +from opendm import log +from opendm import io +from opendm import system +from opendm import context + +import pmvs2nvmcams + +class ODMMvsTexCell(ecto.Cell): + def declare_params(self, params): + params.declare("data_term", 'Data term: [area, gmi] default: gmi', "gmi") + params.declare("outlier_rem_type", 'Type of photometric outlier removal method: [none, gauss_damping, gauss_clamping]. default: none', "none") + params.declare("skip_vis_test", 'Skip geometric visibility test based on ray intersection.', "false") + params.declare("skip_glob_seam_leveling", 'Skip global seam leveling.', "false") + params.declare("skip_loc_seam_leveling", 'Skip local seam leveling (Poisson editing).', "false") + params.declare("skip_hole_fill", 'Skip hole filling.', "false") + params.declare("keep_unseen_faces", 'Keep unseen faces.', "false") + + def declare_io(self, params, inputs, outputs): + inputs.declare("tree", "Struct with paths", []) + inputs.declare("args", "The application arguments.", {}) + inputs.declare("reconstruction", "Clusters output. list of ODMReconstructions", []) + outputs.declare("reconstruction", "Clusters output. list of ODMReconstructions", []) + + + + def process(self, inputs, outputs): + + # Benchmarking + start_time = system.now_raw() + + log.ODM_INFO('Running MVS Texturing Cell') + + # get inputs + args = self.inputs.args + tree = self.inputs.tree + + # define paths and create working directories + system.mkdir_p(tree.odm_texturing) + + # check if we rerun cell or not + rerun_cell = (args['rerun'] is not None and + args['rerun'] == 'mvs_texturing') or \ + (args['rerun_all']) or \ + (args['rerun_from'] is not None and + 'mvs_texturing' in args['rerun_from']) + + if not io.file_exists(tree.odm_textured_model_obj) or rerun_cell: + log.ODM_DEBUG('Writing MVS Textured file in: %s' + % tree.odm_textured_model_obj) + + + # Format arguments to fit Mvs-Texturing app + skipGeometricVisibilityTest = "" + skipGlobalSeamLeveling = "" + skipLocalSeamLeveling = "" + skipHoleFilling = "" + keepUnseenFaces = "" + + if (self.params.skip_vis_test.lower() == "true"): + skipGeometricVisibilityTest = "--skip_geometric_visibility_test" + if (self.params.skip_glob_seam_leveling.lower() == "true"): + skipGlobalSeamLeveling = "--skip_global_seam_leveling" + if (self.params.skip_loc_seam_leveling.lower() == "true"): + skipLocalSeamLeveling = "--skip_local_seam_leveling" + if (self.params.skip_hole_fill.lower() == "true"): + skipHoleFilling = "--skip_hole_filling" + if (self.params.keep_unseen_faces.lower() == "true"): + keepUnseenFaces = "--keep_unseen_faces" + + # mvstex definitions + kwargs = { + 'bin': context.mvstex_path, + 'out_dir': io.join_paths(tree.odm_texturing, "odm_textured_model"), + 'pmvs_folder': tree.pmvs_rec_path, + 'nvm_file': io.join_paths(tree.pmvs_rec_path, "nvmCams.nvm"), + 'model': tree.odm_mesh, + 'dataTerm': self.params.data_term, + 'outlierRemovalType': self.params.outlier_rem_type, + 'skipGeometricVisibilityTest': skipGeometricVisibilityTest, + 'skipGlobalSeamLeveling': skipGlobalSeamLeveling, + 'skipLocalSeamLeveling': skipLocalSeamLeveling, + 'skipHoleFilling': skipHoleFilling, + 'keepUnseenFaces': keepUnseenFaces + } + + log.ODM_DEBUG('Generating .nvm file from pmvs output: %s' + % '{nvm_file}'.format(**kwargs)) + + # Create .nvm camera file. + pmvs2nvmcams.run('{pmvs_folder}'.format(**kwargs), + '{nvm_file}'.format(**kwargs)) + + # run texturing binary + system.run('{bin} {nvm_file} {model} {out_dir} ' + '-d {dataTerm} -o {outlierRemovalType} ' + '{skipGeometricVisibilityTest} ' + '{skipGlobalSeamLeveling} ' + '{skipLocalSeamLeveling} ' + '{skipHoleFilling} ' + '{keepUnseenFaces}'.format(**kwargs)) + else: + log.ODM_WARNING('Found a valid ODM Texture file in: %s' + % tree.odm_textured_model_obj) + + if args['time']: + system.benchmark(start_time, tree.benchmarking, 'Texturing') + + log.ODM_INFO('Running OMD Texturing Cell - Finished') + return ecto.OK if args['end_with'] != 'odm_texturing' else ecto.QUIT diff --git a/scripts/odm_app.py b/scripts/odm_app.py index 8cfd172e..48636d54 100644 --- a/scripts/odm_app.py +++ b/scripts/odm_app.py @@ -13,7 +13,8 @@ from opensfm import ODMOpenSfMCell from pmvs import ODMPmvsCell from cmvs import ODMCmvsCell from odm_meshing import ODMeshingCell -from odm_texturing import ODMTexturingCell +#from odm_texturing import ODMTexturingCell +from mvstex import ODMMvsTexCell from odm_georeferencing import ODMGeoreferencingCell from odm_orthophoto import ODMOrthoPhotoCell @@ -57,9 +58,17 @@ class ODMApp(ecto.BlackBox): oct_tree=p.args['odm_meshing_octreeDepth'], samples=p.args['odm_meshing_samplesPerNode'], solver=p.args['odm_meshing_solverDivide']), - 'texturing': ODMTexturingCell(resize=p.args['resize_to'], - resolution=p.args['odm_texturing_textureResolution'], - size=p.args['odm_texturing_textureWithSize']), + 'texturing': ODMMvsTexCell(data_term=p.args['mvs_texturing_dataTerm'], + outlier_rem_type=p.args['mvs_texturing_outlierRemovalType'], + skip_vis_test=p.args['mvs_texturing_skipGeometricVisibilityTest'], + skip_glob_seam_leveling=p.args['mvs_texturing_skipGlobalSeamLeveling'], + skip_loc_seam_leveling=p.args['mvs_texturing_skipLocalSeamLeveling'], + skip_hole_fill=p.args['mvs_texturing_skipHoleFilling'], + keep_unseen_faces=p.args['mvs_texturing_keepUnseenFaces']), +# Old odm_texturing +# 'texturing': ODMTexturingCell(resize=p.args['resize_to'], +# resolution=p.args['odm_texturing_textureResolution'], +# size=p.args['odm_texturing_textureWithSize']), 'georeferencing': ODMGeoreferencingCell(img_size=p.args['resize_to'], gcp_file=p.args['odm_georeferencing_gcpFile'], use_gcp=p.args['odm_georeferencing_useGcp']), diff --git a/scripts/pmvs2nvmcams.py b/scripts/pmvs2nvmcams.py new file mode 100644 index 00000000..f9754579 --- /dev/null +++ b/scripts/pmvs2nvmcams.py @@ -0,0 +1,144 @@ +import os +import numpy as np + +from opendm import log + +# Go from QR-factorizatoin to corresponding RQ-factorization. +def rq(A): + Q,R = np.linalg.qr(np.flipud(A).T) + R = np.flipud(R.T) + Q = Q.T + return R[:,::-1],Q[::-1,:] + +# Create a unit quaternion from rotation matrix. +def rot2quat(R): + + # Float epsilon (use square root to be well with the stable region). + eps = np.sqrt(np.finfo(float).eps) + + # If the determinant is not 1, it's not a rotation matrix + if np.abs(np.linalg.det(R) - 1.0) > eps: + log.ODM_ERROR('Matrix passed to rot2quat was not a rotation matrix, det != 1.0') + + tr = np.trace(R) + + quat = np.zeros((1,4)) + + # Is trace big enough be computationally stable? + if tr > eps: + S = 0.5 / np.sqrt(tr + 1.0) + quat[0,0] = 0.25 / S + quat[0,1] = (R[2,1] - R[1,2]) * S + quat[0,2] = (R[0,2] - R[2,0]) * S + quat[0,3] = (R[1,0] - R[0,1]) * S + else: # It's not, use the largest diagonal. + if R[0,0] > R[1,1] and R[0,0] > R[2,2]: + S = np.sqrt(1.0 + R[0,0] - R[1,1] - R[2,2]) * 2.0 + quat[0,0] = (R[2,1] - R[1,2]) / S + quat[0,1] = 0.25 * S + quat[0,2] = (R[0,1] + R[1,0]) / S + quat[0,3] = (R[0,2] + R[2,0]) / S + elif R[1,1] > R[2,2]: + S = np.sqrt(1.0 - R[0,0] + R[1,1] - R[2,2]) * 2.0 + quat[0,0] = (R[0,2] - R[2,0]) / S + quat[0,1] = (R[0,1] + R[1,0]) / S + quat[0,2] = 0.25 * S + quat[0,3] = (R[1,2] + R[2,1]) / S + else: + S = np.sqrt(1.0 - R[0,0] - R[1,1] + R[2,2]) * 2.0 + quat[0,0] = (R[1,0] - R[0,1]) / S + quat[0,1] = (R[0,2] + R[2,0]) / S + quat[0,2] = (R[1,2] + R[2,1]) / S + quat[0,3] = 0.25 * S + + return quat + +# Decompose a projection matrix into parts +# (Intrinsic projection, Rotation, Camera position) +def decomposeProjection(projectionMatrix): + + # Check input: + if projectionMatrix.shape != (3,4): + log.ODM_ERROR('Unable to decompose projection matrix, shape != (3,4)') + + RQ = rq(projectionMatrix[:,:3]) + + # Fix sign, since we know K is upper triangular and has a positive diagonal. + signMat = np.diag(np.diag(np.sign(RQ[0]))) + K = signMat*RQ[0] + R = signMat*RQ[1] + + # Calculate camera position from translation vector. + t = np.linalg.inv(-1.0*projectionMatrix[:,:3])*projectionMatrix[:,3] + + return K, R, t + +# Parses pvms contour file. +def parseContourFile(filePath): + + with open(filePath, 'r') as contourFile: + if (contourFile.readline().strip() != "CONTOUR"): + return np.array([]) + else: + pMatData = np.loadtxt(contourFile, float, '#', None, None, 0) + if pMatData.shape == (3,4): + return pMatData + return np.array([]) + + + +# Creates a .nvm camera file in the pmvs folder. +def run(pmvsFolder, outputFile): + + projectionFolder = pmvsFolder + "/txt" + imageFolder = pmvsFolder + "/visualize" + + pMatrices = [] + imageFileNames = [] + + # for all files in the visualize folder: + for imageFileName in os.listdir(imageFolder): + fileNameNoExt = os.path.splitext(imageFileName)[0] + + # look for corresponding projection matrix txt file + projectionFilePath = os.path.join(projectionFolder, fileNameNoExt) + projectionFilePath += ".txt" + if os.path.isfile(projectionFilePath): + pMatData = parseContourFile(projectionFilePath) + if pMatData.size == 0: + log.ODM_WARNING('Unable to parse contour file, skipping: %s' + % projectionFilePath) + else: + pMatrices.append(np.matrix(pMatData)) + imageFileNames.append(imageFileName) + + + # Decompose projection matrices + focals = [] + rotations = [] + translations = [] + for projection in pMatrices: + KRt = decomposeProjection(projection) + focals.append(KRt[0][0,0]) + rotations.append(rot2quat(KRt[1])) + translations.append(KRt[2]) + + # Create .nvm file + with open (outputFile, 'w') as nvmFile: + nvmFile.write("NVM_V3\n\n") + nvmFile.write('%d' % len(rotations) + "\n") + + for idx, imageFileName in enumerate(imageFileNames): + nvmFile.write(os.path.join("visualize", imageFileName)) + nvmFile.write(" " + '%f' % focals[idx]) + nvmFile.write(" " + '%f' % rotations[idx][0,0] + + " " + '%f' % rotations[idx][0,1] + + " " + '%f' % rotations[idx][0,2] + + " " + '%f' % rotations[idx][0,3]) + nvmFile.write(" " + '%f' % translations[idx][0] + + " " + '%f' % translations[idx][1] + + " " + '%f' % translations[idx][2]) + nvmFile.write(" 0 0\n") + nvmFile.write("0\n\n") + nvmFile.write("0\n\n") + nvmFile.write("0") diff --git a/upgradecmake.sh b/upgradecmake.sh new file mode 100644 index 00000000..57ab5ae9 --- /dev/null +++ b/upgradecmake.sh @@ -0,0 +1,7 @@ + +# Install cmake 3.2 since cmake > 3.1 is is required by mvs-texturing. +# Installation as proposed by: +# http://askubuntu.com/questions/610291/how-to-install-cmake-3-2-on-ubuntu-14-04 +sudo add-apt-repository ppa:george-edison55/cmake-3.x -y +sudo apt-get update -y +sudo apt-get install cmake -y