diff --git a/SuperBuild/CMakeLists.txt b/SuperBuild/CMakeLists.txt index a5a1200b..7c77d8dc 100644 --- a/SuperBuild/CMakeLists.txt +++ b/SuperBuild/CMakeLists.txt @@ -142,6 +142,7 @@ set(custom_libs OpenSfM OpenMVS FPCFilter PyPopsift + Obj2Tiles ) # Build entwine only on Linux diff --git a/VERSION b/VERSION index 2701a226..766d7080 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.8.4 +2.8.5 diff --git a/opendm/config.py b/opendm/config.py index 8b4d8dcf..ed5ace94 100755 --- a/opendm/config.py +++ b/opendm/config.py @@ -156,6 +156,12 @@ def config(argv=None, parser=None): 'Can be one of: %(choices)s. 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('--matcher-neighbors', metavar='', action=StoreValue, @@ -773,9 +779,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") diff --git a/opendm/ogctiles.py b/opendm/ogctiles.py new file mode 100644 index 00000000..771ec1ee --- /dev/null +++ b/opendm/ogctiles.py @@ -0,0 +1,93 @@ +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 import concurrency +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 "{input}" --output "{output}" --divisions {divisions} '.format(**kwargs)) + + except Exception as e: + log.ODM_WARNING("Cannot build 3D tiles textured model: %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, "textured_model") + + 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) + + 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) \ No newline at end of file diff --git a/opendm/types.py b/opendm/types.py index b49f7466..7ce99321 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -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) diff --git a/opendm/utils.py b/opendm/utils.py index c01dfe8a..2be031ad 100644 --- a/opendm/utils.py +++ b/opendm/utils.py @@ -64,6 +64,7 @@ def get_processing_results_paths(): "dsm_tiles", "dtm_tiles", "orthophoto_tiles", + "3d_tiles", "images.json", "cameras.json", "log.json", diff --git a/stages/odm_postprocess.py b/stages/odm_postprocess.py index 703b6586..95fac779 100644 --- a/stages/odm_postprocess.py +++ b/stages/odm_postprocess.py @@ -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())