From 0d9460537d53a7fc1c6e3c24da771963985c26fb Mon Sep 17 00:00:00 2001 From: cDc Date: Wed, 5 Jul 2023 00:06:39 +0300 Subject: [PATCH 1/2] v2.2.0 (#1021) - add python API - COLMAP support in MvgMvsPipeline.py - interface for binary COLMAP - interface for Polycam scenes - tower mode #1017 - estimate 3D points scale - transform scene by a given transform matrix - unify Docker scripts and add support for GUI - fix incorrect comparison in BufferedOutputStream #1010 - add lines structure - compute the focus of attention of a set of cameras - add image mask support in mesh texturing --- .devcontainer/Dockerfile | 39 ++ .devcontainer/devcontainer.json | 42 ++ .../library-scripts/common-debian.sh | 454 ++++++++++++ .devcontainer/postCreateCommand.sh | 12 + .gitattributes | 5 + .github/workflows/continuous_integration.yml | 6 +- BUILD.md | 42 ++ CMakeLists.txt | 74 +- MvgMvsPipeline.py | 102 ++- apps/CMakeLists.txt | 1 + apps/DensifyPointCloud/DensifyPointCloud.cpp | 38 +- apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp | 242 +++++-- apps/InterfaceMVSNet/InterfaceMVSNet.cpp | 24 +- .../InterfaceMetashape/InterfaceMetashape.cpp | 106 ++- apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp | 8 +- apps/InterfacePolycam/CMakeLists.txt | 13 + apps/InterfacePolycam/InterfacePolycam.cpp | 363 ++++++++++ .../InterfaceVisualSFM/InterfaceVisualSFM.cpp | 4 +- apps/ReconstructMesh/ReconstructMesh.cpp | 27 +- apps/RefineMesh/RefineMesh.cpp | 11 +- apps/TextureMesh/TextureMesh.cpp | 8 +- apps/TransformScene/TransformScene.cpp | 76 +- apps/Viewer/Scene.cpp | 5 +- build/Templates/ConfigLocal.h.in | 6 + build/Utils.cmake | 11 +- docker/Dockerfile | 47 +- docker/Dockerfile_CUDA | 51 -- docker/README.md | 14 +- docker/buildFromScratch.cmd | 49 ++ docker/buildFromScratch.sh | 49 +- docker/buildFromScratch_CUDA.sh | 2 - docker/buildInDocker.sh | 95 +++ libs/Common/Common.h | 12 +- libs/Common/Config.h | 2 + libs/Common/Filters.h | 2 +- libs/Common/Line.h | 95 +++ libs/Common/Line.inl | 215 ++++++ libs/Common/List.h | 76 +- libs/Common/OBB.inl | 50 +- libs/Common/Plane.h | 8 +- libs/Common/Plane.inl | 89 ++- libs/Common/Sampler.inl | 66 +- libs/Common/Strings.h | 36 + libs/Common/Types.h | 5 + libs/Common/Types.inl | 2 + libs/Common/Util.cpp | 7 + libs/Common/Util.inl | 94 ++- libs/IO/CMakeLists.txt | 6 +- libs/MVS/CMakeLists.txt | 21 +- libs/MVS/Camera.cpp | 49 +- libs/MVS/Camera.h | 38 + libs/MVS/DepthMap.cpp | 99 +-- libs/MVS/DepthMap.h | 4 +- libs/MVS/Interface.h | 17 +- libs/MVS/Mesh.cpp | 315 +++++---- libs/MVS/Mesh.h | 4 +- libs/MVS/PatchMatchCUDA.cpp | 2 +- libs/MVS/PointCloud.cpp | 112 ++- libs/MVS/PointCloud.h | 17 +- libs/MVS/PythonWrapper.cpp | 137 ++++ libs/MVS/Scene.cpp | 654 +++++++++++++++--- libs/MVS/Scene.h | 20 +- libs/MVS/SceneDensify.cpp | 12 +- libs/MVS/SceneRefine.cpp | 2 +- libs/MVS/SceneTexture.cpp | 71 +- libs/MVS/SemiGlobalMatcher.cpp | 4 +- libs/MVS/SemiGlobalMatcher.h | 6 +- libs/Math/RobustNorms.h | 7 + vcpkg.json | 6 + 69 files changed, 3610 insertions(+), 778 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/library-scripts/common-debian.sh create mode 100755 .devcontainer/postCreateCommand.sh create mode 100644 .gitattributes create mode 100644 apps/InterfacePolycam/CMakeLists.txt create mode 100644 apps/InterfacePolycam/InterfacePolycam.cpp delete mode 100644 docker/Dockerfile_CUDA create mode 100644 docker/buildFromScratch.cmd delete mode 100644 docker/buildFromScratch_CUDA.sh create mode 100755 docker/buildInDocker.sh create mode 100644 libs/Common/Line.h create mode 100644 libs/Common/Line.inl create mode 100644 libs/MVS/PythonWrapper.cpp diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..11a6d0e10 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu:22.04 + +ARG USERNAME=openmvs +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +ARG INSTALL_ZSH="true" + +# Prepare and empty machine for building: +RUN apt-get update -yq + +COPY .devcontainer/library-scripts/*.sh /tmp/library-scripts/ +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true"\ + # + # **************************************************************************** + # * TODO: Add any additional OS packages you want included in the definition * + # * here. We want to do this before cleanup to keep the "layer" small. * + # **************************************************************************** + # && apt-get -y install --no-install-recommends \ + # + && apt-get -y install --no-install-recommends build-essential git cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev libglew-dev libglfw3-dev \ + # Boost + libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev \ + # OpenCV + libopencv-dev \ + # CGAL + libcgal-dev libcgal-qt5-dev \ + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts + +# Eigen +RUN git clone https://gitlab.com/libeigen/eigen --branch 3.4 +RUN mkdir eigen_build +RUN cd eigen_build &&\ + cmake . ../eigen &&\ + make && make install &&\ + cd .. + +# VCGLib +RUN git clone https://github.com/cdcseacave/VCG.git vcglib diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f9b245244 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "OpenMVS", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "./.devcontainer/postCreateCommand.sh", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "twxs.cmake", + "josetr.cmake-language-support-vscode" + ] + } + }, + "containerEnv": { + "DISPLAY": "unix:0" + }, + "remoteEnv": { + "PATH": "/usr/local/bin/OpenMVS:${containerEnv:PATH}" + }, + "mounts": [ + "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached" + ], + "features": { + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" + } + }, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "openmvs" +} \ No newline at end of file diff --git a/.devcontainer/library-scripts/common-debian.sh b/.devcontainer/library-scripts/common-debian.sh new file mode 100644 index 000000000..bf1f9e2ed --- /dev/null +++ b/.devcontainer/library-scripts/common-debian.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust[0-9] \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]âžœ" || echo -n "\[\033[0m\]âžœ"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}âžœ :%{$fg_bold[red]%}âžœ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" \ No newline at end of file diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100755 index 000000000..3185a858b --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +rm -rf openMVS_build && mkdir openMVS_build + +cd openMVS_build &&\ + cmake .. -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT=/vcglib + +# add below args for CUDA, refer docker/buildInDocker.sh for base container and additional stuff required +# -DOpenMVS_USE_CUDA=ON -DCMAKE_LIBRARY_PATH=/usr/local/cuda/lib64/stubs/ -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda/ -DCUDA_INCLUDE_DIRS=/usr/local/cuda/include/ -DCUDA_CUDART_LIBRARY=/usr/local/cuda/lib64 -DCUDA_NVCC_EXECUTABLE=/usr/local/cuda/bin/ + +# Install OpenMVS library +make -j && sudo make install \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c0a459940 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Ensure shell scripts uses the correct line ending. +Dockerfile eol=lf +*.sh eol=lf +*.bat eol=crlf +*.cmd eol=crlf diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 7c327e6e9..94e1b985d 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -39,10 +39,10 @@ jobs: uses: actions/checkout@v3 - name: Restore artifacts, or setup vcpkg for building artifacts - uses: lukka/run-vcpkg@v10.6 + uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: '${{ github.workspace }}/vcpkg' - vcpkgGitCommitId: '4cb4a5c5ddcb9de0c83c85837ee6974c8333f032' + vcpkgGitCommitId: '4a3c366f2d0d0eaf034bfa649124768df7cfe813' - name: Install Ubuntu dependencies if: matrix.os == 'ubuntu-latest' @@ -57,7 +57,7 @@ jobs: - name: Configure CMake run: | - cmake -S . -B make -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} + cmake -S . -B make -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} -DOpenMVS_USE_CUDA=OFF - name: Build working-directory: ./make diff --git a/BUILD.md b/BUILD.md index 28fda265c..ffc29becf 100644 --- a/BUILD.md +++ b/BUILD.md @@ -64,3 +64,45 @@ endif() add_executable(your_project source_code.cpp) target_link_libraries(your_project PRIVATE OpenMVS::MVS) ``` + +------------------- +Python API +------------------- + +The Python API can be enable by setting the `OpenMVS_USE_PYTHON` option to `ON` when running `cmake`. The Python API is built as a shared library and can be used in any Python project. Example: +``` +import pyOpenMVS + +def run_mvs(): + # set the working folder; all files used next are relative to this folder (optional) + pyOpenMVS.set_working_folder("folder/containing/the/scene") + # create an empty scene + scene = pyOpenMVS.Scene() + # load a MVS scene from a file + if not scene.load("scene.mvs"): + print("ERROR: scene could not be loaded") + return + # estimate depth-maps and fuse them into a point-cloud + if not scene.dense_reconstruction(): + print("ERROR: could not dense reconstruct the scene") + return + scene.save_pointcloud("pointcloud.ply") + # reconstruct a mesh from the point-cloud + if not scene.reconstruct_mesh(): + print("ERROR: could not reconstruct the mesh for this scene") + return + scene.save_mesh("mesh.ply") + # refine the mesh using gradient descent optimization (optional) + if not scene.refine_mesh(): + print("ERROR: could not refine the mesh for this scene") + return + scene.save_mesh("refined_mesh.ply") + # texture the mesh using the input images + if not scene.texture_mesh(): + print("ERROR: could not texture the mesh for this scene") + return + scene.save_mesh("textured_mesh.ply") + +if __name__ == "__main__": + run_mvs() +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 39ec359ac..5a1345915 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,21 @@ IF(POLICY CMP0104) CMAKE_POLICY(SET CMP0104 NEW) ENDIF() +# List configuration options +SET(OpenMVS_BUILD_TOOLS ON CACHE BOOL "Build example applications") +SET(OpenMVS_USE_OPENMP ON CACHE BOOL "Enable OpenMP library") +SET(OpenMVS_USE_OPENGL ON CACHE BOOL "Enable OpenGL library") +SET(OpenMVS_USE_BREAKPAD ON CACHE BOOL "Enable BreakPad library") +SET(OpenMVS_USE_PYTHON OFF CACHE BOOL "Enable Python library bindings") +SET(OpenMVS_USE_CERES OFF CACHE BOOL "Enable CERES optimization library") +SET(OpenMVS_USE_CUDA ON CACHE BOOL "Enable CUDA library") +SET(OpenMVS_USE_FAST_FLOAT2INT ON CACHE BOOL "Use an optimized code to convert real numbers to int") +SET(OpenMVS_USE_FAST_INVSQRT OFF CACHE BOOL "Use an optimized code to compute the inverse square root (slower in fact on modern compilers)") +SET(OpenMVS_USE_FAST_CBRT ON CACHE BOOL "Use an optimized code to compute the cubic root") +SET(OpenMVS_USE_SSE ON CACHE BOOL "Enable SSE optimizations") +SET(OpenMVS_MAX_CUDA_COMPATIBILITY OFF CACHE BOOL "Build for maximum CUDA device compatibility") +SET(OpenMVS_ENABLE_TESTS ON CACHE BOOL "Enable test code") + # Load automatically VCPKG toolchain if available IF(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT}) SET(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") @@ -21,6 +36,13 @@ IF(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT}) SET(VCPKG_TARGET_TRIPLET "$ENV{VCPKG_DEFAULT_TRIPLET}" CACHE STRING "") ENDIF() ENDIF() +IF(OpenMVS_USE_PYTHON) + LIST(APPEND VCPKG_MANIFEST_FEATURES "python") + SET(PARTIAL_BUILD_SHARED_LIBS ON) +ENDIF() +IF(OpenMVS_USE_CUDA) + LIST(APPEND VCPKG_MANIFEST_FEATURES "cuda") +ENDIF() # Name of the project. # @@ -30,25 +52,10 @@ ENDIF() PROJECT(OpenMVS) SET(OpenMVS_MAJOR_VERSION 2) -SET(OpenMVS_MINOR_VERSION 1) +SET(OpenMVS_MINOR_VERSION 2) SET(OpenMVS_PATCH_VERSION 0) SET(OpenMVS_VERSION ${OpenMVS_MAJOR_VERSION}.${OpenMVS_MINOR_VERSION}.${OpenMVS_PATCH_VERSION}) -# List configuration options -SET(OpenMVS_BUILD_TOOLS ON CACHE BOOL "Build example applications") -SET(OpenMVS_USE_NONFREE ON CACHE BOOL "Build non-free (patented) functionality") -SET(OpenMVS_USE_OPENMP ON CACHE BOOL "Enable OpenMP library") -SET(OpenMVS_USE_OPENGL ON CACHE BOOL "Enable OpenGL library") -SET(OpenMVS_USE_BREAKPAD ON CACHE BOOL "Enable BreakPad library") -SET(OpenMVS_USE_CERES OFF CACHE BOOL "Enable CERES optimization library") -SET(OpenMVS_USE_CUDA ON CACHE BOOL "Enable CUDA library") -SET(OpenMVS_USE_FAST_FLOAT2INT ON CACHE BOOL "Use an optimized code to convert real numbers to int") -SET(OpenMVS_USE_FAST_INVSQRT OFF CACHE BOOL "Use an optimized code to compute the inverse square root (slower in fact on modern compilers)") -SET(OpenMVS_USE_FAST_CBRT ON CACHE BOOL "Use an optimized code to compute the cubic root") -SET(OpenMVS_USE_SSE ON CACHE BOOL "Enable SSE optimizations") -SET(OpenMVS_MAX_CUDA_COMPATIBILITY OFF CACHE BOOL "Build for maximum CUDA device compatibility") -SET(OpenMVS_ENABLE_TESTS ON CACHE BOOL "Enable test code") - # Define helper functions and macros. INCLUDE(build/Utils.cmake) @@ -63,7 +70,6 @@ SET(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/build/Modules) # Find required packages SET(OpenMVS_EXTRA_INCLUDES "") -SET(OpenMVS_DEFINITIONS "") SET(OpenMVS_EXTRA_LIBS "") if(OpenMVS_USE_OPENMP) @@ -71,7 +77,6 @@ if(OpenMVS_USE_OPENMP) FIND_PACKAGE(OpenMP) if(OPENMP_FOUND) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_OPENMP) SET(_USE_OPENMP TRUE) #cmake only check for separate OpenMP library on AppleClang 7+ #https://github.com/Kitware/CMake/blob/42212f7539040139ecec092547b7d58ef12a4d72/Modules/FindOpenMP.cmake#L252 @@ -91,7 +96,6 @@ if(OpenMVS_USE_OPENGL) FIND_PACKAGE(OpenGL) if(OPENGL_FOUND) INCLUDE_DIRECTORIES(${OPENGL_INCLUDE_DIR}) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_OPENGL) ADD_DEFINITIONS(${OpenGL_DEFINITIONS}) SET(_USE_OPENGL TRUE) else() @@ -147,7 +151,6 @@ if(OpenMVS_USE_CUDA) endif() SET(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr") INCLUDE_DIRECTORIES(${CUDA_INCLUDE_DIRS}) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_CUDA) SET(_USE_CUDA TRUE) else() SET(CUDA_CUDA_LIBRARY "") @@ -161,7 +164,6 @@ if(OpenMVS_USE_BREAKPAD) FIND_PACKAGE(BREAKPAD) if(BREAKPAD_FOUND) INCLUDE_DIRECTORIES(${BREAKPAD_INCLUDE_DIRS}) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_BREAKPAD) ADD_DEFINITIONS(${BREAKPAD_DEFINITIONS}) SET(_USE_BREAKPAD TRUE) LIST(APPEND OpenMVS_EXTRA_LIBS ${BREAKPAD_LIBS}) @@ -170,11 +172,22 @@ if(OpenMVS_USE_BREAKPAD) endif() endif() -FIND_PACKAGE(Boost COMPONENTS iostreams program_options system serialization REQUIRED) +if(OpenMVS_USE_PYTHON) + FIND_PACKAGE(PythonLibs 3.0 REQUIRED) + if(PythonLibs_FOUND) + INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_DIRS}) + LIST(APPEND OpenMVS_EXTRA_INCLUDES ${PYTHON_INCLUDE_DIRS}) + LIST(APPEND OpenMVS_EXTRA_LIBS ${PYTHON_LIBRARIES}) + MESSAGE(STATUS "Python ${PYTHON_VERSION} found (include: ${PYTHON_INCLUDE_DIRS})") + else() + MESSAGE("-- Can't find Python. Continuing without it.") + endif() +endif() + +FIND_PACKAGE(Boost REQUIRED COMPONENTS iostreams program_options system serialization OPTIONAL_COMPONENTS python3) if(Boost_FOUND) LIST(APPEND OpenMVS_EXTRA_INCLUDES ${Boost_INCLUDE_DIRS}) INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS}) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_BOOST) ADD_DEFINITIONS(${Boost_DEFINITIONS}) LINK_DIRECTORIES(${Boost_LIBRARY_DIRS}) if(NOT MSVC AND DEFINED CMAKE_TOOLCHAIN_FILE) @@ -182,13 +195,15 @@ if(Boost_FOUND) LIST(APPEND Boost_LIBRARIES zstd) endif() SET(_USE_BOOST TRUE) + if(Boost_python3_FOUND) + SET(_USE_BOOST_PYTHON TRUE) + endif() endif() FIND_PACKAGE(Eigen3 REQUIRED) if(EIGEN3_FOUND) LIST(APPEND OpenMVS_EXTRA_INCLUDES ${EIGEN3_INCLUDE_DIR}) INCLUDE_DIRECTORIES(${EIGEN3_INCLUDE_DIR}) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_EIGEN) ADD_DEFINITIONS(${EIGEN3_DEFINITIONS}) SET(_USE_EIGEN TRUE) MESSAGE(STATUS "Eigen ${EIGEN3_VERSION} found (include: ${EIGEN3_INCLUDE_DIR})") @@ -209,33 +224,22 @@ LIST(REMOVE_DUPLICATES OpenMVS_EXTRA_INCLUDES) LIST(REMOVE_DUPLICATES OpenMVS_EXTRA_LIBS) # Set defines -if(OpenMVS_USE_NONFREE) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_NONFREE) - SET(_USE_NONFREE TRUE) -endif() if(OpenMVS_USE_FAST_FLOAT2INT) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_FAST_FLOAT2INT) SET(_USE_FAST_FLOAT2INT TRUE) endif() if(OpenMVS_USE_FAST_INVSQRT) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_FAST_INVSQRT) SET(_USE_FAST_INVSQRT TRUE) endif() if(OpenMVS_USE_FAST_CBRT) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_FAST_CBRT) SET(_USE_FAST_CBRT TRUE) endif() if(OpenMVS_USE_SSE) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_SSE) SET(_USE_SSE TRUE) endif() if(OpenMVS_USE_CERES) - LIST(APPEND OpenMVS_DEFINITIONS -D_USE_CERES) SET(_USE_CERES TRUE) endif() -ADD_DEFINITIONS(${OpenMVS_DEFINITIONS}) - INCLUDE_DIRECTORIES("${OpenMVS_SOURCE_DIR}") INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}") diff --git a/MvgMvsPipeline.py b/MvgMvsPipeline.py index 53a4a5cc4..edf00e7e3 100644 --- a/MvgMvsPipeline.py +++ b/MvgMvsPipeline.py @@ -3,16 +3,17 @@ # # Created by @FlachyJoe """ -This script is for an easy use of OpenMVG and OpenMVS +This script is for an easy use of OpenMVG, COLMAP, and OpenMVS usage: MvgMvs_Pipeline.py [-h] [--steps STEPS [STEPS ...]] [--preset PRESET] [--0 0 [0 ...]] [--1 1 [1 ...]] [--2 2 [2 ...]] [--3 3 [3 ...]] [--4 4 [4 ...]] [--5 5 [5 ...]] [--6 6 [6 ...]] [--7 7 [7 ...]] [--8 8 [8 ...]] [--9 9 [9 ...]] [--10 10 [10 ...]] [--11 11 [11 ...]] - [--12 12 [12 ...]] [--13 13 [13 ...]] - [--14 14 [14 ...]] [--15 15 [15 ...]] - [--16 16 [16 ...]] [--17 17 [17 ...]] + [--12 12 [12 ...]] [--13 13 [13 ...]] [--14 14 [14 ...]] + [--15 15 [15 ...]] [--16 16 [16 ...]] [--17 17 [17 ...]] + [--18 18 [18 ...]] [--19 19 [19 ...]] [--20 20 [20 ...]] + [--21 21 [21 ...]] [--22 22 [22 ...]] input_dir output_dir Photogrammetry reconstruction with these steps: @@ -21,19 +22,24 @@ 2. Compute pairs openMVG_main_PairGenerator 3. Compute matches openMVG_main_ComputeMatches 4. Filter matches openMVG_main_GeometricFilter - 5. Incremental reconstruction openMVG_main_IncrementalSfM - 6. Global reconstruction openMVG_main_GlobalSfM + 5. Incremental reconstruction openMVG_main_SfM + 6. Global reconstruction openMVG_main_SfM 7. Colorize Structure openMVG_main_ComputeSfM_DataColor 8. Structure from Known Poses openMVG_main_ComputeStructureFromKnownPoses 9. Colorized robust triangulation openMVG_main_ComputeSfM_DataColor 10. Control Points Registration ui_openMVG_control_points_registration 11. Export to openMVS openMVG_main_openMVG2openMVS - 12. Densify point-cloud DensifyPointCloud - 13. Reconstruct the mesh ReconstructMesh - 14. Refine the mesh RefineMesh - 15. Texture the mesh TextureMesh - 16. Estimate disparity-maps DensifyPointCloud - 17. Fuse disparity-maps DensifyPointCloud + 12. Feature Extractor colmap + 13. Exhaustive Matcher colmap + 14. Mapper colmap + 15. Image Undistorter colmap + 16. Export to openMVS InterfaceCOLMAP + 17. Densify point-cloud DensifyPointCloud + 18. Reconstruct the mesh ReconstructMesh + 19. Refine the mesh RefineMesh + 20. Texture the mesh TextureMesh + 21. Estimate disparity-maps DensifyPointCloud + 22. Fuse disparity-maps DensifyPointCloud positional arguments: input_dir the directory which contains the pictures set. @@ -43,20 +49,22 @@ -h, --help show this help message and exit --steps STEPS [STEPS ...] steps to process --preset PRESET steps list preset in - SEQUENTIAL = [0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15] - GLOBAL = [0, 1, 2, 3, 4, 6, 11, 12, 13, 14, 15] - MVG_SEQ = [0, 1, 2, 3, 4, 5, 7, 8, 9] - MVG_GLOBAL = [0, 1, 2, 3, 4, 6, 7, 8, 9] - MVS = [12, 13, 14, 15] - MVS_SGM = [16, 17] + SEQUENTIAL = [0, 1, 2, 3, 4, 5, 11, 17, 18, 19, 20] + GLOBAL = [0, 1, 2, 3, 4, 6, 11, 17, 18, 19, 20] + MVG_SEQ = [0, 1, 2, 3, 4, 5, 7, 8, 9, 11] + MVG_GLOBAL = [0, 1, 2, 3, 4, 6, 7, 8, 9, 11] + COLMAP_MVS = [12, 13, 14, 15, 16, 17, 18, 19, 20] + COLMAP = [12, 13, 14, 15, 16] + MVS = [17, 18, 19, 20] + MVS_SGM = [21, 22] default : SEQUENTIAL Passthrough: Option to be passed to command lines (remove - in front of option names) e.g. --1 p ULTRA to use the ULTRA preset in openMVG_main_ComputeFeatures For example, running the script - [MvgMvsPipeline.py input_dir output_dir --steps 0 1 2 3 4 5 11 12 13 15 --1 p HIGH n 8 --3 n HNSWL2] - [--steps 0 1 2 3 4 5 11 12 13 15] runs only the desired steps + [MvgMvsPipeline.py input_dir output_dir --steps 0 1 2 3 4 5 11 17 18 20 --1 p HIGH n 8 --3 n HNSWL2] + [--steps 0 1 2 3 4 5 11 17 18 20] runs only the desired steps [--1 p HIGH n 8] where --1 refer to openMVG_main_ComputeFeatures, p refers to describerPreset option and set to HIGH, and n refers to numThreads and set to 8. The second step (Compute matches), @@ -110,29 +118,36 @@ def find(afile): return None -# Try to find openMVG and openMVS binaries in PATH +# Try to find openMVG, COLMAP, and openMVS binaries in PATH OPENMVG_BIN = whereis("openMVG_main_SfMInit_ImageListing") +COLMAP_BIN = whereis("colmap") OPENMVS_BIN = whereis("ReconstructMesh") # Try to find openMVG camera sensor database CAMERA_SENSOR_DB_FILE = "sensor_width_camera_database.txt" CAMERA_SENSOR_DB_DIRECTORY = find(CAMERA_SENSOR_DB_FILE) -# Ask user for openMVG and openMVS directories if not found +# Ask user for openMVG, COLMAP, and openMVS directories if not found if not OPENMVG_BIN: OPENMVG_BIN = input("openMVG binary folder?\n") +if not COLMAP_BIN: + COLMAP_BIN = input("COLMAP binary folder?\n") if not OPENMVS_BIN: OPENMVS_BIN = input("openMVS binary folder?\n") if not CAMERA_SENSOR_DB_DIRECTORY: CAMERA_SENSOR_DB_DIRECTORY = input("openMVG camera database (%s) folder?\n" % CAMERA_SENSOR_DB_FILE) +COLMAP_BIN = os.path.join(COLMAP_BIN, "colmap") +if sys.platform.startswith('win'): + COLMAP_BIN += ".bat" - -PRESET = {'SEQUENTIAL': [0, 1, 2, 3, 4, 5, 11, 12, 13, 14, 15], - 'GLOBAL': [0, 1, 2, 3, 4, 6, 11, 12, 13, 14, 15], +PRESET = {'SEQUENTIAL': [0, 1, 2, 3, 4, 5, 11, 17, 18, 19, 20], + 'GLOBAL': [0, 1, 2, 3, 4, 6, 11, 17, 18, 19, 20], 'MVG_SEQ': [0, 1, 2, 3, 4, 5, 7, 8, 9, 11], 'MVG_GLOBAL': [0, 1, 2, 3, 4, 6, 7, 8, 9, 11], - 'MVS': [12, 13, 14, 15], - 'MVS_SGM': [16, 17]} + 'COLMAP_MVS': [12, 13, 14, 15, 16, 17, 18, 19, 20], + 'COLMAP': [12, 13, 14, 15, 16], + 'MVS': [17, 18, 19, 20], + 'MVS_SGM': [21, 22]} PRESET_DEFAULT = 'SEQUENTIAL' @@ -229,22 +244,37 @@ def __init__(self): ["Export to openMVS", # 11 os.path.join(OPENMVG_BIN, "openMVG_main_openMVG2openMVS"), ["-i", "%reconstruction_dir%"+FOLDER_DELIM+"sfm_data.bin", "-o", "%mvs_dir%"+FOLDER_DELIM+"scene.mvs", "-d", "%mvs_dir%"+FOLDER_DELIM+"images"]], - ["Densify point cloud", # 12 + ["Feature Extractor", # 12 + COLMAP_BIN, + ["feature_extractor", "--database_path", "%matches_dir%"+FOLDER_DELIM+"database.db", "--image_path", "%input_dir%"]], + ["Exhaustive Matcher", # 13 + COLMAP_BIN, + ["exhaustive_matcher", "--database_path", "%matches_dir%"+FOLDER_DELIM+"database.db"]], + ["Mapper", # 14 + COLMAP_BIN, + ["mapper", "--database_path", "%matches_dir%"+FOLDER_DELIM+"database.db", "--image_path", "%input_dir%", "--output_path", "%reconstruction_dir%"]], + ["Image Undistorter", # 15 + COLMAP_BIN, + ["image_undistorter", "--image_path", "%input_dir%", "--input_path", "%reconstruction_dir%"+FOLDER_DELIM+"0", "--output_path", "%reconstruction_dir%"+FOLDER_DELIM+"dense", "--output_type", "COLMAP"]], + ["Export to openMVS", # 16 + os.path.join(OPENMVS_BIN, "InterfaceCOLMAP"), + ["-i", "%reconstruction_dir%"+FOLDER_DELIM+"dense", "-o", "scene.mvs", "--image-folder", "%reconstruction_dir%"+FOLDER_DELIM+"dense"+FOLDER_DELIM+"images", "-w", "\"%mvs_dir%\""]], + ["Densify point cloud", # 17 os.path.join(OPENMVS_BIN, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--resolution-level", "1", "--number-views", "8", "-w", "\"%mvs_dir%\""]], - ["Reconstruct the mesh", # 13 + ["Reconstruct the mesh", # 18 os.path.join(OPENMVS_BIN, "ReconstructMesh"), ["scene_dense.mvs", "-w", "\"%mvs_dir%\""]], - ["Refine the mesh", # 14 + ["Refine the mesh", # 19 os.path.join(OPENMVS_BIN, "RefineMesh"), ["scene_dense_mesh.mvs", "--scales", "1", "--gradient-step", "25.05", "-w", "\"%mvs_dir%\""]], - ["Texture the mesh", # 15 + ["Texture the mesh", # 20 os.path.join(OPENMVS_BIN, "TextureMesh"), ["scene_dense_mesh_refine.mvs", "--decimate", "0.5", "-w", "\"%mvs_dir%\""]], - ["Estimate disparity-maps", # 16 + ["Estimate disparity-maps", # 21 os.path.join(OPENMVS_BIN, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--fusion-mode", "-1", "-w", "\"%mvs_dir%\""]], - ["Fuse disparity-maps", # 17 + ["Fuse disparity-maps", # 22 os.path.join(OPENMVS_BIN, "DensifyPointCloud"), ["scene.mvs", "--dense-config-file", "Densify.ini", "--fusion-mode", "-2", "-w", "\"%mvs_dir%\""]] ] @@ -358,10 +388,10 @@ def mkdir_ine(dirname): STEPS.replace_opt(4, FOLDER_DELIM+"matches.f.bin", FOLDER_DELIM+"matches.e.bin") STEPS[4].opt.extend(["-g", "e"]) -if 15 in CONF.steps: # TextureMesh - if 14 not in CONF.steps: # RefineMesh +if 20 in CONF.steps: # TextureMesh + if 19 not in CONF.steps: # RefineMesh # RefineMesh step is not run, use ReconstructMesh output - STEPS.replace_opt(15, "scene_dense_mesh_refine.mvs", "scene_dense_mesh.mvs") + STEPS.replace_opt(20, "scene_dense_mesh_refine.mvs", "scene_dense_mesh.mvs") for cstep in CONF.steps: printout("#%i. %s" % (cstep, STEPS[cstep].info), effect=INVERSE) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index 7f7ea6584..3bb00c6c6 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -2,6 +2,7 @@ ADD_SUBDIRECTORY(InterfaceCOLMAP) ADD_SUBDIRECTORY(InterfaceMetashape) ADD_SUBDIRECTORY(InterfaceMVSNet) +ADD_SUBDIRECTORY(InterfacePolycam) ADD_SUBDIRECTORY(DensifyPointCloud) ADD_SUBDIRECTORY(ReconstructMesh) ADD_SUBDIRECTORY(RefineMesh) diff --git a/apps/DensifyPointCloud/DensifyPointCloud.cpp b/apps/DensifyPointCloud/DensifyPointCloud.cpp index 949314db9..9a50c58ee 100644 --- a/apps/DensifyPointCloud/DensifyPointCloud.cpp +++ b/apps/DensifyPointCloud/DensifyPointCloud.cpp @@ -55,12 +55,15 @@ String strExportROIFileName; String strImportROIFileName; String strDenseConfigFileName; String strExportDepthMapsName; +String strMaskPath; float fMaxSubsceneArea; float fSampleMesh; float fBorderROI; bool bCrop2ROI; int nEstimateROI; +int nTowerMode; int nFusionMode; +float fEstimateScale; int thFilterPointCloud; int nExportNumViews; int nArchiveType; @@ -133,11 +136,13 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("sub-resolution-levels", boost::program_options::value(&nSubResolutionLevels)->default_value(2), "number of patch-match sub-resolution iterations (0 - disabled)") ("number-views", boost::program_options::value(&nNumViews)->default_value(nNumViewsDefault), "number of views used for depth-map estimation (0 - all neighbor views available)") ("number-views-fuse", boost::program_options::value(&nMinViewsFuse)->default_value(3), "minimum number of images that agrees with an estimate during fusion in order to consider it inlier (<2 - only merge depth-maps)") - ("ignore-mask-label", boost::program_options::value(&nIgnoreMaskLabel)->default_value(-1), "integer value for the label to ignore in the segmentation mask (<0 - disabled)") + ("ignore-mask-label", boost::program_options::value(&nIgnoreMaskLabel)->default_value(-1), "label value to ignore in the image mask, stored in the MVS scene or next to each image with '.mask.png' extension (<0 - disabled)") + ("mask-path", boost::program_options::value(&OPT::strMaskPath), "path to folder containing mask images with '.mask.png' extension") ("iters", boost::program_options::value(&nEstimationIters)->default_value(numIters), "number of patch-match iterations") ("geometric-iters", boost::program_options::value(&nEstimationGeometricIters)->default_value(2), "number of geometric consistent patch-match iterations (0 - disabled)") ("estimate-colors", boost::program_options::value(&nEstimateColors)->default_value(2), "estimate the colors for the dense point-cloud (0 - disabled, 1 - final, 2 - estimate)") ("estimate-normals", boost::program_options::value(&nEstimateNormals)->default_value(2), "estimate the normals for the dense point-cloud (0 - disabled, 1 - final, 2 - estimate)") + ("estimate-scale", boost::program_options::value(&OPT::fEstimateScale)->default_value(0.f), "estimate the point-scale for the dense point-cloud (scale multiplier, 0 - disabled)") ("sub-scene-area", boost::program_options::value(&OPT::fMaxSubsceneArea)->default_value(0.f), "split the scene in sub-scenes such that each sub-scene surface does not exceed the given maximum sampling area (0 - disabled)") ("sample-mesh", boost::program_options::value(&OPT::fSampleMesh)->default_value(0.f), "uniformly samples points on a mesh (0 - disabled, <0 - number of points, >0 - sample density per square unit)") ("fusion-mode", boost::program_options::value(&OPT::nFusionMode)->default_value(0), "depth-maps fusion mode (-2 - fuse disparity-maps, -1 - export disparity-maps only, 0 - depth-maps & fusion, 1 - export depth-maps only)") @@ -148,6 +153,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("estimate-roi", boost::program_options::value(&OPT::nEstimateROI)->default_value(2), "estimate and set region-of-interest (0 - disabled, 1 - enabled, 2 - adaptive)") ("crop-to-roi", boost::program_options::value(&OPT::bCrop2ROI)->default_value(true), "crop scene using the region-of-interest") ("remove-dmaps", boost::program_options::value(&bRemoveDmaps)->default_value(false), "remove depth-maps after fusion") + ("tower-mode", boost::program_options::value(&OPT::nTowerMode)->default_value(3), "add a cylinder of points in the center of ROI; scene assume to be Z-up oriented (0 - disabled, 1 - replace, 2 - append, 3 - select neighbors, <0 - force tower mode)") ; // hidden options, allowed both on command line and @@ -296,6 +302,20 @@ int main(int argc, LPCTSTR* argv) // load and estimate a dense point-cloud if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName))) return EXIT_FAILURE; + if (!OPT::strMaskPath.empty()) { + Util::ensureValidFolderPath(OPT::strMaskPath); + for (Image& image : scene.images) { + if (!image.maskName.empty()) { + VERBOSE("error: Image %s has non-empty maskName %s", image.name.c_str(), image.maskName.c_str()); + return EXIT_FAILURE; + } + image.maskName = OPT::strMaskPath + Util::getFileName(image.name) + ".mask.png"; + if (!File::access(image.maskName)) { + VERBOSE("error: Mask image %s not found", image.maskName.c_str()); + return EXIT_FAILURE; + } + } + } if (!OPT::strImportROIFileName.empty()) { std::ifstream fs(MAKE_PATH_SAFE(OPT::strImportROIFileName)); if (!fs) @@ -315,6 +335,8 @@ int main(int argc, LPCTSTR* argv) Finalize(); return EXIT_SUCCESS; } + if (OPT::nTowerMode!=0) + scene.InitTowerScene(OPT::nTowerMode); if (!OPT::strMeshFileName.empty()) scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName)); if (!OPT::strViewNeighborsFileName.empty()) @@ -369,6 +391,20 @@ int main(int argc, LPCTSTR* argv) Finalize(); return EXIT_SUCCESS; } + if (OPT::fEstimateScale > 0 && !scene.pointcloud.IsEmpty() && !scene.images.empty()) { + // simply export existing point-cloud with scale + if (scene.pointcloud.normals.empty()) { + if (!scene.pointcloud.IsValid()) { + VERBOSE("error: can not estimate normals as the point-cloud is not valid"); + return EXIT_FAILURE; + } + EstimatePointNormals(scene.images, scene.pointcloud); + } + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + scene.pointcloud.SaveWithScale(baseFileName+_T("_scale.ply"), scene.images, OPT::fEstimateScale); + Finalize(); + return EXIT_SUCCESS; + } if ((ARCHIVE_TYPE)OPT::nArchiveType != ARCHIVE_MVS) { #if TD_VERBOSE != TD_VERBOSE_OFF if (VERBOSITY_LEVEL > 1 && !scene.pointcloud.IsEmpty()) diff --git a/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp b/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp index 751e2e7d8..03d16b2be 100644 --- a/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp +++ b/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp @@ -68,6 +68,7 @@ namespace { namespace OPT { bool bFromOpenMVS; // conversion direction bool bNormalizeIntrinsics; +bool bForceSparsePointCloud; String strInputFileName; String strOutputFileName; String strImageFolder; @@ -89,7 +90,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) // group of options allowed only on command line boost::program_options::options_description generic("Generic options"); generic.add_options() - ("help,h", "produce this help message") + ("help,h", "imports SfM or MVS scene stored in COLMAP undistoreted format OR exports MVS scene to COLMAP format") ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_DEFAULT), "project archive type: 0-text, 1-binary, 2-compressed binary") @@ -114,6 +115,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("image-folder", boost::program_options::value(&OPT::strImageFolder)->default_value(COLMAP_IMAGES_FOLDER), "folder to the undistorted images") ("max-resolution", boost::program_options::value(&OPT::nMaxResolution)->default_value(0), "make sure image resolution are not not larger than this (0 - disabled)") ("normalize,f", boost::program_options::value(&OPT::bNormalizeIntrinsics)->default_value(false), "normalize intrinsics while exporting to MVS format") + ("force-points,p", boost::program_options::value(&OPT::bForceSparsePointCloud)->default_value(false), "force exporting point-cloud as sparse points also even if dense point-cloud detected") ; boost::program_options::options_description cmdline_options; @@ -220,22 +222,19 @@ typedef uint64_t image_pair_t; typedef uint32_t point2D_t; typedef uint64_t point3D_t; -typedef std::unordered_map CameraModelMap; -CameraModelMap mapCameraModel; - -void DefineCameraModels() { - COLMAP::mapCameraModel.emplace(0, "SIMPLE_PINHOLE"); - COLMAP::mapCameraModel.emplace(1, "PINHOLE"); - COLMAP::mapCameraModel.emplace(2, "SIMPLE_RADIAL"); - COLMAP::mapCameraModel.emplace(3, "RADIAL"); - COLMAP::mapCameraModel.emplace(4, "OPENCV"); - COLMAP::mapCameraModel.emplace(5, "OPENCV_FISHEYE"); - COLMAP::mapCameraModel.emplace(6, "FULL_OPENCV"); - COLMAP::mapCameraModel.emplace(7, "FOV"); - COLMAP::mapCameraModel.emplace(8, "SIMPLE_RADIAL_FISHEYE"); - COLMAP::mapCameraModel.emplace(9, "RADIAL_FISHEYE"); - COLMAP::mapCameraModel.emplace(10, "THIN_PRISM_FISHEYE"); -} +const std::vector mapCameraModel = { + "SIMPLE_PINHOLE", + "PINHOLE", + "SIMPLE_RADIAL", + "RADIAL", + "OPENCV", + "OPENCV_FISHEYE", + "FULL_OPENCV", + "FOV", + "SIMPLE_RADIAL_FISHEYE", + "RADIAL_FISHEYE", + "THIN_PRISM_FISHEYE" +}; // tools bool NextLine(std::istream& stream, std::istringstream& in, bool bIgnoreEmpty=true) { @@ -256,7 +255,7 @@ struct Camera { String model; // camera model name uint32_t width, height; // camera resolution std::vector params; // camera parameters - bool parsedNumCameras = false; + uint64_t numCameras{0}; // only for binary format Camera() {} Camera(uint32_t _ID) : ID(_ID) {} @@ -287,6 +286,12 @@ struct Camera { return ReadTXT(stream); } + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + // Camera list with one line of data per camera: // CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] bool ReadTXT(std::istream& stream) { @@ -310,14 +315,13 @@ struct Camera { if (stream.peek() == EOF) return false; - if (!parsedNumCameras) { + if (numCameras == 0) { // Read the first entry in the binary file - ReadBinaryLittleEndian(&stream); - parsedNumCameras = true; + numCameras = ReadBinaryLittleEndian(&stream); } ID = ReadBinaryLittleEndian(&stream)-1; - model = mapCameraModel.at(ReadBinaryLittleEndian(&stream)); + model = mapCameraModel[ReadBinaryLittleEndian(&stream)]; width = (uint32_t)ReadBinaryLittleEndian(&stream); height = (uint32_t)ReadBinaryLittleEndian(&stream); if (model != _T("PINHOLE")) @@ -327,7 +331,7 @@ struct Camera { return true; } - bool Write(std::ostream& out) const { + bool WriteTXT(std::ostream& out) const { out << ID+1 << _T(" ") << model << _T(" ") << width << _T(" ") << height; if (out.fail()) return false; @@ -339,6 +343,23 @@ struct Camera { out << std::endl; return true; } + + bool WriteBIN(std::ostream& stream) { + if (numCameras != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numCameras); + numCameras = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + const int64 modelId(std::distance(mapCameraModel.begin(), std::find(mapCameraModel.begin(), mapCameraModel.end(), model))); + WriteBinaryLittleEndian(&stream, (int)modelId); + WriteBinaryLittleEndian(&stream, width); + WriteBinaryLittleEndian(&stream, height); + for (REAL param: params) + WriteBinaryLittleEndian(&stream, param); + return !stream.fail(); + } }; typedef std::vector Cameras; // structure describing an image @@ -353,7 +374,7 @@ struct Image { uint32_t idCamera; // ID of the associated camera String name; // image file name std::vector projs; // known image projections - bool parsedNumRegImages = false; + uint64_t numRegImages{0}; // only for binary format Image() {} Image(uint32_t _ID) : ID(_ID) {} @@ -365,6 +386,12 @@ struct Image { return ReadTXT(stream); } + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + // Image list with two lines of data per image: // IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME // POINTS2D[] as (X, Y, POINT3D_ID) @@ -388,6 +415,7 @@ struct Image { in >> proj.p(0) >> proj.p(1) >> (int&)proj.idPoint; if (in.fail()) break; + --proj.idPoint; projs.emplace_back(proj); } return true; @@ -399,10 +427,9 @@ struct Image { if (stream.peek() == EOF) return false; - if (!parsedNumRegImages) { + if (!numRegImages) { // Read the first entry in the binary file - ReadBinaryLittleEndian(&stream); - parsedNumRegImages = true; + numRegImages = ReadBinaryLittleEndian(&stream); } ID = ReadBinaryLittleEndian(&stream)-1; @@ -416,13 +443,13 @@ struct Image { idCamera = ReadBinaryLittleEndian(&stream)-1; name = ""; - char nameChar; - do { + while (true) { + char nameChar; stream.read(&nameChar, 1); - if (nameChar != '\0') { - name += nameChar; - } - } while (nameChar != '\0'); + if (nameChar == '\0') + break; + name += nameChar; + } Util::ensureValidPath(name); const size_t numPoints2D = ReadBinaryLittleEndian(&stream); @@ -431,13 +458,13 @@ struct Image { Proj proj; proj.p(0) = (float)ReadBinaryLittleEndian(&stream); proj.p(1) = (float)ReadBinaryLittleEndian(&stream); - proj.idPoint = (uint32_t)ReadBinaryLittleEndian(&stream); + proj.idPoint = (uint32_t)ReadBinaryLittleEndian(&stream)-1; projs.emplace_back(proj); } return true; } - bool Write(std::ostream& out) const { + bool WriteTXT(std::ostream& out) const { out << ID+1 << _T(" ") << q.w() << _T(" ") << q.x() << _T(" ") << q.y() << _T(" ") << q.z() << _T(" ") << t(0) << _T(" ") << t(1) << _T(" ") << t(2) << _T(" ") @@ -451,6 +478,37 @@ struct Image { out << std::endl; return !out.fail(); } + + bool WriteBIN(std::ostream& stream) { + if (numRegImages != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numRegImages); + numRegImages = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + + WriteBinaryLittleEndian(&stream, q.w()); + WriteBinaryLittleEndian(&stream, q.x()); + WriteBinaryLittleEndian(&stream, q.y()); + WriteBinaryLittleEndian(&stream, q.z()); + + WriteBinaryLittleEndian(&stream, t(0)); + WriteBinaryLittleEndian(&stream, t(1)); + WriteBinaryLittleEndian(&stream, t(2)); + + WriteBinaryLittleEndian(&stream, idCamera+1); + + stream.write(name.c_str(), name.size()+1); + + WriteBinaryLittleEndian(&stream, projs.size()); + for (const Proj& proj: projs) { + WriteBinaryLittleEndian(&stream, proj.p(0)); + WriteBinaryLittleEndian(&stream, proj.p(1)); + WriteBinaryLittleEndian(&stream, proj.idPoint+1); + } + return !stream.fail(); + } }; typedef std::vector Images; // structure describing a 3D point @@ -464,7 +522,7 @@ struct Point { Interface::Col3 c; // BGR color float e; // error std::vector tracks; // point track - bool parsedNumPoints3D = false; + uint64_t numPoints3D{0}; // only for binary format Point() {} Point(uint32_t _ID) : ID(_ID) {} @@ -476,6 +534,12 @@ struct Point { return ReadTXT(stream); } + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + // 3D point list with one line of data per point: // POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) bool ReadTXT(std::istream& stream) { @@ -511,10 +575,9 @@ struct Point { if (stream.peek() == EOF) return false; - if (!parsedNumPoints3D) { + if (!numPoints3D) { // Read the first entry in the binary file - ReadBinaryLittleEndian(&stream); - parsedNumPoints3D = true; + numPoints3D = ReadBinaryLittleEndian(&stream); } int r,g,b; @@ -541,7 +604,7 @@ struct Point { return !tracks.empty(); } - bool Write(std::ostream& out) const { + bool WriteTXT(std::ostream& out) const { ASSERT(!tracks.empty()); const int r(c.z),g(c.y),b(c.x); out << ID+1 << _T(" ") @@ -556,6 +619,31 @@ struct Point { out << std::endl; return !out.fail(); } + + bool WriteBIN(std::ostream& stream) { + ASSERT(!tracks.empty()); + if (numPoints3D != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numPoints3D); + numPoints3D = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + WriteBinaryLittleEndian(&stream, p.x); + WriteBinaryLittleEndian(&stream, p.y); + WriteBinaryLittleEndian(&stream, p.z); + WriteBinaryLittleEndian(&stream, c.z); + WriteBinaryLittleEndian(&stream, c.y); + WriteBinaryLittleEndian(&stream, c.x); + WriteBinaryLittleEndian(&stream, e); + + WriteBinaryLittleEndian(&stream, tracks.size()); + for (const Track& track: tracks) { + WriteBinaryLittleEndian(&stream, track.idImage+1); + WriteBinaryLittleEndian(&stream, track.idProj+1); + } + return !stream.fail(); + } }; typedef std::vector Points; // structure describing an 2D dynamic matrix @@ -624,7 +712,6 @@ bool DetermineInputSource(const String& filenameTXT, const String& filenameBIN, bool ImportScene(const String& strFolder, const String& strOutFolder, Interface& scene) { - COLMAP::DefineCameraModels(); // read camera list typedef std::unordered_map CamerasMap; CamerasMap mapCameras; @@ -871,7 +958,7 @@ bool ImportScene(const String& strFolder, const String& strOutFolder, Interface& } -bool ExportScene(const String& strFolder, const Interface& scene) +bool ExportScene(const String& strFolder, const Interface& scene, bool bForceSparsePointCloud = false, bool binary = true) { Util::ensureFolder(strFolder+COLMAP_SPARSE_FOLDER); @@ -879,16 +966,22 @@ bool ExportScene(const String& strFolder, const Interface& scene) CLISTDEF0IDX(KMatrix,uint32_t) Ks; CLISTDEF0IDX(COLMAP::Camera,uint32_t) cams; { - const String filenameCameras(strFolder+COLMAP_CAMERAS_TXT); + const String filenameCameras(strFolder+(binary?COLMAP_CAMERAS_BIN:COLMAP_CAMERAS_TXT)); LOG_OUT() << "Writing cameras: " << filenameCameras << std::endl; - std::ofstream file(filenameCameras); + std::ofstream file(filenameCameras, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); if (!file.good()) { VERBOSE("error: unable to open file '%s'", filenameCameras.c_str()); return false; } - file << _T("# Camera list with one line of data per camera:") << std::endl; - file << _T("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]") << std::endl; COLMAP::Camera cam; + if (binary) { + cam.numCameras = 0; + for (const Interface::Platform& platform: scene.platforms) + cam.numCameras += (uint32_t)platform.cameras.size(); + } else { + file << _T("# Camera list with one line of data per camera:") << std::endl; + file << _T("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]") << std::endl; + } cam.model = _T("PINHOLE"); cam.params.resize(4); for (uint32_t ID=0; ID<(uint32_t)scene.platforms.size(); ++ID) { @@ -910,7 +1003,7 @@ bool ExportScene(const String& strFolder, const Interface& scene) LOG("error: no image using camera %u of platform %u", 0, ID); continue; } - IMAGEPTR ptrImage(Image::ReadImageHeader(MAKE_PATH_SAFE(pImage->name.c_str()))); + IMAGEPTR ptrImage(Image::ReadImageHeader(MAKE_PATH_SAFE(pImage->name))); if (ptrImage == NULL) return false; cam.width = ptrImage->GetWidth(); @@ -929,7 +1022,7 @@ bool ExportScene(const String& strFolder, const Interface& scene) cam.params[2] = camera.K(0,2); cam.params[3] = camera.K(1,2); } - if (!cam.Write(file)) + if (!cam.Write(file, binary)) return false; KMatrix& K = Ks.emplace_back(KMatrix::IDENTITY); K(0,0) = cam.params[0]; @@ -977,18 +1070,23 @@ bool ExportScene(const String& strFolder, const Interface& scene) // auto-select dense or sparse mode based on number of points const bool bSparsePointCloud(scene.vertices.size() < (size_t)maxNumPointsSparse); - if (bSparsePointCloud) { + if (bSparsePointCloud || bForceSparsePointCloud) { // write points list { - const String filenamePoints(strFolder+COLMAP_POINTS_TXT); + const String filenamePoints(strFolder+(binary?COLMAP_POINTS_BIN:COLMAP_POINTS_TXT)); LOG_OUT() << "Writing points: " << filenamePoints << std::endl; - std::ofstream file(filenamePoints); + std::ofstream file(filenamePoints, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); if (!file.good()) { VERBOSE("error: unable to open file '%s'", filenamePoints.c_str()); return false; } - file << _T("# 3D point list with one line of data per point:") << std::endl; - file << _T("# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)") << std::endl; + uint64_t numPoints3D = 0; + if (binary) { + numPoints3D = scene.vertices.size(); + } else { + file << _T("# 3D point list with one line of data per point:") << std::endl; + file << _T("# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)") << std::endl; + } for (uint32_t ID=0; ID<(uint32_t)scene.vertices.size(); ++ID) { const Interface::Vertex& vertex = scene.vertices[ID]; COLMAP::Point point; @@ -1005,7 +1103,11 @@ bool ExportScene(const String& strFolder, const Interface& scene) } point.c = scene.verticesColor.empty() ? Interface::Col3(255,255,255) : scene.verticesColor[ID].c; point.e = 0; - if (!point.Write(file)) + if (numPoints3D != 0) { + point.numPoints3D = numPoints3D; + numPoints3D = 0; + } + if (!point.Write(file, binary)) return false; } } @@ -1054,7 +1156,8 @@ bool ExportScene(const String& strFolder, const Interface& scene) Util::ensureFolder(strFolder+COLMAP_STEREO_CONSISTENCYGRAPHS_FOLDER); Util::ensureFolder(strFolder+COLMAP_STEREO_DEPTHMAPS_FOLDER); Util::ensureFolder(strFolder+COLMAP_STEREO_NORMALMAPS_FOLDER); - } else { + } + if (!bSparsePointCloud) { // export dense point-cloud const String filenameDensePoints(strFolder+COLMAP_DENSE_POINTS); const String filenameDenseVisPoints(strFolder+COLMAP_DENSE_POINTS_VISIBILITY); @@ -1089,20 +1192,33 @@ bool ExportScene(const String& strFolder, const Interface& scene) // write images list { - const String filenameImages(strFolder+COLMAP_IMAGES_TXT); + const String filenameImages(strFolder+(binary?COLMAP_IMAGES_BIN:COLMAP_IMAGES_TXT)); LOG_OUT() << "Writing images: " << filenameImages << std::endl; - std::ofstream file(filenameImages); + std::ofstream file(filenameImages, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); if (!file.good()) { VERBOSE("error: unable to open file '%s'", filenameImages.c_str()); return false; } - file << _T("# Image list with two lines of data per image:") << std::endl; - file << _T("# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME") << std::endl; - file << _T("# POINTS2D[] as (X, Y, POINT3D_ID)") << std::endl; - for (const COLMAP::Image& img: images) { + uint64_t numRegImages = 0; + if (binary) { + for (const COLMAP::Image& img: images) { + if (bSparsePointCloud && img.projs.empty()) + continue; + ++numRegImages; + } + } else { + file << _T("# Image list with two lines of data per image:") << std::endl; + file << _T("# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME") << std::endl; + file << _T("# POINTS2D[] as (X, Y, POINT3D_ID)") << std::endl; + } + for (COLMAP::Image& img: images) { if (bSparsePointCloud && img.projs.empty()) continue; - if (!img.Write(file)) + if (numRegImages != 0) { + img.numRegImages = numRegImages; + numRegImages = 0; + } + if (!img.Write(file, binary)) return false; } } @@ -1303,7 +1419,7 @@ int main(int argc, LPCTSTR* argv) } else { // write COLMAP input data Util::ensureFolderSlash(OPT::strOutputFileName); - ExportScene(MAKE_PATH_SAFE(OPT::strOutputFileName), scene); + ExportScene(MAKE_PATH_SAFE(OPT::strOutputFileName), scene, OPT::bForceSparsePointCloud); } VERBOSE("Input data exported: %u images & %u vertices (%s)", scene.images.size(), scene.vertices.size(), TD_TIMER_GET_FMT().c_str()); } else { diff --git a/apps/InterfaceMVSNet/InterfaceMVSNet.cpp b/apps/InterfaceMVSNet/InterfaceMVSNet.cpp index 08d76a142..31da44f71 100644 --- a/apps/InterfaceMVSNet/InterfaceMVSNet.cpp +++ b/apps/InterfaceMVSNet/InterfaceMVSNet.cpp @@ -141,7 +141,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); if (OPT::strOutputFileName.IsEmpty()) OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + "scene" MVS_EXT; @@ -250,6 +249,7 @@ bool ParseSceneMVSNet(Scene& scene) String strPath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)); Util::ensureValidFolderPath(strPath); const std::filesystem::path path(static_cast(strPath)); + IIndex prevPlatformID = NO_ID; for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(path / MVSNET_IMAGES_FOLDER)) { if (entry.path().extension() != MVSNET_IMAGES_EXT) continue; @@ -292,12 +292,18 @@ bool ParseSceneMVSNet(Scene& scene) Matrix3x3 K; ImageListParse(argv, K); // setup camera - const IIndex platformID = scene.platforms.size(); - Platform& platform = scene.platforms.emplace_back(); - Platform::Camera& camera = platform.cameras.AddEmpty(); - camera.K = K; - camera.R = RMatrix::IDENTITY; - camera.C = CMatrix::ZERO; + IIndex platformID; + if (prevPlatformID == NO_ID || !K.IsEqual(scene.platforms[prevPlatformID].cameras[0].K, 1e-3)) { + prevPlatformID = platformID = scene.platforms.size(); + Platform& platform = scene.platforms.emplace_back(); + Platform::Camera& camera = platform.cameras.emplace_back(); + camera.K = K; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + } else { + platformID = prevPlatformID; + } + Platform& platform = scene.platforms[platformID]; // setup image const IIndex ID = scene.images.size(); Image& imageData = scene.images.emplace_back(); @@ -315,11 +321,9 @@ bool ParseSceneMVSNet(Scene& scene) imageData.scale = 1; // set camera pose imageData.poseID = platform.poses.size(); - Platform::Pose& pose = platform.poses.AddEmpty(); + Platform::Pose& pose = platform.poses.emplace_back(); DecomposeProjectionMatrix(P, pose.R, pose.C); imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); - const float resolutionScale = Camera::GetNormalizationScale(imageData.width, imageData.height); - camera.K = camera.GetScaledK(REAL(1)/resolutionScale); } if (scene.images.size() < 2) return false; diff --git a/apps/InterfaceMetashape/InterfaceMetashape.cpp b/apps/InterfaceMetashape/InterfaceMetashape.cpp index 4886973ab..068e9505c 100644 --- a/apps/InterfaceMetashape/InterfaceMetashape.cpp +++ b/apps/InterfaceMetashape/InterfaceMetashape.cpp @@ -71,7 +71,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) // group of options allowed only on command line boost::program_options::options_description generic("Generic options"); generic.add_options() - ("help,h", "produce this help message") + ("help,h", "imports SfM scene stored either in Metashape Agisoft/BlocksExchange or ContextCapture BlocksExchange XML format") ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: 0-text, 1-binary, 2-compressed binary") @@ -93,7 +93,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) config.add_options() ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") ("points-file,p", boost::program_options::value(&OPT::strPointsFileName), "input filename containing the 3D points") - ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") ("output-image-folder", boost::program_options::value(&OPT::strOutputImageFolder)->default_value("undistorted_images"), "output folder to store undistorted images") ; @@ -133,9 +133,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) // validate input Util::ensureValidPath(OPT::strPointsFileName); Util::ensureValidPath(OPT::strInputFileName); - Util::ensureValidPath(OPT::strOutputImageFolder); - Util::ensureFolderSlash(OPT::strOutputImageFolder); - const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + Util::ensureValidFolderPath(OPT::strOutputImageFolder); const bool bInvalidCommand(OPT::strInputFileName.empty()); if (OPT::vm.count("help") || bInvalidCommand) { boost::program_options::options_description visible("Available options"); @@ -147,8 +145,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); - if (OPT::strOutputFileName.IsEmpty()) + if (OPT::strOutputFileName.empty()) OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + MVS_EXT; // initialize global options @@ -186,6 +183,7 @@ struct DistCoeff { }; }; DistCoeff() : k1(0), k2(0), p1(0), p2(0), k3(0), k4(0), k5(0), k6(0) {} + bool HasDistortion() const { return k1 != 0 || k2 != 0 || k3 != 0 || k4 != 0 || k5 != 0 || k6 != 0; } }; typedef cList DistCoeffs; typedef cList PlatformDistCoeffs; @@ -512,20 +510,23 @@ bool ParseBlocksExchangeXML(tinyxml2::XMLDocument& doc, Scene& scene, PlatformDi // parse distortion parameters DistCoeff& dc = pltDistCoeffs.AddEmpty().AddEmpty(); { const tinyxml2::XMLElement* distortion=photogroup->FirstChildElement("Distortion"); - if ((elem=distortion->FirstChildElement("K1")) != NULL) - dc.k1 = elem->DoubleText(); - if ((elem=distortion->FirstChildElement("K2")) != NULL) - dc.k2 = elem->DoubleText(); - if ((elem=distortion->FirstChildElement("K3")) != NULL) - dc.k3 = elem->DoubleText(); - if ((elem=distortion->FirstChildElement("P1")) != NULL) - dc.p2 = elem->DoubleText(); - if ((elem=distortion->FirstChildElement("P2")) != NULL) - dc.p1 = elem->DoubleText(); + if (distortion) { + if ((elem=distortion->FirstChildElement("K1")) != NULL) + dc.k1 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("K2")) != NULL) + dc.k2 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("K3")) != NULL) + dc.k3 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("P1")) != NULL) + dc.p2 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("P2")) != NULL) + dc.p1 = elem->DoubleText(); + } } ++nCameras; for (const tinyxml2::XMLElement* photo=photogroup->FirstChildElement("Photo"); photo!=NULL; photo=photo->NextSiblingElement()) { - Image imageData; + const IIndex idxImage = scene.images.size(); + Image& imageData = scene.images.AddEmpty(); imageData.platformID = platformID; imageData.cameraID = 0; // only one camera per platform supported by this format imageData.poseID = NO_ID; @@ -533,6 +534,7 @@ bool ParseBlocksExchangeXML(tinyxml2::XMLDocument& doc, Scene& scene, PlatformDi imageData.name = photo->FirstChildElement("ImagePath")->GetText(); Util::ensureUnifySlash(imageData.name); imageData.name = MAKE_PATH_FULL(strPath, imageData.name); + mapImageID.emplace(imageData.ID, idxImage); // set image resolution const cv::Size& resolution = resolutions[imageData.platformID]; imageData.width = resolution.width; @@ -568,8 +570,8 @@ bool ParseBlocksExchangeXML(tinyxml2::XMLDocument& doc, Scene& scene, PlatformDi imageData.avgDepth = (float)elem->DoubleText(); else if (photo->FirstChildElement("NearDepth") != NULL && photo->FirstChildElement("FarDepth") != NULL) imageData.avgDepth = (float)((photo->FirstChildElement("NearDepth")->DoubleText() + photo->FirstChildElement("FarDepth")->DoubleText())/2); - mapImageID.emplace(imageData.ID, static_cast(scene.images.size())); - scene.images.emplace_back(std::move(imageData)); + else + imageData.avgDepth = 0; ++nPoses; } } @@ -643,6 +645,10 @@ bool ParseSceneXML(Scene& scene, PlatformDistCoeffs& pltDistCoeffs, size_t& nCam // undistort image using Brown's model bool UndistortBrown(Image& imageData, uint32_t ID, const DistCoeff& dc, const String& pathData) { + // do we need to undistort? + if (!dc.HasDistortion()) + return true; + // load image pixels if (!imageData.ReloadImage()) return false; @@ -653,7 +659,7 @@ bool UndistortBrown(Image& imageData, uint32_t ID, const DistCoeff& dc, const St #if 1 const KMatrix& K(prevK); #else - const KMatrix K(cv::getOptimalNewCameraMatrix(prevK, distCoeffs, imageData.GetSize(), 0.0, cv::Size(), NULL, true)); + const KMatrix K(cv::getOptimalNewCameraMatrix(prevK, distCoeffs, imageData.size(), 0.0, cv::Size(), NULL, true)); ASSERT(K(0,2) == Camera::ComposeK(prevK(0,0), prevK(1,1), imageData.width(), imageData.height())(0,2)); ASSERT(K(1,2) == Camera::ComposeK(prevK(0,0), prevK(1,1), imageData.width(), imageData.height())(1,2)); if (K.IsEqual(prevK)) { @@ -686,13 +692,13 @@ void AssignPoints(const Image& imageData, uint32_t ID, PointCloud& pointcloud) const Depth thCloseDepth(0.1f); // sort points by depth - IndexScoreArr points(0, pointcloud.points.GetSize()); + IndexScoreArr points(0, pointcloud.points.size()); FOREACH(p, pointcloud.points) { const PointCloud::Point& X(pointcloud.points[p]); const float d((float)imageData.camera.PointDepth(X)); if (d <= 0) continue; - points.AddConstruct((uint32_t)p, d); + points.emplace_back((uint32_t)p, d); } points.Sort(); @@ -710,7 +716,7 @@ void AssignPoints(const Image& imageData, uint32_t ID, PointCloud& pointcloud) ASSERT(Xc.z > 0); // skip point if the (cos) angle between // its normal and the point to view vector is negative - if (!pointcloud.normals.IsEmpty() && Xc.dot(pointcloud.normals[pPD->idx]) > 0) + if (!pointcloud.normals.empty() && Xc.dot(pointcloud.normals[pPD->idx]) > 0) continue; const Point2f x(imageData.camera.TransformPointC2I(Xc)); const ImageRef ir(ROUND2INT(x)); @@ -787,23 +793,21 @@ int main(int argc, LPCTSTR* argv) Scene scene(OPT::nMaxThreads); - // read the 3D point-cloud if available - if (!OPT::strPointsFileName.empty()) { - if (!scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointsFileName))) - return EXIT_FAILURE; - ASSERT(!scene.pointcloud.IsValid()); - scene.pointcloud.pointViews.Resize(scene.pointcloud.points.GetSize()); - } - // convert data from Metashape format to OpenMVS PlatformDistCoeffs pltDistCoeffs; size_t nCameras(0), nPoses(0); if (!ParseSceneXML(scene, pltDistCoeffs, nCameras, nPoses)) return EXIT_FAILURE; + // read the 3D point-cloud if available + if (!OPT::strPointsFileName.empty() && !scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointsFileName))) + return EXIT_FAILURE; + const bool bAssignPoints(!scene.pointcloud.IsEmpty() && !scene.pointcloud.IsValid()); + if (bAssignPoints) + scene.pointcloud.pointViews.resize(scene.pointcloud.GetSize()); + // undistort images const String pathData(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputImageFolder)); - const bool bAssignPoints(!scene.pointcloud.IsEmpty() && !scene.pointcloud.IsValid()); Util::Progress progress(_T("Processed images"), scene.images.size()); GET_LOGCONSOLE().Pause(); #ifdef _USE_OPENMP @@ -818,6 +822,8 @@ int main(int argc, LPCTSTR* argv) #endif ++progress; Image& imageData = scene.images[ID]; + if (!imageData.IsValid()) + continue; if (!UndistortBrown(imageData, ID, pltDistCoeffs[imageData.platformID][imageData.cameraID], pathData)) { #ifdef _USE_OPENMP bAbort = true; @@ -838,18 +844,46 @@ int main(int argc, LPCTSTR* argv) #endif progress.close(); - // filter invalid points - if (!scene.pointcloud.IsEmpty()) { + if (scene.pointcloud.IsValid()) { + // filter invalid points RFOREACH(i, scene.pointcloud.points) if (scene.pointcloud.pointViews[i].size() < 2) scene.pointcloud.RemovePoint(i); + // compute average scene depth per image + if (!std::any_of(scene.images.begin(), scene.images.end(), [](const Image& imageData) { return imageData.avgDepth > 0; })) { + std::vector avgDepths(scene.images.size(), 0.f); + std::vector numDepths(scene.images.size(), 0u); + FOREACH(idxPoint, scene.pointcloud.points) { + const Point3 X(scene.pointcloud.points[idxPoint]); + for (const PointCloud::View& idxImage: scene.pointcloud.pointViews[idxPoint]) { + const Image& imageData = scene.images[idxImage]; + const float depth((float)imageData.camera.PointDepth(X)); + if (depth > 0) { + avgDepths[idxImage] += depth; + ++numDepths[idxImage]; + } + } + } + FOREACH(idxImage, scene.images) { + Image& imageData = scene.images[idxImage]; + if (numDepths[idxImage] > 0) + imageData.avgDepth = avgDepths[idxImage] / numDepths[idxImage]; + } + } } + // print average scene depth per image stats + MeanStdMinMax acc; + for (const Image& imageData: scene.images) + if (imageData.avgDepth > 0) + acc.Update(imageData.avgDepth); + // write OpenMVS input data scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); - VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images, %u vertices (%s)", + VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images, %u vertices, %g min / %g mean (%g std) / %g max average scene depth per image (%s)", scene.platforms.size(), nCameras, nPoses, scene.images.size(), scene.pointcloud.GetSize(), + acc.minVal, acc.GetMean(), acc.GetStdDev(), acc.maxVal, TD_TIMER_GET_FMT().c_str()); Finalize(); diff --git a/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp b/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp index edc7ccb81..6902a0974 100644 --- a/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp +++ b/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp @@ -415,7 +415,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) Util::ensureValidPath(OPT::strInputFileName); Util::ensureUnifySlash(OPT::strInputFileName); Util::ensureUnifySlash(OPT::strOutputImageFolder); - Util::ensureDirectorySlash(OPT::strOutputImageFolder); + Util::ensureFolderSlash(OPT::strOutputImageFolder); const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); OPT::bOpenMVS2OpenMVG = (strInputFileNameExt == MVS_EXT); #ifdef _USE_OPENMVG @@ -440,10 +440,10 @@ bool Initialize(size_t argc, LPCTSTR* argv) Util::ensureUnifySlash(OPT::strOutputFileName); if (OPT::bOpenMVS2OpenMVG) { if (OPT::strOutputFileName.IsEmpty()) - OPT::strOutputFileName = Util::getFullFileName(OPT::strInputFileName); + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName); } else { if (OPT::strOutputFileName.IsEmpty()) - OPT::strOutputFileName = Util::getFullFileName(OPT::strInputFileName) + MVS_EXT; + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + MVS_EXT; } // initialize global options @@ -602,7 +602,7 @@ int main(int argc, LPCTSTR* argv) image.name = view.second->s_Img_path; Util::ensureUnifySlash(image.name); Util::strTrim(image.name, PATH_SEPARATOR_STR); - String pathRoot(sfm_data.s_root_path); Util::ensureDirectorySlash(pathRoot); + String pathRoot(sfm_data.s_root_path); Util::ensureFolderSlash(pathRoot); const String srcImage(MAKE_PATH_FULL(WORKING_FOLDER_FULL, pathRoot+image.name)); image.name = MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputImageFolder+image.name); Util::ensureDirectory(image.name); diff --git a/apps/InterfacePolycam/CMakeLists.txt b/apps/InterfacePolycam/CMakeLists.txt new file mode 100644 index 000000000..de8fda125 --- /dev/null +++ b/apps/InterfacePolycam/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfacePolycam "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfacePolycam + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfacePolycam/InterfacePolycam.cpp b/apps/InterfacePolycam/InterfacePolycam.cpp new file mode 100644 index 000000000..9100b1b5b --- /dev/null +++ b/apps/InterfacePolycam/InterfacePolycam.cpp @@ -0,0 +1,363 @@ +/* + * InterfacePolycam.cpp + * + * Copyright (c) 2014-2023 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include "../../libs/IO/json.hpp" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfacePolycam") +#define MVS_EXT _T(".mvs") +#define JSON_EXT _T(".json") +#define DEPTH_EXT _T(".png") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strOutputFileName; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +// initialize and parse the command line parameters +bool Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "imports SfM scene stored Polycam format") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input folder containing Polycam camera poses, images and depth-maps") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidFolderPath(OPT::strInputFileName); + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidFolderPath(OPT::strOutputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = "scene" MVS_EXT; + + // initialize global options + Process::setCurrentProcessPriority((Process::Priority)OPT::nProcessPriority); + #ifdef _USE_OPENMP + if (OPT::nMaxThreads != 0) + omp_set_num_threads(OPT::nMaxThreads); + #endif + + #ifdef _USE_BREAKPAD + // start memory dumper + MiniDumper::Create(APPNAME, WORKING_FOLDER); + #endif + return true; +} + +// finalize application instance +void Finalize() +{ + #if TD_VERBOSE != TD_VERBOSE_OFF + // print memory statistics + Util::LogMemoryInfo(); + #endif + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +// parse image containing calibration, pose, and depth-map information +bool ParseImage(Scene& scene, const String& imagePath, const String& cameraPath, const String& depthPath, + const std::unordered_map& mapImageName) +{ + nlohmann::json data = nlohmann::json::parse(std::ifstream(cameraPath)); + if (data.empty()) + return false; + const cv::Size resolution(data["width"].get(), data["height"].get()); + // set platform + const IIndex platformID = scene.platforms.size(); + Platform& platform = scene.platforms.AddEmpty(); + Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = KMatrix::IDENTITY; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + camera.K(0,0) = data["fx"].get(); + camera.K(1,1) = data["fy"].get(); + camera.K(0,2) = data["cx"].get(); + camera.K(1,2) = data["cy"].get(); + // set image + const IIndex imageID = scene.images.size(); + Image& imageData = scene.images.AddEmpty(); + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = imageID; + imageData.name = imagePath; + ASSERT(Util::isFullPath(imageData.name)); + // set image resolution + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + // set camera pose + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.AddEmpty(); + const Eigen::Matrix3d R_session_arkitcam{ + {data["t_00"].get(), data["t_01"].get(), data["t_02"].get()}, + {data["t_10"].get(), data["t_11"].get(), data["t_12"].get()}, + {data["t_20"].get(), data["t_21"].get(), data["t_22"].get()} + }; + const Eigen::Vector3d t_session_arkitcam{ + data["t_03"].get(), + data["t_13"].get(), + data["t_23"].get() + }; + const Eigen::Affine3d T_session_arkitcam{ + Eigen::Affine3d(Eigen::Translation3d(t_session_arkitcam)) * Eigen::Affine3d(Eigen::AngleAxisd(R_session_arkitcam)) + }; + const Eigen::Affine3d T_cam_arkitcam{ + Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitX()) + }; + const Eigen::Matrix4d P{ + (T_cam_arkitcam * T_session_arkitcam.inverse()).matrix() + }; + pose.R = P.topLeftCorner<3, 3>().eval(); + pose.R.EnforceOrthogonality(); + const Point3d t = P.topRightCorner<3, 1>().eval(); + pose.C = pose.R.t() * (-t); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + // set image neighbors if available + nlohmann::json::const_iterator itNeighbors = data.find("neighbors"); + if (itNeighbors != data.end()) { + const std::vector neighborTimestamps = itNeighbors->get>(); + for (uint64_t timestamp: neighborTimestamps) { + const String neighborName = std::to_string(timestamp); + const IIndex neighborID = mapImageName.at(neighborName); + if (neighborID != imageData.ID) + imageData.neighbors.emplace_back(ViewScore{ViewInfo{neighborID, 0, 1.f, FD2R(15.f), 0.5f}, 3.f}); + } + } + // load and convert depth-map + DepthMap depthMap; { + constexpr double depthScale{1000.0}; + const cv::Mat imgDepthMap = cv::imread(depthPath, cv::IMREAD_ANYDEPTH); + if (imgDepthMap.empty()) + return false; + imgDepthMap.convertTo(depthMap, CV_32FC1, 1.0/depthScale); + } + IIndexArr IDs = {imageData.ID}; + IDs.JoinFunctor(imageData.neighbors.size(), [&imageData](IIndex i) { + return imageData.neighbors[i].idx.ID; + }); + double dMin, dMax; + cv::minMaxIdx(depthMap, &dMin, &dMax, NULL, NULL, depthMap > 0); + const NormalMap normalMap; + const ConfidenceMap confMap; + const ViewsMap viewsMap; + if (!ExportDepthDataRaw(MAKE_PATH(String::FormatString("depth%04u.dmap", imageData.ID)), + imageData.name, IDs, resolution, + camera.K, pose.R, pose.C, + (float)dMin, (float)dMax, + depthMap, normalMap, confMap, viewsMap)) + return false; + return true; +} + +// parse scene stored in Polycam format +bool ParseScene(Scene& scene, const String& scenePath) +{ + #ifdef _SUPPORT_CPP17 + size_t numCorrectedFolders(0), numFolders(0); + for (const auto& file: std::filesystem::directory_iterator(scenePath.c_str())) { + if (file.path().stem() == "corrected_cameras" || + file.path().stem() == "corrected_depth" || + file.path().stem() == "corrected_images") + ++numCorrectedFolders; + else + if (file.path().stem() == "cameras" || + file.path().stem() == "depth" || + file.path().stem() == "images") + ++numFolders; + } + if (numFolders != 3) { + VERBOSE("Invalid scene folder"); + return false; + } + if (numCorrectedFolders == 3) { + // corrected data + CLISTDEFIDX(String, IIndex) imagePaths; + for (const auto& file: std::filesystem::directory_iterator((scenePath + "corrected_images").c_str())) + imagePaths.emplace_back(file.path().string()); + VERBOSE("Parsing corrected data: %u...", imagePaths.size()); + std::unordered_map mapImageName; + mapImageName.reserve(imagePaths.size()); + for (String& imagePath: imagePaths) { + Util::ensureValidPath(imagePath); + mapImageName.emplace(Util::getFileName(imagePath), static_cast(mapImageName.size())); + } + for (const String& imagePath: imagePaths) { + const String imageName = Util::getFileName(imagePath); + const String cameraPath(scenePath + "corrected_cameras" + PATH_SEPARATOR_STR + imageName + JSON_EXT); + const String depthPath(scenePath + "corrected_depth" + PATH_SEPARATOR_STR + imageName + DEPTH_EXT); + if (!ParseImage(scene, imagePath, cameraPath, depthPath, mapImageName)) + return false; + } + } else { + // raw data + CLISTDEFIDX(String, IIndex) imagePaths; + for (const auto& file: std::filesystem::directory_iterator((scenePath + "images").c_str())) + imagePaths.emplace_back(file.path().string()); + VERBOSE("Parsing raw data: %u...", imagePaths.size()); + std::unordered_map mapImageName; + mapImageName.reserve(imagePaths.size()); + for (String& imagePath: imagePaths) { + Util::ensureValidPath(imagePath); + mapImageName.emplace(Util::getFileName(imagePath), static_cast(mapImageName.size())); + } + for (const String& imagePath: imagePaths) { + const String imageName = Util::getFileName(imagePath); + const String cameraPath(scenePath + "cameras" + PATH_SEPARATOR_STR + imageName + JSON_EXT); + const String depthPath(scenePath + "depth" + PATH_SEPARATOR_STR + imageName + DEPTH_EXT); + if (!ParseImage(scene, imagePath, cameraPath, depthPath, mapImageName)) + return false; + } + } + return true; + #else + return false; + #endif +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + if (!Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + Scene scene(OPT::nMaxThreads); + + // convert data from Polycam format to OpenMVS + if (!ParseScene(scene, MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName))) + return EXIT_FAILURE; + + // write OpenMVS input data + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + + VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images (%s)", + scene.platforms.size(), scene.images.size(), scene.images.size(), scene.images.size(), + TD_TIMER_GET_FMT().c_str()); + + Finalize(); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp b/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp index 8398b6cf4..3bab64dc1 100644 --- a/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp +++ b/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp @@ -270,9 +270,9 @@ void UndistortImage(const Camera& camera, const REAL& k1, const Image8U3 imgIn, for (int u=0; u(&OPT::strMeshFileName), "mesh file name to clean (skips the reconstruction step)") ("mesh-export", boost::program_options::value(&OPT::bMeshExport)->default_value(false), "just export the mesh contained in loaded project") ("split-max-area", boost::program_options::value(&OPT::fSplitMaxArea)->default_value(0.f), "maximum surface area that a sub-mesh can contain (0 - disabled)") + ("import-roi-file", boost::program_options::value(&OPT::strImportROIFileName), "ROI file name to be imported into the scene") ("image-points-file", boost::program_options::value(&OPT::strImagePointsFileName), "input filename containing the list of points from an image to project on the mesh (optional)") ; @@ -181,7 +183,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // validate input Util::ensureValidPath(OPT::strInputFileName); - Util::ensureUnifySlash(OPT::strInputFileName); if (OPT::vm.count("help") || OPT::strInputFileName.IsEmpty()) { boost::program_options::options_description visible("Available options"); visible.add(generic).add(config_main).add(config_clean); @@ -193,7 +194,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); + Util::ensureValidPath(OPT::strImportROIFileName); Util::ensureValidPath(OPT::strImagePointsFileName); if (OPT::strOutputFileName.IsEmpty()) OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_mesh.mvs"); @@ -344,7 +345,7 @@ int main(int argc, LPCTSTR* argv) Scene scene(OPT::nMaxThreads); // load project - if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), OPT::fSplitMaxArea > 0 || OPT::fDecimateMesh < 1 || OPT::nTargetFaceNum > 0)) + if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), OPT::fSplitMaxArea > 0 || OPT::fDecimateMesh < 1 || OPT::nTargetFaceNum > 0 || !OPT::strImportROIFileName.empty())) return EXIT_FAILURE; const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); if (OPT::fSplitMaxArea > 0) { @@ -356,6 +357,24 @@ int main(int argc, LPCTSTR* argv) return EXIT_SUCCESS; } + if (!OPT::strImportROIFileName.empty()) { + std::ifstream fs(MAKE_PATH_SAFE(OPT::strImportROIFileName)); + if (!fs) + return EXIT_FAILURE; + fs >> scene.obb; + if (OPT::bCrop2ROI && !scene.mesh.IsEmpty() && !scene.IsValid()) { + TD_TIMER_START(); + const size_t numVertices = scene.mesh.vertices.size(); + const size_t numFaces = scene.mesh.faces.size(); + scene.mesh.RemoveFacesOutside(scene.obb); + VERBOSE("Mesh trimmed to ROI: %u vertices and %u faces removed (%s)", + numVertices-scene.mesh.vertices.size(), numFaces-scene.mesh.faces.size(), TD_TIMER_GET_FMT().c_str()); + scene.mesh.Save(baseFileName+OPT::strExportType); + Finalize(); + return EXIT_SUCCESS; + } + } + if (!OPT::strImagePointsFileName.empty() && !scene.mesh.IsEmpty()) { Export3DProjections(scene, MAKE_PATH_SAFE(OPT::strImagePointsFileName)); return EXIT_SUCCESS; diff --git a/apps/RefineMesh/RefineMesh.cpp b/apps/RefineMesh/RefineMesh.cpp index 7b68db118..c0960935d 100644 --- a/apps/RefineMesh/RefineMesh.cpp +++ b/apps/RefineMesh/RefineMesh.cpp @@ -115,14 +115,14 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("close-holes", boost::program_options::value(&OPT::nCloseHoles)->default_value(30), "try to close small holes in the input surface (0 - disabled)") ("ensure-edge-size", boost::program_options::value(&OPT::nEnsureEdgeSize)->default_value(1), "ensure edge size and improve vertex valence of the input surface (0 - disabled, 1 - auto, 2 - force)") ("max-face-area", boost::program_options::value(&OPT::nMaxFaceArea)->default_value(32), "maximum face area projected in any pair of images that is not subdivided (0 - disabled)") - ("scales", boost::program_options::value(&OPT::nScales)->default_value(3), "how many iterations to run mesh optimization on multi-scale images") + ("scales", boost::program_options::value(&OPT::nScales)->default_value(2), "how many iterations to run mesh optimization on multi-scale images") ("scale-step", boost::program_options::value(&OPT::fScaleStep)->default_value(0.5f), "image scale factor used at each mesh optimization step") - ("reduce-memory", boost::program_options::value(&OPT::nReduceMemory)->default_value(1), "recompute some data in order to reduce memory requirements") ("alternate-pair", boost::program_options::value(&OPT::nAlternatePair)->default_value(0), "refine mesh using an image pair alternatively as reference (0 - both, 1 - alternate, 2 - only left, 3 - only right)") ("regularity-weight", boost::program_options::value(&OPT::fRegularityWeight)->default_value(0.2f), "scalar regularity weight to balance between photo-consistency and regularization terms during mesh optimization") ("rigidity-elasticity-ratio", boost::program_options::value(&OPT::fRatioRigidityElasticity)->default_value(0.9f), "scalar ratio used to compute the regularity gradient as a combination of rigidity and elasticity") ("gradient-step", boost::program_options::value(&OPT::fGradientStep)->default_value(45.05f), "gradient step to be used instead (0 - auto)") ("planar-vertex-ratio", boost::program_options::value(&OPT::fPlanarVertexRatio)->default_value(0.f), "threshold used to remove vertices on planar patches (0 - disabled)") + ("reduce-memory", boost::program_options::value(&OPT::nReduceMemory)->default_value(1), "recompute some data in order to reduce memory requirements") ; // hidden options, allowed both on command line and @@ -167,7 +167,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // validate input Util::ensureValidPath(OPT::strInputFileName); - Util::ensureUnifySlash(OPT::strInputFileName); if (OPT::vm.count("help") || OPT::strInputFileName.IsEmpty()) { boost::program_options::options_description visible("Available options"); visible.add(generic).add(config); @@ -179,7 +178,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); if (OPT::strOutputFileName.IsEmpty()) OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_refine.mvs"); @@ -252,11 +250,12 @@ int main(int argc, LPCTSTR* argv) OPT::fDecimateMesh, OPT::nCloseHoles, OPT::nEnsureEdgeSize, OPT::nMaxFaceArea, OPT::nScales, OPT::fScaleStep, - OPT::nReduceMemory, OPT::nAlternatePair, + OPT::nAlternatePair, OPT::fRegularityWeight, OPT::fRatioRigidityElasticity, + OPT::fGradientStep, OPT::fPlanarVertexRatio, - OPT::fGradientStep)) + OPT::nReduceMemory)) return EXIT_FAILURE; VERBOSE("Mesh refinement completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); diff --git a/apps/TextureMesh/TextureMesh.cpp b/apps/TextureMesh/TextureMesh.cpp index e2b1dc1c4..8409a2ef2 100644 --- a/apps/TextureMesh/TextureMesh.cpp +++ b/apps/TextureMesh/TextureMesh.cpp @@ -63,6 +63,7 @@ unsigned nTextureSizeMultiple; unsigned nRectPackingHeuristic; uint32_t nColEmpty; float fSharpnessWeight; +int nIgnoreMaskLabel; unsigned nOrthoMapResolution; unsigned nArchiveType; int nProcessPriority; @@ -122,6 +123,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("empty-color", boost::program_options::value(&OPT::nColEmpty)->default_value(0x00FF7F27), "color used for faces not covered by any image") ("sharpness-weight", boost::program_options::value(&OPT::fSharpnessWeight)->default_value(0.5f), "amount of sharpness to be applied on the texture (0 - disabled)") ("orthographic-image-resolution", boost::program_options::value(&OPT::nOrthoMapResolution)->default_value(0), "orthographic image resolution to be generated from the textured mesh - the mesh is expected to be already geo-referenced or at least properly oriented (0 - disabled)") + ("ignore-mask-label", boost::program_options::value(&OPT::nIgnoreMaskLabel)->default_value(-1), "label value to ignore in the image mask, stored in the MVS scene or next to each image with '.mask.png' extension (-1 - auto estimate mask for lens distortion, -2 - disabled)") ; // hidden options, allowed both on command line and @@ -167,7 +169,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // validate input Util::ensureValidPath(OPT::strInputFileName); - Util::ensureUnifySlash(OPT::strInputFileName); if (OPT::vm.count("help") || OPT::strInputFileName.IsEmpty()) { boost::program_options::options_description visible("Available options"); visible.add(generic).add(config); @@ -189,7 +190,6 @@ bool Initialize(size_t argc, LPCTSTR* argv) // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); if (OPT::strOutputFileName.IsEmpty()) OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_texture.mvs"); Util::ensureValidPath(OPT::strViewsFileName); @@ -305,7 +305,9 @@ int main(int argc, LPCTSTR* argv) // compute mesh texture TD_TIMER_START(); - if (!scene.TextureMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::minCommonCameras, OPT::fOutlierThreshold, OPT::fRatioDataSmoothness, OPT::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty), OPT::fSharpnessWeight, views)) + if (!scene.TextureMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::minCommonCameras, OPT::fOutlierThreshold, OPT::fRatioDataSmoothness, + OPT::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty), + OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, views)) return EXIT_FAILURE; VERBOSE("Mesh texturing completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); diff --git a/apps/TransformScene/TransformScene.cpp b/apps/TransformScene/TransformScene.cpp index 580750fac..ec293a7af 100644 --- a/apps/TransformScene/TransformScene.cpp +++ b/apps/TransformScene/TransformScene.cpp @@ -50,7 +50,9 @@ namespace OPT { String strInputFileName; String strOutputFileName; String strAlignFileName; + String strTransformFileName; String strTransferTextureFileName; + String strIndicesFileName; bool bComputeVolume; float fPlaneThreshold; float fSampleMesh; @@ -58,6 +60,7 @@ namespace OPT { unsigned nArchiveType; int nProcessPriority; unsigned nMaxThreads; + String strExportType; String strConfigFileName; boost::program_options::variables_map vm; } // namespace OPT @@ -75,6 +78,7 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("help,h", "produce this help message") ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType)->default_value(_T("ply")), "file type used to export the 3D scene (ply, obj, glb or gltf)") ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: 0-text, 1-binary, 2-compressed binary") ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") @@ -95,7 +99,9 @@ bool Initialize(size_t argc, LPCTSTR* argv) ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input scene filename") ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") ("align-file,a", boost::program_options::value(&OPT::strAlignFileName), "input scene filename to which the scene will be cameras aligned") - ("transfer-texture-file,t", boost::program_options::value(&OPT::strTransferTextureFileName), "input mesh filename to which the texture of the scene's mesh will be transfered to (the two meshes should be aligned and the new mesh to have UV-map)") + ("transform-file,t", boost::program_options::value(&OPT::strTransformFileName), "input transform filename by which the scene will transformed") + ("transfer-texture-file", boost::program_options::value(&OPT::strTransferTextureFileName), "input mesh filename to which the texture of the scene's mesh will be transfered to (the two meshes should be aligned and the new mesh to have UV-map)") + ("indices-file", boost::program_options::value(&OPT::strIndicesFileName), "input indices filename to be used with ex. texture transfer to select a subset of the scene's mesh") ("compute-volume", boost::program_options::value(&OPT::bComputeVolume)->default_value(false), "compute the volume of the given watertight mesh, or else try to estimate the ground plane and assume the mesh is bounded by it") ("plane-threshold", boost::program_options::value(&OPT::fPlaneThreshold)->default_value(0.f), "threshold used to estimate the ground plane (<0 - disabled, 0 - auto, >0 - desired threshold)") ("sample-mesh", boost::program_options::value(&OPT::fSampleMesh)->default_value(-300000.f), "uniformly samples points on a mesh (0 - disabled, <0 - number of points, >0 - sample density per square unit)") @@ -138,9 +144,12 @@ bool Initialize(size_t argc, LPCTSTR* argv) // validate input Util::ensureValidPath(OPT::strInputFileName); Util::ensureValidPath(OPT::strAlignFileName); + Util::ensureValidPath(OPT::strTransformFileName); Util::ensureValidPath(OPT::strTransferTextureFileName); + Util::ensureValidPath(OPT::strIndicesFileName); const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); - const bool bInvalidCommand(OPT::strInputFileName.empty() || (OPT::strAlignFileName.empty() && OPT::strTransferTextureFileName.empty() && !OPT::bComputeVolume)); + const bool bInvalidCommand(OPT::strInputFileName.empty() || + (OPT::strAlignFileName.empty() && OPT::strTransformFileName.empty() && OPT::strTransferTextureFileName.empty() && !OPT::bComputeVolume)); if (OPT::vm.count("help") || bInvalidCommand) { boost::program_options::options_description visible("Available options"); visible.add(generic).add(config); @@ -148,10 +157,20 @@ bool Initialize(size_t argc, LPCTSTR* argv) } if (bInvalidCommand) return false; + OPT::strExportType = OPT::strExportType.ToLower(); + if (OPT::strExportType == _T("obj")) + OPT::strExportType = _T(".obj"); + else + if (OPT::strExportType == _T("glb")) + OPT::strExportType = _T(".glb"); + else + if (OPT::strExportType == _T("gltf")) + OPT::strExportType = _T(".gltf"); + else + OPT::strExportType = _T(".ply"); // initialize optional options Util::ensureValidPath(OPT::strOutputFileName); - Util::ensureUnifySlash(OPT::strOutputFileName); if (OPT::strOutputFileName.IsEmpty()) OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + "_transformed" MVS_EXT; @@ -199,7 +218,7 @@ int main(int argc, LPCTSTR* argv) Scene scene(OPT::nMaxThreads); // load given scene - if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), !OPT::strTransferTextureFileName.empty() || OPT::bComputeVolume)) + if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), !OPT::strTransformFileName.empty() || !OPT::strTransferTextureFileName.empty() || OPT::bComputeVolume)) return EXIT_FAILURE; if (!OPT::strAlignFileName.empty()) { @@ -212,15 +231,51 @@ int main(int argc, LPCTSTR* argv) VERBOSE("Scene aligned to the given reference scene (%s)", TD_TIMER_GET_FMT().c_str()); } + if (!OPT::strTransformFileName.empty()) { + // transform this scene by the given transform matrix + std::ifstream file(MAKE_PATH_SAFE(OPT::strTransformFileName)); + std::string strLine; + std::vector transformValues; + while (std::getline(file, strLine)) { + errno = 0; + char* strEnd{}; + const double v = std::strtod(strLine.c_str(), &strEnd); + if (errno == ERANGE || strEnd == strLine.c_str()) + continue; + transformValues.push_back(v); + } + if (transformValues.size() != 12 && + (transformValues.size() != 16 || transformValues[12] != 0 || transformValues[13] != 0 || transformValues[14] != 0 || transformValues[15] != 1)) { + VERBOSE("error: invalid transform"); + return EXIT_FAILURE; + } + Matrix3x4 transform; + for (unsigned i=0; i<12; ++i) + transform[i] = transformValues[i]; + scene.Transform(transform); + VERBOSE("Scene transformed by the given transformation matrix (%s)", TD_TIMER_GET_FMT().c_str()); + } + if (!OPT::strTransferTextureFileName.empty()) { // transfer the texture of the scene's mesh to the new mesh; // the two meshes should be aligned and the new mesh to have UV-coordinates Mesh newMesh; if (!newMesh.Load(MAKE_PATH_SAFE(OPT::strTransferTextureFileName))) return EXIT_FAILURE; - if (!scene.mesh.TransferTexture(newMesh)) + Mesh::FaceIdxArr faceSubsetIndices; + if (!OPT::strIndicesFileName.empty()) { + std::ifstream in(OPT::strIndicesFileName.c_str()); + while (true) { + String index; + in >> index; + if (!in.good()) + break; + faceSubsetIndices.emplace_back(index.From()); + } + } + if (!scene.mesh.TransferTexture(newMesh, faceSubsetIndices)) return EXIT_FAILURE; - newMesh.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + _T(".ply")); + newMesh.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + OPT::strExportType); VERBOSE("Texture transfered (%s)", TD_TIMER_GET_FMT().c_str()); return EXIT_SUCCESS; } @@ -229,14 +284,19 @@ int main(int argc, LPCTSTR* argv) // compute the mesh volume const REAL volume(scene.ComputeLeveledVolume(OPT::fPlaneThreshold, OPT::fSampleMesh, OPT::nUpAxis)); VERBOSE("Mesh volume: %g (%s)", volume, TD_TIMER_GET_FMT().c_str()); - scene.mesh.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + _T(".ply")); + scene.mesh.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + OPT::strExportType); if (scene.images.empty()) return EXIT_SUCCESS; OPT::nArchiveType = ARCHIVE_DEFAULT; } // write transformed scene - scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + if (scene.IsValid()) + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + else if (!scene.pointcloud.IsEmpty()) + scene.pointcloud.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + ".ply"); + else if (!scene.mesh.IsEmpty()) + scene.mesh.Save(Util::getFileFullName(MAKE_PATH_SAFE(OPT::strOutputFileName)) + ".ply"); Finalize(); return EXIT_SUCCESS; diff --git a/apps/Viewer/Scene.cpp b/apps/Viewer/Scene.cpp index 1ea0431a8..69683dc75 100644 --- a/apps/Viewer/Scene.cpp +++ b/apps/Viewer/Scene.cpp @@ -235,7 +235,10 @@ bool Scene::Open(LPCTSTR fileName, LPCTSTR meshFileName) return false; if (meshFileName) { // load given mesh - scene.mesh.Load(meshFileName); + if (!scene.mesh.Load(meshFileName)) { + // try to load as a point-cloud + scene.pointcloud.Load(meshFileName); + } } if (!scene.pointcloud.IsEmpty()) scene.pointcloud.PrintStatistics(scene.images.data(), &scene.obb); diff --git a/build/Templates/ConfigLocal.h.in b/build/Templates/ConfigLocal.h.in index 24ba104d3..39ac2925a 100644 --- a/build/Templates/ConfigLocal.h.in +++ b/build/Templates/ConfigLocal.h.in @@ -24,6 +24,9 @@ // Boost support #cmakedefine _USE_BOOST +// Boost with Python support +#cmakedefine _USE_BOOST_PYTHON + // Eigen Matrix & Linear Algebra Library #cmakedefine _USE_EIGEN @@ -53,6 +56,9 @@ // Fast float to int support #cmakedefine _USE_FAST_FLOAT2INT +// Fast INVSQRT support +#cmakedefine _USE_FAST_INVSQRT + // Fast CBRT support #cmakedefine _USE_FAST_CBRT diff --git a/build/Utils.cmake b/build/Utils.cmake index 0cb2d8e1a..9c2430faa 100644 --- a/build/Utils.cmake +++ b/build/Utils.cmake @@ -646,12 +646,8 @@ macro(optimize_default_compiler_settings) # Extra link libs if the user selects building static libs: # Android does not need these settings because they are already set by toolchain file - if(CMAKE_COMPILER_IS_GNUCXX AND NOT ANDROID) - if(BUILD_SHARED_LIBS) - set(BUILD_EXTRA_FLAGS "${BUILD_EXTRA_FLAGS} -fPIC") - else() - set(BUILD_EXTRA_LINKER_LIBS "${BUILD_EXTRA_LINKER_LIBS} stdc++") - endif() + if(CMAKE_COMPILER_IS_GNUCXX AND NOT ANDROID AND NOT BUILD_SHARED_LIBS) + set(BUILD_EXTRA_LINKER_LIBS "${BUILD_EXTRA_LINKER_LIBS} stdc++") endif() # Add user supplied extra options (optimization, etc...) @@ -839,6 +835,9 @@ function(cxx_library_with_type name folder type cxx_flags) endif() # Set project folder set_target_properties("${name}" PROPERTIES FOLDER "${folder}") + if(BUILD_SHARED_LIBS OR PARTIAL_BUILD_SHARED_LIBS) + set_target_properties("${name}" PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() endfunction() # cxx_executable_with_flags(name cxx_flags libs srcs...) diff --git a/docker/Dockerfile b/docker/Dockerfile index 8fb77b421..95a5c5d43 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,48 +1,15 @@ -FROM ubuntu:22.04 +ARG BASE_IMAGE=ubuntu:22.04 -ARG MASTER +FROM $BASE_IMAGE + +ARG MASTER=0 ARG USER_ID ARG GROUP_ID +ARG CUDA=0 -# Prepare and empty machine for building: -RUN apt-get update -yq -RUN apt-get -yq install build-essential git cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev - -# Eigen -RUN git clone https://gitlab.com/libeigen/eigen --branch 3.4 -RUN mkdir eigen_build -RUN cd eigen_build &&\ - cmake . ../eigen &&\ - make && make install &&\ - cd .. - -# Boost -RUN apt-get -y install libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev - -# OpenCV -RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq libopencv-dev - -# CGAL -RUN apt-get -yq install libcgal-dev libcgal-qt5-dev - -# VCGLib -RUN git clone https://github.com/cdcseacave/VCG.git vcglib - -# Build from stable openMVS release or the latest commit from the develop branch -RUN if [[ -n "$MASTER" ]] ; then git clone https://github.com/cdcseacave/openMVS.git --branch master ; else git clone https://github.com/cdcseacave/openMVS.git --branch develop ; fi - -RUN mkdir openMVS_build -RUN cd openMVS_build &&\ - cmake . ../openMVS -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT=/vcglib -DOpenMVS_USE_CUDA=OFF - -# Install OpenMVS library -RUN cd openMVS_build &&\ - make -j4 &&\ - make install +COPY buildInDocker.sh /tmp/buildInDocker.sh +RUN /tmp/buildInDocker.sh --cuda $CUDA --user_id $USER_ID --group_id $GROUP_ID --master $MASTER && rm /tmp/buildInDocker.sh -# Set permissions such that the output files can be accessed by the current user (optional) -RUN addgroup --gid $GROUP_ID user &&\ - adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user USER user # Add binaries to path diff --git a/docker/Dockerfile_CUDA b/docker/Dockerfile_CUDA deleted file mode 100644 index 46170d2ef..000000000 --- a/docker/Dockerfile_CUDA +++ /dev/null @@ -1,51 +0,0 @@ -FROM nvidia/cuda:11.7.1-devel-ubuntu22.04 - -ARG MASTER -ARG USER_ID -ARG GROUP_ID - -# Prepare and empty machine for building: -RUN apt-get update -yq -RUN apt-get -yq install build-essential git cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev - -ENV PATH=/usr/local/cuda/bin:$PATH - -# Eigen -RUN git clone https://gitlab.com/libeigen/eigen --branch 3.4 -RUN mkdir eigen_build -RUN cd eigen_build &&\ - cmake . ../eigen -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda/ &&\ - make && make install &&\ - cd .. - -# Boost -RUN apt-get -yq install libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev - -# OpenCV -RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install libopencv-dev - -# CGAL -RUN apt-get -yq install libcgal-dev libcgal-qt5-dev - -# VCGLib -RUN git clone https://github.com/cdcseacave/VCG.git vcglib - -# Build from stable openMVS release or the latest commit from the develop branch -RUN if [[ -n "$MASTER" ]] ; then git clone https://github.com/cdcseacave/openMVS.git --branch master ; else git clone https://github.com/cdcseacave/openMVS.git --branch develop ; fi - -RUN mkdir openMVS_build -RUN cd openMVS_build &&\ - cmake . ../openMVS -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT=/vcglib -DOpenMVS_USE_CUDA=ON -DCMAKE_LIBRARY_PATH=/usr/local/cuda/lib64/stubs/ -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda/ -DCUDA_INCLUDE_DIRS=/usr/local/cuda/include/ -DCUDA_CUDART_LIBRARY=/usr/local/cuda/lib64 -DCUDA_NVCC_EXECUTABLE=/usr/local/cuda/bin/ - -# Install OpenMVS library -RUN cd openMVS_build &&\ - make -j4 &&\ - make install - -# Set permissions such that the output files can be accessed by the current user (optional) -RUN addgroup --gid $GROUP_ID user &&\ - adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user -USER user - -# Add binaries to path -ENV PATH /usr/local/bin/OpenMVS:$PATH diff --git a/docker/README.md b/docker/README.md index d0008569e..f80a7cefc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,9 @@ 1. Make sure docker is installed on your local machine. 2. Run the 'easy start' script, using the *full local path* to the folder with your SFM input files (perhaps output from openMVG or COLMAP): - ./QUICK_START.sh /path/where/your/SFM/results/are +``` +./QUICK_START.sh /path/where/your/SFM/results/are +``` 3. This will put you in a directory (inside the Docker container) mounted to the local path you specified so that you can run openMVS binaries on your own SFM inputs. Enjoy! @@ -14,12 +16,20 @@ You can also build the docker image from scratch based on the **Dockerfile** (perhaps with your own changes / modifications) using: - ./buildFromScratch.sh /path/where/your/SFM/results/are +```sh +./buildFromScratch.sh --workspace /path/where/your/SFM/results/are +# With CUDA support: +./buildFromScratch.sh --cuda --workspace /path/where/your/SFM/results/are +# From master branch: +./buildFromScratch.sh --master --workspace /path/where/your/SFM/results/are +``` ## NOTES + This workflow is pinned to build from [openMVS 1.0](https://github.com/cdcseacave/openMVS/releases/tag/v1.0). To build from a different release, or build from the latest commit on master, open up the Dockerfile and comment/uncomment as directed. ++ Display is also supported inside docker. If there are any issues with `xauth` make sure to run `xhost +` on your host machine. + + Running openMVS binaries can use a lot of memory (depending on the size of your data set/ imagery). Docker has a relatively small default memory setting (2Gb on Mac). You will probably want to increase this before you run any larger workflows. From Docker desktop on Mac for example, just open the Docker GUI, go to the *Advanced* tab and increase via the slider: ![alt text][dockerParam] diff --git a/docker/buildFromScratch.cmd b/docker/buildFromScratch.cmd new file mode 100644 index 000000000..bbb10a12b --- /dev/null +++ b/docker/buildFromScratch.cmd @@ -0,0 +1,49 @@ +@echo off + +set WORKSPACE=%CD% + +set CUDA_BUILD_ARGS= +set CUDA_RUNTIME_ARGS= +set CUDA_CONTAINER_SUFFIX= +set MASTER_ARGS= +set DISPLAY_ARGS= + +:parse_args +if "%~1" == "" goto build_image +if "%~1" == "--cuda" ( + set "CUDA_BUILD_ARGS=--build-arg CUDA=1 --build-arg BASE_IMAGE=nvidia/cuda:11.8.0-devel-ubuntu22.04" + set "CUDA_RUNTIME_ARGS=--gpus all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics" + set "CUDA_CONTAINER_SUFFIX=-cuda" + shift + goto parse_args +) +if "%~1" == "--master" ( + set "MASTER_ARGS=--build-arg MASTER=1" + shift + goto parse_args +) +if "%~1" == "--workspace" ( + set "WORKSPACE=%~2" + shift + shift + goto parse_args +) +if "%~1" == "--gui" ( + set "XSOCK=\\.\pipe\X11-unix" + set "XAUTH=%TEMP%\docker.xauth" + set "DISPLAY_ARGS=-e ^"DISPLAY=%COMPUTERNAME%:0.0^" -v ^"%XAUTH%:%XAUTH%:rw^" -e ^"XAUTHORITY=%XAUTH%^"" + shift + goto parse_args +) +echo Unknown argument: %~1 +exit /b 1 + +:build_image +echo Running with workspace: %WORKSPACE% + +docker build -t="openmvs-ubuntu%CUDA_CONTAINER_SUFFIX%" --build-arg "USER_ID=1001" --build-arg "GROUP_ID=1001" %CUDA_BUILD_ARGS% %MASTER_ARGS% . +if errorlevel 1 exit /b 1 + +docker run %CUDA_RUNTIME_ARGS% --entrypoint bash --ipc=host --shm-size=4gb -w /work -v "%WORKSPACE%:/work" %DISPLAY_ARGS% -it openmvs-ubuntu%CUDA_CONTAINER_SUFFIX% + +exit /b 0 diff --git a/docker/buildFromScratch.sh b/docker/buildFromScratch.sh index c3b873d74..3d3e6dd73 100755 --- a/docker/buildFromScratch.sh +++ b/docker/buildFromScratch.sh @@ -1,2 +1,47 @@ -docker build --no-cache -t="openmvs-ubuntu" --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) .; -docker run --entrypoint bash -w /work -v $1:/work -it openmvs-ubuntu; +#!/bin/bash + +# Example use: +# ./buildFromScratch.sh --cuda --master --workspace /home/username/datapath/ + +WORKSPACE=$(pwd) + + +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + --cuda) + CUDA_BUILD_ARGS="--build-arg CUDA=1 --build-arg BASE_IMAGE=nvidia/cuda:11.8.0-devel-ubuntu22.04" + + CUDA_RUNTIME_ARGS="--gpus all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,graphics" + + CUDA_CONTAINER_SUFFIX="-cuda" + shift + ;; + --master) + MASTER_ARGS="--build-arg MASTER=1" + shift + ;; + --workspace) + WORKSPACE=$2 + shift + shift + ;; + *) + echo "Unknown argument: $key" + exit 1 + ;; + esac +done + +# no need to do `xhost +` anymore +XSOCK=/tmp/.X11-unix +XAUTH=/tmp/.docker.xauth +touch $XAUTH +xauth nlist "$DISPLAY" | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - +DISPLAY_ARGS="--volume=$XSOCK:$XSOCK:rw --volume=$XAUTH:$XAUTH:rw --env=XAUTHORITY=$XAUTH --env=DISPLAY=unix$DISPLAY" + +echo Running with workspace: "$WORKSPACE" + +docker build -t="openmvs-ubuntu$CUDA_CONTAINER_SUFFIX" --build-arg "USER_ID=$(id -u)" --build-arg "GROUP_ID=$(id -g)" $CUDA_BUILD_ARGS $MASTER_ARGS . +docker run $CUDA_RUNTIME_ARGS --entrypoint bash --ipc=host --shm-size=4gb -w /work -v "$WORKSPACE:/work" $DISPLAY_ARGS -it openmvs-ubuntu$CUDA_CONTAINER_SUFFIX + diff --git a/docker/buildFromScratch_CUDA.sh b/docker/buildFromScratch_CUDA.sh deleted file mode 100644 index eb9149fd5..000000000 --- a/docker/buildFromScratch_CUDA.sh +++ /dev/null @@ -1,2 +0,0 @@ -docker build --no-cache -t="openmvs-ubuntu" --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) --file Dockerfile_CUDA .; -docker run --gpus=all --entrypoint bash -w /work -v $1:/work -it openmvs-ubuntu; diff --git a/docker/buildInDocker.sh b/docker/buildInDocker.sh new file mode 100755 index 000000000..c33917a88 --- /dev/null +++ b/docker/buildInDocker.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +set -e + +# Parse arguments to see if --cuda was passed and equals 1 or --master was passed and equals 1 +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + --cuda) + CUDA="$2" + shift + shift + ;; + --master) + MASTER="$2" + shift + shift + ;; + --user_id) + USER_ID="$2" + shift + shift + ;; + --group_id) + GROUP_ID="$2" + shift + shift + ;; + *) + echo "Unknown argument: $key" + exit 1 + ;; + esac +done + +if [[ "$CUDA" == "1" ]]; then + echo "Building with CUDA support" + EIGEN_BUILD_ARG="-DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda/" + OPENMVS_BUILD_ARG="-DOpenMVS_USE_CUDA=ON -DCMAKE_LIBRARY_PATH=/usr/local/cuda/lib64/stubs/ -DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda/ -DCUDA_INCLUDE_DIRS=/usr/local/cuda/include/ -DCUDA_CUDART_LIBRARY=/usr/local/cuda/lib64 -DCUDA_NVCC_EXECUTABLE=/usr/local/cuda/bin/" +else + echo "Building without CUDA support" + EIGEN_BUILD_ARG="" + OPENMVS_BUILD_ARG="-DOpenMVS_USE_CUDA=OFF" +fi + +if [[ "$MASTER" == "1" ]]; then + echo "Pulling from master branch" +else + echo "Pulling from develop branch" +fi + +apt-get update -yq + +apt-get -yq install build-essential git cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev libglew-dev libglfw3-dev + +# Eigen +git clone https://gitlab.com/libeigen/eigen --branch 3.4 +mkdir eigen_build +cd eigen_build &&\ + cmake . ../eigen $EIGEN_BUILD_ARG &&\ + make && make install &&\ + cd .. && rm -rf eigen_build eigen + +# Boost +apt-get -y install libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev + +# OpenCV +DEBIAN_FRONTEND=noninteractive apt-get install -yq libopencv-dev + +# CGAL +apt-get -yq install libcgal-dev libcgal-qt5-dev + +# VCGLib +git clone https://github.com/cdcseacave/VCG.git vcglib + +# Build from stable openMVS release or the latest commit from the develop branch +if [[ "$MASTER" == "1" ]]; then + git clone https://github.com/cdcseacave/openMVS.git --branch master +else + git clone https://github.com/cdcseacave/openMVS.git --branch develop +fi + +mkdir openMVS_build +cd openMVS_build &&\ + cmake . ../openMVS -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT=/vcglib $OPENMVS_BUILD_ARG + +# Install OpenMVS library +make -j4 &&\ + make install &&\ + cd .. && rm -rf openMVS_build vcglib + +# Set permissions such that the output files can be accessed by the current user (optional) +echo "Setting permissions for user $USER_ID:$GROUP_ID" +addgroup --gid $GROUP_ID user &&\ + adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user diff --git a/libs/Common/Common.h b/libs/Common/Common.h index 5db8877eb..eba19aeae 100644 --- a/libs/Common/Common.h +++ b/libs/Common/Common.h @@ -91,10 +91,10 @@ extern String g_strWorkingFolderFull; // full path to current folder #define WORKING_FOLDER_FULL g_strWorkingFolderFull // full path to current folder #endif #define INIT_WORKING_FOLDER {SEACAVE::Util::ensureValidFolderPath(WORKING_FOLDER); WORKING_FOLDER_FULL = SEACAVE::Util::getFullPath(WORKING_FOLDER);} // initialize working folders -#define MAKE_PATH(str) SEACAVE::Util::getSimplifiedPath(WORKING_FOLDER+(str)) // add working directory to the given file name -#define MAKE_PATH_SAFE(str) (SEACAVE::Util::isFullPath(str) ? SEACAVE::String(str) : MAKE_PATH(str)) // add working directory to the given file name only if not full path already +#define MAKE_PATH(str) SEACAVE::Util::getSimplifiedPath(WORKING_FOLDER+SEACAVE::String(str)) // add working directory to the given file name +#define MAKE_PATH_SAFE(str) (SEACAVE::Util::isFullPath((str).c_str()) ? SEACAVE::String(str) : MAKE_PATH(str)) // add working directory to the given file name only if not full path already #define MAKE_PATH_FULL(p,s) (SEACAVE::Util::isFullPath((s).c_str()) ? SEACAVE::String(s) : SEACAVE::Util::getSimplifiedPath((p)+(s))) // add the given path to the given file name -#define MAKE_PATH_REL(p,s) ((s).compare(0,(p).size(),p) ? SEACAVE::String(s) : SEACAVE::String(SEACAVE::String((s).substr((p).size())))) // remove the given path from the given file name +#define MAKE_PATH_REL(p,s) SEACAVE::Util::getRelativePath(p,s) // remove the given path from the given file name #define GET_PATH_FULL(str) (SEACAVE::Util::isFullPath((str).c_str()) ? SEACAVE::Util::getFilePath(str) : SEACAVE::Util::getSimplifiedPath(WORKING_FOLDER_FULL+SEACAVE::Util::getFilePath(str))) // retrieve the full path to the given file @@ -188,6 +188,8 @@ typedef TOBB OBB2f; typedef TOBB OBB3f; typedef TRay Ray2f; typedef TRay Ray3f; +typedef TLine Line2f; +typedef TLine Line3f; typedef TTriangle Triangle2f; typedef TTriangle Triangle3f; typedef TPlane Planef; @@ -209,6 +211,8 @@ typedef TOBB OBB2d; typedef TOBB OBB3d; typedef TRay Ray2d; typedef TRay Ray3d; +typedef TLine Line2d; +typedef TLine Line3d; typedef TTriangle Triangle2d; typedef TTriangle Triangle3d; typedef TPlane Planed; @@ -230,6 +234,8 @@ typedef TOBB OBB2; typedef TOBB OBB3; typedef TRay Ray2; typedef TRay Ray3; +typedef TLine Line2; +typedef TLine Line3; typedef TTriangle Triangle2; typedef TTriangle Triangle3; typedef TPlane Plane; diff --git a/libs/Common/Config.h b/libs/Common/Config.h index cc37aab23..d74b5c8df 100644 --- a/libs/Common/Config.h +++ b/libs/Common/Config.h @@ -10,6 +10,8 @@ // Configure everything that needs to be globally known +#include "ConfigLocal.h" + // D E F I N E S /////////////////////////////////////////////////// diff --git a/libs/Common/Filters.h b/libs/Common/Filters.h index 55218cc59..e84f3547c 100644 --- a/libs/Common/Filters.h +++ b/libs/Common/Filters.h @@ -103,7 +103,7 @@ class BufferedOutputStream : public LayerOutputStream { if (this->s->write(buf, bufSize) == STREAM_ERROR) return STREAM_ERROR; pos = 0; - if (len >= bufSize) { + if (len < bufSize) { if (this->s->write(b, len) == STREAM_ERROR) return STREAM_ERROR; break; diff --git a/libs/Common/Line.h b/libs/Common/Line.h new file mode 100644 index 000000000..408a1a864 --- /dev/null +++ b/libs/Common/Line.h @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////// +// Line.h +// +// Copyright 2023 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_LINE_H__ +#define __SEACAVE_LINE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Generic line class represented as two points +template +class TLine +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TRay RAY; + enum { numScalar = (2*DIMS) }; + enum { numParams = numScalar-1 }; + + POINT pt1, pt2; // line description + + //--------------------------------------- + + inline TLine() {} + inline TLine(const POINT& pt1, const POINT& pt2); + template + inline TLine(const TLine&); + + inline void Set(const POINT& pt1, const POINT& pt2); + + int Optimize(const POINT*, size_t, int maxIters=100); + template + int Optimize(const POINT*, size_t, const RobustNormFunctor& robust, int maxIters=100); + + inline TYPE GetLength() const; + inline TYPE GetLengthSq() const; + inline POINT GetCenter() const; + inline VECTOR GetDir() const; + inline VECTOR GetNormDir() const; + inline RAY GetRay() const; + + inline bool IsSame(const TLine&, TYPE th) const; + + bool Intersects(const AABB& aabb) const; + bool Intersects(const AABB& aabb, TYPE& t) const; + + inline TYPE DistanceSq(const POINT&) const; + inline TYPE Distance(const POINT&) const; + + inline TYPE Classify(const POINT&) const; + inline POINT ProjectPoint(const POINT&) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & pt1; + ar & pt2; + } + #endif +}; // class TLine +/*----------------------------------------------------------------*/ + +template +struct FitLineOnline : FitPlaneOnline { + template TPoint3 GetLine(TLine& line) const; +}; +/*----------------------------------------------------------------*/ + + +#include "Line.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_LINE_H__ diff --git a/libs/Common/Line.inl b/libs/Common/Line.inl new file mode 100644 index 000000000..bd7897cd8 --- /dev/null +++ b/libs/Common/Line.inl @@ -0,0 +1,215 @@ +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + + +// C L A S S ////////////////////////////////////////////////////// + +template +inline TLine::TLine(const POINT& _pt1, const POINT& _pt2) + : + pt1(_pt1), pt2(_pt2) +{ + ASSERT(!ISZERO((_pt1-_pt2).norm())); +} // constructor +template +template +inline TLine::TLine(const TLine& rhs) + : + pt1(rhs.pt1.template cast()), pt2(rhs.pt2.template cast()) +{ +} // copy constructor +/*----------------------------------------------------------------*/ + + +// set attributes +template +inline void TLine::Set(const POINT& _pt1, const POINT& _pt2) +{ + ASSERT(!ISZERO((_pt1-_pt2).norm())); + pt1 = _pt1; + pt2 = _pt2; +} +/*----------------------------------------------------------------*/ + + +// least squares refinement of the line to the given 3D point set +// (return the number of iterations) +template +template +int TLine::Optimize(const POINT* points, size_t size, const RobustNormFunctor& robust, int maxIters) +{ + ASSERT(DIMS == 3); + ASSERT(size >= numParams); + struct OptimizationFunctor { + const POINT* points; + size_t size; + double scale; + const RobustNormFunctor& robust; + // construct with the data points + OptimizationFunctor(const POINT* _points, size_t _size, const RobustNormFunctor& _robust) + : points(_points), size(_size), robust(_robust) { ASSERT(size < std::numeric_limits::max()); } + static void Residuals(const double* x, int nPoints, const void* pData, double* fvec, double* fjac, int* /*info*/) { + const OptimizationFunctor& data = *reinterpret_cast(pData); + ASSERT((size_t)nPoints == data.size && fvec != NULL && fjac == NULL); + TLine line; + for (int j=0; j())); + } + } functor(points, size, robust); + double arrParams[numParams]; + for (int j=0; j +int TLine::Optimize(const POINT* points, size_t size, int maxIters) +{ + const auto identity = [](double x) { return x; }; + return Optimize(points, size, identity, maxIters); +} // Optimize +/*----------------------------------------------------------------*/ + + +// get attributes +template +inline TYPE TLine::GetLength() const +{ + return (pt2 - pt1).norm(); +} +template +inline TYPE TLine::GetLengthSq() const +{ + return (pt2 - pt1).squaredNorm(); +} +template +inline typename TLine::POINT TLine::GetCenter() const +{ + return (pt2 + pt1) / TYPE(2); +} +template +inline typename TLine::VECTOR TLine::GetDir() const +{ + return (pt2 - pt1); +} +template +inline typename TLine::VECTOR TLine::GetNormDir() const +{ + return (pt2 - pt1).normalized(); +} +template +inline typename TLine::RAY TLine::GetRay() const +{ + return RAY(pt1, GetNormDir()); +} +/*----------------------------------------------------------------*/ + + +template +inline bool TLine::IsSame(const TLine& line, TYPE th) const +{ + const TYPE thSq(SQUARE(th)); + const VECTOR l(pt2-pt1); + const TYPE invLenSq(INVERT(l.squaredNorm())); + const VECTOR r1(pt1-line.pt1); + const TYPE dSq1((l.cross(r1)).squaredNorm()*invLenSq); + if (dSq1 > thSq) + return false; + const VECTOR r2(pt1-line.pt2); + const TYPE dSq2((l.cross(r2)).squaredNorm()*invLenSq); + return dSq2 <= thSq; +} +/*----------------------------------------------------------------*/ + + +// test for intersection with aabb +template +bool TLine::Intersects(const AABB &aabb) const +{ + return GetRay().Intersects(aabb); +} // Intersects(AABB) +template +bool TLine::Intersects(const AABB &aabb, TYPE& t) const +{ + return GetRay().Intersects(aabb, t); +} // Intersects(AABB) +/*----------------------------------------------------------------*/ + +// Computes the distance between the line and a point. +template +inline TYPE TLine::DistanceSq(const POINT& pt) const +{ + const VECTOR l(pt2-pt1), r(pt1-pt); + if (DIMS == 2) + return TYPE(SQUARE(l[0]*r[1]-r[0]*l[1])/(l[0]*l[0]+l[1]*l[1])); + ASSERT(DIMS == 3); + return TYPE((l.cross(r)).squaredNorm()/l.squaredNorm()); +} // DistanceSq(POINT) +template +inline TYPE TLine::Distance(const POINT& pt) const +{ + return SQRT(DistanceSq(pt)); +} // Distance(POINT) +/*----------------------------------------------------------------*/ + + +// Computes the position on the line segment of the point projection. +// Returns 0 if it coincides with the first point, and 1 if it coincides with the second point. +template +inline TYPE TLine::Classify(const POINT& p) const +{ + const VECTOR vL(pt2 - pt1); + ASSERT(!ISZERO(vL.squaredNorm())); + const VECTOR vP(p - pt1); + return vL.dot(vP) / vL.squaredNorm(); +} // Classify(POINT) +// Calculate point's projection on this line (closest point to this line). +template +inline typename TLine::POINT TLine::ProjectPoint(const POINT& p) const +{ + const VECTOR vL(pt2 - pt1); + ASSERT(!ISZERO(vL.squaredNorm())); + const VECTOR vP(p - pt1); + return pt1 + vL * (vL.dot(vP) / vL.squaredNorm()); +} // ProjectPoint +/*----------------------------------------------------------------*/ + + +template +template +TPoint3 FitLineOnline::GetLine(TLine& line) const +{ + TPoint3 avg, dir; + const TPoint3 quality(this->GetModel(avg, dir)); + const TPoint3 pt2(avg+dir); + line.Set(TPoint3(avg), TPoint3(pt2)); + return TPoint3(quality); +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/List.h b/libs/Common/List.h index 2404c4832..4e2906381 100644 --- a/libs/Common/List.h +++ b/libs/Common/List.h @@ -23,8 +23,10 @@ // cList index type #ifdef _SUPPORT_CPP11 #define ARR2IDX(arr) typename std::remove_reference::type::size_type +#define SIZE2IDX(arr) typename std::remove_const::type>::type #else #define ARR2IDX(arr) IDX +#define SIZE2IDX(arr) IDX #endif // cList iterator by index @@ -44,10 +46,10 @@ // raw data array iterator by index #ifndef FOREACHRAW -#define FOREACHRAW(var, sz) for (IDX var=0, var##Size=(sz); var0; ) +#define RFOREACHRAW(var, sz) for (SIZE2IDX(sz) var=sz; var-->0; ) #endif // raw data array iterator by pointer #ifndef FOREACHRAWPTR @@ -170,15 +172,15 @@ class cList } // copy the content from the given list - inline cList& operator=(const cList& rList) + inline cList& operator=(const cList& rList) { return CopyOf(rList); } - inline cList& CopyOf(const cList& rList, bool bForceResize=false) + inline cList& CopyOf(const cList& rList, bool bForceResize=false) { if (this == &rList) - return (*this); + return *this; if (bForceResize || _vectorSize < rList._vectorSize) { _Release(); _vectorSize = rList._vectorSize; @@ -194,13 +196,13 @@ class cList } } _size = rList._size; - return (*this); + return *this; } - inline cList& CopyOf(const TYPE* pData, IDX nSize, bool bForceResize=false) + inline cList& CopyOf(const TYPE* pData, IDX nSize, bool bForceResize=false) { if (_vector == pData) - return (*this); + return *this; if (bForceResize || _vectorSize < nSize) { _Release(); _vectorSize = nSize; @@ -216,56 +218,72 @@ class cList } } _size = nSize; - return (*this); + return *this; } // release current list and swap the content with the given list - inline cList& CopyOfRemove(cList& rList) + inline cList& CopyOfRemove(cList& rList) { if (this == &rList) - return (*this); + return *this; _Release(); _size = rList._size; _vectorSize = rList._vectorSize; _vector = rList._vector; rList._vector = NULL; rList._size = rList._vectorSize = 0; - return (*this); + return *this; } - inline void Join(const cList& rList) + inline cList& Join(const cList& rList) { if (this == &rList || rList._size == 0) - return; + return *this; const IDX newSize = _size + rList._size; Reserve(newSize); _ArrayCopyConstruct(_vector+_size, rList._vector, rList._size); _size = newSize; + return *this; } - inline void Join(const TYPE* pData, IDX nSize) + inline cList& Join(const TYPE* pData, IDX nSize) { const IDX newSize = _size + nSize; Reserve(newSize); _ArrayCopyConstruct(_vector+_size, pData, nSize); _size = newSize; + return *this; + } + + template + inline cList& JoinFunctor(IDX nSize, const Functor& functor) { + Reserve(_size + nSize); + if (useConstruct) { + for (IDX n=0; n(_vector+_size, rList._vector, rList._size); _size = newSize; rList._size = 0; + return *this; } // Swap the elements of the two lists. - inline void Swap(cList& rList) + inline cList& Swap(cList& rList) { if (this == &rList) - return; + return *this; const IDX tmpSize = _size; _size = rList._size; rList._size = tmpSize; @@ -275,16 +293,26 @@ class cList TYPE* const tmpVector = _vector; _vector = rList._vector; rList._vector = tmpVector; + return *this; } // Swap the two elements. - inline void Swap(IDX idx1, IDX idx2) + inline void Swap(IDX idx1, IDX idx2) { ASSERT(idx1 < _size && idx2 < _size); TYPE tmp = _vector[idx1]; _vector[idx1] = _vector[idx2]; _vector[idx2] = tmp; } + + inline bool operator==(const cList& rList) const { + if (_size != rList._size) + return false; + for (IDX i = 0; i < _size; ++i) + if (_vector[i] != rList._vector[i]) + return false; + return true; + } // Set the allocated memory (normally used for types without constructor). inline void Memset(uint8_t val) @@ -1574,6 +1602,14 @@ class cListFixed { ASSERT(index < _size); return _vector[index]; } + inline bool operator==(const cListFixed& rList) const { + if (_size != rList._size) + return false; + for (IDX i = 0; i < _size; ++i) + if (_vector[i] != rList._vector[i]) + return false; + return true; + } inline TYPE& AddEmpty() { ASSERT(_size < N); return _vector[_size++]; diff --git a/libs/Common/OBB.inl b/libs/Common/OBB.inl index 1e2784d17..6a8dd7622 100644 --- a/libs/Common/OBB.inl +++ b/libs/Common/OBB.inl @@ -341,10 +341,11 @@ inline void TOBB::GetCorners(POINT pts[numCorners]) const m_rot.row(0)*m_ext[0], m_rot.row(1)*m_ext[1] }; - pts[0] = m_pos - pEAxis[0] - pEAxis[1]; - pts[1] = m_pos + pEAxis[0] - pEAxis[1]; - pts[2] = m_pos + pEAxis[0] + pEAxis[1]; - pts[3] = m_pos - pEAxis[0] + pEAxis[1]; + const POINT pos(m_rot.transpose()*m_pos); + pts[0] = pos - pEAxis[0] - pEAxis[1]; + pts[1] = pos + pEAxis[0] - pEAxis[1]; + pts[2] = pos + pEAxis[0] + pEAxis[1]; + pts[3] = pos - pEAxis[0] + pEAxis[1]; } if (DIMS == 3) { const POINT pEAxis[3] = { @@ -352,47 +353,24 @@ inline void TOBB::GetCorners(POINT pts[numCorners]) const m_rot.row(1)*m_ext[1], m_rot.row(2)*m_ext[2] }; - pts[0] = m_pos - pEAxis[0] - pEAxis[1] - pEAxis[2]; - pts[1] = m_pos - pEAxis[0] - pEAxis[1] + pEAxis[2]; - pts[2] = m_pos + pEAxis[0] - pEAxis[1] - pEAxis[2]; - pts[3] = m_pos + pEAxis[0] - pEAxis[1] + pEAxis[2]; - pts[4] = m_pos + pEAxis[0] + pEAxis[1] - pEAxis[2]; - pts[5] = m_pos + pEAxis[0] + pEAxis[1] + pEAxis[2]; - pts[6] = m_pos - pEAxis[0] + pEAxis[1] - pEAxis[2]; - pts[7] = m_pos - pEAxis[0] + pEAxis[1] + pEAxis[2]; + const POINT pos(m_rot.transpose()*m_pos); + pts[0] = pos - pEAxis[0] - pEAxis[1] - pEAxis[2]; + pts[1] = pos - pEAxis[0] - pEAxis[1] + pEAxis[2]; + pts[2] = pos + pEAxis[0] - pEAxis[1] - pEAxis[2]; + pts[3] = pos + pEAxis[0] - pEAxis[1] + pEAxis[2]; + pts[4] = pos + pEAxis[0] + pEAxis[1] - pEAxis[2]; + pts[5] = pos + pEAxis[0] + pEAxis[1] + pEAxis[2]; + pts[6] = pos - pEAxis[0] + pEAxis[1] - pEAxis[2]; + pts[7] = pos - pEAxis[0] + pEAxis[1] + pEAxis[2]; } } // GetCorners // constructs the corner of the aligned bounding box in world space template inline typename TOBB::AABB TOBB::GetAABB() const { - #if 0 - if (DIMS == 2) { - const POINT pEAxis[2] = { - m_rot.row(0)*m_ext[0], - m_rot.row(1)*m_ext[1] - }; - return AABB( - m_pos - pEAxis[0] - pEAxis[1], - m_pos + pEAxis[0] + pEAxis[1] - ); - } - if (DIMS == 3) { - const POINT pEAxis[3] = { - m_rot.row(0)*m_ext[0], - m_rot.row(1)*m_ext[1], - m_rot.row(2)*m_ext[2] - }; - return AABB( - m_pos - pEAxis[0] - pEAxis[1] - pEAxis[2], - m_pos + pEAxis[0] + pEAxis[1] + pEAxis[2] - ); - } - #else POINT pts[numCorners]; GetCorners(pts); return AABB(pts, numCorners); - #endif } // GetAABB /*----------------------------------------------------------------*/ diff --git a/libs/Common/Plane.h b/libs/Common/Plane.h index 681d044c9..8623928e9 100644 --- a/libs/Common/Plane.h +++ b/libs/Common/Plane.h @@ -50,6 +50,10 @@ class TPlane inline void Set(const POINT&, const POINT&, const POINT&); inline void Set(const TYPE p[DIMS+1]); + int Optimize(const POINT*, size_t, int maxIters=100); + template + int Optimize(const POINT*, size_t, const RobustNormFunctor& robust, int maxIters=100); + inline void Invalidate(); inline bool IsValid() const; @@ -85,7 +89,7 @@ class TPlane }; // class TPlane /*----------------------------------------------------------------*/ -template +template struct FitPlaneOnline { TYPEW sumX, sumSqX, sumXY, sumXZ; TYPEW sumY, sumSqY, sumYZ; @@ -93,7 +97,7 @@ struct FitPlaneOnline { size_t size; FitPlaneOnline(); void Update(const TPoint3& P); - TPoint3 GetPlane(TPoint3& avg, TPoint3& dir) const; + TPoint3 GetModel(TPoint3& avg, TPoint3& dir) const; template TPoint3 GetPlane(TPlane& plane) const; }; /*----------------------------------------------------------------*/ diff --git a/libs/Common/Plane.inl b/libs/Common/Plane.inl index 0d15cc319..bd7e61083 100644 --- a/libs/Common/Plane.inl +++ b/libs/Common/Plane.inl @@ -69,6 +69,72 @@ inline void TPlane::Set(const TYPE p[DIMS+1]) /*----------------------------------------------------------------*/ +// least squares refinement of the given plane to the 3D point set +// (return the number of iterations) +template +template +int TPlane::Optimize(const POINT* points, size_t size, const RobustNormFunctor& robust, int maxIters) +{ + ASSERT(DIMS == 3); + ASSERT(size >= numParams); + struct OptimizationFunctor { + const POINT* points; + const size_t size; + const RobustNormFunctor& robust; + // construct with the data points + OptimizationFunctor(const POINT* _points, size_t _size, const RobustNormFunctor& _robust) + : points(_points), size(_size), robust(_robust) { ASSERT(size < (size_t)std::numeric_limits::max()); } + static void Residuals(const double* x, int nPoints, const void* pData, double* fvec, double* fjac, int* /*info*/) { + const OptimizationFunctor& data = *reinterpret_cast(pData); + ASSERT((size_t)nPoints == data.size && fvec != NULL && fjac == NULL); + TPlane plane; { + Point3d N; + plane.m_fD = x[0]; + Dir2Normal(reinterpret_cast(x[1]), N); + plane.m_vN = N; + } + for (size_t i=0; i())); + } + } functor(points, size, robust); + double arrParams[numParams]; { + arrParams[0] = (double)m_fD; + const Point3d N(m_vN.x(), m_vN.y(), m_vN.z()); + Normal2Dir(N, reinterpret_cast(arrParams[1])); + } + lm_control_struct control = {1.e-6, 1.e-7, 1.e-8, 1.e-7, 100.0, maxIters}; // lm_control_float; + lm_status_struct status; + lmmin(numParams, arrParams, (int)size, &functor, OptimizationFunctor::Residuals, &control, &status); + switch (status.info) { + //case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + case 12: + DEBUG_ULTIMATE("error: refine plane: %s", lm_infmsg[status.info]); + return 0; + } + // set plane + { + Point3d N; + Dir2Normal(reinterpret_cast(arrParams[1]), N); + Set(Cast(N), (TYPE)arrParams[0]); + } + return status.nfev; +} +template +int TPlane::Optimize(const POINT* points, size_t size, int maxIters) +{ + const auto identity = [](double x) { return x; }; + return Optimize(points, size, identity, maxIters); +} // Optimize +/*----------------------------------------------------------------*/ + + template inline void TPlane::Invalidate() { @@ -281,13 +347,13 @@ bool TPlane::Intersects(const AABB& aabb) const // same as above, but online version -template -FitPlaneOnline::FitPlaneOnline() +template +FitPlaneOnline::FitPlaneOnline() : sumX(0), sumSqX(0), sumXY(0), sumXZ(0), sumY(0), sumSqY(0), sumYZ(0), sumZ(0), sumSqZ(0), size(0) { } -template -void FitPlaneOnline::Update(const TPoint3& P) +template +void FitPlaneOnline::Update(const TPoint3& P) { const TYPEW X((TYPEW)P.x), Y((TYPEW)P.y), Z((TYPEW)P.z); sumX += X; sumSqX += X*X; sumXY += X*Y; sumXZ += X*Z; @@ -295,8 +361,8 @@ void FitPlaneOnline::Update(const TPoint3& P) sumZ += Z; sumSqZ += Z*Z; ++size; } -template -TPoint3 FitPlaneOnline::GetPlane(TPoint3& avg, TPoint3& dir) const +template +TPoint3 FitPlaneOnline::GetModel(TPoint3& avg, TPoint3& dir) const { const TYPEW avgX(sumX/(TYPEW)size), avgY(sumY/(TYPEW)size), avgZ(sumZ/(TYPEW)size); // assemble covariance (lower-triangular) matrix @@ -309,20 +375,21 @@ TPoint3 FitPlaneOnline::GetPlane(TPoint3& avg, TPoint3 A(2,1) = sumYZ - sumY*avgZ - avgY*sumZ + avgY*avgZ*(TYPEW)size; A(2,2) = sumSqZ - TYPEW(2)*sumZ*avgZ + avgZ*avgZ*(TYPEW)size; // the plane normal is simply the eigenvector corresponding to least eigenvalue + const int nAxis(bFitLineMode ? 2 : 0); const Eigen::SelfAdjointEigenSolver es(A); - ASSERT(ISEQUAL(es.eigenvectors().col(0).norm(), TYPEW(1))); + ASSERT(ISEQUAL(es.eigenvectors().col(nAxis).norm(), TYPEW(1))); avg = TPoint3(avgX,avgY,avgZ); - dir = es.eigenvectors().col(0); + dir = es.eigenvectors().col(nAxis); const TYPEW* const vals(es.eigenvalues().data()); ASSERT(vals[0] <= vals[1] && vals[1] <= vals[2]); return *reinterpret_cast*>(vals); } -template +template template -TPoint3 FitPlaneOnline::GetPlane(TPlane& plane) const +TPoint3 FitPlaneOnline::GetPlane(TPlane& plane) const { TPoint3 avg, dir; - const TPoint3 quality(GetPlane(avg, dir)); + const TPoint3 quality(GetModel(avg, dir)); plane.Set(TPoint3(dir), TPoint3(avg)); return TPoint3(quality); } diff --git a/libs/Common/Sampler.inl b/libs/Common/Sampler.inl index 9870facda..e22b21f84 100644 --- a/libs/Common/Sampler.inl +++ b/libs/Common/Sampler.inl @@ -22,12 +22,12 @@ namespace Sampler { // Note: The following functors generalize the sampling to more than two neighbors // They all contains the width variable that specify the number of neighbors used for sampling // -// All contain the operator () with the following definition: +// All contain the operator() with the following definition: // // @brief Computes weight associated to neighboring pixels // @author Romuald Perrot // @param x Sampling position -// @param[out] weigth Sampling factors associated to the neighboring +// @param[out] weight Sampling factors associated to the neighboring // @note weight must be at least width length // Linear sampling (ie: linear interpolation between two pixels) template @@ -39,9 +39,9 @@ struct Linear { inline Linear() {} - inline void operator () (const TYPE x, TYPE* const weigth) const { - weigth[0] = TYPE(1) - x; - weigth[1] = x; + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = TYPE(1) - x; + weight[1] = x; } }; @@ -70,19 +70,19 @@ struct Cubic { const TYPE sharpness; inline Cubic(const TYPE& _sharpness=TYPE(0.5)) : sharpness(_sharpness) {} - inline void operator () (const TYPE x, TYPE* const weigth) const { + inline void operator() (const TYPE x, TYPE* const weight) const { // remember : // A B x C D - // weigth[0] -> weight for A + // weight[0] -> weight for A // weight[1] -> weight for B // weight[2] -> weight for C - // weight[3] -> weigth for D + // weight[3] -> weight for D - weigth[0] = CubicInter12(x + TYPE(1)); - weigth[1] = CubicInter01(x); - weigth[2] = CubicInter01(TYPE(1) - x); - weigth[3] = CubicInter12(TYPE(2) - x); + weight[0] = CubicInter12(x + TYPE(1)); + weight[1] = CubicInter01(x); + weight[2] = CubicInter01(TYPE(1) - x); + weight[3] = CubicInter12(TYPE(2) - x); } // Cubic interpolation for x (in [0,1]) @@ -170,11 +170,11 @@ struct Spline16 { inline Spline16() {} - inline void operator () (const TYPE x, TYPE* const weigth) const { - weigth[0] = ((TYPE(-1) / TYPE(3) * x + TYPE(4) / TYPE(5)) * x - TYPE(7) / TYPE(15)) * x; - weigth[1] = ((x - TYPE(9) / TYPE(5)) * x - TYPE(1) / TYPE(5)) * x + TYPE(1); - weigth[2] = ((TYPE(6) / TYPE(5) - x) * x + TYPE(4) / TYPE(5)) * x; - weigth[3] = ((TYPE(1) / TYPE(3) * x - TYPE(1) / TYPE(5)) * x - TYPE(2) / TYPE(15)) * x; + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(-1) / TYPE(3) * x + TYPE(4) / TYPE(5)) * x - TYPE(7) / TYPE(15)) * x; + weight[1] = ((x - TYPE(9) / TYPE(5)) * x - TYPE(1) / TYPE(5)) * x + TYPE(1); + weight[2] = ((TYPE(6) / TYPE(5) - x) * x + TYPE(4) / TYPE(5)) * x; + weight[3] = ((TYPE(1) / TYPE(3) * x - TYPE(1) / TYPE(5)) * x - TYPE(2) / TYPE(15)) * x; } }; @@ -189,13 +189,13 @@ struct Spline36 { inline Spline36() {} - inline void operator () (const TYPE x, TYPE* const weigth) const { - weigth[0] = ((TYPE(1) / TYPE(11) * x - TYPE(45) / TYPE(209)) * x + TYPE(26) / TYPE(209)) * x; - weigth[1] = ((TYPE(-6) / TYPE(11) * x + TYPE(270) / TYPE(209)) * x - TYPE(156) / TYPE(209)) * x; - weigth[2] = ((TYPE(13) / TYPE(11) * x - TYPE(453) / TYPE(209)) * x - TYPE(3) / TYPE(209)) * x + TYPE(1); - weigth[3] = ((TYPE(-13) / TYPE(11) * x + TYPE(288) / TYPE(209)) * x + TYPE(168) / TYPE(209)) * x; - weigth[4] = ((TYPE(6) / TYPE(11) * x - TYPE(72) / TYPE(209)) * x - TYPE(42) / TYPE(209)) * x; - weigth[5] = ((TYPE(-1) / TYPE(11) * x + TYPE(12) / TYPE(209)) * x + TYPE(7) / TYPE(209)) * x; + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(1) / TYPE(11) * x - TYPE(45) / TYPE(209)) * x + TYPE(26) / TYPE(209)) * x; + weight[1] = ((TYPE(-6) / TYPE(11) * x + TYPE(270) / TYPE(209)) * x - TYPE(156) / TYPE(209)) * x; + weight[2] = ((TYPE(13) / TYPE(11) * x - TYPE(453) / TYPE(209)) * x - TYPE(3) / TYPE(209)) * x + TYPE(1); + weight[3] = ((TYPE(-13) / TYPE(11) * x + TYPE(288) / TYPE(209)) * x + TYPE(168) / TYPE(209)) * x; + weight[4] = ((TYPE(6) / TYPE(11) * x - TYPE(72) / TYPE(209)) * x - TYPE(42) / TYPE(209)) * x; + weight[5] = ((TYPE(-1) / TYPE(11) * x + TYPE(12) / TYPE(209)) * x + TYPE(7) / TYPE(209)) * x; } }; @@ -210,15 +210,15 @@ struct Spline64 { inline Spline64() {} - inline void operator () (const TYPE x, TYPE* const weigth) const { - weigth[0] = ((TYPE(-1) / TYPE(41) * x + TYPE(168) / TYPE(2911)) * x - TYPE(97) / TYPE(2911)) * x; - weigth[1] = ((TYPE(6) / TYPE(41) * x - TYPE(1008) / TYPE(2911)) * x + TYPE(582) / TYPE(2911)) * x; - weigth[2] = ((TYPE(-24) / TYPE(41) * x + TYPE(4032) / TYPE(2911)) * x - TYPE(2328) / TYPE(2911)) * x; - weigth[3] = ((TYPE(49) / TYPE(41) * x - TYPE(6387) / TYPE(2911)) * x - TYPE(3) / TYPE(2911)) * x + TYPE(1); - weigth[4] = ((TYPE(-49) / TYPE(41) * x + TYPE(4050) / TYPE(2911)) * x + TYPE(2340) / TYPE(2911)) * x; - weigth[5] = ((TYPE(24) / TYPE(41) * x - TYPE(1080) / TYPE(2911)) * x - TYPE(624) / TYPE(2911)) * x; - weigth[6] = ((TYPE(-6) / TYPE(41) * x + TYPE(270) / TYPE(2911)) * x + TYPE(156) / TYPE(2911)) * x; - weigth[7] = ((TYPE(1) / TYPE(41) * x - TYPE(45) / TYPE(2911)) * x - TYPE(26) / TYPE(2911)) * x; + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(-1) / TYPE(41) * x + TYPE(168) / TYPE(2911)) * x - TYPE(97) / TYPE(2911)) * x; + weight[1] = ((TYPE(6) / TYPE(41) * x - TYPE(1008) / TYPE(2911)) * x + TYPE(582) / TYPE(2911)) * x; + weight[2] = ((TYPE(-24) / TYPE(41) * x + TYPE(4032) / TYPE(2911)) * x - TYPE(2328) / TYPE(2911)) * x; + weight[3] = ((TYPE(49) / TYPE(41) * x - TYPE(6387) / TYPE(2911)) * x - TYPE(3) / TYPE(2911)) * x + TYPE(1); + weight[4] = ((TYPE(-49) / TYPE(41) * x + TYPE(4050) / TYPE(2911)) * x + TYPE(2340) / TYPE(2911)) * x; + weight[5] = ((TYPE(24) / TYPE(41) * x - TYPE(1080) / TYPE(2911)) * x - TYPE(624) / TYPE(2911)) * x; + weight[6] = ((TYPE(-6) / TYPE(41) * x + TYPE(270) / TYPE(2911)) * x + TYPE(156) / TYPE(2911)) * x; + weight[7] = ((TYPE(1) / TYPE(41) * x - TYPE(45) / TYPE(2911)) * x - TYPE(26) / TYPE(2911)) * x; } }; diff --git a/libs/Common/Strings.h b/libs/Common/Strings.h index 711d0a5de..187f32d0e 100644 --- a/libs/Common/Strings.h +++ b/libs/Common/Strings.h @@ -34,6 +34,22 @@ class GENERAL_API String : public std::string inline String(size_t n, value_type v) : Base(n, v) {} inline String(LPCTSTR sz, size_t count) : Base(sz, count) {} inline String(LPCTSTR sz, size_t offset, size_t count) : Base(sz, offset, count) {} + #ifdef _SUPPORT_CPP11 + inline String(Base&& rhs) : Base(std::forward(rhs)) {} + inline String(String&& rhs) : Base(std::forward(rhs)) {} + inline String(const String& rhs) : Base(rhs) {} + + inline String& operator=(Base&& rhs) { Base::operator=(std::forward(rhs)); return *this; } + inline String& operator=(String&& rhs) { Base::operator=(std::forward(rhs)); return *this; } + inline String& operator=(TCHAR rhs) { Base::operator=(rhs); return *this; } + inline String& operator=(LPCTSTR rhs) { Base::operator=(rhs); return *this; } + inline String& operator=(const String& rhs) { Base::operator=(rhs); return *this; } + #endif + + inline String& operator+=(TCHAR rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(LPCTSTR rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(const Base& rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(const String& rhs) { *this = (*this) + rhs; return *this; } inline void Release() { return clear(); } inline bool IsEmpty() const { return empty(); } @@ -189,6 +205,26 @@ class GENERAL_API String : public std::string }; /*----------------------------------------------------------------*/ +inline String operator+(const String& lhs, TCHAR rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, LPCTSTR rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, const std::string& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(TCHAR lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(LPCTSTR lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const std::string& lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, const String& rhs) { return std::operator+(lhs, rhs); } +#ifdef _SUPPORT_CPP11 +inline String operator+(String&& lhs, TCHAR rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, LPCTSTR rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, const std::string& rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(TCHAR lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(LPCTSTR lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(const std::string& lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(const String& lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(String&& lhs, const String& rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, String&& rhs) { return std::operator+(std::forward(lhs), std::forward(rhs)); } +#endif +/*----------------------------------------------------------------*/ + } // namespace SEACAVE diff --git a/libs/Common/Types.h b/libs/Common/Types.h index d5dcb27e2..d5b567431 100644 --- a/libs/Common/Types.h +++ b/libs/Common/Types.h @@ -2201,7 +2201,10 @@ class TImage : public TDMatrix bool Load(const String&); bool Save(const String&) const; + + #ifndef _RELEASE void Show(const String& winname, int delay=0, bool bDestroy=true) const; + #endif #ifdef _USE_BOOST // serialize @@ -2800,6 +2803,7 @@ class SO2 #endif // _USE_EIGEN +#include "../Math/LMFit/lmmin.h" #include "Types.inl" #include "Util.inl" #include "Rotation.h" @@ -2808,6 +2812,7 @@ class SO2 #include "OBB.h" #include "Plane.h" #include "Ray.h" +#include "Line.h" #include "Octree.h" #include "UtilCUDA.h" diff --git a/libs/Common/Types.inl b/libs/Common/Types.inl index b951c7931..d4129e1ad 100644 --- a/libs/Common/Types.inl +++ b/libs/Common/Types.inl @@ -3077,6 +3077,7 @@ bool TImage::Save(const String& fileName) const } /*----------------------------------------------------------------*/ +#ifndef _RELEASE template void TImage::Show(const String& winname, int delay, bool bDestroy) const { @@ -3086,6 +3087,7 @@ void TImage::Show(const String& winname, int delay, bool bDestroy) const cv::destroyWindow(winname); } /*----------------------------------------------------------------*/ +#endif // C L A S S ////////////////////////////////////////////////////// diff --git a/libs/Common/Util.cpp b/libs/Common/Util.cpp index 95e5f6ae8..1ae22cb66 100644 --- a/libs/Common/Util.cpp +++ b/libs/Common/Util.cpp @@ -667,6 +667,13 @@ bool OSSupportsAVX() // print details about the current build and PC void Util::LogBuild() { + LOG(_T("OpenMVS %s v%u.%u.%u"), + #ifdef _ENVIRONMENT64 + _T("x64"), + #else + _T("x32"), + #endif + OpenMVS_MAJOR_VERSION, OpenMVS_MINOR_VERSION, OpenMVS_PATCH_VERSION); #if TD_VERBOSE == TD_VERBOSE_OFF LOG(_T("Build date: ") __DATE__); #else diff --git a/libs/Common/Util.inl b/libs/Common/Util.inl index 381975929..b0ffa0ff1 100644 --- a/libs/Common/Util.inl +++ b/libs/Common/Util.inl @@ -177,6 +177,98 @@ inline TMatrix CreateF(const TMatrix& R, const TMatrix +inline void RQDecomp3x3(Eigen::Matrix M, Eigen::Matrix& R, Eigen::Matrix& Q) { + // find Givens rotation for x axis: + // | 1 0 0 | + // Qx = | 0 c s |, c = m33/sqrt(m32^2 + m33^2), s = m32/sqrt(m32^2 + m33^2) + // | 0 -s c | + Eigen::Matrix cs = Eigen::Matrix(M(2,2), M(2,1)).normalized(); + Eigen::Matrix Qx{ {1, 0, 0}, {0, cs.x(), cs.y()}, {0, -cs.y(), cs.x()} }; + R.noalias() = M * Qx; + ASSERT(std::abs(R(2,1)) < FLT_EPSILON); + R(2,1) = 0; + // find Givens rotation for y axis: + // | c 0 -s | + // Qy = | 0 1 0 |, c = m33/sqrt(m31^2 + m33^2), s = -m31/sqrt(m31^2 + m33^2) + // | s 0 c | + cs = Eigen::Matrix(R(2,2), -R(2,0)).normalized(); + Eigen::Matrix Qy{ {cs.x(), 0, -cs.y()}, {0, 1, 0}, {cs.y(), 0, cs.x()} }; + M.noalias() = R * Qy; + ASSERT(std::abs(M(2,0)) < FLT_EPSILON); + M(2,0) = 0; + // find Givens rotation for z axis: + // | c s 0 | + // Qz = |-s c 0 |, c = m22/sqrt(m21^2 + m22^2), s = m21/sqrt(m21^2 + m22^2) + // | 0 0 1 | + cs = Eigen::Matrix(M(1,1), M(1,0)).normalized(); + Eigen::Matrix Qz{ {cs.x(), cs.y(), 0}, {-cs.y(), cs.x(), 0}, {0, 0, 1} }; + R.noalias() = M * Qz; + ASSERT(std::abs(R(1,0)) < FLT_EPSILON); + R(1,0) = 0; + // solve the decomposition ambiguity: + // - diagonal entries of R, except the last one, shall be positive + // - rotate R by 180 degree if necessary + if (R(0,0) < 0) { + if (R(1,1) < 0) { + // rotate around z for 180 degree: + // |-1, 0, 0| + // | 0, -1, 0| + // | 0, 0, 1| + R(0,0) *= -1; + R(0,1) *= -1; + R(1,1) *= -1; + Qz(0,0) *= -1; + Qz(0,1) *= -1; + Qz(1,0) *= -1; + Qz(1,1) *= -1; + } else { + // rotate around y for 180 degree: + // |-1, 0, 0| + // | 0, 1, 0| + // | 0, 0, -1| + R(0,0) *= -1; + R(0,2) *= -1; + R(1,2) *= -1; + R(2,2) *= -1; + Qz.transposeInPlace(); + Qy(0,0) *= -1; + Qy(0,2) *= -1; + Qy(2,0) *= -1; + Qy(2,2) *= -1; + } + } else if (R(1,1) < 0) { + // rotate around x for 180 degree: + // | 1, 0, 0| + // | 0, -1, 0| + // | 0, 0, -1| + R(0,1) *= -1; + R(0,2) *= -1; + R(1,1) *= -1; + R(1,2) *= -1; + R(2,2) *= -1; + Qz.transposeInPlace(); + Qy.transposeInPlace(); + Qx(1,1) *= -1; + Qx(1,2) *= -1; + Qx(2,1) *= -1; + Qx(2,2) *= -1; + } + // calculate orthogonal matrix + Q.noalias() = Qz.transpose() * Qy.transpose() * Qx.transpose(); +} +template +inline void RQDecomp3x3(const TMatrix& M, TMatrix& R, TMatrix& Q) { + const Eigen::Matrix _M((typename TMatrix::CEMatMap)M); + Eigen::Matrix _R, _Q; + RQDecomp3x3(_M, _R, _Q); + R = _R; Q = _Q; +} // RQDecomp3x3 +/*----------------------------------------------------------------*/ + + // compute the angle between the two rotations given // as in: "Disambiguating Visual Relations Using Loop Constraints", 2010 // returns cos(angle) (same as cos(ComputeAngleSO3(I)) @@ -1097,7 +1189,7 @@ inline void ExampleKDE() { File f; SamplesInserter(const String& fileName) : f(fileName, File::WRITE, File::CREATE | File::TRUNCATE) {} - inline void operator () (Real x, Real y) { + inline void operator() (Real x, Real y) { f.print("%g\t%g\n", x, y); } inline bool SamplePeak(bool bMaxRegion) const { diff --git a/libs/IO/CMakeLists.txt b/libs/IO/CMakeLists.txt index da3c15920..0f0595194 100644 --- a/libs/IO/CMakeLists.txt +++ b/libs/IO/CMakeLists.txt @@ -2,7 +2,7 @@ FIND_PACKAGE(PNG QUIET) if(PNG_FOUND) INCLUDE_DIRECTORIES(${PNG_INCLUDE_DIRS}) - ADD_DEFINITIONS(${PNG_DEFINITIONS} -D_USE_PNG) + ADD_DEFINITIONS(${PNG_DEFINITIONS}) SET(_USE_PNG TRUE CACHE INTERNAL "") else() SET(PNG_LIBRARIES "") @@ -10,7 +10,7 @@ endif() FIND_PACKAGE(JPEG QUIET) if(JPEG_FOUND) INCLUDE_DIRECTORIES(${JPEG_INCLUDE_DIR}) - ADD_DEFINITIONS(${JPEG_DEFINITIONS} -D_USE_JPG) + ADD_DEFINITIONS(${JPEG_DEFINITIONS}) SET(_USE_JPG TRUE CACHE INTERNAL "") else() SET(JPEG_LIBRARIES "") @@ -18,7 +18,7 @@ endif() FIND_PACKAGE(TIFF QUIET) if(TIFF_FOUND) INCLUDE_DIRECTORIES(${TIFF_INCLUDE_DIR}) - ADD_DEFINITIONS(${TIFF_DEFINITIONS} -D_USE_TIFF) + ADD_DEFINITIONS(${TIFF_DEFINITIONS}) SET(_USE_TIFF TRUE CACHE INTERNAL "") else() SET(TIFF_LIBRARIES "") diff --git a/libs/MVS/CMakeLists.txt b/libs/MVS/CMakeLists.txt index 79dbaba65..853386508 100644 --- a/libs/MVS/CMakeLists.txt +++ b/libs/MVS/CMakeLists.txt @@ -32,6 +32,9 @@ if(_USE_CUDA) LIST(APPEND LIBRARY_FILES_C ${LIBRARY_FILES_CUDA}) endif() +GET_FILENAME_COMPONENT(PATH_PythonWrapper_cpp ${CMAKE_CURRENT_SOURCE_DIR}/PythonWrapper.cpp ABSOLUTE) +LIST(REMOVE_ITEM LIBRARY_FILES_C "${PATH_PythonWrapper_cpp}") + cxx_library_with_type(MVS "Libs" "" "${cxx_default}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H} ) @@ -43,10 +46,26 @@ endif() # Link its dependencies if(_USE_CUDA) - set_target_properties(MVS PROPERTIES CUDA_ARCHITECTURES "50;72;75") + SET_TARGET_PROPERTIES(MVS PROPERTIES CUDA_ARCHITECTURES "50;72;75") endif() TARGET_LINK_LIBRARIES(MVS PRIVATE Common Math IO CGAL::CGAL ${CERES_LIBRARIES} ${CUDA_CUDA_LIBRARY}) +if(OpenMVS_USE_PYTHON) + # Create the Python wrapper + cxx_library_with_type(pyOpenMVS "Libs" "SHARED" "${cxx_default}" + ${PATH_PythonWrapper_cpp} + ) + # Link its dependencies + if(_USE_CUDA) + SET_TARGET_PROPERTIES(pyOpenMVS PROPERTIES CUDA_ARCHITECTURES "50;72;75") + endif() + TARGET_LINK_LIBRARIES(pyOpenMVS PRIVATE MVS ${OpenMVS_EXTRA_LIBS}) + # Suppress prefix "lib" because Python does not allow this prefix + SET_TARGET_PROPERTIES(pyOpenMVS PROPERTIES PREFIX "") + # Install + INSTALL(TARGETS pyOpenMVS DESTINATION "${PYTHON_INSTALL_PATH}") +endif() + # Install SET_TARGET_PROPERTIES(MVS PROPERTIES PUBLIC_HEADER "${LIBRARY_FILES_H}") diff --git a/libs/MVS/Camera.cpp b/libs/MVS/Camera.cpp index 01464b713..17e049e4a 100644 --- a/libs/MVS/Camera.cpp +++ b/libs/MVS/Camera.cpp @@ -137,8 +137,7 @@ void MVS::DecomposeProjectionMatrix(const PMatrix& P, KMatrix& K, RMatrix& R, CM const Vec4 hC(P.RightNullVector()); C = CMatrix(hC[0],hC[1],hC[2]) * INVERT(hC[3]); // perform RQ decomposition - const cv::Mat mP(3,4,cv::DataType::type,const_cast(P.val)); - cv::RQDecomp3x3(mP(cv::Rect(0,0, 3,3)), K, R); + RQDecomp3x3(cv::Mat(3,4,cv::DataType::type,const_cast(P.val))(cv::Rect(0,0, 3,3)), K, R); // normalize calibration matrix K *= INVERT(K(2,2)); // ensure positive focal length @@ -158,7 +157,7 @@ void MVS::DecomposeProjectionMatrix(const PMatrix& P, RMatrix& R, CMatrix& C) #ifndef _RELEASE KMatrix K; DecomposeProjectionMatrix(P, K, R, C); - ASSERT(K.IsEqual(Matrix3x3::IDENTITY)); + ASSERT(K.IsEqual(Matrix3x3::IDENTITY, 1e-5)); #endif // extract camera center as the right null vector of P const Vec4 hC(P.RightNullVector()); @@ -186,6 +185,50 @@ void MVS::AssembleProjectionMatrix(const RMatrix& R, const CMatrix& C, PMatrix& } // AssembleProjectionMatrix /*----------------------------------------------------------------*/ +// compute the focus of attention of a set of cameras; only cameras +// that have the focus of attention in front of them are considered +Point3 MVS::ComputeCamerasFocusPoint(const CameraArr& cameras, const Point3* pInitialFocus) +{ + // set initial focus point + Point3 focus; + if (pInitialFocus == NULL) { + // initialize focus point to the average camera center + focus = Point3::ZERO; + for (const Camera& camera: cameras) + focus += camera.C; + focus /= cameras.size(); + // compute average distance to the cameras + REAL avgDist = 0; + for (const Camera& camera: cameras) + avgDist += SQRT(camera.DistanceSq(focus)); + avgDist /= cameras.size(); + // move focus point to the average distance in front of first camera + focus = cameras.front().C + cameras.front().Direction()*avgDist; + } else + focus = *pInitialFocus; + // compute focus point as the point in front of the active cameras, + // closest to the view direction of each camera + uint32_t numActive = 0; + Point3::EVec smp(Point3::EVec::Zero()); + Matrix3x3::EMat smTm(Matrix3x3::EMat::Zero()); + for (const Camera& camera: cameras) { + if (!camera.IsInFront(focus)) + continue; + // https://en.wikipedia.org/wiki/Line–line_intersection#In_more_than_two_dimensions + const Point3::EVec dir = camera.Direction(); + const Matrix3x3::EMat m(Matrix3x3::EMat::Identity() - dir * dir.transpose()); + const Matrix3x3::EMat mTm(m.transpose() * m); + const Point3::EVec s = mTm * Point3::EVec(camera.C); + smp += s; + smTm += mTm; + ++numActive; + } + if (numActive >= 2) + focus = smTm.inverse() * smp; + return focus; +} // ComputeCamerasFocusPoint +/*----------------------------------------------------------------*/ + namespace MVS { diff --git a/libs/MVS/Camera.h b/libs/MVS/Camera.h index 063ede568..0d27daf03 100644 --- a/libs/MVS/Camera.h +++ b/libs/MVS/Camera.h @@ -402,6 +402,43 @@ class MVS_API Camera : public CameraIntern return TransformPointOrthoC2I(TransformPointW2C(X)); } + // compute the projection scale in this camera of the given world point + template + inline TYPE GetFootprintImage(const TPoint3& X) const { + #if 0 + const TYPE fSphereRadius(1); + const TPoint3 camX(TransformPointW2C(X)); + return norm(TransformPointC2I(TPoint3(camX.x+fSphereRadius,camX.y,camX.z))-TransformPointC2I(camX)); + #else + return static_cast(GetFocalLength() / PointDepth(X)); + #endif + } + // compute the surface the projected pixel covers at the given depth + template + inline TYPE GetFootprintWorldSq(const TPoint2& x, TYPE depth) const { + #if 0 + return SQUARE(GetFocalLength()); + #else + // improved version of the above + return SQUARE(depth) / (SQUARE(GetFocalLength()) + normSq(TransformPointI2V(x))); + #endif + } + template + inline TYPE GetFootprintWorld(const TPoint2& x, TYPE depth) const { + return depth / SQRT(SQUARE(GetFocalLength()) + normSq(TransformPointI2V(x))); + } + // same as above, but the 3D point is given + template + inline TYPE GetFootprintWorldSq(const TPoint3& X) const { + const TPoint3 camX(TransformPointW2C(X)); + return GetFootprintWorldSq(TPoint2(camX.x/camX.z,camX.y/camX.z), camX.z); + } + template + inline TYPE GetFootprintWorld(const TPoint3& X) const { + const TPoint3 camX(TransformPointW2C(X)); + return GetFootprintWorld(TPoint2(camX.x/camX.z,camX.y/camX.z), camX.z); + } + #ifdef _USE_BOOST // implement BOOST serialization template @@ -423,6 +460,7 @@ MVS_API void DecomposeProjectionMatrix(const PMatrix& P, KMatrix& K, RMatrix& R, MVS_API void DecomposeProjectionMatrix(const PMatrix& P, RMatrix& R, CMatrix& C); MVS_API void AssembleProjectionMatrix(const KMatrix& K, const RMatrix& R, const CMatrix& C, PMatrix& P); MVS_API void AssembleProjectionMatrix(const RMatrix& R, const CMatrix& C, PMatrix& P); +MVS_API Point3 ComputeCamerasFocusPoint(const CameraArr& cameras, const Point3* pInitialFocus=NULL); /*----------------------------------------------------------------*/ } // namespace MVS diff --git a/libs/MVS/DepthMap.cpp b/libs/MVS/DepthMap.cpp index b72690de4..142a5e41b 100644 --- a/libs/MVS/DepthMap.cpp +++ b/libs/MVS/DepthMap.cpp @@ -296,24 +296,29 @@ unsigned DepthData::DecRef() // S T R U C T S /////////////////////////////////////////////////// // try to load and apply mask to the depth map; -// the mask marks as false pixels that should be ignored -bool DepthEstimator::ImportIgnoreMask(const Image& image0, const Image8U::Size& size, BitMatrix& bmask, uint16_t nIgnoreMaskLabel) +// the mask for each image is stored in the MVS scene or next to each image with '.mask.png' extension; +// the mask marks as false (or 0) pixels that should be ignored +// - pMask: optional output mask; if defined, the mask is returned in this image instead of the BitMatrix +bool DepthEstimator::ImportIgnoreMask(const Image& image0, const Image8U::Size& size, uint16_t nIgnoreMaskLabel, BitMatrix& bmask, Image8U* pMask) { ASSERT(image0.IsValid() && !image0.image.empty()); - if (image0.maskName.empty()) - return false; + const String maskFileName(image0.maskName.empty() ? Util::getFileFullName(image0.name)+".mask.png" : image0.maskName); Image16U mask; - if (!mask.Load(image0.maskName)) { - DEBUG("warning: can not load the segmentation mask '%s'", image0.maskName.c_str()); + if (!mask.Load(maskFileName)) { + DEBUG("warning: can not load the segmentation mask '%s'", maskFileName.c_str()); return false; } cv::resize(mask, mask, size, 0, 0, cv::INTER_NEAREST); - bmask.create(size); - bmask.memset(0xFF); - for (int r=0; r -int OptimizePlane(TPlane& plane, const Eigen::Matrix* points, size_t size, int maxIters, TYPE threshold) -{ - typedef TPlane PLANE; - typedef Eigen::Matrix POINT; - ASSERT(size >= PLANE::numParams); - struct OptimizationFunctor { - const POINT* points; - const size_t size; - const RobustNorm::GemanMcClure robust; - // construct with the data points - OptimizationFunctor(const POINT* _points, size_t _size, double _th) - : points(_points), size(_size), robust(_th) { ASSERT(size < (size_t)std::numeric_limits::max()); } - static void Residuals(const double* x, int nPoints, const void* pData, double* fvec, double* fjac, int* /*info*/) { - const OptimizationFunctor& data = *reinterpret_cast(pData); - ASSERT((size_t)nPoints == data.size && fvec != NULL && fjac == NULL); - TPlane plane; { - Point3d N; - plane.m_fD = x[0]; - Dir2Normal(reinterpret_cast(x[1]), N); - plane.m_vN = N; - } - for (size_t i=0; i())); - } - } functor(points, size, threshold); - double arrParams[PLANE::numParams]; { - arrParams[0] = (double)plane.m_fD; - const Point3d N(plane.m_vN.x(), plane.m_vN.y(), plane.m_vN.z()); - Normal2Dir(N, reinterpret_cast(arrParams[1])); - } - lm_control_struct control = {1.e-6, 1.e-7, 1.e-8, 1.e-7, 100.0, maxIters}; // lm_control_float; - lm_status_struct status; - lmmin(PLANE::numParams, arrParams, (int)size, &functor, OptimizationFunctor::Residuals, &control, &status); - switch (status.info) { - //case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - case 10: - case 11: - case 12: - DEBUG_ULTIMATE("error: refine plane: %s", lm_infmsg[status.info]); - return 0; - } - { - Point3d N; - plane.m_fD = (TYPE)arrParams[0]; - Dir2Normal(reinterpret_cast(arrParams[1]), N); - plane.m_vN = Cast(N); - } - return status.nfev; -} - template class TPlaneSolverAdaptor { @@ -1455,11 +1402,6 @@ unsigned MVS::EstimatePlaneThLockFirstPoint(const Point3dArr& points, Planed& pl { return TEstimatePlane(points, plane, maxThreshold, arrInliers, maxIters); } // EstimatePlaneThLockFirstPoint -// least squares refinement of the given plane to the 3D point set -int MVS::OptimizePlane(Planed& plane, const Eigen::Vector3d* points, size_t size, int maxIters, double threshold) -{ - return OptimizePlane(plane, points, size, maxIters, threshold); -} // OptimizePlane /*----------------------------------------------------------------*/ // Robustly estimate the plane that fits best the given points @@ -1482,11 +1424,6 @@ unsigned MVS::EstimatePlaneThLockFirstPoint(const Point3fArr& points, Planef& pl { return TEstimatePlane(points, plane, maxThreshold, arrInliers, maxIters); } // EstimatePlaneThLockFirstPoint -// least squares refinement of the given plane to the 3D point set -int MVS::OptimizePlane(Planef& plane, const Eigen::Vector3f* points, size_t size, int maxIters, float threshold) -{ - return OptimizePlane(plane, points, size, maxIters, threshold); -} // OptimizePlane /*----------------------------------------------------------------*/ @@ -1535,6 +1472,8 @@ void MVS::EstimatePointNormals(const ImageArr& images, PointCloud& pointcloud, i { TD_TIMER_START(); + ASSERT(pointcloud.IsValid()); + typedef CGAL::Simple_cartesian kernel_t; typedef kernel_t::Point_3 point_t; typedef kernel_t::Vector_3 vector_t; diff --git a/libs/MVS/DepthMap.h b/libs/MVS/DepthMap.h index cda6e74b8..3bbd77c44 100644 --- a/libs/MVS/DepthMap.h +++ b/libs/MVS/DepthMap.h @@ -452,7 +452,7 @@ struct MVS_API DepthEstimator { ASSERT(ISEQUAL(norm(normal), 1.f)); } - static bool ImportIgnoreMask(const Image&, const Image8U::Size&, BitMatrix&, uint16_t nIgnoreMaskLabel); + static bool ImportIgnoreMask(const Image&, const Image8U::Size&, uint16_t nIgnoreMaskLabel, BitMatrix&, Image8U* =NULL); static void MapMatrix2ZigzagIdx(const Image8U::Size& size, DepthEstimator::MapRefArr& coords, const BitMatrix& mask, int rawStride=16); const float smoothBonusDepth, smoothBonusNormal; @@ -482,13 +482,11 @@ MVS_API unsigned EstimatePlane(const Point3Arr&, Plane&, double& maxThreshold, b MVS_API unsigned EstimatePlaneLockFirstPoint(const Point3Arr&, Plane&, double& maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); MVS_API unsigned EstimatePlaneTh(const Point3Arr&, Plane&, double maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); MVS_API unsigned EstimatePlaneThLockFirstPoint(const Point3Arr&, Plane&, double maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); -MATH_API int OptimizePlane(Planed& plane, const Eigen::Vector3d* points, size_t size, int maxIters, double threshold); // same for float points MATH_API unsigned EstimatePlane(const Point3fArr&, Planef&, double& maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); MATH_API unsigned EstimatePlaneLockFirstPoint(const Point3fArr&, Planef&, double& maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); MATH_API unsigned EstimatePlaneTh(const Point3fArr&, Planef&, double maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); MATH_API unsigned EstimatePlaneThLockFirstPoint(const Point3fArr&, Planef&, double maxThreshold, bool arrInliers[]=NULL, size_t maxIters=0); -MATH_API int OptimizePlane(Planef& plane, const Eigen::Vector3f* points, size_t size, int maxIters, float threshold); MVS_API void EstimatePointColors(const ImageArr& images, PointCloud& pointcloud); MVS_API void EstimatePointNormals(const ImageArr& images, PointCloud& pointcloud, int numNeighbors=16/*K-nearest neighbors*/); diff --git a/libs/MVS/Interface.h b/libs/MVS/Interface.h index 666c588e9..6cbad1eb4 100644 --- a/libs/MVS/Interface.h +++ b/libs/MVS/Interface.h @@ -386,6 +386,21 @@ struct Interface static uint32_t GetNormalizationScale(uint32_t width, uint32_t height) { return std::max(width, height); } uint32_t GetNormalizationScale() const { return GetNormalizationScale(width, height); } + // project point: camera to image (homogeneous) coordinates + inline Pos3d operator * (const Pos3d& X) const { + return Pos3d( + K(0,2)+K(0,0)*X.x/X.z, + K(1,2)+K(1,1)*X.y/X.z, + 1.0); + } + // back-project point: image (z is the depth) to camera coordinates + inline Pos3d operator / (const Pos3d& x) const { + return Pos3d( + (x.x-K(0,2))*x.z/K(0,0), + (x.y-K(1,2))*x.z/K(1,1), + 1.0); + } + template void serialize(Archive& ar, const unsigned int version) { ar & name; @@ -731,7 +746,7 @@ struct HeaderDepthDataRaw { float dMin, dMax; // depth range for this view // image file name length followed by the characters: uint16_t nFileNameSize; char* FileName // number of view IDs followed by view ID and neighbor view IDs: uint32_t nIDs; uint32_t* IDs - // camera, rotation and position matrices (row-major): double K[3][3], R[3][3], C[3] + // camera, rotation and position matrices (row-major) at image resolution: double K[3][3], R[3][3], C[3] // depth, normal, confidence maps: float depthMap[height][width], normalMap[height][width][3], confMap[height][width] inline HeaderDepthDataRaw() : name(0), type(0), padding(0) {} static uint16_t HeaderDepthDataRawName() { return *reinterpret_cast("DR"); } diff --git a/libs/MVS/Mesh.cpp b/libs/MVS/Mesh.cpp index 5bb7088d4..e708f6767 100644 --- a/libs/MVS/Mesh.cpp +++ b/libs/MVS/Mesh.cpp @@ -1551,6 +1551,7 @@ bool Mesh::LoadPLY(const String& fileName) } } if (vertices.IsEmpty() || faces.IsEmpty()) { + Release(); DEBUG_EXTRA("error: invalid mesh file"); return false; } @@ -4280,7 +4281,7 @@ void Mesh::ProjectOrtho(const Camera& camera, DepthMap& depthMap) const : Base(_vertices, _camera, _depthMap) {} inline bool ProjectVertex(const Mesh::Vertex& pt, int v) { return (ptc[v] = camera.TransformPointW2C(Cast(pt))).z > 0 && - depthMap.isInsideWithBorder(pti[v] = camera.TransformPointC2I(Point2(ptc[v].x,ptc[v].y))); + depthMap.isInsideWithBorder(pti[v] = camera.TransformPointOrthoC2I(ptc[v])); } void Raster(const ImageRef& pt, const Point3f& bary) { const Depth z(ComputeDepth(bary)); @@ -4312,7 +4313,7 @@ void Mesh::ProjectOrtho(const Camera& camera, DepthMap& depthMap, Image8U3& imag } inline bool ProjectVertex(const Mesh::Vertex& pt, int v) { return (ptc[v] = camera.TransformPointW2C(Cast(pt))).z > 0 && - depthMap.isInsideWithBorder(pti[v] = camera.TransformPointC2I(Point2(ptc[v].x,ptc[v].y))); + depthMap.isInsideWithBorder(pti[v] = camera.TransformPointOrthoC2I(ptc[v])); } void Raster(const ImageRef& pt, const Point3f& bary) { const Depth z(ComputeDepth(bary)); @@ -4462,144 +4463,206 @@ inline Eigen::AlignedBox3f bounding_box(const FaceBox& faceBox) { return faceBox.box; } #endif -bool Mesh::TransferTexture(Mesh& mesh, unsigned textureSize) +bool Mesh::TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices, unsigned borderSize, unsigned textureSize) { ASSERT(HasTexture() && mesh.HasTexture()); - if (vertexFaces.size() != vertices.size()) - ListIncidenteFaces(); - if (mesh.vertexNormals.size() != mesh.vertices.size()) - mesh.ComputeNormalVertices(); if (mesh.textureDiffuse.empty()) mesh.textureDiffuse.create(textureSize, textureSize); - #if USE_MESH_INT == USE_MESH_BVH - std::vector boxes; - boxes.reserve(faces.size()); - FOREACH(idxFace, faces) - boxes.emplace_back([this](FIndex idxFace) { - const Face& face = faces[idxFace]; - Eigen::AlignedBox3f box; - box.extend(vertices[face[0]]); - box.extend(vertices[face[1]]); - box.extend(vertices[face[2]]); - return FaceBox{box, idxFace}; - } (idxFace)); - typedef Eigen::KdBVH BVH; - BVH tree(boxes.begin(), boxes.end()); - #endif - struct IntersectRayMesh { - const Mesh& mesh; - const Ray3f& ray; - IndexDist pick; - IntersectRayMesh(const Mesh& _mesh, const Ray3f& _ray) - : mesh(_mesh), ray(_ray) { - #if USE_MESH_INT == USE_MESH_BF - FOREACH(idxFace, mesh.faces) - IntersectsRayFace(idxFace); - #endif - } - inline void IntersectsRayFace(FIndex idxFace) { - const Face& face = mesh.faces[idxFace]; - Type dist; - if (ray.Intersects(Triangle3f(mesh.vertices[face[0]], mesh.vertices[face[1]], mesh.vertices[face[2]]), &dist)) { - ASSERT(dist >= 0); - if (pick.dist > dist) { - pick.dist = dist; - pick.idx = idxFace; + Image8U mask(mesh.textureDiffuse.size(), uint8_t(255)); + const FIndex num_faces(faceSubsetIndices.empty() ? faces.size() : faceSubsetIndices.size()); + if (vertices == mesh.vertices && faces == mesh.faces) { + // the two meshes are identical, only the texture coordinates are different; + // directly transfer the texture onto the new coordinates + #ifndef MESH_USE_OPENMP + #pragma omp parallel for schedule(dynamic) + for (int_t i=0; i<(int_t)num_faces; ++i) { + const FIndex idx((FIndex)i); + #else + FOREACHRAW(idx, num_faces) { + #endif + const FIndex idxFace(faceSubsetIndices.empty() ? idx : faceSubsetIndices[idx]); + struct RasterTriangle { + const Mesh& meshRef; + Mesh& meshTrg; + Image8U& mask; + const TexCoord* tri; + inline cv::Size Size() const { return meshTrg.textureDiffuse.size(); } + inline void operator()(const ImageRef& pt, const Point3f& bary) { + ASSERT(meshTrg.textureDiffuse.isInside(pt)); + const TexCoord x(tri[0]*bary.x + tri[1]*bary.y + tri[2]*bary.z); + const Pixel8U color(meshRef.textureDiffuse.sample(x)); + meshTrg.textureDiffuse(pt) = color; + mask(pt) = 0; } - } + } data{*this, mesh, mask, faceTexcoords.data()+idxFace*3}; + // render triangle and for each pixel interpolate the color + // from the triangle corners using barycentric coordinates + const TexCoord* tri = mesh.faceTexcoords.data()+idxFace*3; + Image8U::RasterizeTriangleBary(tri[0], tri[1], tri[2], data); } + } else { + // the two meshes are different, transfer the texture by finding the closest point + // on the two surfaces + if (vertexFaces.size() != vertices.size()) + ListIncidenteFaces(); + if (mesh.vertexNormals.size() != mesh.vertices.size()) + mesh.ComputeNormalVertices(); #if USE_MESH_INT == USE_MESH_BVH - inline bool intersectVolume(const BVH::Volume &volume) { - return ray.Intersects(AABB3f(volume.min(), volume.max())); - } - inline bool intersectObject(const BVH::Object &object) { - IntersectsRayFace(object.idxFace); - return false; - } + std::vector boxes; + boxes.reserve(faces.size()); + FOREACH(idxFace, faces) + boxes.emplace_back([this](FIndex idxFace) { + const Face& face = faces[idxFace]; + Eigen::AlignedBox3f box; + box.extend(vertices[face[0]]); + box.extend(vertices[face[1]]); + box.extend(vertices[face[2]]); + return FaceBox{box, idxFace}; + } (idxFace)); + typedef Eigen::KdBVH BVH; + BVH tree(boxes.begin(), boxes.end()); #endif - }; - #if USE_MESH_INT == USE_MESH_BF || USE_MESH_INT == USE_MESH_BVH - const float diagonal(GetAABB().GetSize().norm()); - #elif USE_MESH_INT == USE_MESH_OCTREE - const Octree octree(vertices, [](Octree::IDX_TYPE size, Octree::Type /*radius*/) { - return size > 8; - }); - const float diagonal(octree.GetAabb().GetSize().norm()); - struct OctreeIntersectRayMesh : IntersectRayMesh { - OctreeIntersectRayMesh(const Octree& octree, const Mesh& _mesh, const Ray3f& _ray) - : IntersectRayMesh(_mesh, _ray) { - octree.Collect(*this, *this); - } - inline bool Intersects(const Octree::POINT_TYPE& center, Octree::Type radius) const { - return ray.Intersects(AABB3f(center, radius)); - } - void operator () (const Octree::IDX_TYPE* idices, Octree::IDX_TYPE size) { - // store all contained faces only once - std::unordered_set set; - FOREACHRAWPTR(pIdx, idices, size) { - const VIndex idxVertex((VIndex)*pIdx); - const FaceIdxArr& faces = mesh.vertexFaces[idxVertex]; - set.insert(faces.begin(), faces.end()); - } - // test face intersection and keep the closest - for (FIndex idxFace : set) - IntersectsRayFace(idxFace); - } - }; - #endif - #ifdef MESH_USE_OPENMP - #pragma omp parallel for schedule(dynamic) - for (int_t i=0; i<(int_t)mesh.faces.size(); ++i) { - const FIndex idxFace((FIndex)i); - #else - FOREACH(idxFace, mesh.faces) { - #endif - struct RasterTraiangle { - #if USE_MESH_INT == USE_MESH_OCTREE - const Octree& octree; - #elif USE_MESH_INT == USE_MESH_BVH - BVH& tree; - #endif - const Mesh& meshRef; - Mesh& meshTrg; - const Face& face; - float diagonal; - inline cv::Size Size() const { return meshTrg.textureDiffuse.size(); } - inline void operator()(const ImageRef& pt, const Point3f& bary) { - ASSERT(meshTrg.textureDiffuse.isInside(pt)); - const Vertex X(meshTrg.vertices[face[0]]*bary.x + meshTrg.vertices[face[1]]*bary.y + meshTrg.vertices[face[2]]*bary.z); - const Normal N(normalized(meshTrg.vertexNormals[face[0]]*bary.x + meshTrg.vertexNormals[face[1]]*bary.y + meshTrg.vertexNormals[face[2]]*bary.z)); - const Ray3f ray(Vertex(X+N*diagonal), Normal(-N)); + struct IntersectRayMesh { + const Mesh& mesh; + const Ray3f& ray; + IndexDist pick; + IntersectRayMesh(const Mesh& _mesh, const Ray3f& _ray) + : mesh(_mesh), ray(_ray) { #if USE_MESH_INT == USE_MESH_BF - const IntersectRayMesh intRay(meshRef, ray); - #elif USE_MESH_INT == USE_MESH_BVH - IntersectRayMesh intRay(meshRef, ray); - Eigen::BVIntersect(tree, intRay); - #else - const OctreeIntersectRayMesh intRay(octree, meshRef, ray); + FOREACH(idxFace, mesh.faces) + IntersectsRayFace(idxFace); #endif - if (intRay.pick.IsValid()) { - const FIndex refIdxFace((FIndex)intRay.pick.idx); - const Face& refFace = meshRef.faces[refIdxFace]; - const Vertex refX(ray.GetPoint((Type)intRay.pick.dist)); - const Vertex baryRef(CorrectBarycentricCoordinates(BarycentricCoordinatesUV(meshRef.vertices[refFace[0]], meshRef.vertices[refFace[1]], meshRef.vertices[refFace[2]], refX))); - const TexCoord* tri = meshRef.faceTexcoords.data()+refIdxFace*3; - const TexCoord x(tri[0]*baryRef.x + tri[1]*baryRef.y + tri[2]*baryRef.z); - const Pixel8U color(meshRef.textureDiffuse.sample(x)); - meshTrg.textureDiffuse(pt) = color; + } + inline void IntersectsRayFace(FIndex idxFace) { + const Face& face = mesh.faces[idxFace]; + Type dist; + if (ray.Intersects(Triangle3f(mesh.vertices[face[0]], mesh.vertices[face[1]], mesh.vertices[face[2]]), &dist)) { + ASSERT(dist >= 0); + if (pick.dist > dist) { + pick.dist = dist; + pick.idx = idxFace; + } + } + } + #if USE_MESH_INT == USE_MESH_BVH + inline bool intersectVolume(const BVH::Volume &volume) { + return ray.Intersects(AABB3f(volume.min(), volume.max())); + } + inline bool intersectObject(const BVH::Object &object) { + IntersectsRayFace(object.idxFace); + return false; + } + #endif + }; + #if USE_MESH_INT == USE_MESH_BF || USE_MESH_INT == USE_MESH_BVH + const float diagonal(GetAABB().GetSize().norm()); + #elif USE_MESH_INT == USE_MESH_OCTREE + const Octree octree(vertices, [](Octree::IDX_TYPE size, Octree::Type /*radius*/) { + return size > 8; + }); + const float diagonal(octree.GetAabb().GetSize().norm()); + struct OctreeIntersectRayMesh : IntersectRayMesh { + OctreeIntersectRayMesh(const Octree& octree, const Mesh& _mesh, const Ray3f& _ray) + : IntersectRayMesh(_mesh, _ray) { + octree.Collect(*this, *this); + } + inline bool Intersects(const Octree::POINT_TYPE& center, Octree::Type radius) const { + return ray.Intersects(AABB3f(center, radius)); + } + void operator() (const Octree::IDX_TYPE* idices, Octree::IDX_TYPE size) { + // store all contained faces only once + std::unordered_set set; + FOREACHRAWPTR(pIdx, idices, size) { + const VIndex idxVertex((VIndex)*pIdx); + const FaceIdxArr& faces = mesh.vertexFaces[idxVertex]; + set.insert(faces.begin(), faces.end()); } + // test face intersection and keep the closest + for (FIndex idxFace : set) + IntersectsRayFace(idxFace); } - #if USE_MESH_INT == USE_MESH_BF - } data{*this, mesh, mesh.faces[idxFace], diagonal}; - #elif USE_MESH_INT == USE_MESH_BVH - } data{tree, *this, mesh, mesh.faces[idxFace], diagonal}; + }; + #endif + #ifdef MESH_USE_OPENMP + #pragma omp parallel for schedule(dynamic) + for (int_t i=0; i<(int_t)num_faces; ++i) { + const FIndex idx((FIndex)i); #else - } data{octree, *this, mesh, mesh.faces[idxFace], diagonal}; + FOREACHRAW(idx, num_faces) { #endif - // render triangle and for each pixel interpolate the color - // from the triangle corners using barycentric coordinates - const TexCoord* tri = mesh.faceTexcoords.data()+idxFace*3; - Image8U::RasterizeTriangleBary(tri[0], tri[1], tri[2], data); + const FIndex idxFace(faceSubsetIndices.empty() ? idx : faceSubsetIndices[idx]); + struct RasterTriangle { + #if USE_MESH_INT == USE_MESH_OCTREE + const Octree& octree; + #elif USE_MESH_INT == USE_MESH_BVH + BVH& tree; + #endif + const Mesh& meshRef; + Mesh& meshTrg; + Image8U& mask; + const Face& face; + float diagonal; + inline cv::Size Size() const { return meshTrg.textureDiffuse.size(); } + inline void operator()(const ImageRef& pt, const Point3f& bary) { + ASSERT(meshTrg.textureDiffuse.isInside(pt)); + const Vertex X(meshTrg.vertices[face[0]]*bary.x + meshTrg.vertices[face[1]]*bary.y + meshTrg.vertices[face[2]]*bary.z); + const Normal N(normalized(meshTrg.vertexNormals[face[0]]*bary.x + meshTrg.vertexNormals[face[1]]*bary.y + meshTrg.vertexNormals[face[2]]*bary.z)); + const Ray3f ray(Vertex(X+N*diagonal), Normal(-N)); + #if USE_MESH_INT == USE_MESH_BF + const IntersectRayMesh intRay(meshRef, ray); + #elif USE_MESH_INT == USE_MESH_BVH + IntersectRayMesh intRay(meshRef, ray); + Eigen::BVIntersect(tree, intRay); + #else + const OctreeIntersectRayMesh intRay(octree, meshRef, ray); + #endif + if (intRay.pick.IsValid()) { + const FIndex refIdxFace((FIndex)intRay.pick.idx); + const Face& refFace = meshRef.faces[refIdxFace]; + const Vertex refX(ray.GetPoint((Type)intRay.pick.dist)); + const Vertex baryRef(CorrectBarycentricCoordinates(BarycentricCoordinatesUV(meshRef.vertices[refFace[0]], meshRef.vertices[refFace[1]], meshRef.vertices[refFace[2]], refX))); + const TexCoord* tri = meshRef.faceTexcoords.data()+refIdxFace*3; + const TexCoord x(tri[0]*baryRef.x + tri[1]*baryRef.y + tri[2]*baryRef.z); + const Pixel8U color(meshRef.textureDiffuse.sample(x)); + meshTrg.textureDiffuse(pt) = color; + mask(pt) = 0; + } + } + #if USE_MESH_INT == USE_MESH_BF + } data{*this, mesh, mask, mesh.faces[idxFace], diagonal}; + #elif USE_MESH_INT == USE_MESH_BVH + } data{tree, *this, mesh, mask, mesh.faces[idxFace], diagonal}; + #else + } data{octree, *this, mesh, mask, mesh.faces[idxFace], diagonal}; + #endif + // render triangle and for each pixel interpolate the color + // from the triangle corners using barycentric coordinates + const TexCoord* tri = mesh.faceTexcoords.data()+idxFace*3; + Image8U::RasterizeTriangleBary(tri[0], tri[1], tri[2], data); + } + } + // fill border + if (borderSize > 0) { + ASSERT(mask.size().area() == mesh.textureDiffuse.size().area()); + const int border(static_cast(borderSize)); + CLISTDEF0(int) idx_valid_pixels; + idx_valid_pixels.push_back(-1); + ASSERT(mask.isContinuous()); + const int size(mask.size().area()); + for (int i=0; i labels; + cv::distanceTransform(mask, dists, labels, cv::DIST_L1, 3, cv::DIST_LABEL_PIXEL); + ASSERT(mesh.textureDiffuse.isContinuous()); + for (int i=0; i(dists(i)); + if (dist > 0 && dist <= border) { + const int label(labels(i)); + const int idx_closest_pixel(idx_valid_pixels[label]); + mesh.textureDiffuse(i) = mesh.textureDiffuse(idx_closest_pixel); + } + } } return true; } // TransferTexture diff --git a/libs/MVS/Mesh.h b/libs/MVS/Mesh.h index 0d0640363..214e2f21c 100644 --- a/libs/MVS/Mesh.h +++ b/libs/MVS/Mesh.h @@ -223,7 +223,7 @@ class MVS_API Mesh bool Split(FacesChunkArr&, float maxArea); Mesh SubMesh(const FaceIdxArr& faces) const; - bool TransferTexture(Mesh& mesh, unsigned textureSize=1024); + bool TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices={}, unsigned borderSize=3, unsigned textureSize=1024); // file IO bool Load(const String& fileName); @@ -360,7 +360,7 @@ struct IntersectRayMesh { return ray.Intersects(AABB3f(center, radius)); } - void operator () (const IDX* idices, IDX size) { + void operator() (const IDX* idices, IDX size) { // store all intersected faces only once typedef std::unordered_set FaceSet; FaceSet set; diff --git a/libs/MVS/PatchMatchCUDA.cpp b/libs/MVS/PatchMatchCUDA.cpp index ac872df0b..1a6c95f99 100644 --- a/libs/MVS/PatchMatchCUDA.cpp +++ b/libs/MVS/PatchMatchCUDA.cpp @@ -404,7 +404,7 @@ void PatchMatchCUDA::EstimateDepthMap(DepthData& depthData) if (OPTDENSE::nIgnoreMaskLabel >= 0) { const DepthData::ViewData& view = depthData.GetView(); BitMatrix mask; - if (DepthEstimator::ImportIgnoreMask(*view.pImageData, depthData.depthMap.size(), mask, (uint16_t)OPTDENSE::nIgnoreMaskLabel)) + if (DepthEstimator::ImportIgnoreMask(*view.pImageData, depthData.depthMap.size(), (uint16_t)OPTDENSE::nIgnoreMaskLabel, mask)) depthData.ApplyIgnoreMask(mask); } diff --git a/libs/MVS/PointCloud.cpp b/libs/MVS/PointCloud.cpp index 6c6bf04f8..2920b8978 100644 --- a/libs/MVS/PointCloud.cpp +++ b/libs/MVS/PointCloud.cpp @@ -41,6 +41,17 @@ using namespace MVS; // S T R U C T S /////////////////////////////////////////////////// +PointCloud& MVS::PointCloud::Swap(PointCloud& rhs) +{ + points.Swap(rhs.points); + pointViews.Swap(rhs.pointViews); + pointWeights.Swap(rhs.pointWeights); + normals.Swap(rhs.normals); + colors.Swap(rhs.colors); + return *this; +} +/*----------------------------------------------------------------*/ + void PointCloud::Release() { points.Release(); @@ -178,7 +189,8 @@ Planef PointCloud::EstimateGroundPlane(const ImageArr& images, float planeThresh for (const Point& X: *pPoints) if (plane.DistanceAbs(X) < maxThreshold) inliers.emplace_back(X); - OptimizePlane(plane, inliers.data(), inliers.size(), 100, static_cast(threshold)); + const RobustNorm::GemanMcClure robust(threshold); + plane.Optimize(inliers.data(), inliers.size(), robust); // make sure the plane is well oriented, negate plane normal if it faces same direction as cameras on average if (!images.empty()) { @@ -223,6 +235,8 @@ namespace BasicPLY { Point p; Color c; Normal n; + float scale; + float confidence; }; static const PLY::PlyProperty vert_props[] = { {"x", PLY::Float32, PLY::Float32, offsetof(PointColNormal,p.x), 0, 0, 0, 0}, @@ -233,7 +247,9 @@ namespace BasicPLY { {"blue", PLY::Uint8, PLY::Uint8, offsetof(PointColNormal,c.b), 0, 0, 0, 0}, {"nx", PLY::Float32, PLY::Float32, offsetof(PointColNormal,n.x), 0, 0, 0, 0}, {"ny", PLY::Float32, PLY::Float32, offsetof(PointColNormal,n.y), 0, 0, 0, 0}, - {"nz", PLY::Float32, PLY::Float32, offsetof(PointColNormal,n.z), 0, 0, 0, 0} + {"nz", PLY::Float32, PLY::Float32, offsetof(PointColNormal,n.z), 0, 0, 0, 0}, + {"scale", PLY::Float32, PLY::Float32, offsetof(PointColNormal,scale), 0, 0, 0, 0}, + {"confidence", PLY::Float32, PLY::Float32, offsetof(PointColNormal,confidence), 0, 0, 0, 0} }; // list of the kinds of elements in the PLY static const char* elem_names[] = { @@ -296,7 +312,7 @@ bool PointCloud::Load(const String& fileName) } // Load // save the dense point cloud as PLY file -bool PointCloud::Save(const String& fileName, bool bLegacyTypes) const +bool PointCloud::Save(const String& fileName, bool bLegacyTypes, bool bBinary) const { if (points.IsEmpty()) return false; @@ -308,7 +324,7 @@ bool PointCloud::Save(const String& fileName, bool bLegacyTypes) const PLY ply; if (bLegacyTypes) ply.set_legacy_type_names(); - if (!ply.write(fileName, 1, BasicPLY::elem_names, PLY::BINARY_LE, 64*1024)) + if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII, 64*1024)) return false; if (normals.IsEmpty()) { @@ -353,7 +369,7 @@ bool PointCloud::Save(const String& fileName, bool bLegacyTypes) const } // Save // save the dense point cloud having >=N views as PLY file -bool PointCloud::SaveNViews(const String& fileName, uint32_t minViews, bool bLegacyTypes) const +bool PointCloud::SaveNViews(const String& fileName, uint32_t minViews, bool bLegacyTypes, bool bBinary) const { if (points.IsEmpty()) return false; @@ -365,7 +381,7 @@ bool PointCloud::SaveNViews(const String& fileName, uint32_t minViews, bool bLeg PLY ply; if (bLegacyTypes) ply.set_legacy_type_names(); - if (!ply.write(fileName, 1, BasicPLY::elem_names, PLY::BINARY_LE, 64*1024)) + if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII, 64*1024)) return false; if (normals.IsEmpty()) { @@ -407,6 +423,90 @@ bool PointCloud::SaveNViews(const String& fileName, uint32_t minViews, bool bLeg DEBUG_EXTRA("Point-cloud saved: %u points with at least %u views each (%s)", numPoints, minViews, TD_TIMER_GET_FMT().c_str()); return true; } // SaveNViews + +// save the dense point cloud + scale as PLY file +bool PointCloud::SaveWithScale(const String& fileName, const ImageArr& images, float scaleMult, bool bLegacyTypes, bool bBinary) const +{ + if (points.empty()) + return false; + + TD_TIMER_STARTD(); + + // create PLY object + ASSERT(!fileName.empty()); + Util::ensureFolder(fileName); + PLY ply; + if (bLegacyTypes) + ply.set_legacy_type_names(); + if (!ply.write(fileName, 1, BasicPLY::elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII, 64*1024)) + return false; + + // describe what properties go into the vertex elements + ply.describe_property(BasicPLY::elem_names[0], 11, BasicPLY::vert_props); + + // export the array of 3D points + BasicPLY::PointColNormal vertex; + FOREACH(i, points) { + // export the vertex position, normal and scale + vertex.p = points[i]; + if (colors.empty()) + vertex.c = Color::WHITE; + else + vertex.c = colors[i]; + if (normals.empty()) + vertex.n = Vec3f::ZERO; + else + vertex.n = normals[i]; + #if 0 + // one sample per view + vertex.confidence = 1; + for (IIndex idxView: pointViews[i]) { + const float scale((float)images[idxView].camera.GetFootprintWorld(Cast(vertex.p))); + ASSERT(scale > 0); + vertex.scale = scale*scaleMult; + ply.put_element(&vertex); + } + #else + // one sample per point + vertex.scale = FLT_MAX; + if (pointWeights.empty()) { + vertex.confidence = (float)pointViews[i].size(); + for (IIndex idxView: pointViews[i]) { + const float scale((float)images[idxView].camera.GetFootprintWorld(Cast(vertex.p))); + ASSERT(scale > 0); + if (vertex.scale > scale) + vertex.scale = scale; + } + } else { + vertex.confidence = 0; + float scaleWeightBest = FLT_MAX; + FOREACH(j, pointViews[i]) { + const IIndex idxView = pointViews[i][j]; + const float scale((float)images[idxView].camera.GetFootprintWorld(Cast(vertex.p))); + ASSERT(scale > 0); + const float conf(pointWeights[i][j]); + const float scaleWeight(scale/conf); + if (scaleWeightBest > scaleWeight) { + scaleWeightBest = scaleWeight; + vertex.scale = scale; + vertex.confidence = conf; + } + } + } + ASSERT(vertex.scale != FLT_MAX); + vertex.scale *= scaleMult; + ply.put_element(&vertex); + #endif + } + ASSERT(ply.get_current_element_count() == (int)points.size()); + + // write the header + if (!ply.header_complete()) + return false; + + DEBUG_EXTRA("Point-cloud saved: %u points with scale (%s)", points.size(), TD_TIMER_GET_FMT().c_str()); + return true; +} // SaveWithScale /*----------------------------------------------------------------*/ diff --git a/libs/MVS/PointCloud.h b/libs/MVS/PointCloud.h index 901823757..fe43372ff 100644 --- a/libs/MVS/PointCloud.h +++ b/libs/MVS/PointCloud.h @@ -82,13 +82,13 @@ class MVS_API PointCloud ColorArr colors; public: - inline PointCloud() {} + PointCloud& Swap(PointCloud&); void Release(); - inline bool IsEmpty() const { ASSERT(points.GetSize() == pointViews.GetSize() || pointViews.IsEmpty()); return points.IsEmpty(); } - inline bool IsValid() const { ASSERT(points.GetSize() == pointViews.GetSize() || pointViews.IsEmpty()); return !pointViews.IsEmpty(); } - inline size_t GetSize() const { ASSERT(points.GetSize() == pointViews.GetSize() || pointViews.IsEmpty()); return points.GetSize(); } + inline bool IsEmpty() const { ASSERT(points.size() == pointViews.size() || pointViews.empty()); return points.empty(); } + inline bool IsValid() const { ASSERT(points.size() == pointViews.size() || pointViews.empty()); return !pointViews.empty(); } + inline size_t GetSize() const { ASSERT(points.size() == pointViews.size() || pointViews.empty()); return points.size(); } void RemovePoint(IDX); void RemovePointsOutside(const OBB3f&); @@ -102,8 +102,9 @@ class MVS_API PointCloud Planef EstimateGroundPlane(const ImageArr& images, float planeThreshold=0, const String& fileExportPlane="") const; bool Load(const String& fileName); - bool Save(const String& fileName, bool bLegacyTypes=false) const; - bool SaveNViews(const String& fileName, uint32_t minViews, bool bLegacyTypes=false) const; + bool Save(const String& fileName, bool bLegacyTypes=false, bool bBinary=true) const; + bool SaveNViews(const String& fileName, uint32_t minViews, bool bLegacyTypes=false, bool bBinary=true) const; + bool SaveWithScale(const String& fileName, const ImageArr& images, float scaleMult, bool bLegacyTypes=false, bool bBinary=true) const; void PrintStatistics(const Image* pImages = NULL, const OBB3f* pObb = NULL) const; @@ -152,11 +153,11 @@ struct IntersectRayPoints { return coneIntersect(Sphere3(center.cast(), REAL(radius) * SQRT_3)); } - void operator () (const IDX* idices, IDX size) { + void operator() (const IDX* idices, IDX size) { // test ray-point intersection and keep the closest FOREACHRAWPTR(pIdx, idices, size) { const PointCloud::Index idx(*pIdx); - if (!pointcloud.pointViews.IsEmpty() && pointcloud.pointViews[idx].size() < minViews) + if (!pointcloud.pointViews.empty() && pointcloud.pointViews[idx].size() < minViews) continue; const PointCloud::Point& X = pointcloud.points[idx]; REAL dist; diff --git a/libs/MVS/PythonWrapper.cpp b/libs/MVS/PythonWrapper.cpp new file mode 100644 index 000000000..62a8a1d71 --- /dev/null +++ b/libs/MVS/PythonWrapper.cpp @@ -0,0 +1,137 @@ +/* +* PythonWrapper.cpp +* +* Copyright (c) 2014-2022 SEACAVE +* +* Author(s): +* +* cDc +* +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +* +* +* Additional Terms: +* +* You are required to preserve legal notices and author attributions in +* that material or in the Appropriate Legal Notices displayed by works +* containing it. +*/ + +#ifdef _USE_BOOST_PYTHON + +#undef _USRDLL +#define _LIB +#include "Common.h" +#include "Scene.h" +#undef _LIB +#define _USRDLL +#define BOOST_PYTHON_STATIC_LIB +#include + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace pyMVS { + +class Scene : public MVS::Scene +{ +public: + Scene(unsigned _nMaxThreads=0) : MVS::Scene(_nMaxThreads) { + Util::Init(); + INIT_WORKING_FOLDER; + MVS::OPTDENSE::init(); + } + + bool pyLoad(const std::string& fileName, bool bImport=false) { + return Load(MAKE_PATH_SAFE(fileName), bImport); + } + bool pySave(const std::string& fileName, int type=ARCHIVE_DEFAULT) const { + return Save(MAKE_PATH_SAFE(fileName), static_cast(type)); + } + + bool pySavePointCloud(const std::string& fileName) const { + return pointcloud.Save(MAKE_PATH_SAFE(fileName)); + } + + bool pyLoadMesh(const std::string& fileName) { + return mesh.Load(MAKE_PATH_SAFE(fileName)); + } + bool pySaveMesh(const std::string& fileName) const { + return mesh.Save(MAKE_PATH_SAFE(fileName)); + } + + bool pyDenseReconstruction(unsigned nResolutionLevel=1, int nFusionMode=0, bool bCrop2ROI=true, float fBorderROI=0) { + MVS::OPTDENSE::nResolutionLevel = nResolutionLevel; + return DenseReconstruction(nFusionMode, bCrop2ROI, fBorderROI); + } + bool pyReconstructMesh(float distInsert=2, bool bUseFreeSpaceSupport=false, bool bUseOnlyROI=false) { + return ReconstructMesh(distInsert, bUseFreeSpaceSupport, bUseOnlyROI); + } + void pyCleanMesh(float fDecimate=1.f, float fRemoveSpurious=20.f, bool bRemoveSpikes=true, unsigned nCloseHoles=30, unsigned nSmoothMesh=2, float fEdgeLength=0.f, bool bCrop2ROI=false) { + if (bCrop2ROI && IsBounded()) { + const size_t numVertices = mesh.vertices.size(); + const size_t numFaces = mesh.faces.size(); + mesh.RemoveFacesOutside(obb); + } + mesh.Clean(fDecimate, fRemoveSpurious, bRemoveSpikes, nCloseHoles, nSmoothMesh, fEdgeLength, false); + mesh.Clean(1.f, 0.f, bRemoveSpikes, nCloseHoles, 0u, 0.f, false); // extra cleaning trying to close more holes + mesh.Clean(1.f, 0.f, false, 0u, 0u, 0.f, true); // extra cleaning to remove non-manifold problems created by closing holes + } + bool pyRefineMesh(unsigned nResolutionLevel=0, unsigned nEnsureEdgeSize=1, unsigned nMaxFaceArea=32, unsigned nScales=2, float fScaleStep=0.5f, float fRegularityWeight=0.2f) { + return RefineMesh(nResolutionLevel, 640/*nMinResolution*/, 8/*nMaxViews*/, 0.f/*fDecimateMesh*/, 30/*nCloseHoles*/, nEnsureEdgeSize, + nMaxFaceArea, nScales, fScaleStep, 0/*nAlternatePair*/, fRegularityWeight, 0.9f/*fRatioRigidityElasticity*/, 45.05f/*fGradientStep*/); + } + bool pyTextureMesh(unsigned nResolutionLevel=0, uint32_t nColEmpty=0x00FF7F27) { + return TextureMesh(nResolutionLevel, 640/*nMinResolution*/, 0/*minCommonCameras*/, 0.f/*fOutlierThreshold*/, 0.3f/*fRatioDataSmoothness*/, + true/*bGlobalSeamLeveling*/, true/*bLocalSeamLeveling*/, 0/*nTextureSizeMultiple*/, 3/*nRectPackingHeuristic*/, Pixel8U(nColEmpty)); + } +}; + +void SetWorkingFolder(const std::string& folder) { + WORKING_FOLDER = folder; + Util::ensureValidFolderPath(WORKING_FOLDER); + INIT_WORKING_FOLDER; +} + +BOOST_PYTHON_MODULE(pyOpenMVS) { + using namespace boost::python; + + class_>("Scene", init(arg("max_threads")=0)) + .def("load", &Scene::pyLoad, (arg("file_path"), arg("import")=true)) + .def("save", &Scene::pySave, (arg("file_path"), arg("type")=static_cast(ARCHIVE_DEFAULT))) + .def("save_pointcloud", &Scene::pySavePointCloud, (arg("file_path"))) + .def("load_mesh", &Scene::pyLoadMesh, (arg("file_path"))) + .def("save_mesh", &Scene::pySaveMesh, (arg("file_path"))) + .def("scale_images", &Scene::ScaleImages) + .def("transform", static_cast(&Scene::Transform)) + .def("transform34", static_cast(&Scene::Transform)) + .def("align_to", &Scene::AlignTo) + .def("dense_reconstruction", &Scene::pyDenseReconstruction, (arg("resolution_level")=0, arg("fusion_mode")=0, arg("crop_to_roi")=true, arg("roi_border")=0.f)) + .def("reconstruct_mesh", &Scene::pyReconstructMesh, (arg("dist_insert")=2, arg("use_free_space_support")=false, arg("use_only_roi")=false)) + .def("clean_mesh", &Scene::pyCleanMesh, (arg("decimate")=1.f, arg("remove_spurious")=20.f, arg("remove_spikes")=true, arg("close_holes")=30, arg("smooth_mesh")=2, arg("edge_length")=0.f, arg("crop_to_roi")=true)) + .def("refine_mesh", &Scene::pyRefineMesh, (arg("resolution_level")=0, arg("ensure_edge_size")=1, arg("max_face_area")=32, arg("scales")=2, arg("scale_step")=0.5f, arg("regularity_weight")=0.2f)) + .def("texture_mesh", &Scene::pyTextureMesh, (arg("resolution_level")=0, arg("empty_color")=0x00FF7F27)) + .def("compute_leveled_volume", &Scene::ComputeLeveledVolume); + + def("set_working_folder", &SetWorkingFolder); +} // BOOST_PYTHON_MODULE +/*----------------------------------------------------------------*/ + +} // namespace pyMVS + +#endif // _USE_BOOST_PYTHON diff --git a/libs/MVS/Scene.cpp b/libs/MVS/Scene.cpp index 3b8023207..165d46ebe 100644 --- a/libs/MVS/Scene.cpp +++ b/libs/MVS/Scene.cpp @@ -59,6 +59,11 @@ void Scene::Release() mesh.Release(); } +bool Scene::IsValid() const +{ + return !platforms.IsEmpty() && !images.IsEmpty(); +} + bool Scene::IsEmpty() const { return pointcloud.IsEmpty() && mesh.IsEmpty(); @@ -84,13 +89,13 @@ bool Scene::LoadInterface(const String & fileName) // import platforms and cameras ASSERT(!obj.platforms.empty()); - platforms.Reserve((uint32_t)obj.platforms.size()); + platforms.reserve((uint32_t)obj.platforms.size()); for (const Interface::Platform& itPlatform: obj.platforms) { - Platform& platform = platforms.AddEmpty(); + Platform& platform = platforms.emplace_back(); platform.name = itPlatform.name; - platform.cameras.Reserve((uint32_t)itPlatform.cameras.size()); + platform.cameras.reserve((uint32_t)itPlatform.cameras.size()); for (const Interface::Platform::Camera& itCamera: itPlatform.cameras) { - Platform::Camera& camera = platform.cameras.AddEmpty(); + Platform::Camera& camera = platform.cameras.emplace_back(); camera.K = itCamera.K; camera.R = itCamera.R; camera.C = itCamera.C; @@ -101,27 +106,28 @@ bool Scene::LoadInterface(const String & fileName) } DEBUG_EXTRA("Camera model loaded: platform %u; camera %2u; f %.3fx%.3f; poses %u", platforms.size()-1, platform.cameras.size()-1, camera.K(0,0), camera.K(1,1), itPlatform.poses.size()); } - ASSERT(platform.cameras.GetSize() == itPlatform.cameras.size()); - platform.poses.Reserve((uint32_t)itPlatform.poses.size()); + ASSERT(platform.cameras.size() == itPlatform.cameras.size()); + platform.poses.reserve((uint32_t)itPlatform.poses.size()); for (const Interface::Platform::Pose& itPose: itPlatform.poses) { - Platform::Pose& pose = platform.poses.AddEmpty(); + Platform::Pose& pose = platform.poses.emplace_back(); pose.R = itPose.R; pose.C = itPose.C; } - ASSERT(platform.poses.GetSize() == itPlatform.poses.size()); + ASSERT(platform.poses.size() == itPlatform.poses.size()); } - ASSERT(platforms.GetSize() == obj.platforms.size()); - if (platforms.IsEmpty()) + ASSERT(platforms.size() == obj.platforms.size()); + if (platforms.empty()) return false; // import images nCalibratedImages = 0; size_t nTotalPixels(0); ASSERT(!obj.images.empty()); - images.Reserve((uint32_t)obj.images.size()); + images.reserve((uint32_t)obj.images.size()); for (const Interface::Image& image: obj.images) { const uint32_t ID(images.size()); - Image& imageData = images.AddEmpty(); + Image& imageData = images.emplace_back(); + imageData.ID = (image.ID == NO_ID ? ID : image.ID); imageData.name = image.name; Util::ensureUnifySlash(imageData.name); imageData.name = MAKE_PATH_FULL(WORKING_FOLDER_FULL, imageData.name); @@ -137,7 +143,6 @@ bool Scene::LoadInterface(const String & fileName) } imageData.platformID = image.platformID; imageData.cameraID = image.cameraID; - imageData.ID = (image.ID == NO_ID ? ID : image.ID); // init camera const Interface::Platform::Camera& camera = obj.platforms[image.platformID].cameras[image.cameraID]; if (camera.HasResolution()) { @@ -155,26 +160,26 @@ bool Scene::LoadInterface(const String & fileName) nTotalPixels += imageData.width * imageData.height; DEBUG_ULTIMATE("Image loaded %3u: %s", ID, Util::getFileNameExt(imageData.name).c_str()); } - if (images.GetSize() < 2) + if (images.size() < 2) return false; // import 3D points if (!obj.vertices.empty()) { bool bValidWeights(false); - pointcloud.points.Resize(obj.vertices.size()); - pointcloud.pointViews.Resize(obj.vertices.size()); - pointcloud.pointWeights.Resize(obj.vertices.size()); + pointcloud.points.resize(obj.vertices.size()); + pointcloud.pointViews.resize(obj.vertices.size()); + pointcloud.pointWeights.resize(obj.vertices.size()); FOREACH(i, pointcloud.points) { const Interface::Vertex& vertex = obj.vertices[i]; PointCloud::Point& point = pointcloud.points[i]; point = vertex.X; PointCloud::ViewArr& views = pointcloud.pointViews[i]; - views.Resize((PointCloud::ViewArr::IDX)vertex.views.size()); + views.resize((PointCloud::ViewArr::IDX)vertex.views.size()); PointCloud::WeightArr& weights = pointcloud.pointWeights[i]; - weights.Resize((PointCloud::ViewArr::IDX)vertex.views.size()); - CLISTDEF0(PointCloud::ViewArr::IDX) indices(views.GetSize()); - std::iota(indices.Begin(), indices.End(), 0); - std::sort(indices.Begin(), indices.End(), [&](IndexArr::Type i0, IndexArr::Type i1) -> bool { + weights.resize((PointCloud::ViewArr::IDX)vertex.views.size()); + CLISTDEF0(PointCloud::ViewArr::IDX) indices(views.size()); + std::iota(indices.begin(), indices.end(), 0); + std::sort(indices.begin(), indices.end(), [&](IndexArr::Type i0, IndexArr::Type i1) -> bool { return vertex.views[i0].imageID < vertex.views[i1].imageID; }); ASSERT(vertex.views.size() >= 2); @@ -205,8 +210,8 @@ bool Scene::LoadInterface(const String & fileName) "\t%u images (%u calibrated) with a total of %.2f MPixels (%.2f MPixels/image)\n" "\t%u points, %u vertices, %u faces", TD_TIMER_GET_FMT().c_str(), - images.GetSize(), nCalibratedImages, (double)nTotalPixels/(1024.0*1024.0), (double)nTotalPixels/(1024.0*1024.0*nCalibratedImages), - pointcloud.points.GetSize(), mesh.vertices.GetSize(), mesh.faces.GetSize()); + images.size(), nCalibratedImages, (double)nTotalPixels/(1024.0*1024.0), (double)nTotalPixels/(1024.0*1024.0*nCalibratedImages), + pointcloud.points.size(), mesh.vertices.size(), mesh.faces.size()); return true; } // LoadInterface @@ -216,29 +221,29 @@ bool Scene::SaveInterface(const String & fileName, int version) const Interface obj; // export platforms - obj.platforms.reserve(platforms.GetSize()); + obj.platforms.reserve(platforms.size()); for (const Platform& platform: platforms) { Interface::Platform plat; - plat.cameras.reserve(platform.cameras.GetSize()); + plat.cameras.reserve(platform.cameras.size()); for (const Platform::Camera& camera: platform.cameras) { Interface::Platform::Camera cam; cam.K = camera.K; cam.R = camera.R; cam.C = camera.C; - plat.cameras.push_back(cam); + plat.cameras.emplace_back(cam); } - plat.poses.reserve(platform.poses.GetSize()); + plat.poses.reserve(platform.poses.size()); for (const Platform::Pose& pose: platform.poses) { Interface::Platform::Pose p; p.R = pose.R; p.C = pose.C; - plat.poses.push_back(p); + plat.poses.emplace_back(p); } - obj.platforms.push_back(plat); + obj.platforms.emplace_back(std::move(plat)); } // export images - obj.images.resize(images.GetSize()); + obj.images.resize(images.size()); FOREACH(i, images) { const Image& imageData = images[i]; MVS::Interface::Image& image = obj.images[i]; @@ -257,14 +262,14 @@ bool Scene::SaveInterface(const String & fileName, int version) const } // export 3D points - obj.vertices.resize(pointcloud.points.GetSize()); + obj.vertices.resize(pointcloud.points.size()); FOREACH(i, pointcloud.points) { const PointCloud::Point& point = pointcloud.points[i]; const PointCloud::ViewArr& views = pointcloud.pointViews[i]; MVS::Interface::Vertex& vertex = obj.vertices[i]; ASSERT(sizeof(vertex.X.x) == sizeof(point.x)); vertex.X = point; - vertex.views.resize(views.GetSize()); + vertex.views.resize(views.size()); views.ForEach([&](PointCloud::ViewArr::IDX v) { MVS::Interface::Vertex::View& view = vertex.views[v]; view.imageID = views[v]; @@ -272,7 +277,7 @@ bool Scene::SaveInterface(const String & fileName, int version) const }); } if (!pointcloud.normals.IsEmpty()) { - obj.verticesNormal.resize(pointcloud.normals.GetSize()); + obj.verticesNormal.resize(pointcloud.normals.size()); FOREACH(i, pointcloud.normals) { const PointCloud::Normal& normal = pointcloud.normals[i]; MVS::Interface::Normal& vertexNormal = obj.verticesNormal[i]; @@ -280,7 +285,7 @@ bool Scene::SaveInterface(const String & fileName, int version) const } } if (!pointcloud.colors.IsEmpty()) { - obj.verticesColor.resize(pointcloud.colors.GetSize()); + obj.verticesColor.resize(pointcloud.colors.size()); FOREACH(i, pointcloud.colors) { const PointCloud::Color& color = pointcloud.colors[i]; MVS::Interface::Color& vertexColor = obj.verticesColor[i]; @@ -301,8 +306,8 @@ bool Scene::SaveInterface(const String & fileName, int version) const "\t%u images (%u calibrated)\n" "\t%u points, %u vertices, %u faces", TD_TIMER_GET_FMT().c_str(), - images.GetSize(), nCalibratedImages, - pointcloud.points.GetSize(), mesh.vertices.GetSize(), mesh.faces.GetSize()); + images.size(), nCalibratedImages, + pointcloud.points.size(), mesh.vertices.size(), mesh.faces.size()); return true; } // SaveInterface /*----------------------------------------------------------------*/ @@ -781,16 +786,6 @@ bool Scene::EstimateNeighborViewsPointCloud(unsigned maxResolution) } // EstimateNeighborViewsPointCloud /*----------------------------------------------------------------*/ -inline float Footprint(const Camera& camera, const Point3f& X) { - #if 0 - const REAL fSphereRadius(1); - const Point3 cX(camera.TransformPointW2C(Cast(X))); - return (float)norm(camera.TransformPointC2I(Point3(cX.x+fSphereRadius,cX.y,cX.z))-camera.TransformPointC2I(cX))+std::numeric_limits::epsilon(); - #else - return (float)(camera.GetFocalLength()/camera.PointDepth(X)); - #endif -} - // compute visibility for the reference image // and select the best views for reconstructing the dense point-cloud; // extract also all 3D points seen by the reference image; @@ -843,7 +838,7 @@ bool Scene::SelectNeighborViews(uint32_t ID, IndexArr& points, unsigned nMinView ++nPoints; // score shared views const Point3f V1(imageData.camera.C - Cast(point)); - const float footprint1(Footprint(imageData.camera, point)); + const float footprint1(imageData.camera.GetFootprintImage(point)); for (const PointCloud::View& view: views) { if (view == ID) continue; @@ -851,7 +846,7 @@ bool Scene::SelectNeighborViews(uint32_t ID, IndexArr& points, unsigned nMinView const Point3f V2(imageData2.camera.C - Cast(point)); const float fAngle(ACOS(ComputeAngle(V1.ptr(), V2.ptr()))); const float wAngle(EXP(SQUARE(fAngle-fOptimAngle)*(fAngle 1.6f) @@ -867,67 +862,85 @@ bool Scene::SelectNeighborViews(uint32_t ID, IndexArr& points, unsigned nMinView ++score.points; } } - imageData.avgDepth /= nPoints; - ASSERT(nPoints > 3); + if(nPoints > 3) + imageData.avgDepth /= nPoints; // select best neighborViews - Point2fArr projs(0, points.size()); - FOREACH(IDB, images) { - const Image& imageDataB = images[IDB]; - if (!imageDataB.IsValid()) - continue; - const Score& score = scores[IDB]; - if (score.points < 3) - continue; - ASSERT(ID != IDB); - // compute how well the matched features are spread out (image covered area) - const Point2f boundsA(imageData.GetSize()); - const Point2f boundsB(imageDataB.GetSize()); - ASSERT(projs.empty()); - for (uint32_t idx: points) { - const PointCloud::ViewArr& views = pointcloud.pointViews[idx]; - ASSERT(views.IsSorted()); - ASSERT(views.FindFirst(ID) != PointCloud::ViewArr::NO_INDEX); - if (views.FindFirst(IDB) == PointCloud::ViewArr::NO_INDEX) + if (neighbors.empty()) { + Point2fArr projs(0, points.size()); + FOREACH(IDB, images) { + const Image& imageDataB = images[IDB]; + if (!imageDataB.IsValid()) + continue; + const Score& score = scores[IDB]; + if (score.points < 3) continue; - const PointCloud::Point& point = pointcloud.points[idx]; - Point2f& ptA = projs.emplace_back(imageData.camera.ProjectPointP(point)); - Point2f ptB = imageDataB.camera.ProjectPointP(point); - if (!imageData.camera.IsInside(ptA, boundsA) || !imageDataB.camera.IsInside(ptB, boundsB)) - projs.RemoveLast(); + ASSERT(ID != IDB); + // compute how well the matched features are spread out (image covered area) + const Point2f boundsA(imageData.GetSize()); + const Point2f boundsB(imageDataB.GetSize()); + ASSERT(projs.empty()); + for (uint32_t idx: points) { + const PointCloud::ViewArr& views = pointcloud.pointViews[idx]; + ASSERT(views.IsSorted()); + ASSERT(views.FindFirst(ID) != PointCloud::ViewArr::NO_INDEX); + if (views.FindFirst(IDB) == PointCloud::ViewArr::NO_INDEX) + continue; + const PointCloud::Point& point = pointcloud.points[idx]; + Point2f& ptA = projs.emplace_back(imageData.camera.ProjectPointP(point)); + Point2f ptB = imageDataB.camera.ProjectPointP(point); + if (!imageData.camera.IsInside(ptA, boundsA) || !imageDataB.camera.IsInside(ptB, boundsB)) + projs.RemoveLast(); + } + ASSERT(projs.size() <= score.points); + if (projs.empty()) + continue; + const float area(ComputeCoveredArea((const float*)projs.data(), projs.size(), boundsA.ptr())); + projs.Empty(); + // store image score + ViewScore& neighbor = neighbors.AddEmpty(); + neighbor.idx.ID = IDB; + neighbor.idx.points = score.points; + neighbor.idx.scale = score.avgScale/score.points; + neighbor.idx.angle = score.avgAngle/score.points; + neighbor.idx.area = area; + neighbor.score = score.score*MAXF(area,0.01f); } - ASSERT(projs.size() <= score.points); - if (projs.empty()) - continue; - const float area(ComputeCoveredArea((const float*)projs.data(), projs.size(), boundsA.ptr())); - projs.Empty(); - // store image score - ViewScore& neighbor = neighbors.AddEmpty(); - neighbor.idx.ID = IDB; - neighbor.idx.points = score.points; - neighbor.idx.scale = score.avgScale/score.points; - neighbor.idx.angle = score.avgAngle/score.points; - neighbor.idx.area = area; - neighbor.score = score.score*MAXF(area,0.01f); - } - neighbors.Sort(); - #if TD_VERBOSE != TD_VERBOSE_OFF - // print neighbor views - if (VERBOSITY_LEVEL > 2) { - String msg; - FOREACH(n, neighbors) - msg += String::FormatString(" %3u(%upts,%.2fscl)", neighbors[n].idx.ID, neighbors[n].idx.points, neighbors[n].idx.scale); - VERBOSE("Reference image %3u sees %u views:%s (%u shared points)", ID, neighbors.size(), msg.c_str(), nPoints); + neighbors.Sort(); + #if TD_VERBOSE != TD_VERBOSE_OFF + // print neighbor views + if (VERBOSITY_LEVEL > 2) { + String msg; + FOREACH(n, neighbors) + msg += String::FormatString(" %3u(%upts,%.2fscl)", neighbors[n].idx.ID, neighbors[n].idx.points, neighbors[n].idx.scale); + VERBOSE("Reference image %3u sees %u views:%s (%u shared points)", ID, neighbors.size(), msg.c_str(), nPoints); + } + #endif } - #endif if (points.size() <= 3 || neighbors.size() < MINF(nMinViews,nCalibratedImages-1)) { DEBUG_EXTRA("error: reference image %3u has not enough images in view", ID); return false; } return true; } // SelectNeighborViews + +void Scene::SelectNeighborViews(unsigned nMinViews, unsigned nMinPointViews, float fOptimAngle, unsigned nInsideROI) +{ + #ifdef DENSE_USE_OPENMP + #pragma omp parallel for shared(data, bAbort) + for (int_t ID=0; ID<(int_t)images.GetSize(); ++ID) { + const IIndex idxImage((IIndex)ID); + #else + FOREACH(idxImage, images) { + #endif + // select image neighbors + IndexArr points; + SelectNeighborViews(idxImage, points, nMinViews, nMinPointViews, fOptimAngle, nInsideROI); + } +} // SelectNeighborViews /*----------------------------------------------------------------*/ + // keep only the best neighbors for the reference image bool Scene::FilterNeighborViews(ViewScoreArr& neighbors, float fMinArea, float fMinScale, float fMaxScale, float fMinAngle, float fMaxAngle, unsigned nMaxViews) { @@ -1013,6 +1026,78 @@ bool Scene::ExportCamerasMLP(const String& fileName, const String& fileNameScene return true; } // ExportCamerasMLP + +bool Scene::ExportLinesPLY(const String& fileName, const CLISTDEF0IDX(Line3f,uint32_t)& lines, const Pixel8U* colors, bool bBinary) { + // define a PLY file format composed only of vertices and edges + // vertex definition + struct PLYVertex { + float x, y, z; + }; + // list of property information for a vertex + static PLY::PlyProperty vert_props[] = { + {"x", PLY::Float32, PLY::Float32, offsetof(PLYVertex,x), 0, 0, 0, 0}, + {"y", PLY::Float32, PLY::Float32, offsetof(PLYVertex,y), 0, 0, 0, 0}, + {"z", PLY::Float32, PLY::Float32, offsetof(PLYVertex,z), 0, 0, 0, 0}, + }; + // edge definition + struct PLYEdge { + int v1, v2; + uint8_t r, g, b; + }; + // list of property information for a edge + static PLY::PlyProperty edge_props[] = { + {"vertex1", PLY::Uint32, PLY::Uint32, offsetof(PLYEdge,v1), 0, 0, 0, 0}, + {"vertex2", PLY::Uint32, PLY::Uint32, offsetof(PLYEdge,v2), 0, 0, 0, 0}, + {"red", PLY::Uint8, PLY::Uint8, offsetof(PLYEdge,r), 0, 0, 0, 0}, + {"green", PLY::Uint8, PLY::Uint8, offsetof(PLYEdge,g), 0, 0, 0, 0}, + {"blue", PLY::Uint8, PLY::Uint8, offsetof(PLYEdge,b), 0, 0, 0, 0}, + }; + // list of the kinds of elements in the PLY + static const char* elem_names[] = { + "vertex", "edge" + }; + + // create PLY object + ASSERT(!fileName.empty()); + Util::ensureFolder(fileName); + const size_t memBufferSize(2 * (8 * 3/*pos*/ + 3 * 3/*color*/ + 6/*space*/ + 2/*eol*/) + 2048/*extra size*/); + PLY ply; + if (!ply.write(fileName, 2, elem_names, bBinary?PLY::BINARY_LE:PLY::ASCII, memBufferSize)) + return false; + + // describe what properties go into the vertex elements + ply.describe_property("vertex", 3, vert_props); + PLYVertex v; + FOREACH(i, lines) { + const Line3f& line = lines[i]; + v.x = line.pt1.x(); v.y = line.pt1.y(); v.z = line.pt1.z(); + ply.put_element(&v); + v.x = line.pt2.x(); v.y = line.pt2.y(); v.z = line.pt2.z(); + ply.put_element(&v); + } + + // describe what properties go into the edge elements + if (colors) { + ply.describe_property("edge", 5, edge_props); + PLYEdge edge; + FOREACH(i, lines) { + const Pixel8U& color = colors[i]; + edge.r = color.r; edge.g = color.g; edge.b = color.b; + edge.v1 = i*2+0; edge.v2 = i*2+1; + ply.put_element(&edge); + } + } else { + ply.describe_property("edge", 2, edge_props); + PLYEdge edge; + FOREACH(i, lines) { + edge.v1 = i*2+0; edge.v2 = i*2+1; + ply.put_element(&edge); + } + } + + // write to file + return ply.header_complete(); +} // ExportLinesPLY /*----------------------------------------------------------------*/ @@ -1055,7 +1140,7 @@ unsigned Scene::Split(ImagesChunkArr& chunks, float maxArea, int depthMapStep) c const Point3f X(Cast(camera.TransformPointI2W(Point3(c,r,depth)))); if (IsBounded() && !obb.Intersects(X)) continue; - areas.emplace_back(Footprint(camera, X)*areaScale); + areas.emplace_back(camera.GetFootprintImage(X)*areaScale); visibility.emplace_back(idxImage); samples.emplace_back(X); } @@ -1440,7 +1525,8 @@ bool Scene::ScaleImages(unsigned nMaxResolution, REAL scale, const String& folde } // ScaleImages // apply similarity transform -void Scene::Transform(const Matrix3x3& rotation, const Point3& translation, REAL scale) { +void Scene::Transform(const Matrix3x3& rotation, const Point3& translation, REAL scale) +{ const Matrix3x3 rotationScale(rotation * scale); for (Platform& platform : platforms) { for (Platform::Pose& pose : platform.poses) { @@ -1449,7 +1535,8 @@ void Scene::Transform(const Matrix3x3& rotation, const Point3& translation, REAL } } for (Image& image : images) { - image.UpdateCamera(platforms); + if (image.IsValid()) + image.UpdateCamera(platforms); } FOREACH(i, pointcloud.points) { pointcloud.points[i] = rotationScale * Cast(pointcloud.points[i]) + translation; @@ -1469,6 +1556,25 @@ void Scene::Transform(const Matrix3x3& rotation, const Point3& translation, REAL obb.Translate(Cast(translation)); } } +void Scene::Transform(const Matrix3x4& transform) +{ + #if 1 + Matrix3x3 mscale, rotation; + RQDecomp3x3(cv::Mat(3,4,cv::DataType::type,const_cast(transform.val))(cv::Rect(0,0, 3,3)), mscale, rotation); + const Point3 translation = transform.col(3); + #else + Eigen::Matrix transform4x4 = Eigen::Matrix::Identity(); + transform4x4.topLeftCorner<3,4>() = static_cast(transform); + Eigen::Transform transformIsometry(transform4x4); + Eigen::Matrix mrotation; + Eigen::Matrix mscale; + transformIsometry.computeRotationScaling(&mrotation, &mscale); + const Point3 translation = transformIsometry.translation(); + const Matrix3x3 rotation = mrotation; + #endif + ASSERT(mscale(0,0) > 0 && ISEQUAL(mscale(0,0), mscale(1,1)) && ISEQUAL(mscale(0,0), mscale(2,2))); + Transform(rotation, translation, mscale(0,0)); +} // Transform // transform this scene such that it best aligns with the given scene based on the camera positions bool Scene::AlignTo(const Scene& scene) @@ -1625,3 +1731,351 @@ bool Scene::EstimateROI(int nEstimateROI, float scale) return true; } // EstimateROI /*----------------------------------------------------------------*/ + + +// calculate the center(X,Y) of the cylinder, the radius and min/max Z +// from camera position and sparse point cloud, if that exists +// returns result of checks if the scene camera positions satisfies tower criteria: +// - cameras fit a long and slim bounding box +// - majority of cameras focus toward a middle line +bool Scene::ComputeTowerCylinder(Point2f& centerPoint, float& fRadius, float& fROIRadius, float& zMin, float& zMax, float& minCamZ, const int towerMode) +{ + // disregard tower mode for scenes with less than 20 cameras + if (towerMode > 0 && images.size() < 20) { + DEBUG("error: too few images to be a tower: '%d'", images.size()); + return false; + } + + AABB3f aabbOutsideCameras(true); + CLISTDEF0(Point2f) cameras2D(images.size()); + FloatArr camHeigths; + FitLineOnline fitline; + FOREACH(imgIdx, images) { + const Eigen::Vector3f camPos(Cast(images[imgIdx].camera.C)); + fitline.Update(camPos); + aabbOutsideCameras.InsertFull(camPos); + cameras2D[imgIdx] = Point2f(camPos.x(), camPos.y()); + camHeigths.InsertSortUnique(camPos.z()); + } + Line3f camCenterLine; + Point3f quality = fitline.GetLine(camCenterLine); + // check if ROI is mostly long and narrow on one direction + if (quality.y / quality.z > 0.6f || quality.x / quality.y < 0.8f) { + // does not seem to be a line + if (towerMode > 0) { + DEBUG("error: does not seem to be a tower: X(%.2f), Y(%.2f), Z(%.2f)", quality.x, quality.y, quality.z); + return false; + } + } + + // get the height of the lowest camera + minCamZ = aabbOutsideCameras.ptMin.z(); + centerPoint = ((camCenterLine.pt1+camCenterLine.pt2)*0.5f).topLeftCorner<2,1>(); + zMin = MINF(aabbOutsideCameras.ptMax.z(), aabbOutsideCameras.ptMin.z()) - 5; + // if sparse point cloud is loaded use lowest point as zMin + float fMinPointsZ = std::numeric_limits::max(); + float fMaxPointsZ = std::numeric_limits::lowest(); + FOREACH(pIdx, pointcloud.points) { + if (!obb.IsValid() || obb.Intersects(pointcloud.points[pIdx])) { + const float pz = pointcloud.points[pIdx].z; + if (pz < fMinPointsZ) + fMinPointsZ = pz; + if (pz > fMaxPointsZ) + fMaxPointsZ = pz; + } + } + zMin = MINF(zMin, fMinPointsZ); + zMax = MAXF(aabbOutsideCameras.ptMax.z(), fMaxPointsZ); + + // calculate tower radius as median distance from tower center to cameras + FloatArr cameraDistancesToMiddle(cameras2D.size()); + FOREACH (camIdx, cameras2D) + cameraDistancesToMiddle[camIdx] = norm(cameras2D[camIdx] - centerPoint); + const float fMedianDistance = cameraDistancesToMiddle.GetMedian(); + fRadius = MAXF(0.2f, (fMedianDistance - 1.f) / 3.f); + // get the average of top 85 to 95% of the highest distances to center + if (!cameraDistancesToMiddle.empty()) { + float avgTopDistance(0); + cameraDistancesToMiddle.Sort(); + const size_t topIdx(CEIL2INT(cameraDistancesToMiddle.size() * 0.95f)); + const size_t botIdx(FLOOR2INT(cameraDistancesToMiddle.size() * 0.85f)); + for (size_t i = botIdx; i < topIdx; ++i) { + avgTopDistance += cameraDistancesToMiddle[i]; + } + avgTopDistance /= topIdx - botIdx; + fROIRadius = avgTopDistance; + } else { + fROIRadius = fRadius; + } + return true; +} // ComputeTowerCylinder + +size_t Scene::DrawCircle(PointCloud& pc, PointCloud::PointArr& outCircle, const Point3f& circleCenter, const float circleRadius, const unsigned nTargetPoints, const float fStartAngle, const float fAngleBetweenPoints) +{ + outCircle.Release(); + for (unsigned pIdx = 0; pIdx < nTargetPoints; ++pIdx) { + const float fAngle(fStartAngle + fAngleBetweenPoints * pIdx); + ASSERT(fAngle <= FTWO_PI); + const Normal n(cos(fAngle), sin(fAngle), 0); + ASSERT(ISEQUAL(norm(n), 1.f)); + const Point3f newPoint(circleCenter + circleRadius * n); + // select cameras seeing this point + PointCloud::ViewArr views; + FOREACH(idxImg, images) { + const Image& image = images[idxImg]; + const Point3f xz(image.camera.TransformPointW2I3(Cast(newPoint))); + const Point2f x(xz.x, xz.y); + if (!Image8U::isInside(x, image.GetSize()) || + xz.z <= 0) + continue; + if (n.dot(Cast(image.camera.RayPoint(x))) >= 0) + continue; + views.emplace_back(idxImg); + } + if (views.size() >= 2) { + outCircle.emplace_back(newPoint); + pc.points.emplace_back(newPoint); + pc.pointViews.emplace_back(views); + pc.normals.emplace_back(n); + pc.colors.emplace_back(Pixel8U::YELLOW); + pc.pointWeights.emplace_back(PointCloud::WeightArr{1.f}); + } + } + return outCircle.size(); +} // DrawCircle + +PointCloud Scene::BuildTowerMesh(const PointCloud& origPointCloud, const Point2f& centerPoint, const float fRadius, const float fROIRadius, const float zMin, const float zMax, const float minCamZ, bool bFixRadius) +{ + const unsigned nTargetDensity(10); + const unsigned nTargetCircles(ROUND2INT((zMax - zMin) * nTargetDensity)); // how many circles in cylinder + const float fCircleFrequence((zMax - zMin) / nTargetCircles); // the distance between neighbor circles + PointCloud towerPC; + PointCloud::PointArr circlePoints; + Mesh::VertexVerticesArr meshCircles; + if (bFixRadius) { + const unsigned nTargetPoints(MAX(10, ROUND2INT(FTWO_PI * fRadius * nTargetDensity))); // how many points on each circle + const float fAngleBetweenPoints(FTWO_PI / nTargetPoints); // the angle between neighbor points on the circle + for (unsigned cIdx = 0; cIdx < nTargetCircles; ++cIdx) { + const Point3f circleCenter(centerPoint, zMin + fCircleFrequence * cIdx); // center point of the circle + const float fStartAngle(fAngleBetweenPoints * SEACAVE::random()); // starting angle for the first point + DrawCircle(towerPC, circlePoints, circleCenter, fRadius, nTargetPoints, fStartAngle, fAngleBetweenPoints); + if (!circlePoints.empty()) { + // add points to vertex list + Mesh::VertexIdxArr circleVertices; + Mesh::VIndex vIdx = mesh.vertices.size(); + for (const Point3f& p: circlePoints) { + mesh.vertices.emplace_back(p); + circleVertices.emplace_back(vIdx++); + } + meshCircles.emplace_back(circleVertices); + } + } + } else { + cList sliceDistances(nTargetCircles); + for (const Point3f& P : origPointCloud.points) { + const float d(norm(Point2f(P.x, P.y) - centerPoint)); + if (d <= fROIRadius) { + const float fIdx((zMax - P.z) * nTargetDensity); + int bIdx(FLOOR2INT(fIdx)); + int tIdx(FLOOR2INT(fIdx+0.5f)); + if (bIdx == tIdx && bIdx > 0) + bIdx--; + if (tIdx >= (int)nTargetCircles) + tIdx = nTargetCircles - 1; + if (bIdx < (int)nTargetCircles - 1) + sliceDistances[bIdx].emplace_back(d); + if (tIdx > 0) + sliceDistances[tIdx].emplace_back(d); + } + } + FloatArr circleRadii; + for (unsigned cIdx = 0; cIdx < nTargetCircles; ++cIdx) { + const float circleZ(zMax - fCircleFrequence * cIdx); + FloatArr& pDistances = sliceDistances[cIdx]; + float circleRadius(fRadius); + if (circleZ < minCamZ) { + // use fixed radius under lowest camera position + circleRadius = fRadius; + } else { + if (pDistances.size() > 2) { + pDistances.Sort(); + const size_t topIdx(MIN(pDistances.size() - 1, CEIL2INT(pDistances.size() * 0.95f))); + const size_t botIdx(MAX(1, FLOOR2INT(pDistances.size() * 0.5f))); + float avgTopDistance(0); + for (size_t i = botIdx; i < topIdx; ++i) + avgTopDistance += pDistances[i]; + avgTopDistance /= topIdx - botIdx; + if (avgTopDistance < fROIRadius * 0.8f) + circleRadius = avgTopDistance; + } + } + circleRadii.emplace_back(circleRadius); + } + // smoothen radii + if (circleRadii.size() > 2) { + for (int ri = 1; ri < circleRadii.size() - 1; ++ri) { + const float aboveRad(circleRadii[ri - 1]); + float& circleRadius = circleRadii[ri]; + const float beloweRad(circleRadii[ri + 1]); + const float AbvCrtDeltaPrc = ABS(aboveRad - circleRadius) / aboveRad; + const float BelCrtDeltaPrc = ABS(circleRadius - beloweRad) / circleRadius; + // set current radius as average of the most similar values in the closest 7 neighbors + if (ri > 2 && ri < circleRadii.size() - 5) { + FloatArr neighSeven(7); + FOREACH(i, neighSeven) + neighSeven[i] = circleRadii[ri - 2 + i]; + neighSeven.Sort(); + const float medianRadius(neighSeven.GetMedian()); + circleRadius = ABS(medianRadius-aboveRad) < ABS(medianRadius-beloweRad) ? aboveRad : beloweRad; + } + else { + circleRadius = (aboveRad + beloweRad) / 2.f; + } + } + } + // add circles + FOREACH(rIdx, circleRadii) { + float circleRadius(circleRadii[rIdx]); + const float circleZ(zMax - fCircleFrequence * rIdx); + const Point3f circleCenter(centerPoint, circleZ); // center point of the circle + const unsigned nTargetPoints(MAX(10, ROUND2INT(FTWO_PI * circleRadius * nTargetDensity))); // how many points on each circle + const float fAngleBetweenPoints(FTWO_PI / nTargetPoints); // the angle between neighbor points on the circle + const float fStartAngle(fAngleBetweenPoints * SEACAVE::random()); // starting angle for the first point + DrawCircle(towerPC, circlePoints, circleCenter, circleRadius, nTargetPoints, fStartAngle, fAngleBetweenPoints); + if (!circlePoints.IsEmpty()) { + //add points to vertex list + Mesh::VertexIdxArr circleVertices; + Mesh::VIndex vIdx = mesh.vertices.size(); + FOREACH(pIdx, circlePoints) { + const Point3f& p = circlePoints[pIdx]; + mesh.vertices.emplace_back(p); + circleVertices.emplace_back(vIdx); + ++vIdx; + } + meshCircles.emplace_back(circleVertices); + } + } + } + + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) { + // Build faces from meshCircles + for (Mesh::VIndex cIdx = 1; cIdx < meshCircles.size(); ++cIdx) { + if (meshCircles[cIdx - 1].size() > 1 || meshCircles[cIdx].size() > 1) { + Mesh::VertexIdxArr& topPoints = meshCircles[cIdx - 1]; + Mesh::VertexIdxArr& botPoints = meshCircles[cIdx]; + // build faces with all the points in the two lists + bool bInverted(false); + if (topPoints.size() > botPoints.size()) { + topPoints.swap(botPoints); + bInverted = true; + } + const float topStep(1.0f / topPoints.size()); + const float botStep(1.0f / botPoints.size()); + for (Mesh::VIndex ti=0, bi=0; ti < topPoints.size() && bi (bi+1)*botStep); + if (topPoints.size() > 1) { + const Mesh::VIndex& v0(topPoints[ti]); + const Mesh::VIndex& v1(botPoints[bi%botPoints.size()]); + const Mesh::VIndex& v2(topPoints[(ti+1)%topPoints.size()]); + if (!bInverted) + mesh.faces.emplace_back(v0, v1, v2); + else + mesh.faces.emplace_back(v0, v2, v1); + } + if (topPoints.size() != botPoints.size()) { + // add closing face + const Mesh::VIndex& v0(topPoints[0]); + const Mesh::VIndex& v1(botPoints[botPoints.size()-1]); + const Mesh::VIndex& v2(botPoints[0]); + if (!bInverted) + mesh.faces.emplace_back(v0, v1, v2); + else + mesh.faces.emplace_back(v0, v2, v1); + } + } + if (bInverted) { + topPoints.swap(botPoints); + } + } + } + mesh.Save("towermesh_dbg.ply"); + towerPC.Save("cylinder.ply"); + } + #endif + return towerPC; +} + + +// compute points on a cylinder placed in the middle of scene's cameras +// this function assumes the scene is Z-up and units are meters +// - towerMode: 0 - disabled, 1 - replace, 2 - append, 3 - select neighbors, 4 - select neighbors and append, <0 - force tower mode +void Scene::InitTowerScene(const int towerMode) +{ + float fRadius; + float fROIRadius; + float zMax, zMin, minCamZ; + Point2f centerPoint; + if (!ComputeTowerCylinder(centerPoint, fRadius, fROIRadius, zMin, zMax, minCamZ, towerMode)) + return; + DEBUG("Scene camera positions identified ROI as a tower, select neighbors as if ROI is a tower"); + + // add nTargetPoints points on each circle + PointCloud towerPC(BuildTowerMesh(pointcloud, centerPoint, fRadius, fROIRadius, zMin, zMax, minCamZ, false)); + + switch (ABS(towerMode)) { + case 1: { // replace + pointcloud = std::move(towerPC); + break; + } + case 2: { // append + bool bHasNormal(pointcloud.normals.size() == pointcloud.GetSize()); + bool bHasColor(pointcloud.colors.size() == pointcloud.GetSize()); + bool bHasWeights(pointcloud.pointWeights.size() == pointcloud.GetSize()); + FOREACH(idxPoint, towerPC.points) { + pointcloud.points.emplace_back(towerPC.points[idxPoint]); + pointcloud.pointViews.emplace_back(towerPC.pointViews[idxPoint]); + if (bHasNormal) + pointcloud.normals.emplace_back(towerPC.normals[idxPoint]); + if (bHasColor) + pointcloud.colors.emplace_back(towerPC.colors[idxPoint]); + if (bHasWeights) + pointcloud.pointWeights.emplace_back(towerPC.pointWeights[idxPoint]); + } + break; + } + case 3: { // select neighbors and remove added points + pointcloud.Swap(towerPC); + SelectNeighborViews(OPTDENSE::nMinViews, OPTDENSE::nMinViewsTrustPoint>1?OPTDENSE::nMinViewsTrustPoint:2, FD2R(OPTDENSE::fOptimAngle), OPTDENSE::nPointInsideROI); + pointcloud.Swap(towerPC); + break; + } + case 4: { // select neighbors + pointcloud.Swap(towerPC); + SelectNeighborViews(OPTDENSE::nMinViews, OPTDENSE::nMinViewsTrustPoint>1?OPTDENSE::nMinViewsTrustPoint:2, FD2R(OPTDENSE::fOptimAngle), OPTDENSE::nPointInsideROI); + pointcloud.Swap(towerPC); + bool bHasNormal(pointcloud.normals.size() == pointcloud.GetSize()); + bool bHasColor(pointcloud.colors.size() == pointcloud.GetSize()); + bool bHasWeights(pointcloud.pointWeights.size() == pointcloud.GetSize()); + FOREACH(idxPoint, towerPC.points) { + pointcloud.points.emplace_back(towerPC.points[idxPoint]); + pointcloud.pointViews.emplace_back(towerPC.pointViews[idxPoint]); + if (bHasNormal) + pointcloud.normals.emplace_back(towerPC.normals[idxPoint]); + if (bHasColor) + pointcloud.colors.emplace_back(towerPC.colors[idxPoint]); + if (bHasWeights) + pointcloud.pointWeights.emplace_back(towerPC.pointWeights[idxPoint]); + } + break; + } + } +} // InitTowerScene diff --git a/libs/MVS/Scene.h b/libs/MVS/Scene.h index c3807e966..308367a9a 100644 --- a/libs/MVS/Scene.h +++ b/libs/MVS/Scene.h @@ -67,6 +67,7 @@ class MVS_API Scene : obb(true), nMaxThreads(Thread::getMaxThreads(_nMaxThreads)) {} void Release(); + bool IsValid() const; bool IsEmpty() const; bool ImagesHaveNeighbors() const; bool IsBounded() const { return obb.IsValid(); } @@ -86,10 +87,12 @@ class MVS_API Scene void SampleMeshWithVisibility(unsigned maxResolution=320); bool ExportMeshToDepthMaps(const String& baseName); - bool SelectNeighborViews(uint32_t ID, IndexArr& points, unsigned nMinViews=3, unsigned nMinPointViews=2, float fOptimAngle=FD2R(12), unsigned nInsideROI=1); + bool SelectNeighborViews(uint32_t ID, IndexArr& points, unsigned nMinViews = 3, unsigned nMinPointViews = 2, float fOptimAngle = FD2R(12), unsigned nInsideROI = 1); + void SelectNeighborViews(unsigned nMinViews = 3, unsigned nMinPointViews = 2, float fOptimAngle = FD2R(12), unsigned nInsideROI = 1); static bool FilterNeighborViews(ViewScoreArr& neighbors, float fMinArea=0.1f, float fMinScale=0.2f, float fMaxScale=2.4f, float fMinAngle=FD2R(3), float fMaxAngle=FD2R(45), unsigned nMaxViews=12); bool ExportCamerasMLP(const String& fileName, const String& fileNameScene) const; + static bool ExportLinesPLY(const String& fileName, const CLISTDEF0IDX(Line3f,uint32_t)& lines, const Pixel8U* colors=NULL, bool bBinary=true); // sub-scene split and save struct ImagesChunk { @@ -105,12 +108,19 @@ class MVS_API Scene bool Scale(const REAL* pScale = NULL); bool ScaleImages(unsigned nMaxResolution = 0, REAL scale = 0, const String& folderName = String()); void Transform(const Matrix3x3& rotation, const Point3& translation, REAL scale); + void Transform(const Matrix3x4& transform); bool AlignTo(const Scene&); REAL ComputeLeveledVolume(float planeThreshold=0, float sampleMesh=-100000, unsigned upAxis=2, bool verbose=true); // Estimate and set region-of-interest bool EstimateROI(int nEstimateROI=0, float scale=1.f); - + + // Tower scene + bool ComputeTowerCylinder(Point2f& centerPoint, float& fRadius, float& fROIRadius, float& zMin, float& zMax, float& minCamZ, const int towerMode); + void InitTowerScene(const int towerMode); + size_t DrawCircle(PointCloud& pc,PointCloud::PointArr& outCircle, const Point3f& circleCenter, const float circleRadius, const unsigned nTargetPoints, const float fStartAngle, const float fAngleBetweenPoints); + PointCloud BuildTowerMesh(const PointCloud& origPointCloud, const Point2f& centerPoint, const float fRadius, const float fROIRadius, const float zMin, const float zMax, const float minCamZ, bool bFixRadius = false); + // Dense reconstruction bool DenseReconstruction(int nFusionMode=0, bool bCrop2ROI=true, float fBorderROI=0); bool ComputeDepthMaps(DenseDepthMapData& data); @@ -126,8 +136,8 @@ class MVS_API Scene // Mesh refinement bool RefineMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned nMaxViews, float fDecimateMesh, unsigned nCloseHoles, unsigned nEnsureEdgeSize, - unsigned nMaxFaceArea, unsigned nScales, float fScaleStep, unsigned nReduceMemory, unsigned nAlternatePair, float fRegularityWeight, float fRatioRigidityElasticity, - float fThPlanarVertex, float fGradientStep); + unsigned nMaxFaceArea, unsigned nScales, float fScaleStep, unsigned nAlternatePair, float fRegularityWeight, float fRatioRigidityElasticity, float fGradientStep, + float fThPlanarVertex=0.f, unsigned nReduceMemory=1); #ifdef _USE_CUDA bool RefineMeshCUDA(unsigned nResolutionLevel, unsigned nMinResolution, unsigned nMaxViews, float fDecimateMesh, unsigned nCloseHoles, unsigned nEnsureEdgeSize, unsigned nMaxFaceArea, unsigned nScales, float fScaleStep, unsigned nAlternatePair, float fRegularityWeight, float fRatioRigidityElasticity, float fGradientStep); @@ -136,7 +146,7 @@ class MVS_API Scene // Mesh texturing bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned minCommonCameras=0, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f, bool bGlobalSeamLeveling=true, bool bLocalSeamLeveling=true, unsigned nTextureSizeMultiple=0, unsigned nRectPackingHeuristic=3, Pixel8U colEmpty=Pixel8U(255,127,39), - float fSharpnessWeight=0.5f, const IIndexArr& views=IIndexArr()); + float fSharpnessWeight=0.5f, int ignoreMaskLabel = -1, const IIndexArr& views=IIndexArr()); #ifdef _USE_BOOST // implement BOOST serialization diff --git a/libs/MVS/SceneDensify.cpp b/libs/MVS/SceneDensify.cpp index ee7929612..02c31ae3b 100644 --- a/libs/MVS/SceneDensify.cpp +++ b/libs/MVS/SceneDensify.cpp @@ -389,6 +389,7 @@ bool DepthMapsData::InitViews(DepthData& depthData, IIndex idxNeighbor, IIndex n ImportDepthDataRaw(ComposeDepthFilePath(view.GetID(), "dmap"), imageFileName, IDs, imageSize, view.cameraDepthMap.K, view.cameraDepthMap.R, view.cameraDepthMap.C, dMin, dMax, view.depthMap, normalMap, confMap, viewsMap, 1); + ASSERT(viewRef.image.size() == view.depthMap.size()); } view.Init(viewRef.camera); } @@ -406,6 +407,11 @@ bool DepthMapsData::InitViews(DepthData& depthData, IIndex idxNeighbor, IIndex n depthData.depthMap, depthData.normalMap, confMap, viewsMap, 3)) return false; ASSERT(viewRef.image.size() == depthData.depthMap.size()); + ASSERT(depthData.normalMap.empty() || viewRef.image.size() == depthData.normalMap.size()); + if (depthData.normalMap.empty()) { + // estimate normal map + EstimateNormalMap(viewRef.camera.K, depthData.depthMap, depthData.normalMap); + } } else if (loadDepthMaps == 0) { // initialize depth and normal maps if (OPTDENSE::nMinViewsTrustPoint < 2 || depthData.points.empty()) { @@ -672,10 +678,10 @@ bool DepthMapsData::EstimateDepthMap(IIndex idxImage, int nGeometricIter) #endif if (prevDepthMapSize != size || OPTDENSE::nIgnoreMaskLabel >= 0) { BitMatrix mask; - if (OPTDENSE::nIgnoreMaskLabel >= 0 && DepthEstimator::ImportIgnoreMask(*image.pImageData, depthData.depthMap.size(), mask, (uint16_t)OPTDENSE::nIgnoreMaskLabel)) + if (OPTDENSE::nIgnoreMaskLabel >= 0 && DepthEstimator::ImportIgnoreMask(*image.pImageData, depthData.depthMap.size(), (uint16_t)OPTDENSE::nIgnoreMaskLabel, mask)) depthData.ApplyIgnoreMask(mask); DepthEstimator::MapMatrix2ZigzagIdx(size, coords, mask, MAXF(64,(int)nMaxThreads*8)); - #if 0 + #if 0 && !defined(_RELEASE) // show pixels to be processed Image8U cmask(size); cmask.memset(0); @@ -2212,7 +2218,7 @@ void Scene::PointCloudFilter(int thRemove) inline bool Intersects(const Octree::POINT_TYPE& center, Octree::Type radius) const { return coneIntersect(Sphere(center, radius*Real(SQRT_3))); } - inline void operator () (const IDX* idices, IDX size) { + inline void operator() (const IDX* idices, IDX size) { const Real thSimilar(0.01f); Real dist; FOREACHRAWPTR(pIdx, idices, size) { diff --git a/libs/MVS/SceneRefine.cpp b/libs/MVS/SceneRefine.cpp index bf1201d14..79875f302 100644 --- a/libs/MVS/SceneRefine.cpp +++ b/libs/MVS/SceneRefine.cpp @@ -1280,7 +1280,7 @@ class MeshProblem : public FirstOrderFunction, public IterationCallback bool Scene::RefineMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned nMaxViews, float fDecimateMesh, unsigned nCloseHoles, unsigned nEnsureEdgeSize, unsigned nMaxFaceArea, unsigned nScales, float fScaleStep, - unsigned nReduceMemory, unsigned nAlternatePair, float fRegularityWeight, float fRatioRigidityElasticity, float fThPlanarVertex, float fGradientStep) + unsigned nAlternatePair, float fRegularityWeight, float fRatioRigidityElasticity, float fGradientStep, float fThPlanarVertex, unsigned nReduceMemory) { if (pointcloud.IsEmpty() && !ImagesHaveNeighbors()) SampleMeshWithVisibility(); diff --git a/libs/MVS/SceneTexture.cpp b/libs/MVS/SceneTexture.cpp index ff7dbbd26..e8404ba50 100644 --- a/libs/MVS/SceneTexture.cpp +++ b/libs/MVS/SceneTexture.cpp @@ -151,6 +151,9 @@ struct MeshTexture { typedef TRasterMesh Base; FaceMap& faceMap; FIndex idxFace; + Image8U mask; + bool validFace; + RasterMesh(const Mesh::VertexArr& _vertices, const Camera& _camera, DepthMap& _depthMap, FaceMap& _faceMap) : Base(_vertices, _camera, _depthMap), faceMap(_faceMap) {} void Clear() { @@ -164,7 +167,7 @@ struct MeshTexture { Depth& depth = depthMap(pt); if (depth == 0 || depth > z) { depth = z; - faceMap(pt) = idxFace; + faceMap(pt) = validFace && (validFace = (mask(pt) != 0)) ? idxFace : NO_ID; } } }; @@ -323,7 +326,7 @@ struct MeshTexture { void ListVertexFaces(); - bool ListCameraFaces(FaceDataViewArr&, float fOutlierThreshold, const IIndexArr& views); + bool ListCameraFaces(FaceDataViewArr&, float fOutlierThreshold, int nIgnoreMaskLabel, const IIndexArr& views); #if TEXOPT_FACEOUTLIER != TEXOPT_FACEOUTLIER_NA bool FaceOutlierDetection(FaceDataArr& faceDatas, float fOutlierThreshold) const; @@ -332,7 +335,7 @@ struct MeshTexture { void CreateVirtualFaces(const FaceDataViewArr& facesDatas, FaceDataViewArr& virtualFacesDatas, VirtualFaceIdxsArr& virtualFaces, unsigned minCommonCameras=2, float thMaxNormalDeviation=25.f) const; IIndexArr SelectBestView(const FaceDataArr& faceDatas, FIndex fid, unsigned minCommonCameras, float ratioAngleToQuality) const; - bool FaceViewSelection(unsigned minCommonCameras, float fOutlierThreshold, float fRatioDataSmoothness, const IIndexArr& views); + bool FaceViewSelection(unsigned minCommonCameras, float fOutlierThreshold, float fRatioDataSmoothness, int nIgnoreMaskLabel, const IIndexArr& views); void CreateSeamVertices(); void GlobalSeamLeveling(); @@ -394,6 +397,37 @@ struct MeshTexture { Scene& scene; // the mesh vertices and faces }; +// creating an invalid mask for the given image corresponding to +// the invalid pixels generated during image correction for the lens distortion; +// the returned mask has the same size as the image and is set to zero for invalid pixels +static Image8U DetectInvalidImageRegions(const Image8U3& image) +{ + const cv::Scalar upDiff(3); + const int flags(8 | (255 << 8)); + Image8U mask(image.rows + 2, image.cols + 2); + mask.memset(0); + Image8U imageGray; + cv::cvtColor(image, imageGray, cv::COLOR_BGR2GRAY); + if (imageGray(0, 0) == 0) + cv::floodFill(imageGray, mask, cv::Point(0, 0), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(image.rows / 2, 0) == 0) + cv::floodFill(imageGray, mask, cv::Point(0, image.rows / 2), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(image.rows - 1, 0) == 0) + cv::floodFill(imageGray, mask, cv::Point(0, image.rows - 1), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(image.rows - 1, image.cols / 2) == 0) + cv::floodFill(imageGray, mask, cv::Point(image.cols / 2, image.rows - 1), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(image.rows - 1, image.cols - 1) == 0) + cv::floodFill(imageGray, mask, cv::Point(image.cols - 1, image.rows - 1), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(image.rows / 2, image.cols - 1) == 0) + cv::floodFill(imageGray, mask, cv::Point(image.cols - 1, image.rows / 2), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(0, image.cols - 1) == 0) + cv::floodFill(imageGray, mask, cv::Point(image.cols - 1, 0), 255, NULL, cv::Scalar(0), upDiff, flags); + if (imageGray(0, image.cols / 2) == 0) + cv::floodFill(imageGray, mask, cv::Point(image.cols / 2, 0), 255, NULL, cv::Scalar(0), upDiff, flags); + mask = (mask(cv::Rect(1,1, imageGray.cols,imageGray.rows)) == 0); + return mask; +} + MeshTexture::MeshTexture(Scene& _scene, unsigned _nResolutionLevel, unsigned _nMinResolution) : nResolutionLevel(_nResolutionLevel), @@ -427,7 +461,7 @@ void MeshTexture::ListVertexFaces() } // extract array of faces viewed by each image -bool MeshTexture::ListCameraFaces(FaceDataViewArr& facesDatas, float fOutlierThreshold, const IIndexArr& _views) +bool MeshTexture::ListCameraFaces(FaceDataViewArr& facesDatas, float fOutlierThreshold, int nIgnoreMaskLabel, const IIndexArr& _views) { // create faces octree Mesh::Octree octree; @@ -506,20 +540,36 @@ bool MeshTexture::ListCameraFaces(FaceDataViewArr& facesDatas, float fOutlierThr const Frustum frustum(Frustum::MATRIX3x4(((PMatrix::CEMatMap)imageData.camera.P).cast()), (float)imageData.width, (float)imageData.height); octree.Traverse(frustum, inserter); // project all triangles in this view and keep the closest ones - faceMap.create(imageData.height, imageData.width); - depthMap.create(imageData.height, imageData.width); + faceMap.create(imageData.GetSize()); + depthMap.create(imageData.GetSize()); RasterMesh rasterer(vertices, imageData.camera, depthMap, faceMap); + if (nIgnoreMaskLabel >= 0) { + // import mask + BitMatrix bmask; + DepthEstimator::ImportIgnoreMask(imageData, imageData.GetSize(), (uint16_t)OPTDENSE::nIgnoreMaskLabel, bmask, &rasterer.mask); + } else if (nIgnoreMaskLabel == -1) { + // creating mask to discard invalid regions created during image radial undistortion + rasterer.mask = DetectInvalidImageRegions(imageData.image); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + cv::imwrite(String::FormatString("umask%04d.png", idxView), rasterer.mask); + #endif + } rasterer.Clear(); for (auto idxFace : cameraFaces) { + rasterer.validFace = true; const Face& facet = faces[idxFace]; rasterer.idxFace = idxFace; rasterer.Project(facet); + if (!rasterer.validFace) + rasterer.Project(facet); } // compute the projection area of visible faces #if TEXOPT_FACEOUTLIER != TEXOPT_FACEOUTLIER_NA CLISTDEF0IDX(uint32_t,FIndex) areas(faces.GetSize()); areas.Memset(0); #endif + #ifdef TEXOPT_USE_OPENMP #pragma omp critical #endif @@ -974,7 +1024,7 @@ bool MeshTexture::FaceOutlierDetection(FaceDataArr& faceDatas, float thOutlier) } #endif -bool MeshTexture::FaceViewSelection(unsigned minCommonCameras, float fOutlierThreshold, float fRatioDataSmoothness, const IIndexArr& views) +bool MeshTexture::FaceViewSelection(unsigned minCommonCameras, float fOutlierThreshold, float fRatioDataSmoothness, int nIgnoreMaskLabel, const IIndexArr& views) { // extract array of triangles incident to each vertex ListVertexFaces(); @@ -986,7 +1036,7 @@ bool MeshTexture::FaceViewSelection(unsigned minCommonCameras, float fOutlierThr // list all views for each face FaceDataViewArr facesDatas; - if (!ListCameraFaces(facesDatas, fOutlierThreshold, views)) + if (!ListCameraFaces(facesDatas, fOutlierThreshold, nIgnoreMaskLabel, views)) return false; // create faces graph @@ -2245,16 +2295,17 @@ void MeshTexture::GenerateTexture(bool bGlobalSeamLeveling, bool bLocalSeamLevel // texture mesh // - minCommonCameras: generate texture patches using virtual faces composed of coplanar triangles sharing at least this number of views (0 - disabled, 3 - good value) // - fSharpnessWeight: sharpness weight to be applied on the texture (0 - disabled, 0.5 - good value) +// - nIgnoreMaskLabel: label value to ignore in the image mask, stored in the MVS scene or next to each image with '.mask.png' extension (-1 - auto estimate mask for lens distortion, -2 - disabled) bool Scene::TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned minCommonCameras, float fOutlierThreshold, float fRatioDataSmoothness, bool bGlobalSeamLeveling, bool bLocalSeamLeveling, unsigned nTextureSizeMultiple, unsigned nRectPackingHeuristic, Pixel8U colEmpty, float fSharpnessWeight, - const IIndexArr& views) + int nIgnoreMaskLabel, const IIndexArr& views) { MeshTexture texture(*this, nResolutionLevel, nMinResolution); // assign the best view to each face { TD_TIMER_STARTD(); - if (!texture.FaceViewSelection(minCommonCameras, fOutlierThreshold, fRatioDataSmoothness, views)) + if (!texture.FaceViewSelection(minCommonCameras, fOutlierThreshold, fRatioDataSmoothness, nIgnoreMaskLabel, views)) return false; DEBUG_EXTRA("Assigning the best view to each face completed: %u faces (%s)", mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); } diff --git a/libs/MVS/SemiGlobalMatcher.cpp b/libs/MVS/SemiGlobalMatcher.cpp index 89bcd0e47..2b0757921 100644 --- a/libs/MVS/SemiGlobalMatcher.cpp +++ b/libs/MVS/SemiGlobalMatcher.cpp @@ -866,7 +866,7 @@ void SemiGlobalMatcher::Match(const ViewData& leftImage, const ViewData& rightIm const cv::Size sizeValid(size.width-2*halfWindowSizeX, size.height-2*halfWindowSizeY); ASSERT(leftImage.imageColor.size() == size); - #if 0 + #if 0 && !defined(_RELEASE) // display search info (average disparity and range) DisplayState(sizeValid); #endif @@ -1811,6 +1811,7 @@ void SemiGlobalMatcher::RefineDisparityMap(DisparityMap& disparityMap) const } +#ifndef _RELEASE // extract disparity and range from the pixel-map void SemiGlobalMatcher::DisplayState(const cv::Size& size) const { @@ -1831,6 +1832,7 @@ void SemiGlobalMatcher::DisplayState(const cv::Size& size) const cv::destroyAllWindows(); } +#endif // Compute the disparity-map for the rectified image from the given depth-map of the un-rectified image; // the disparity map needs to be already constructed at the desired size (valid size, excluding the border) diff --git a/libs/MVS/SemiGlobalMatcher.h b/libs/MVS/SemiGlobalMatcher.h index 740840db4..36117b495 100644 --- a/libs/MVS/SemiGlobalMatcher.h +++ b/libs/MVS/SemiGlobalMatcher.h @@ -142,8 +142,8 @@ class MVS_API SemiGlobalMatcher } }; - typedef MVS_API Point2f DepthRange; - typedef MVS_API TImage DepthRangeMap; + typedef Point2f DepthRange; + typedef TImage DepthRangeMap; public: SemiGlobalMatcher(SgmSubpixelMode subpixelMode=SUBPIXEL_LC_BLEND, Disparity subpixelSteps=4, AccumCost P1=3, AccumCost P2=4, float P2alpha=14, float P2beta=38); @@ -178,7 +178,9 @@ class MVS_API SemiGlobalMatcher static void FlipDirection(const DisparityMap& l2r, DisparityMap& r2l); static void UpscaleMask(MaskMap& maskMap, const cv::Size& size2x); void RefineDisparityMap(DisparityMap& disparityMap) const; + #ifndef _RELEASE void DisplayState(const cv::Size& size) const; + #endif static CLISTDEF0IDX(AccumCost,int) GenerateP2s(AccumCost P2, float P2alpha, float P2beta); static void Depth2DisparityMap(const DepthMap&, const Matrix3x3& invH, const Matrix4x4& invQ, Disparity subpixelSteps, DisparityMap&); diff --git a/libs/Math/RobustNorms.h b/libs/Math/RobustNorms.h index e31d05fe4..5935bb433 100644 --- a/libs/Math/RobustNorms.h +++ b/libs/Math/RobustNorms.h @@ -47,6 +47,13 @@ namespace SEACAVE { // new_residual = robustNorm(residual) namespace RobustNorm { +template +struct Identity { + inline TYPE operator()(const TYPE& r) const { + return r; + } +}; + template struct L1 { inline TYPE operator()(const TYPE& r) const { diff --git a/vcpkg.json b/vcpkg.json index 17eae3888..a74f701ea 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -24,6 +24,12 @@ "zlib" ], "features": { + "python": { + "description": "Python bindings for OpenMVS", + "dependencies": [ + "boost-python" + ] + }, "cuda": { "description": "CUDA support for OpenMVS", "dependencies": [ From 42b8816f1363a1b34d6e42f2a3b207c5dfa6dfbc Mon Sep 17 00:00:00 2001 From: Bouke van der Bijl Date: Thu, 21 Sep 2023 06:29:43 +0200 Subject: [PATCH 2/2] io: add .jpeg file extension support (#1060) --- libs/IO/Image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/IO/Image.cpp b/libs/IO/Image.cpp index 0a9de141f..3d3e1cecb 100644 --- a/libs/IO/Image.cpp +++ b/libs/IO/Image.cpp @@ -893,7 +893,7 @@ CImage* CImage::Create(LPCTSTR szName, IMCREATE mode) pImage = new CImagePNG(); #endif #ifdef _IMAGE_JPG - else if (_tcsncicmp(fext, _T(".jpg"), 4) == 0) + else if (_tcsncicmp(fext, _T(".jpg"), 4) == 0 || _tcsncicmp(fext, _T(".jpeg"), 5) == 0) pImage = new CImageJPG(); #endif #ifdef _IMAGE_TIFF