diff --git a/.dockerignore b/.dockerignore index f8457a6a..870eb99a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,12 @@ tests/test_data SuperBuild/build SuperBuild/download SuperBuild/install -SuperBuild/src \ No newline at end of file +SuperBuild/src +build +opensfm +pmvs +odm_orthophoto +odm_texturing +odm_meshing +odm_georeferencing +images_resize diff --git a/Dockerfile b/Dockerfile index f35ac0a2..23fa6f8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,80 +1,32 @@ -FROM ubuntu:14.04 -MAINTAINER Danilo Bargen - -# Env variables -ENV DEBIAN_FRONTEND noninteractive - -# Install dependencies -RUN apt-get update \ - && sudo apt-get remove libdc1394-22-dev \ - && apt-get install -y --install-recommends \ - build-essential \ - cmake \ - git \ - python-pip \ - libgdal-dev \ - libgeotiff-dev \ - pkg-config \ - libgtk2.0-dev \ - libavcodec-dev \ - libavformat-dev \ - libswscale-dev \ - python-dev \ - python-numpy \ - libtbb2 \ - libtbb-dev \ - libjpeg-dev \ - libpng-dev \ - libtiff-dev \ - libjasper-dev \ - libflann-dev \ - libproj-dev \ - libxext-dev \ - liblapack-dev \ - libeigen3-dev \ - libvtk5-dev \ - python-networkx \ - libgoogle-glog-dev \ - libsuitesparse-dev \ - libboost-filesystem-dev \ - libboost-iostreams-dev \ - libboost-regex-dev \ - libboost-python-dev \ - libboost-date-time-dev \ - libboost-thread-dev \ - python-empy \ - python-nose \ - python-pyside \ - python-pyexiv2 \ - python-scipy \ - jhead \ - liblas-bin \ - && apt-get autoremove \ - && apt-get clean - -# Add users -RUN useradd -m -U odm +#Pull in previously built packages image with lots of libraries. +FROM packages # Prepare directories RUN mkdir /code WORKDIR /code -# Add repository files -ADD . /code/ +# Copy repository files +COPY ccd_defs_check.py /code/ccd_defs_check.py +COPY CMakeLists.txt /code/CMakeLists.txt +COPY configure.sh /code/configure.sh +COPY /.git/ /code/.git/ +COPY .gitignore /code/.gitignore +COPY .gitmodules /code/.gitmodules +COPY /modules/ /code/modules/ +COPY /opendm/ /code/opendm/ +COPY /patched_files/ /code/patched_files/ +COPY run.py /code/run.py +COPY /scripts/ /code/scripts/ +COPY /SuperBuild/cmake/ /code/SuperBuild/cmake/ +COPY /SuperBuild/CMakeLists.txt /code/SuperBuild/CMakeLists.txt +COPY /tests/ /code/tests/ # Update submodules RUN git submodule init && git submodule update -# Build OpenDroneMap -RUN bash ./configure.sh && \ - mkdir build && cd build && cmake .. && make && cd .. && \ - chown -R odm:odm /code -USER odm - -ENV PYTHONPATH=${PYTHONPATH}:/code/SuperBuild/install/lib/python2.7/dist-packages:/code/SuperBuild/src/opensfm \ - LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/code/SuperBuild/install/lib +#Compile code in SuperBuild and root directories +RUN cd SuperBuild && mkdir build && cd build && cmake .. && make -j$(nproc) \ + && cd ../.. && mkdir build && cd build && cmake .. && make -j$(nproc) # Entry point -VOLUME ["/images"] -# WORKDIR /images -ENTRYPOINT ["python", "/code/run.py", "--project-path", "/images"] +ENTRYPOINT ["python", "/code/run.py", "--project-path", "/code/"] diff --git a/README.md b/README.md index 678d1242..0f28f986 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ ![](https://raw.githubusercontent.com/OpenDroneMap/OpenDroneMap/master/img/odm_image.png) -What is it? -=========== +## What is it? OpenDroneMap is an open source toolkit for processing aerial drone imagery. Typical drones use simple point-and-shoot cameras, so the images from drones, while from a different perspective, are similar to any pictures taken from point-and-shoot cameras, i.e. non-metric imagery. OpenDroneMap turns those simple images into three dimensional geographic data that can be used in combination with other geographic datasets. @@ -19,16 +18,15 @@ In a word, OpenDroneMap is a toolchain for processing raw civilian UAS imagery t 6. Digital Elevation Models 7. etc. -So far, it does Point Clouds, Digital Surface Models, Textured Digital Surface Models, and Orthorectified Imagery. +So far, it does Point Clouds, Digital Surface Models, Textured Digital Surface Models, and Orthorectified Imagery. Open Drone Map now includes state-of-the-art 3D reconstruction work by Michael Waechter, Nils Moehrle, and Michael Goesele. See their publication at http://www.gcc.tu-darmstadt.de/media/gcc/papers/Waechter-2014-LTB.pdf. Users' mailing list: http://lists.osgeo.org/cgi-bin/mailman/listinfo/opendronemap-users -Developer's mailing list: http://lists.osgeo.org/cgi-bin/mailman/listinfo/opendronemap-dev +Developers' mailing list: http://lists.osgeo.org/cgi-bin/mailman/listinfo/opendronemap-dev Overview video: https://www.youtube.com/watch?v=0UctfoeNB_Y -Developers -================= +## Developers Help improve our software! @@ -37,40 +35,44 @@ Help improve our software! 1. Try to keep commits clean and simple 2. Submit a pull request with detailed changes and test results -Steps to get OpenDroneMap running: -================================== +## Build and Run OpenDroneMap in Ubuntu: (Requires Ubuntu 14.04 or later, see https://github.com/OpenDroneMap/odm_vagrant for running on Windows in a VM) -Support for Ubuntu 12.04 is currently BROKEN with the addition of OpenSfM and Ceres-Solver. We are working hard to get it working again in the future. +Support for Ubuntu 12.04 is currently BROKEN with the addition of OpenSfM and Ceres-Solver. We are working hard to get it working again in the future. -#### Building OpenDroneMap using git +### Build OpenDroneMap +Start with the following: - cd path/to/odm/dir - git clone https://github.com/OpenDroneMap/OpenDroneMap.git . - export PYTHONPATH=$PYTHONPATH:`pwd`/SuperBuild/install/lib/python2.7/dist-packages:`pwd`/SuperBuild/src/opensfm - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:`pwd`/SuperBuild/install/lib + git clone https://github.com/OpenDroneMap/OpenDroneMap.git + +Next, open the ~/.bashrc file on your machine and add the following 3 lines at the end. The file can be opened with ```gedit ~/.bashrc```. Be sure to replace the "/your/path/" with the correct path to the location where you cloned OpenDroneMap: + + export PYTHONPATH=$PYTHONPATH:/your/path/OpenDroneMap/SuperBuild/install/lib/python2.7/dist-packages + export PYTHONPATH=$PYTHONPATH:/your/path/OpenDroneMap/SuperBuild/src/opensfm + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/path/OpenDroneMap/SuperBuild/install/lib + +Now, enter the OpenDroneMap directory and compile all of the code by executing a single configuration script: + + cd OpenDroneMap bash configure.sh - mkdir build && cd build && cmake .. && make && cd .. - For Ubuntu 15.10 users, this will help you get running: +For Ubuntu 15.10 users, this will help you get running: sudo apt-get install python-xmltodict sudo ln -s /usr/lib/x86_64-linux-gnu/libproj.so.9 /usr/lib/libproj.so -#### Running OpenDroneMap +### Run OpenDroneMap First you need a set of images, which may or may not be georeferenced. There are two ways OpenDroneMap can understand geographic coordinates. First, the images can be geotagged in their EXIF data. This is the default. Alternatively, you can create a GCP file, [a process detailed here](https://github.com/OpenDroneMap/OpenDroneMap/wiki/2.-Running-OpenDroneMap#running-odm-with-ground-control) Create a project folder and places your images in an "images" directory: - |-- /path/to/project/ |-- images/ |-- img-1234.jpg |-- ... - Example data can be cloned from https://github.com/OpenDroneMap/odm_data Then run: @@ -79,7 +81,9 @@ Then run: There are many options for tuning your project. See the [wiki](https://github.com/OpenDroneMap/OpenDroneMap/wiki/3.-Run-Time-Parameters) or run `python run.py -h` -When the process finishes, the results will be organized as follows +### View Results + +When the process finishes, the results will be organized as follows: |-- images/ |-- img-1234.jpg @@ -112,44 +116,65 @@ When the process finishes, the results will be organized as follows |-- odm_orthophoto_log.txt # Log file |-- gdal_translate_log.txt # Log for georeferencing the png file -##### Viewing your results - Any file ending in .obj or .ply can be opened and viewed in [MeshLab](http://meshlab.sourceforge.net/) or similar software. That includes `pmvs/recon0/models/option-000.ply`, `odm_meshing/odm_mesh.ply`, `odm_texturing/odm_textured_model[_geo].obj`, or `odm_georeferencing/odm_georeferenced_model.ply`. Below is an example textured mesh: -![](https://raw.githubusercontent.com/OpenDroneMap/OpenDroneMap/master/img/tol_text.png) +![](https://raw.githubusercontent.com/alexhagiopol/OpenDroneMap/feature-better-docker/toledo_dataset_example_mesh.jpg) You can also view the orthophoto GeoTIFF in QGIS or other mapping software: ![](https://raw.githubusercontent.com/OpenDroneMap/OpenDroneMap/master/img/bellus_map.png) -#### Using Docker +## Build and Run Using Docker -You can build and run OpenDroneMap in a Docker container: +(Instructions below apply to Ubuntu 14.04, but the Docker image workflow +has equivalent procedures for Mac OS X and Windows. See [docs.docker.com](docs.docker.com)) - export IMAGES=/absolute/path/to/your/project - docker build -t opendronemap:latest . - docker run -v $IMAGES:/images opendronemap:latest +OpenDroneMap is Dockerized, meaning you can use containerization to build and run it without tampering with the configuration of libraries and packages already +installed on your machine. Docker software is free to install and use in this context. If you don't have it installed, +see the [Docker Ubuntu installation tutorial] (https://docs.docker.com/engine/installation/linux/ubuntulinux/) and follow the +instructions up until "Create a Docker group" inclusive. Once Docker is installed, an OpenDroneMap Docker image can be created +like so: -Replace /absolute/path/to/your/images with an absolute path to the directory containing your project (where the images are) -To pass in custom parameters to the `run.py` script, simply pass it as arguments to the `docker run` command. + git clone https://github.com/OpenDroneMap/OpenDroneMap.git + cd OpenDroneMap + docker build -t packages -f packages.Dockerfile . + docker build -t odm_image . + docker run -it --user root\ + -v $(pwd)/images:/code/images\ + -v $(pwd)/odm_orthophoto:/code/odm_orthophoto\ + -v $(pwd)/odm_texturing:/code/odm_texturing\ + --rm odm_image ---- +Using this method, the containerized ODM will process the images in the OpenDroneMap/images directory and output results +to the OpenDroneMao/odm_orthophoto and OpenDroneMap/odm_texturing directories as described in the **Viewing Results** section. +If you want to view other results outside the Docker image simply add which directories you're interested in to the run command in the same pattern +established above. For example, if you're interested in the dense cloud results generated by PMVS and in the orthophoto, +simply use the following `docker run` command after building the image: + + docker run -it --user root\ + -v $(pwd)/images:/code/images\ + -v $(pwd)/pmvs:/code/pmvs\ + -v $(pwd)/odm_orthophoto:/code/odm_orthophoto\ + --rm odm_image + +To pass in custom parameters to the run.py script, simply pass it as arguments to the `docker run` command. + +## Examples Here are some other videos, which may be outdated: - https://www.youtube.com/watch?v=7ZTufQkODLs (2015-01-30) - https://www.youtube.com/watch?v=m0i4GQdfl8A (2015-03-15) -Now that texturing is in the code base, you can access the full textured meshes using MeshLab. Open MeshLab, choose `File:Import Mesh` and choose your textured mesh from a location similar to the following: `reconstruction-with-image-size-1200-results\odm_texturing\odm_textured_model.obj` +Now that texturing is in the code base, you can access the full textured meshes using MeshLab. +Open MeshLab, choose `File:Import Mesh` and choose your textured mesh from a location similar to the following: +`reconstruction-with-image-size-1200-results\odm_texturing\odm_textured_model.obj`. Long term, the aim is for +the toolchain to also be able to optionally push to a variety of online data repositories, pushing hi-resolution +aerials to [OpenAerialMap](https://openaerialmap.org/), point clouds to [OpenTopography](http://opentopography.org/), +and pushing digital elevation models to an emerging global repository (yet to be named...). That leaves only +digital surface model meshes and UV textured meshes with no global repository home. ---- - -Long term, the aim is for the toolchain to also be able to optionally push to a variety of online data repositories, pushing hi-resolution aerials to [OpenAerialMap](http://opentopography.org/), point clouds to [OpenTopography](http://opentopography.org/), and pushing digital elevation models to an emerging global repository (yet to be named...). That leaves only digital surface model meshes and UV textured meshes with no global repository home. ---- - - -Documentation: -============== +## Documentation: For documentation, please take a look at our [wiki](https://github.com/OpenDroneMap/OpenDroneMap/wiki). diff --git a/SuperBuild/CMakeLists.txt b/SuperBuild/CMakeLists.txt index b704578c..8522922d 100644 --- a/SuperBuild/CMakeLists.txt +++ b/SuperBuild/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8) +cmake_minimum_required(VERSION 3.1) project(ODM-SuperBuild) @@ -101,7 +101,9 @@ set(custom_libs OpenGV CMVS Catkin Ecto - 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 old mode 100644 new mode 100755 index d4a524ea..dbebd753 --- a/configure.sh +++ b/configure.sh @@ -1,43 +1,27 @@ #!/bin/bash -# Check OS -if [ ! $(command -v apt-get) ]; then - echo -e "\e[1;31mERROR: Not a Debian-based linux system. - Impossible to install OpenCV with this script\e[0;39m" - exit 1 -fi - ## Before installing -echo -e "\e[1;34mUpdating the system\e[0;39m" +echo "Updating the system" sudo apt-get update -END_CMD1=$? -# sudo apt-get upgrade -y -# END_CMD2=$? -if [ $END_CMD1 -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mWhen Updating the system\e[0m" - exit 1 -fi -## Install Required Requisites -echo -e "\e[1;34mInstalling Required Requisites\e[0;39m" -sudo apt-get install build-essential \ - cmake \ +echo "Installing Required Requisites" +sudo apt-get install -y -qq build-essential \ git \ + cmake \ python-pip \ libgdal-dev \ gdal-bin \ libgeotiff-dev \ - pkg-config -y -qq -if [ $? -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mWhen Installing Required Requisites\e[0m" - exit 1 -fi + pkg-config -## Installing Optional Requisites -echo -e "\e[1;34mInstalling OpenCV Dependencies\e[0;39m" -sudo apt-get install libgtk2.0-dev \ +echo "Getting CMake 3.1 for MVS-Texturing" +sudo apt-get install -y software-properties-common python-software-properties +sudo add-apt-repository -y ppa:george-edison55/cmake-3.x +sudo apt-get update -y +sudo apt-get install -y --only-upgrade cmake + +echo "Installing OpenCV Dependencies" +sudo apt-get install -y -qq libgtk2.0-dev \ libavcodec-dev \ libavformat-dev \ libswscale-dev \ @@ -54,20 +38,14 @@ sudo apt-get install libgtk2.0-dev \ libxext-dev \ liblapack-dev \ libeigen3-dev \ - libvtk5-dev -y -qq -if [ $? -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mError when Installing Dependencies Requisites\e[0m" - exit 1 -fi + libvtk5-dev -## Remove libdc1394-22-dev due to python opencv issue -echo -e "\e[1;34mRemoving libdc1394-22-dev\e[0;39m" +echo "Removing libdc1394-22-dev due to python opencv issue" sudo apt-get remove libdc1394-22-dev ## Installing OpenSfM Requisites -echo -e "\e[1;34mInstalling OpenSfM Dependencies\e[0;39m" -sudo apt-get install python-networkx \ +echo "Installing OpenSfM Dependencies" +sudo apt-get install -y -qq python-networkx \ libgoogle-glog-dev \ libsuitesparse-dev \ libboost-filesystem-dev \ @@ -75,51 +53,33 @@ sudo apt-get install python-networkx \ libboost-regex-dev \ libboost-python-dev \ libboost-date-time-dev \ - libboost-thread-dev -y -qq + libboost-thread-dev sudo pip install -U PyYAML \ exifread \ gpxpy \ xmltodict -if [ $? -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mError when Installing OpenSfM Dependencies\e[0m" - exit 1 -fi -## Installing Ecto Requisites -echo -e "\e[1;34mInstalling Ecto Dependencies\e[0;39m" +echo "Installing Ecto Dependencies" sudo pip install -U catkin-pkg -sudo apt-get install python-empy \ +sudo apt-get install -y -qq python-empy \ python-nose \ - python-pyside -y -qq -if [ $? -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mError when Installing Ecto Dependencies\e[0m" - exit 1 -fi + python-pyside -## Installing OpenDroneMap Requisites -echo -e "\e[1;34mInstalling OpenDroneMap Dependencies\e[0;39m" -sudo apt-get install python-pyexiv2 \ +echo "Installing OpenDroneMap Dependencies" +sudo apt-get install -y -qq python-pyexiv2 \ python-scipy \ jhead \ - liblas-bin -y -qq -if [ $? -ne 0 ] -then - echo -e "\e[1;31mERROR: \e[39mError when Installing OpenDroneMap Dependencies\e[0m" - exit 1 -fi + liblas-bin -## Get sys vars -NUM_CORES=`grep -c processor /proc/cpuinfo` - -## Add SuperBuild path to the python path -export PYTHONPATH=$PYTHONPATH:`pwd`/SuperBuild/install/lib/python2.7/dist-packages:`pwd`/SuperBuild/src/opensfm - -## Compile SuperBuild +echo "Compiling SuperBuild" cd SuperBuild mkdir -p build && cd build -cmake .. && make -j${NUM_CORES} +cmake .. && make -j$(nproc) -echo -e "\e[1;34mScript finished\e[0;39m" +echo "Compiling build" +cd ../.. +mkdir -p build && cd build +cmake .. && make -j$(nproc) + +echo "Configuration Finished" diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100755 new mode 100644 diff --git a/licenses/license.md b/licenses/license.md index 18d04305..7cd375fd 100644 --- a/licenses/license.md +++ b/licenses/license.md @@ -24,3 +24,4 @@ Licensing for portions of OpenDroneMap are as follows: * vtk5 - BSD - http://www.vtk.org/VTK/project/license.html * libext - https://github.com/OpenDroneMap/OpenDroneMap/blob/gh-pages/licenses/libext_copyright.txt * libx11 - https://github.com/OpenDroneMap/OpenDroneMap/blob/gh-pages/licenses/libx11_copyright.txt +* MVS Texturing - BSD - https://github.com/nmoehrle/mvs-texturing/blob/master/LICENSE.txt 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 78daf9fd..14273665 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 72bc9fbb..e0394113 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'] @@ -199,6 +199,46 @@ def config(): '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, @@ -213,6 +253,8 @@ def config(): 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='', default='gcp_list.txt', 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 b560e9d3..a58a5272 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -369,11 +369,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/packages.Dockerfile b/packages.Dockerfile new file mode 100644 index 00000000..ed5eba36 --- /dev/null +++ b/packages.Dockerfile @@ -0,0 +1,90 @@ +FROM ubuntu:14.04 +MAINTAINER Alex Hagiopol + +# Env variables +ENV DEBIAN_FRONTEND noninteractive + +#Install dependencies +#Required Requisites +RUN apt-get update \ + && apt-get install -y -qq \ + build-essential \ + cmake \ + git \ + python-pip \ + libgdal-dev \ + gdal-bin \ + libgeotiff-dev \ + pkg-config + +#CMake 3.1 for MVS-Texturing +RUN sudo apt-get install -y software-properties-common python-software-properties +RUN sudo add-apt-repository -y ppa:george-edison55/cmake-3.x +RUN sudo apt-get update -y +RUN sudo apt-get install -y --only-upgrade cmake + +#Installing OpenCV Dependencies +RUN sudo apt-get install -y -qq libgtk2.0-dev \ + libavcodec-dev \ + libavformat-dev \ + libswscale-dev \ + python-dev \ + python-numpy \ + libtbb2 \ + libtbb-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libjasper-dev \ + libflann-dev \ + libproj-dev \ + libxext-dev \ + liblapack-dev \ + libeigen3-dev \ + libvtk5-dev + +#Removing libdc1394-22-dev due to python opencv issue +RUN sudo apt-get remove libdc1394-22-dev + +#Installing OpenSfM Dependencies +RUN sudo apt-get install -y -qq python-networkx \ + libgoogle-glog-dev \ + libsuitesparse-dev \ + libboost-filesystem-dev \ + libboost-iostreams-dev \ + libboost-regex-dev \ + libboost-python-dev \ + libboost-date-time-dev \ + libboost-thread-dev +RUN sudo pip install -U PyYAML \ + exifread \ + gpxpy \ + xmltodict \ + catkin-pkg + +#Installing Ecto Dependencies +RUN sudo apt-get install -y -qq python-empy \ + python-nose \ + python-pyside + +#"Installing OpenDroneMap Dependencies" +RUN sudo apt-get install -y python-pyexiv2 \ + python-scipy \ + jhead \ + liblas-bin -y -qq + +RUN sudo apt-get install -y python-empy \ + python-nose \ + python-pyside \ + python-pyexiv2 \ + python-scipy \ + jhead \ + liblas-bin \ + python-matplotlib \ + libatlas-base-dev \ + libatlas3gf-base + +ENV PYTHONPATH="$PYTHONPATH:/code/SuperBuild/install/lib/python2.7/dist-packages" +ENV PYTHONPATH="$PYTHONPATH:/code/SuperBuild/src/opensfm" +ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" + diff --git a/run.py b/run.py index 44ba6cc7..4888eeac 100644 --- a/run.py +++ b/run.py @@ -6,6 +6,7 @@ from opendm import system import sys import ecto +import os from scripts.odm_app import ODMApp @@ -26,6 +27,17 @@ if __name__ == '__main__': if args.project_path is None: usage() + #If user asks to rerun everything, delete all of the existing progress directories. + if args.rerun_all: + os.system("rm -rf " + + args.project_path + "images_resize/ " + + args.project_path + "odm_georeferencing/ " + + args.project_path + "odm_meshing/ " + + args.project_path + "odm_orthophoto/ " + + args.project_path + "odm_texturing/ " + + args.project_path + "opensfm/ " + + args.project_path + "pmvs/") + # create an instance of my App BlackBox # internally configure all tasks app = ODMApp(args=args) diff --git a/scripts/mvstex.py b/scripts/mvstex.py new file mode 100644 index 00000000..fa860995 --- /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 ODM 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 4622769a..008049a9 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,16 @@ 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'], '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/toledo_dataset_example_mesh.jpg b/toledo_dataset_example_mesh.jpg new file mode 100644 index 00000000..90129cab Binary files /dev/null and b/toledo_dataset_example_mesh.jpg differ