Merge pull request #1464 from pierotofy/3dtiles

Adds support for OGC 3D Tiles outputs + Multi Subresolution Patch Match OpenMVS
pull/1470/head
Piero Toffanin 2022-06-04 17:57:00 -04:00 zatwierdzone przez GitHub
commit 41ad541aa2
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
16 zmienionych plików z 255 dodań i 42 usunięć

Wyświetl plik

@ -138,17 +138,14 @@ set(custom_libs OpenSfM
LASzip
PDAL
Untwine
Entwine
MvsTexturing
OpenMVS
FPCFilter
PyPopsift
Obj2Tiles
)
# Build entwine only on Linux
if (NOT WIN32)
set(custom_libs ${custom_libs} Entwine)
endif()
externalproject_add(mve
GIT_REPOSITORY https://github.com/OpenDroneMap/mve.git
GIT_TAG 262

Wyświetl plik

@ -13,7 +13,7 @@ ExternalProject_Add(${_proj_name}
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
GIT_REPOSITORY https://github.com/OpenDroneMap/entwine/
GIT_TAG 250
GIT_TAG 285
#--Update/Patch step----------
UPDATE_COMMAND ""
#--Configure step-------------

Wyświetl plik

@ -0,0 +1,33 @@
set(_proj_name obj2tiles)
set(_SB_BINARY_DIR "${SB_BINARY_DIR}/${_proj_name}")
set(OBJ2TILES_VERSION v1.0.7)
set(OBJ2TILES_EXT "")
set(OBJ2TILES_ARCH "Linux64")
if (WIN32)
set(OBJ2TILES_ARCH "Win64")
set(OBJ2TILES_EXT ".exe")
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64")
set(OBJ2TILES_ARCH "LinuxArm")
endif()
ExternalProject_Add(${_proj_name}
PREFIX ${_SB_BINARY_DIR}
TMP_DIR ${_SB_BINARY_DIR}/tmp
STAMP_DIR ${_SB_BINARY_DIR}/stamp
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
URL https://github.com/OpenDroneMap/Obj2Tiles/releases/download/${OBJ2TILES_VERSION}/Obj2Tiles-${OBJ2TILES_ARCH}.zip
SOURCE_DIR ${SB_SOURCE_DIR}/${_proj_name}
UPDATE_COMMAND ""
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_COMMAND ""
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${SB_SOURCE_DIR}/${_proj_name}/Obj2Tiles${OBJ2TILES_EXT} ${SB_INSTALL_DIR}/bin
#--Output logging-------------
LOG_DOWNLOAD OFF
LOG_CONFIGURE OFF
LOG_BUILD OFF
)

Wyświetl plik

@ -3,7 +3,7 @@ set(_SB_BINARY_DIR "${SB_BINARY_DIR}/${_proj_name}")
externalproject_add(vcg
GIT_REPOSITORY https://github.com/OpenDroneMap/VCG.git
GIT_TAG 280
GIT_TAG 285
UPDATE_COMMAND ""
SOURCE_DIR ${SB_SOURCE_DIR}/vcg
CONFIGURE_COMMAND ""
@ -52,7 +52,7 @@ ExternalProject_Add(${_proj_name}
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
GIT_REPOSITORY https://github.com/OpenDroneMap/openMVS
GIT_TAG 270
GIT_TAG 285
#--Update/Patch step----------
UPDATE_COMMAND ""
#--Configure step-------------

Wyświetl plik

@ -1 +1 @@
2.8.4
2.8.5

Wyświetl plik

@ -269,7 +269,13 @@ def config(argv=None, parser=None):
'caps the maximum resolution of image outputs and '
'resizes images when necessary, resulting in faster processing and '
'lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s')
parser.add_argument('--no-gpu',
action=StoreTrue,
nargs=0,
default=False,
help='Do not use GPU acceleration, even if it\'s available. Default: %(default)s')
parser.add_argument('--mesh-size',
metavar='<positive integer>',
action=StoreValue,
@ -608,6 +614,12 @@ def config(argv=None, parser=None):
'suitable for viewers like Leaflet or OpenLayers. '
'Default: %(default)s')
parser.add_argument('--3d-tiles',
action=StoreTrue,
nargs=0,
default=False,
help='Generate OGC 3D Tiles outputs. Default: %(default)s')
parser.add_argument('--build-overviews',
action=StoreTrue,
nargs=0,
@ -773,9 +785,6 @@ def config(argv=None, parser=None):
if args.fast_orthophoto:
log.ODM_INFO('Fast orthophoto is turned on, automatically setting --skip-3dmodel')
args.skip_3dmodel = True
# if not 'sfm_algorithm_is_set' in args:
# log.ODM_INFO('Fast orthophoto is turned on, automatically setting --sfm-algorithm to triangulation')
# args.sfm_algorithm = 'triangulation'
if args.pc_rectify and not args.pc_classify:
log.ODM_INFO("Ground rectify is turned on, automatically turning on point cloud classification")

Wyświetl plik

@ -23,6 +23,16 @@ import threading
from .ground_rectification.rectify import run_rectification
from . import pdal
try:
# GDAL >= 3.3
from osgeo_utils.gdal_proximity import main as gdal_proximity
except ModuleNotFoundError:
# GDAL <= 3.2
try:
from osgeo.utils.gdal_proximity import main as gdal_proximity
except:
pass
def classify(lasFile, scalar, slope, threshold, window, verbose=False):
start = datetime.now()
@ -290,12 +300,20 @@ def compute_euclidean_map(geotiff_path, output_path, overwrite=False):
if not os.path.exists(output_path) or overwrite:
log.ODM_INFO("Computing euclidean distance: %s" % output_path)
run('gdal_proximity.py "%s" "%s" -values %s' % (geotiff_path, output_path, nodata))
if os.path.exists(output_path):
return output_path
if gdal_proximity is not None:
try:
gdal_proximity(['gdal_proximity.py', geotiff_path, output_path, '-values', str(nodata)])
except Exception as e:
log.ODM_WARNING("Cannot compute euclidean distance: %s" % str(e))
if os.path.exists(output_path):
return output_path
else:
log.ODM_WARNING("Cannot compute euclidean distance file: %s" % output_path)
else:
log.ODM_WARNING("Cannot compute euclidean distance file: %s" % output_path)
log.ODM_WARNING("Cannot compute euclidean map, gdal_proximity is missing")
else:
log.ODM_INFO("Found a euclidean distance map: %s" % output_path)
return output_path

Wyświetl plik

@ -28,34 +28,28 @@ def build(input_point_cloud_files, output_path, max_concurrency=8, rerun=False):
if rerun:
dir_cleanup()
# On Windows we always use Untwine
if sys.platform == 'win32':
# Attempt with entwine (faster, more memory hungry)
try:
build_entwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=max_concurrency)
except Exception as e:
log.ODM_WARNING("Cannot build EPT using entwine (%s), attempting with untwine..." % str(e))
dir_cleanup()
build_untwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=max_concurrency)
else:
# Attempt with entwine (faster, more memory hungry)
try:
build_entwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=max_concurrency)
except Exception as e:
log.ODM_WARNING("Cannot build EPT using entwine (%s), attempting with untwine..." % str(e))
dir_cleanup()
build_untwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=max_concurrency)
if os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
def build_entwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=8):
def build_entwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=8, reproject=None):
kwargs = {
'threads': max_concurrency,
'tmpdir': tmpdir,
'all_inputs': "-i " + " ".join(map(double_quote, input_point_cloud_files)),
'outputdir': output_path
'outputdir': output_path,
'reproject': (" -r %s " % reproject) if reproject is not None else ""
}
# for _ in range(len(input_point_cloud_files)):
# # One at a time
# system.run('entwine build --threads {threads} --tmp "{tmpdir}" -i "{input}" -o "{outputdir}"'.format(**kwargs))
system.run('entwine build --threads {threads} --tmp "{tmpdir}" {all_inputs} -o "{outputdir}"'.format(**kwargs))
system.run('entwine build --threads {threads} --tmp "{tmpdir}" {all_inputs} -o "{outputdir}" {reproject}'.format(**kwargs))
def build_untwine(input_point_cloud_files, tmpdir, output_path, max_concurrency=8, rerun=False):
kwargs = {

Wyświetl plik

@ -5,7 +5,7 @@ import ctypes
from opendm import log
from repoze.lru import lru_cache
def gpu_disabled_by_user():
def gpu_disabled_by_user_env():
return bool(os.environ.get('ODM_NO_GPU'))
@lru_cache(maxsize=None)
@ -68,11 +68,13 @@ def get_cuda_compute_version(device_id = 0):
return (compute_major.value, compute_minor.value)
@lru_cache(maxsize=None)
def has_gpu():
if gpu_disabled_by_user():
def has_gpu(args):
if gpu_disabled_by_user_env():
log.ODM_INFO("Disabling GPU features (ODM_NO_GPU is set)")
return False
if args.no_gpu:
log.ODM_INFO("Disabling GPU features (--no-gpu is set)")
return False
if sys.platform == 'win32':
nvcuda_path = os.path.join(os.environ.get('SYSTEMROOT'), 'system32', 'nvcuda.dll')

135
opendm/ogctiles.py 100644
Wyświetl plik

@ -0,0 +1,135 @@
import os
import sys
import shutil
import json
import math
from opendm.utils import double_quote
from opendm import io
from opendm import log
from opendm import system
from opendm.entwine import build_entwine
import fiona
from shapely.geometry import shape
def build_textured_model(input_obj, output_path, reference_lla = None, model_bounds_file=None, rerun=False):
if not os.path.isfile(input_obj):
log.ODM_WARNING("No input OBJ file to process")
return
if rerun and io.dir_exists(output_path):
log.ODM_WARNING("Removing previous 3D tiles directory: %s" % output_path)
shutil.rmtree(output_path)
log.ODM_INFO("Generating OGC 3D Tiles textured model")
lat = lon = alt = 0
# Read reference_lla.json (if provided)
if reference_lla is not None and os.path.isfile(reference_lla):
try:
with open(reference_lla) as f:
reference_lla = json.loads(f.read())
lat = reference_lla['latitude']
lon = reference_lla['longitude']
alt = reference_lla['altitude']
except Exception as e:
log.ODM_WARNING("Cannot read %s: %s" % (reference_lla, str(e)))
# Read model bounds (if provided)
divisions = 1 # default
DIV_THRESHOLD = 10000 # m^2 (this is somewhat arbitrary)
if model_bounds_file is not None and os.path.isfile(model_bounds_file):
try:
with fiona.open(model_bounds_file, 'r') as f:
if len(f) == 1:
poly = shape(f[1]['geometry'])
area = poly.area
log.ODM_INFO("Approximate area: %s m^2" % round(area, 2))
if area < DIV_THRESHOLD:
divisions = 0
else:
divisions = math.ceil(math.log((area / DIV_THRESHOLD), 4))
else:
log.ODM_WARNING("Invalid boundary file: %s" % model_bounds_file)
except Exception as e:
log.ODM_WARNING("Cannot read %s: %s" % (model_bounds_file, str(e)))
try:
kwargs = {
'input': input_obj,
'output': output_path,
'divisions': divisions,
'lat': lat,
'lon': lon,
'alt': alt,
}
system.run('Obj2Tiles "{input}" "{output}" --divisions {divisions} '.format(**kwargs))
except Exception as e:
log.ODM_WARNING("Cannot build 3D tiles textured model: %s" % str(e))
def build_pointcloud(input_pointcloud, output_path, max_concurrency, rerun=False):
if not os.path.isfile(input_pointcloud):
log.ODM_WARNING("No input point cloud file to process")
return
if rerun and io.dir_exists(output_path):
log.ODM_WARNING("Removing previous 3D tiles directory: %s" % output_path)
shutil.rmtree(output_path)
log.ODM_INFO("Generating OGC 3D Tiles point cloud")
try:
if not os.path.isdir(output_path):
os.mkdir(output_path)
tmpdir = os.path.join(output_path, "tmp")
entwine_output = os.path.join(output_path, "entwine")
build_entwine([input_pointcloud], tmpdir, entwine_output, max_concurrency, "EPSG:4978")
kwargs = {
'input': entwine_output,
'output': output_path,
}
system.run('entwine convert -i "{input}" -o "{output}"'.format(**kwargs))
for d in [tmpdir, entwine_output]:
if os.path.isdir(d):
shutil.rmtree(d)
except Exception as e:
log.ODM_WARNING("Cannot build 3D tiles point cloud: %s" % str(e))
def build_3dtiles(args, tree, reconstruction, rerun=False):
tiles_output_path = tree.ogc_tiles
model_output_path = os.path.join(tiles_output_path, "model")
pointcloud_output_path = os.path.join(tiles_output_path, "pointcloud")
if rerun and os.path.exists(tiles_output_path):
shutil.rmtree(tiles_output_path)
if not os.path.isdir(tiles_output_path):
os.mkdir(tiles_output_path)
# Model
if not os.path.isdir(model_output_path) or rerun:
reference_lla = os.path.join(tree.opensfm, "reference_lla.json")
model_bounds_file = os.path.join(tree.odm_georeferencing, 'odm_georeferenced_model.bounds.gpkg')
input_obj = os.path.join(tree.odm_texturing, tree.odm_textured_model_obj)
if not os.path.isfile(input_obj):
input_obj = os.path.join(tree.odm_25dtexturing, tree.odm_textured_model_obj)
build_textured_model(input_obj, model_output_path, reference_lla, model_bounds_file, rerun)
else:
log.ODM_WARNING("OGC 3D Tiles model %s already generated" % model_output_path)
# Point cloud
if not os.path.isdir(pointcloud_output_path) or rerun:
build_pointcloud(tree.odm_georeferencing_model_laz, pointcloud_output_path, args.max_concurrency, rerun)
else:
log.ODM_WARNING("OGC 3D Tiles model %s already generated" % model_output_path)

Wyświetl plik

@ -251,7 +251,7 @@ class OSFMContext:
config.append("matcher_type: %s" % osfm_matchers[matcher_type])
# GPU acceleration?
if has_gpu():
if has_gpu(args):
max_photo = find_largest_photo(photos)
w, h = max_photo.width, max_photo.height
if w > h:

Wyświetl plik

@ -81,6 +81,11 @@ def get_mm_per_unit(resolution_unit):
class PhotoCorruptedException(Exception):
pass
class GPSRefMock:
def __init__(self, ref):
self.values = [ref]
class ODM_Photo:
"""ODMPhoto - a class for ODMPhotos"""
@ -209,8 +214,14 @@ class ODM_Photo:
self.altitude *= -1
if 'GPS GPSLatitude' in tags and 'GPS GPSLatitudeRef' in tags:
self.latitude = self.dms_to_decimal(tags['GPS GPSLatitude'], tags['GPS GPSLatitudeRef'])
elif 'GPS GPSLatitude' in tags:
log.ODM_WARNING("GPS position for %s might be incorrect, GPSLatitudeRef tag is missing (assuming N)" % self.filename)
self.latitude = self.dms_to_decimal(tags['GPS GPSLatitude'], GPSRefMock('N'))
if 'GPS GPSLongitude' in tags and 'GPS GPSLongitudeRef' in tags:
self.longitude = self.dms_to_decimal(tags['GPS GPSLongitude'], tags['GPS GPSLongitudeRef'])
elif 'GPS GPSLongitude' in tags:
log.ODM_WARNING("GPS position for %s might be incorrect, GPSLongitudeRef tag is missing (assuming E)" % self.filename)
self.longitude = self.dms_to_decimal(tags['GPS GPSLongitude'], GPSRefMock('E'))
if 'Image Orientation' in tags:
self.orientation = self.int_value(tags['Image Orientation'])
except (IndexError, ValueError) as e:

Wyświetl plik

@ -289,6 +289,7 @@ class ODM_Tree(object):
# Tiles
self.entwine_pointcloud = self.path("entwine_pointcloud")
self.ogc_tiles = self.path("3d_tiles")
def path(self, *args):
return os.path.join(self.root_path, *args)

Wyświetl plik

@ -64,6 +64,7 @@ def get_processing_results_paths():
"dsm_tiles",
"dtm_tiles",
"orthophoto_tiles",
"3d_tiles",
"images.json",
"cameras.json",
"log.json",

Wyświetl plik

@ -5,6 +5,7 @@ from opendm import io
from opendm import log
from opendm import types
from opendm.utils import copy_paths, get_processing_results_paths
from opendm.ogctiles import build_3dtiles
class ODMPostProcess(types.ODM_Stage):
def process(self, args, outputs):
@ -43,7 +44,10 @@ class ODMPostProcess(types.ODM_Stage):
break
else:
log.ODM_WARNING("Cannot open %s for writing, skipping GCP embedding" % product)
if getattr(args, '3d_tiles'):
build_3dtiles(args, tree, reconstruction, self.rerun())
if args.copy_to:
try:
copy_paths([os.path.join(args.project_path, p) for p in get_processing_results_paths()], args.copy_to, self.rerun())

Wyświetl plik

@ -59,21 +59,25 @@ class ODMOpenMVSStage(types.ODM_Stage):
log.ODM_INFO("Estimating depthmaps")
number_views_fuse = 2
densify_ini_file = os.path.join(tree.openmvs, 'Densify.ini')
subres_levels = 2 # The number of lower resolutions to process before estimating output resolution depthmap.
config = [
" --resolution-level %s" % int(resolution_level),
'--dense-config-file "%s"' % densify_ini_file,
"--min-resolution %s" % depthmap_resolution,
"--max-resolution %s" % int(outputs['undist_image_max_size']),
"--max-threads %s" % args.max_concurrency,
"--number-views-fuse %s" % number_views_fuse,
"--sub-resolution-levels %s" % subres_levels,
'-w "%s"' % depthmaps_dir,
"-v 0"
]
gpu_config = []
if not has_gpu():
gpu_config.append("--cuda-device -1")
if not has_gpu(args):
gpu_config.append("--cuda-device -2")
if args.pc_tile:
config.append("--fusion-mode 1")
@ -81,6 +85,10 @@ class ODMOpenMVSStage(types.ODM_Stage):
if not args.pc_geometric:
config.append("--geometric-iters 0")
sharp = args.pc_filter > 0
with open(densify_ini_file, 'w+') as f:
f.write("Optimize = %s\n" % (7 if sharp else 3))
def run_densify():
system.run('"%s" "%s" %s' % (context.omvs_densify_path,
openmvs_scene_file,
@ -93,7 +101,7 @@ class ODMOpenMVSStage(types.ODM_Stage):
# try to run it again without GPU
if e.errorCode == 1 and len(gpu_config) == 0:
log.ODM_WARNING("OpenMVS failed with GPU, is your graphics card driver up to date? Falling back to CPU.")
gpu_config.append("--cuda-device -1")
gpu_config.append("--cuda-device -2")
run_densify()
else:
raise e