From 1f1365708c698405e4e57ed8737ea622c5312dd3 Mon Sep 17 00:00:00 2001 From: twchambers Date: Thu, 15 Dec 2022 10:26:01 +0000 Subject: [PATCH 01/21] Update rollingshutter.py Added calibration value for Mavic Pro (=64). Value is valid for both Mavic Pro and Mavic Pro Platinum, as both use camera 'DJI FC220' --- opendm/rollingshutter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendm/rollingshutter.py b/opendm/rollingshutter.py index 28c8d23d..b8227f61 100644 --- a/opendm/rollingshutter.py +++ b/opendm/rollingshutter.py @@ -15,6 +15,7 @@ RS_DATABASE = { 'dji fc3170': 27, # DJI Mavic Air 2 'dji fc3411': 32, # DJI Mavic Air 2S + 'dji fc220': 64, # DJI Mavic Pro (Platinum) 'hasselblad l1d-20c': lambda p: 47 if p.get_capture_megapixels() < 17 else 56, # DJI Mavic 2 Pro (at 16:10 => 16.8MP 47ms, at 3:2 => 19.9MP 56ms. 4:3 has 17.7MP with same image height as 3:2 which can be concluded as same sensor readout) 'dji fc3582': lambda p: 26 if p.get_capture_megapixels() < 48 else 60, # DJI Mini 3 pro (at 48MP readout is 60ms, at 12MP it's 26ms) From 70155e35e8dc31bf63347479b0e6b7f9cdbf91ca Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 20 Dec 2022 10:50:42 -0500 Subject: [PATCH 02/21] Add --feature-quality help warning --- opendm/gpu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/gpu.py b/opendm/gpu.py index c92cad12..fa0712dc 100644 --- a/opendm/gpu.py +++ b/opendm/gpu.py @@ -26,7 +26,7 @@ def has_popsift_and_can_handle_texsize(width, height): from opensfm import pypopsift fits = pypopsift.fits_texture(int(width * 1.02), int(height * 1.02)) if not fits: - log.ODM_WARNING("Image size (%sx%spx) would not fit in GPU memory, falling back to CPU" % (width, height)) + log.ODM_WARNING("Image size (%sx%spx) would not fit in GPU memory, try lowering --feature-quality. Falling back to CPU" % (width, height)) return fits except (ModuleNotFoundError, ImportError): return False From 214b6ef9afa0f84644722d4f42ed1476ee3ba4a8 Mon Sep 17 00:00:00 2001 From: Julien <6372605+fetzu@users.noreply.github.com> Date: Sat, 24 Dec 2022 15:23:32 +0100 Subject: [PATCH 03/21] feat: added "DJI Mavic Air 1" to rollingshutter.py --- opendm/rollingshutter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendm/rollingshutter.py b/opendm/rollingshutter.py index b8227f61..908af1cb 100644 --- a/opendm/rollingshutter.py +++ b/opendm/rollingshutter.py @@ -12,6 +12,7 @@ RS_DATABASE = { 'dji fc6310': 33, # Phantom 4 Professional 'dji fc7203': 20, # Mavic Mini v1 + 'dji fc2103': 32, # DJI Mavic Air 1 'dji fc3170': 27, # DJI Mavic Air 2 'dji fc3411': 32, # DJI Mavic Air 2S From 3d08c09ac9da56852a63826e939b90934c18d356 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 28 Dec 2022 14:21:29 -0500 Subject: [PATCH 04/21] Cleanup orthophoto_render.tif --- stages/odm_orthophoto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stages/odm_orthophoto.py b/stages/odm_orthophoto.py index 39e2bf60..16efdd7c 100644 --- a/stages/odm_orthophoto.py +++ b/stages/odm_orthophoto.py @@ -165,5 +165,5 @@ class ODMOrthoPhotoStage(types.ODM_Stage): else: log.ODM_WARNING('Found a valid orthophoto in: %s' % tree.odm_orthophoto_tif) - if args.optimize_disk_space and io.file_exists(tree.odm_orthophoto_render): + if io.file_exists(tree.odm_orthophoto_render): os.remove(tree.odm_orthophoto_render) From 8410186b663645e5480b058a57fde477aead0c40 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 3 Jan 2023 08:07:54 -0500 Subject: [PATCH 05/21] Don't let PDAL resolve canonical paths while searching for drivers --- Dockerfile | 3 ++- gpu.Dockerfile | 3 ++- portable.Dockerfile | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index dbe6e7ae..85708413 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ FROM ubuntu:21.04 # Env variables ENV DEBIAN_FRONTEND=noninteractive \ PYTHONPATH="$PYTHONPATH:/code/SuperBuild/install/lib/python3.9:/code/SuperBuild/install/lib/python3.8/dist-packages:/code/SuperBuild/install/bin/opensfm" \ - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" \ + PDAL_DRIVER_PATH="/code/SuperBuild/install/bin" WORKDIR /code diff --git a/gpu.Dockerfile b/gpu.Dockerfile index b7e6d7ec..e37c76a7 100644 --- a/gpu.Dockerfile +++ b/gpu.Dockerfile @@ -27,7 +27,8 @@ FROM nvidia/cuda:11.2.0-runtime-ubuntu20.04 # Env variables ENV DEBIAN_FRONTEND=noninteractive \ PYTHONPATH="$PYTHONPATH:/code/SuperBuild/install/lib/python3.9/dist-packages:/code/SuperBuild/install/lib/python3.8/dist-packages:/code/SuperBuild/install/bin/opensfm" \ - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" \ + PDAL_DRIVER_PATH="/code/SuperBuild/install/bin" WORKDIR /code diff --git a/portable.Dockerfile b/portable.Dockerfile index c4b288f5..75c9aa11 100644 --- a/portable.Dockerfile +++ b/portable.Dockerfile @@ -29,7 +29,8 @@ FROM ubuntu:21.04 # Env variables ENV DEBIAN_FRONTEND=noninteractive \ PYTHONPATH="$PYTHONPATH:/code/SuperBuild/install/lib/python3.9/dist-packages:/code/SuperBuild/install/lib/python3.8/dist-packages:/code/SuperBuild/install/bin/opensfm" \ - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/code/SuperBuild/install/lib" \ + PDAL_DRIVER_PATH="/code/SuperBuild/install/bin" WORKDIR /code From 8cb70002f570d69c8510c485e202da83ff94ed77 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 9 Jan 2023 14:05:38 -0500 Subject: [PATCH 06/21] Multispectral split-merge, GCP names fix, bump version --- SuperBuild/cmake/External-OpenSfM.cmake | 2 +- VERSION | 2 +- opendm/dem/commands.py | 2 +- opendm/orthophoto.py | 4 ++-- opendm/system.py | 10 ++++++++++ opendm/types.py | 2 +- stages/splitmerge.py | 16 +++++++++++++++- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/SuperBuild/cmake/External-OpenSfM.cmake b/SuperBuild/cmake/External-OpenSfM.cmake index 7ce9a002..428675c9 100644 --- a/SuperBuild/cmake/External-OpenSfM.cmake +++ b/SuperBuild/cmake/External-OpenSfM.cmake @@ -25,7 +25,7 @@ ExternalProject_Add(${_proj_name} #--Download step-------------- DOWNLOAD_DIR ${SB_DOWNLOAD_DIR} GIT_REPOSITORY https://github.com/OpenDroneMap/OpenSfM/ - GIT_TAG 302 + GIT_TAG 303 #--Update/Patch step---------- UPDATE_COMMAND git submodule update --init --recursive #--Configure step------------- diff --git a/VERSION b/VERSION index b5021469..75a22a26 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.2 +3.0.3 diff --git a/opendm/dem/commands.py b/opendm/dem/commands.py index 47903a68..d8d194f1 100755 --- a/opendm/dem/commands.py +++ b/opendm/dem/commands.py @@ -58,7 +58,7 @@ def rectify(lasFile, debug=False, reclassify_threshold=5, min_area=750, min_poin min_area=min_area, min_points=min_points) except Exception as e: - raise Exception("Error rectifying ground in file %s: %s" % (lasFile, str(e))) + log.ODM_WARNING("Error rectifying ground in file %s: %s" % (lasFile, str(e))) log.ODM_INFO('Created %s in %s' % (lasFile, datetime.now() - start)) return lasFile diff --git a/opendm/orthophoto.py b/opendm/orthophoto.py index 9b25e6d1..79865c71 100644 --- a/opendm/orthophoto.py +++ b/opendm/orthophoto.py @@ -216,8 +216,8 @@ def merge(input_ortho_and_ortho_cuts, output_orthophoto, orthophoto_vars={}): left, bottom, right, top = src.bounds xs.extend([left, right]) ys.extend([bottom, top]) - if src.profile["count"] < 4: - raise ValueError("Inputs must be at least 4-band rasters") + if src.profile["count"] < 2: + raise ValueError("Inputs must be at least 2-band rasters") dst_w, dst_s, dst_e, dst_n = min(xs), min(ys), max(xs), max(ys) log.ODM_INFO("Output bounds: %r %r %r %r" % (dst_w, dst_s, dst_e, dst_n)) diff --git a/opendm/system.py b/opendm/system.py index 4feff283..adeeb3f5 100644 --- a/opendm/system.py +++ b/opendm/system.py @@ -144,3 +144,13 @@ def which(program): p=os.path.join(p,program) if os.path.exists(p) and os.access(p,os.X_OK): return p + +def link_file(src, dst): + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + + if not os.path.isfile(dst): + if sys.platform == 'win32': + os.link(src, dst) + else: + os.symlink(os.path.relpath(os.path.abspath(src), os.path.dirname(os.path.abspath(dst))), dst) diff --git a/opendm/types.py b/opendm/types.py index be4b44ea..06a10bc0 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -113,7 +113,7 @@ class ODM_Reconstruction(object): # Convert GCP file to a UTM projection since the rest of the pipeline # does not handle other SRS well. rejected_entries = [] - utm_gcp = GCPFile(gcp.create_utm_copy(output_gcp_file, filenames=[p.filename for p in self.photos], rejected_entries=rejected_entries, include_extras=False)) + utm_gcp = GCPFile(gcp.create_utm_copy(output_gcp_file, filenames=[p.filename for p in self.photos], rejected_entries=rejected_entries, include_extras=True)) if not utm_gcp.exists(): raise RuntimeError("Could not project GCP file to UTM. Please double check your GCP file for mistakes.") diff --git a/stages/splitmerge.py b/stages/splitmerge.py index ce70f512..74f6afea 100644 --- a/stages/splitmerge.py +++ b/stages/splitmerge.py @@ -20,6 +20,7 @@ from opendm import point_cloud from opendm.utils import double_quote from opendm.tiles.tiler import generate_dem_tiles from opendm.cogeo import convert_to_cogeo +from opendm import multispectral class ODMSplitStage(types.ODM_Stage): def process(self, args, outputs): @@ -88,12 +89,12 @@ class ODMSplitStage(types.ODM_Stage): for sp in submodel_paths: sp_octx = OSFMContext(sp) + submodel_images_dir = os.path.abspath(sp_octx.path("..", "images")) # Copy filtered GCP file if needed # One in OpenSfM's directory, one in the submodel project directory if reconstruction.gcp and reconstruction.gcp.exists(): submodel_gcp_file = os.path.abspath(sp_octx.path("..", "gcp_list.txt")) - submodel_images_dir = os.path.abspath(sp_octx.path("..", "images")) if reconstruction.gcp.make_filtered_copy(submodel_gcp_file, submodel_images_dir): log.ODM_INFO("Copied filtered GCP file to %s" % submodel_gcp_file) @@ -107,6 +108,19 @@ class ODMSplitStage(types.ODM_Stage): io.copy(tree.odm_geo_file, geo_dst_path) log.ODM_INFO("Copied GEO file to %s" % geo_dst_path) + # If this is a multispectral dataset, + # we need to link the multispectral images + if reconstruction.multi_camera: + submodel_images = os.listdir(submodel_images_dir) + + primary_band_name = multispectral.get_primary_band_name(reconstruction.multi_camera, args.primary_band) + _, p2s = multispectral.compute_band_maps(reconstruction.multi_camera, primary_band_name) + for filename in p2s: + if filename in submodel_images: + secondary_band_photos = p2s[filename] + for p in secondary_band_photos: + system.link_file(os.path.join(tree.dataset_raw, p.filename), submodel_images_dir) + # Reconstruct each submodel log.ODM_INFO("Dataset has been split into %s submodels. Reconstructing each submodel..." % len(submodel_paths)) self.update_progress(25) From 697727aeefc47c7431439a5b4a15c5e6ae050f45 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 9 Jan 2023 14:08:49 -0500 Subject: [PATCH 07/21] Move info log --- opendm/dem/commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opendm/dem/commands.py b/opendm/dem/commands.py index d8d194f1..dbba8794 100755 --- a/opendm/dem/commands.py +++ b/opendm/dem/commands.py @@ -56,11 +56,10 @@ def rectify(lasFile, debug=False, reclassify_threshold=5, min_area=750, min_poin reclassify_plan='median', reclassify_threshold=reclassify_threshold, \ extend_plan='surrounding', extend_grid_distance=5, \ min_area=min_area, min_points=min_points) - + log.ODM_INFO('Created %s in %s' % (lasFile, datetime.now() - start)) except Exception as e: log.ODM_WARNING("Error rectifying ground in file %s: %s" % (lasFile, str(e))) - log.ODM_INFO('Created %s in %s' % (lasFile, datetime.now() - start)) return lasFile error = None From 0c0aa24dd9da0732a7beff245086dc34538c0df2 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 9 Jan 2023 15:57:26 -0500 Subject: [PATCH 08/21] More logical split value for multispec --- stages/splitmerge.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stages/splitmerge.py b/stages/splitmerge.py index 74f6afea..8e1ca322 100644 --- a/stages/splitmerge.py +++ b/stages/splitmerge.py @@ -55,11 +55,13 @@ class ODMSplitStage(types.ODM_Stage): log.ODM_INFO("Setting max-concurrency to %s to better handle remote splits" % args.max_concurrency) log.ODM_INFO("Large dataset detected (%s photos) and split set at %s. Preparing split merge." % (len(photos), args.split)) + multiplier = (1.0 / len(reconstruction.multi_camera)) if reconstruction.multi_camera else 1.0 + config = [ "submodels_relpath: " + os.path.join("..", "submodels", "opensfm"), "submodel_relpath_template: " + os.path.join("..", "submodels", "submodel_%04d", "opensfm"), "submodel_images_relpath_template: " + os.path.join("..", "submodels", "submodel_%04d", "images"), - "submodel_size: %s" % args.split, + "submodel_size: %s" % max(2, int(float(args.split) * multiplier)), "submodel_overlap: %s" % args.split_overlap, ] From e997e13e750cc1e1898597de8822890d1009cbfe Mon Sep 17 00:00:00 2001 From: Stephen Vincent Mather Date: Mon, 9 Jan 2023 23:59:11 -0500 Subject: [PATCH 09/21] add --pc-skip-geometric --- opendm/config.py | 7 +++++++ stages/openmvs.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/opendm/config.py b/opendm/config.py index edb66b95..9e73e466 100755 --- a/opendm/config.py +++ b/opendm/config.py @@ -389,6 +389,13 @@ def config(argv=None, parser=None): help='Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. ' 'Default: %(default)s') + parser.add_argument('--pc-skip-geometric + action=StoreTrue, + nargs=0, + default=False, + help='Geometric estimates improve the accuracy of the point cloud by computing geometrically consistent depthmaps but may not be usable in larger datasets. This flag disables geometric estimates. ' + 'Default: %(default)s') + parser.add_argument('--pc-tile', action=StoreTrue, nargs=0, diff --git a/stages/openmvs.py b/stages/openmvs.py index df37f6a0..ea0c590e 100644 --- a/stages/openmvs.py +++ b/stages/openmvs.py @@ -86,12 +86,16 @@ class ODMOpenMVSStage(types.ODM_Stage): config.append("--fusion-mode 1") extra_config = [] - + + if args.pc_skip_geometric: + extra_config.append("--geometric-iters 0") + masks_dir = os.path.join(tree.opensfm, "undistorted", "masks") masks = os.path.exists(masks_dir) and len(os.listdir(masks_dir)) > 0 if masks: extra_config.append("--ignore-mask-label 0") + sharp = args.pc_geometric with open(densify_ini_file, 'w+') as f: f.write("Optimize = 7\n") From 999434e1610ccab3794d8ebc3d42cc176ff967c7 Mon Sep 17 00:00:00 2001 From: Stephen Vincent Mather Date: Tue, 10 Jan 2023 00:04:43 -0500 Subject: [PATCH 10/21] add missing logic --- stages/openmvs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stages/openmvs.py b/stages/openmvs.py index ea0c590e..75293eb5 100644 --- a/stages/openmvs.py +++ b/stages/openmvs.py @@ -95,12 +95,12 @@ class ODMOpenMVSStage(types.ODM_Stage): if masks: extra_config.append("--ignore-mask-label 0") - sharp = args.pc_geometric + sharp = args.pc_skip_geometric with open(densify_ini_file, 'w+') as f: - f.write("Optimize = 7\n") + f.write("Optimize = %s\n" % (3 if sharp else 7)) def run_densify(): - system.run('"%s" "%s" %s' % (context.omvs_densify_path, + .run('"%s" "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config + gpu_config + extra_config))) try: From 3f6b8b49362ba2df412b4aaaf55764bbf51b7a23 Mon Sep 17 00:00:00 2001 From: Stephen Vincent Mather Date: Tue, 10 Jan 2023 00:05:42 -0500 Subject: [PATCH 11/21] typo fix:wq --- stages/openmvs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stages/openmvs.py b/stages/openmvs.py index 75293eb5..4133042e 100644 --- a/stages/openmvs.py +++ b/stages/openmvs.py @@ -100,7 +100,7 @@ class ODMOpenMVSStage(types.ODM_Stage): f.write("Optimize = %s\n" % (3 if sharp else 7)) def run_densify(): - .run('"%s" "%s" %s' % (context.omvs_densify_path, + system.run('"%s" "%s" %s' % (context.omvs_densify_path, openmvs_scene_file, ' '.join(config + gpu_config + extra_config))) try: From aa82f767477f332eb71ace7989a3ade6d2fce0d5 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 10 Jan 2023 09:38:13 -0500 Subject: [PATCH 12/21] Syntax fix, always optimize --- opendm/config.py | 2 +- stages/openmvs.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/opendm/config.py b/opendm/config.py index 9e73e466..966a4f2e 100755 --- a/opendm/config.py +++ b/opendm/config.py @@ -389,7 +389,7 @@ def config(argv=None, parser=None): help='Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud and remove duplicate points. Set to 0 to disable sampling. ' 'Default: %(default)s') - parser.add_argument('--pc-skip-geometric + parser.add_argument('--pc-skip-geometric', action=StoreTrue, nargs=0, default=False, diff --git a/stages/openmvs.py b/stages/openmvs.py index 4133042e..eec1bba4 100644 --- a/stages/openmvs.py +++ b/stages/openmvs.py @@ -95,9 +95,8 @@ class ODMOpenMVSStage(types.ODM_Stage): if masks: extra_config.append("--ignore-mask-label 0") - sharp = args.pc_skip_geometric with open(densify_ini_file, 'w+') as f: - f.write("Optimize = %s\n" % (3 if sharp else 7)) + f.write("Optimize = 7\n") def run_densify(): system.run('"%s" "%s" %s' % (context.omvs_densify_path, From 6e50ed8fcd708a2cf8727a621f37fda863feca63 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 13:16:04 -0500 Subject: [PATCH 13/21] Add --texturing-single-material --- opendm/config.py | 7 + opendm/objpacker/__init__.py | 1 + opendm/objpacker/imagepacker/__init__.py | 1 + opendm/objpacker/imagepacker/imagepacker.py | 240 ++++++++++++++++++++ opendm/objpacker/imagepacker/utils.py | 55 +++++ opendm/objpacker/objpacker.py | 228 +++++++++++++++++++ opendm/system.py | 17 ++ stages/mvstex.py | 23 +- 8 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 opendm/objpacker/__init__.py create mode 100644 opendm/objpacker/imagepacker/__init__.py create mode 100644 opendm/objpacker/imagepacker/imagepacker.py create mode 100644 opendm/objpacker/imagepacker/utils.py create mode 100644 opendm/objpacker/objpacker.py diff --git a/opendm/config.py b/opendm/config.py index 966a4f2e..e9de9e5b 100755 --- a/opendm/config.py +++ b/opendm/config.py @@ -454,6 +454,13 @@ def config(argv=None, parser=None): help=('Keep faces in the mesh that are not seen in any camera. ' 'Default: %(default)s')) + parser.add_argument('--texturing-single-material', + action=StoreTrue, + nargs=0, + default=False, + help=('Generate OBJs that have a single material and a single texture file instead of multiple ones. ' + 'Default: %(default)s')) + parser.add_argument('--gcp', metavar='', action=StoreValue, diff --git a/opendm/objpacker/__init__.py b/opendm/objpacker/__init__.py new file mode 100644 index 00000000..f3bbb024 --- /dev/null +++ b/opendm/objpacker/__init__.py @@ -0,0 +1 @@ +from .objpacker import obj_pack \ No newline at end of file diff --git a/opendm/objpacker/imagepacker/__init__.py b/opendm/objpacker/imagepacker/__init__.py new file mode 100644 index 00000000..d89deac5 --- /dev/null +++ b/opendm/objpacker/imagepacker/__init__.py @@ -0,0 +1 @@ +from .imagepacker import pack diff --git a/opendm/objpacker/imagepacker/imagepacker.py b/opendm/objpacker/imagepacker/imagepacker.py new file mode 100644 index 00000000..a0a589b2 --- /dev/null +++ b/opendm/objpacker/imagepacker/imagepacker.py @@ -0,0 +1,240 @@ +#! /usr/bin/python + +# The MIT License (MIT) + +# Copyright (c) 2015 Luke Gaynor + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import rasterio +import numpy as np +#from PIL import Image, ImageDraw +import math + +# Based off of the great writeup, demo and code at: +# http://codeincomplete.com/posts/2011/5/7/bin_packing/ + +class Block(): + """A rectangular block, to be packed""" + def __init__(self, w, h, data=None, padding=0): + self.w = w + self.h = h + self.x = None + self.y = None + self.fit = None + self.data = data + self.padding = padding # not implemented yet + + def __str__(self): + return "({x},{y}) ({w}x{h}): {data}".format( + x=self.x,y=self.y, w=self.w,h=self.h, data=self.data) + + +class _BlockNode(): + """A BlockPacker node""" + def __init__(self, x, y, w, h, used=False, right=None, down=None): + self.x = x + self.y = y + self.w = w + self.h = h + self.used = used + self.right = right + self.down = down + + def __repr__(self): + return "({x},{y}) ({w}x{h})".format(x=self.x,y=self.y,w=self.w,h=self.h) + + +class BlockPacker(): + """Packs blocks of varying sizes into a single, larger block""" + def __init__(self): + self.root = None + + def fit(self, blocks): + nblocks = len(blocks) + w = blocks[0].w# if nblocks > 0 else 0 + h = blocks[0].h# if nblocks > 0 else 0 + + self.root = _BlockNode(0,0, w,h) + + for block in blocks: + node = self.find_node(self.root, block.w, block.h) + if node: + # print("split") + node_fit = self.split_node(node, block.w, block.h) + block.x = node_fit.x + block.y = node_fit.y + else: + # print("grow") + node_fit = self.grow_node(block.w, block.h) + block.x = node_fit.x + block.y = node_fit.y + + def find_node(self, root, w, h): + if root.used: + # raise Exception("used") + node = self.find_node(root.right, w, h) + if node: + return node + return self.find_node(root.down, w, h) + elif w <= root.w and h <= root.h: + return root + else: + return None + + def split_node(self, node, w, h): + node.used = True + node.down = _BlockNode( + node.x, node.y + h, + node.w, node.h - h + ) + node.right = _BlockNode( + node.x + w, node.y, + node.w - w, h + ) + return node + + def grow_node(self, w, h): + can_grow_down = w <= self.root.w + can_grow_right = h <= self.root.h + + # try to keep the packing square + should_grow_right = can_grow_right and self.root.h >= (self.root.w + w) + should_grow_down = can_grow_down and self.root.w >= (self.root.h + h) + + if should_grow_right: + return self.grow_right(w, h) + elif should_grow_down: + return self.grow_down(w, h) + elif can_grow_right: + return self.grow_right(w, h) + elif can_grow_down: + return self.grow_down(w, h) + else: + raise Exception("no valid expansion avaliable!") + + def grow_right(self, w, h): + old_root = self.root + self.root = _BlockNode( + 0, 0, + old_root.w + w, old_root.h, + down=old_root, + right=_BlockNode(self.root.w, 0, w, self.root.h), + used=True + ) + + node = self.find_node(self.root, w, h) + if node: + return self.split_node(node, w, h) + else: + return None + + def grow_down(self, w, h): + old_root = self.root + self.root = _BlockNode( + 0, 0, + old_root.w, old_root.h + h, + down=_BlockNode(0, self.root.h, self.root.w, h), + right=old_root, + used=True + ) + + node = self.find_node(self.root, w, h) + if node: + return self.split_node(node, w, h) + else: + return None + + +def crop_by_extents(image, extent): + if min(extent.min_x,extent.min_y) < 0 or max(extent.max_x,extent.max_y) > 1: + print("\tWARNING! UV Coordinates lying outside of [0:1] space!") + + _, h, w = image.shape + minx = max(math.floor(extent.min_x*w), 0) + miny = max(math.floor(extent.min_y*h), 0) + maxx = min(math.ceil(extent.max_x*w), w) + maxy = min(math.ceil(extent.max_y*h), h) + + image = image[:, miny:maxy, minx:maxx] + delta_w = maxx - minx + delta_h = maxy - miny + + # offset from origin x, y, horizontal scale, vertical scale + changes = (minx, miny, delta_w / w, delta_h / h) + + return (image, changes) + +def pack(obj, background=(0,0,0,0), format="PNG", extents=None): + blocks = [] + image_name_map = {} + profile = None + + for mat in obj['materials']: + filename = obj['materials'][mat] + + with rasterio.open(filename, 'r') as f: + profile = f.profile + image = f.read() + + image = np.flip(image, axis=1) + + changes = None + if extents and extents[mat]: + image, changes = crop_by_extents(image, extents[mat]) + + image_name_map[filename] = image + _, h, w = image.shape + + # using filename so we can pass back UV info without storing it in image + blocks.append(Block(w, h, data=(filename, mat, changes))) + + # sort by width, descending (widest first) + blocks.sort(key=lambda block: -block.w) + + packer = BlockPacker() + packer.fit(blocks) + + # output_image = Image.new("RGBA", (packer.root.w, packer.root.h)) + output_image = np.zeros((profile['count'], packer.root.h, packer.root.w), dtype=profile['dtype']) + + uv_changes = {} + for block in blocks: + fname, mat, changes = block.data + image = image_name_map[fname] + _, im_h, im_w = image.shape + + uv_changes[mat] = { + "offset": ( + # should be in [0, 1] range + (block.x - (changes[0] if changes else 0))/output_image.shape[2], + # UV origin is bottom left, PIL assumes top left! + (block.y - (changes[1] if changes else 0))/output_image.shape[1] + ), + + "aspect": ( + ((1/changes[2]) if changes else 1) * (im_w/output_image.shape[2]), + ((1/changes[3]) if changes else 1) * (im_h/output_image.shape[1]) + ), + } + + output_image[:, block.y:block.y + im_h, block.x:block.x + im_w] = image + output_image = np.flip(output_image, axis=1) + + return output_image, uv_changes, profile diff --git a/opendm/objpacker/imagepacker/utils.py b/opendm/objpacker/imagepacker/utils.py new file mode 100644 index 00000000..072d27d4 --- /dev/null +++ b/opendm/objpacker/imagepacker/utils.py @@ -0,0 +1,55 @@ +#! /usr/bin/python + +# The MIT License (MIT) + +# Copyright (c) 2015 Luke Gaynor + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class AABB(): + def __init__(self, min_x=None, min_y=None, max_x=None, max_y=None): + self.min_x = min_x + self.min_y = min_y + self.max_x = max_x + self.max_y = max_y + + self.to_tile = False # TODO: remove? + + def add(self, x,y): + self.min_x = min(self.min_x, x) if self.min_x is not None else x + self.min_y = min(self.min_y, y) if self.min_y is not None else y + self.max_x = max(self.max_x, x) if self.max_x is not None else x + self.max_y = max(self.max_y, y) if self.max_y is not None else y + + def uv_wrap(self): + return (self.max_x - self.min_x, self.max_y - self.min_y) + + def tiling(self): + if self.min_x and self.max_x and self.min_y and self.max_y: + if self.min_x < 0 or self.min_y < 0 or self.max_x > 1 or self.max_y > 1: + return (self.max_x - self.min_x, self.max_y - self.min_y) + return None + + def __repr__(self): + return "({},{}) ({},{})".format( + self.min_x, + self.min_y, + self.max_x, + self.max_y + ) \ No newline at end of file diff --git a/opendm/objpacker/objpacker.py b/opendm/objpacker/objpacker.py new file mode 100644 index 00000000..533c310d --- /dev/null +++ b/opendm/objpacker/objpacker.py @@ -0,0 +1,228 @@ +import os +import rasterio +import warnings +import numpy as np +from .imagepacker.utils import AABB +from .imagepacker import pack + +warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) + +def load_obj(obj_path, _info=print): + if not os.path.isfile(obj_path): + raise IOError("Cannot open %s" % obj_path) + + obj_base_path = os.path.dirname(os.path.abspath(obj_path)) + obj = { + 'filename': os.path.basename(obj_path), + 'root_dir': os.path.dirname(os.path.abspath(obj_path)), + 'mtl_filenames': [], + 'materials': {}, + } + uvs = [] + + faces = {} + current_material = "_" + + with open(obj_path) as f: + _info("Loading %s" % obj_path) + + for line in f: + if line.startswith("mtllib "): + # Materials + mtl_file = "".join(line.split()[1:]).strip() + obj['materials'].update(load_mtl(mtl_file, obj_base_path, _info=_info)) + obj['mtl_filenames'].append(mtl_file) + # elif line.startswith("v "): + # # Vertices + # vertices.append(list(map(float, line.split()[1:4]))) + elif line.startswith("vt "): + # UVs + uvs.append(list(map(float, line.split()[1:3]))) + # elif line.startswith("vn "): + # normals.append(list(map(float, line.split()[1:4]))) + elif line.startswith("usemtl "): + mtl_name = "".join(line.split()[1:]).strip() + if not mtl_name in obj['materials']: + raise Exception("%s material is missing" % mtl_name) + + current_material = mtl_name + elif line.startswith("f "): + if current_material not in faces: + faces[current_material] = [] + + a,b,c = line.split()[1:] + at = int(a.split("/")[1]) + bt = int(b.split("/")[1]) + ct = int(c.split("/")[1]) + faces[current_material].append((at - 1, bt - 1, ct - 1)) + + obj['uvs'] = np.array(uvs, dtype=np.float32) + obj['faces'] = faces + + return obj + +def load_mtl(mtl_file, obj_base_path, _info=print): + mtl_file = os.path.join(obj_base_path, mtl_file) + + if not os.path.isfile(mtl_file): + raise IOError("Cannot open %s" % mtl_file) + + mats = {} + current_mtl = "" + + with open(mtl_file) as f: + for line in f: + if line.startswith("newmtl "): + current_mtl = "".join(line.split()[1:]).strip() + elif line.startswith("map_Kd ") and current_mtl: + map_kd_filename = "".join(line.split()[1:]).strip() + map_kd = os.path.join(obj_base_path, map_kd_filename) + if not os.path.isfile(map_kd): + raise IOError("Cannot open %s" % map_kd) + + mats[current_mtl] = map_kd + return mats + + +def write_obj_changes(obj_file, mtl_file, uv_changes, single_mat, output_dir, _info=print): + with open(obj_file) as f: + obj_lines = f.readlines() + + out_lines = [] + uv_lines = [] + current_material = None + + printed_mtllib = False + printed_usemtl = False + + _info("Transforming UV coordinates") + + for line_idx, line in enumerate(obj_lines): + if line.startswith("mtllib"): + if not printed_mtllib: + out_lines.append("mtllib %s\n" % mtl_file) + printed_mtllib = True + else: + out_lines.append("# \n") + elif line.startswith("usemtl"): + if not printed_usemtl: + out_lines.append("usemtl %s\n" % single_mat) + printed_usemtl = True + else: + out_lines.append("# \n") + current_material = line[7:].strip() + elif line.startswith("vt"): + uv_lines.append(line_idx) + out_lines.append(line) + elif line.startswith("f"): + for v in line[2:].split(): + parts = v.split("/") + if len(parts) >= 2 and parts[1]: + uv_idx = int(parts[1]) - 1 # uv indexes start from 1 + uv_line_idx = uv_lines[uv_idx] + uv_line = obj_lines[uv_line_idx][3:] + uv = [float(uv.strip()) for uv in uv_line.split()] + + if current_material and current_material in uv_changes: + changes = uv_changes[current_material] + uv[0] = uv[0] * changes["aspect"][0] + changes["offset"][0] + uv[1] = uv[1] * changes["aspect"][1] + changes["offset"][1] + out_lines[uv_line_idx] = "vt %s %s\n" % (uv[0], uv[1]) + out_lines.append(line) + else: + out_lines.append(line) + + out_file = os.path.join(output_dir, os.path.basename(obj_file)) + _info("Writing %s" % out_file) + + with open(out_file, 'w') as f: + f.writelines(out_lines) + +def write_output_tex(img, profile, path, _info=print): + _, h, w = img.shape + profile['width'] = w + profile['height'] = h + + _info("Writing %s (%sx%s pixels)" % (path, w, h)) + with rasterio.open(path, 'w', **profile) as dst: + for b in range(1, img.shape[0] + 1): + dst.write(img[b - 1], b) + + sidecar = path + '.aux.xml' + if os.path.isfile(sidecar): + os.unlink(sidecar) + +def write_output_mtl(src_mtl, mat_file, dst_mtl): + with open(src_mtl, 'r') as src: + lines = src.readlines() + + out = [] + found_map = False + single_mat = None + + for l in lines: + if l.startswith("newmtl"): + single_mat = "".join(l.split()[1:]).strip() + out.append(l) + elif l.startswith("map_Kd"): + out.append("map_Kd %s\n" % mat_file) + break + else: + out.append(l) + + with open(dst_mtl, 'w') as dst: + dst.write("".join(out)) + + if single_mat is None: + raise Exception("Could not find material name in file") + + return single_mat + +def obj_pack(obj_file, output_dir=None, _info=print): + if not output_dir: + output_dir = os.path.join(os.path.dirname(os.path.abspath(obj_file)), "packed") + + obj = load_obj(obj_file, _info=_info) + if not obj['mtl_filenames']: + raise Exception("No MTL files found, nothing to do") + + if os.path.abspath(obj_file) == os.path.abspath(os.path.join(output_dir, os.path.basename(obj_file))): + raise Exception("This will overwrite %s. Choose a different output directory" % obj_file) + + if len(obj['mtl_filenames']) <= 1 and len(obj['materials']) <= 1: + raise Exception("File already has a single material, nothing to do") + + # Compute AABB for UVs + _info("Computing texture bounds") + extents = {} + for material in obj['materials']: + bounds = AABB() + + faces = obj['faces'][material] + for f in faces: + for uv_idx in f: + uv = obj['uvs'][uv_idx] + bounds.add(uv[0], uv[1]) + + extents[material] = bounds + + _info("Binary packing...") + output_image, uv_changes, profile = pack(obj, extents=extents) + mtl_file = obj['mtl_filenames'][0] + mat_file = os.path.basename(obj['materials'][next(iter(obj['materials']))]) + + if not os.path.isdir(output_dir): + os.mkdir(output_dir) + + write_output_tex(output_image, profile, os.path.join(output_dir, mat_file), _info=_info) + single_mat = write_output_mtl(os.path.join(obj['root_dir'], mtl_file), mat_file, os.path.join(output_dir, mtl_file)) + write_obj_changes(obj_file, mtl_file, uv_changes, single_mat, output_dir, _info=_info) + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser(description="Packs textured .OBJ Wavefront files into a single materials") + parser.add_argument("obj", help="Path to the .OBJ file") + parser.add_argument("-o","--output-dir", help="Output directory") + args = parser.parse_args() + + obj_pack(args.obj, args.output_dir) \ No newline at end of file diff --git a/opendm/system.py b/opendm/system.py index adeeb3f5..1d0b0ab2 100644 --- a/opendm/system.py +++ b/opendm/system.py @@ -7,6 +7,7 @@ import subprocess import string import signal import io +import shutil from collections import deque from opendm import context @@ -154,3 +155,19 @@ def link_file(src, dst): os.link(src, dst) else: os.symlink(os.path.relpath(os.path.abspath(src), os.path.dirname(os.path.abspath(dst))), dst) + +def move_files(src, dst): + if not os.path.isdir(dst): + raise IOError("Not a directory: %s" % dst) + + for f in os.listdir(src): + if os.path.isfile(os.path.join(src, f)): + shutil.move(os.path.join(src, f), dst) + +def delete_files(folder): + if not os.path.isdir(folder): + return + + for f in os.listdir(folder): + if os.path.isfile(os.path.join(folder, f)): + os.unlink(os.path.join(folder, f)) \ No newline at end of file diff --git a/stages/mvstex.py b/stages/mvstex.py index 3f557627..46b6820a 100644 --- a/stages/mvstex.py +++ b/stages/mvstex.py @@ -7,6 +7,7 @@ from opendm import context from opendm import types from opendm.multispectral import get_primary_band_name from opendm.photo import find_largest_photo_dim +from opendm.objpacker import obj_pack class ODMMvsTexStage(types.ODM_Stage): def process(self, args, outputs): @@ -129,6 +130,26 @@ class ODMMvsTexStage(types.ODM_Stage): '{labelingFile} ' '{maxTextureSize} '.format(**kwargs)) + # Single material? + if r['primary'] and args.texturing_single_material: + log.ODM_INFO("Packing to single material") + + packed_dir = os.path.join(r['out_dir'], 'packed') + if io.dir_exists(packed_dir): + log.ODM_INFO("Removing old packed directory {}".format(packed_dir)) + shutil.rmtree(packed_dir) + + try: + obj_pack(os.path.join(r['out_dir'], tree.odm_textured_model_obj), packed_dir, _info=log.ODM_INFO) + + # Move packed/* into texturing folder + system.delete_files(r['out_dir']) + system.move_files(packed_dir, r['out_dir']) + if os.path.isdir(packed_dir): + os.rmdir(packed_dir) + except Exception as e: + log.ODM_WARNING(str(e)) + # Backward compatibility: copy odm_textured_model_geo.mtl to odm_textured_model.mtl # for certain older WebODM clients which expect a odm_textured_model.mtl # to be present for visualization @@ -137,7 +158,7 @@ class ODMMvsTexStage(types.ODM_Stage): if io.file_exists(geo_mtl): nongeo_mtl = os.path.join(r['out_dir'], 'odm_textured_model.mtl') shutil.copy(geo_mtl, nongeo_mtl) - + progress += progress_per_run self.update_progress(progress) else: From 15131be6cc88daf277b4f63bf95730e70b1982e2 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 13:49:07 -0500 Subject: [PATCH 14/21] Don't accidentally delete texture label file --- opendm/system.py | 5 +++-- stages/mvstex.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/opendm/system.py b/opendm/system.py index 1d0b0ab2..ade8ccd0 100644 --- a/opendm/system.py +++ b/opendm/system.py @@ -164,10 +164,11 @@ def move_files(src, dst): if os.path.isfile(os.path.join(src, f)): shutil.move(os.path.join(src, f), dst) -def delete_files(folder): +def delete_files(folder, exclude=()): if not os.path.isdir(folder): return for f in os.listdir(folder): if os.path.isfile(os.path.join(folder, f)): - os.unlink(os.path.join(folder, f)) \ No newline at end of file + if not exclude or not f.endswith(exclude): + os.unlink(os.path.join(folder, f)) \ No newline at end of file diff --git a/stages/mvstex.py b/stages/mvstex.py index 46b6820a..0e07151f 100644 --- a/stages/mvstex.py +++ b/stages/mvstex.py @@ -143,7 +143,7 @@ class ODMMvsTexStage(types.ODM_Stage): obj_pack(os.path.join(r['out_dir'], tree.odm_textured_model_obj), packed_dir, _info=log.ODM_INFO) # Move packed/* into texturing folder - system.delete_files(r['out_dir']) + system.delete_files(r['out_dir'], (".vec", )) system.move_files(packed_dir, r['out_dir']) if os.path.isdir(packed_dir): os.rmdir(packed_dir) From 2b2875bec6f4ba358530e293caec80d011b71ea9 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 15:48:37 -0500 Subject: [PATCH 15/21] Fix width/height --- opendm/objpacker/objpacker.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/opendm/objpacker/objpacker.py b/opendm/objpacker/objpacker.py index 533c310d..75ddf863 100644 --- a/opendm/objpacker/objpacker.py +++ b/opendm/objpacker/objpacker.py @@ -2,8 +2,12 @@ import os import rasterio import warnings import numpy as np -from .imagepacker.utils import AABB -from .imagepacker import pack +try: + from .imagepacker.utils import AABB + from .imagepacker import pack +except ImportError: + from imagepacker.utils import AABB + from imagepacker import pack warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) @@ -139,10 +143,13 @@ def write_obj_changes(obj_file, mtl_file, uv_changes, single_mat, output_dir, _i f.writelines(out_lines) def write_output_tex(img, profile, path, _info=print): - _, h, w = img.shape + _, w, h = img.shape profile['width'] = w profile['height'] = h + if 'tiled' in profile: + profile['tiled'] = False + _info("Writing %s (%sx%s pixels)" % (path, w, h)) with rasterio.open(path, 'w', **profile) as dst: for b in range(1, img.shape[0] + 1): From 9f17e8451abac64a283d87f0f443d1388cebdea2 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 15:57:59 -0500 Subject: [PATCH 16/21] Only pack textures where needed --- stages/mvstex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stages/mvstex.py b/stages/mvstex.py index 0e07151f..bd0d3acc 100644 --- a/stages/mvstex.py +++ b/stages/mvstex.py @@ -131,7 +131,7 @@ class ODMMvsTexStage(types.ODM_Stage): '{maxTextureSize} '.format(**kwargs)) # Single material? - if r['primary'] and args.texturing_single_material: + if args.texturing_single_material and r['primary'] and (not r['nadir'] or args.skip_3dmodel): log.ODM_INFO("Packing to single material") packed_dir = os.path.join(r['out_dir'], 'packed') From 64544f3c4154520004800674494b3acd6a3cff00 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 16:00:15 -0500 Subject: [PATCH 17/21] Cleanup --- opendm/objpacker/imagepacker/imagepacker.py | 3 +-- opendm/objpacker/imagepacker/utils.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/opendm/objpacker/imagepacker/imagepacker.py b/opendm/objpacker/imagepacker/imagepacker.py index a0a589b2..9031d28d 100644 --- a/opendm/objpacker/imagepacker/imagepacker.py +++ b/opendm/objpacker/imagepacker/imagepacker.py @@ -24,7 +24,6 @@ import rasterio import numpy as np -#from PIL import Image, ImageDraw import math # Based off of the great writeup, demo and code at: @@ -204,7 +203,7 @@ def pack(obj, background=(0,0,0,0), format="PNG", extents=None): # using filename so we can pass back UV info without storing it in image blocks.append(Block(w, h, data=(filename, mat, changes))) - +use_3dmesh # sort by width, descending (widest first) blocks.sort(key=lambda block: -block.w) diff --git a/opendm/objpacker/imagepacker/utils.py b/opendm/objpacker/imagepacker/utils.py index 072d27d4..8124648c 100644 --- a/opendm/objpacker/imagepacker/utils.py +++ b/opendm/objpacker/imagepacker/utils.py @@ -29,8 +29,6 @@ class AABB(): self.max_x = max_x self.max_y = max_y - self.to_tile = False # TODO: remove? - def add(self, x,y): self.min_x = min(self.min_x, x) if self.min_x is not None else x self.min_y = min(self.min_y, y) if self.min_y is not None else y From 280ba2c50b1d04d2512ec93f6fc5bce8792b4247 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 11 Jan 2023 16:01:48 -0500 Subject: [PATCH 18/21] Remove typo --- opendm/objpacker/imagepacker/imagepacker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/objpacker/imagepacker/imagepacker.py b/opendm/objpacker/imagepacker/imagepacker.py index 9031d28d..2090d73f 100644 --- a/opendm/objpacker/imagepacker/imagepacker.py +++ b/opendm/objpacker/imagepacker/imagepacker.py @@ -203,7 +203,7 @@ def pack(obj, background=(0,0,0,0), format="PNG", extents=None): # using filename so we can pass back UV info without storing it in image blocks.append(Block(w, h, data=(filename, mat, changes))) -use_3dmesh + # sort by width, descending (widest first) blocks.sort(key=lambda block: -block.w) From 3680b54d6429929d4ce2aede48cc24f1bde6bb5c Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 12 Jan 2023 11:41:16 -0500 Subject: [PATCH 19/21] Tweak file glob pattern in Windows build --- .github/workflows/publish-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-windows.yml b/.github/workflows/publish-windows.yml index b9737f55..7c0cacd0 100644 --- a/.github/workflows/publish-windows.yml +++ b/.github/workflows/publish-windows.yml @@ -53,7 +53,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: dist\*.exe + file: dist/*.exe file_glob: true tag: ${{ github.ref }} overwrite: true From 7c4da76280738d8727685253c1adff6dda7f9660 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 12 Jan 2023 12:45:20 -0500 Subject: [PATCH 20/21] Include python3.8 folder in setup --- innosetup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/innosetup.iss b/innosetup.iss index 945e1fee..ed877007 100644 --- a/innosetup.iss +++ b/innosetup.iss @@ -43,6 +43,7 @@ Source: "licenses\*"; DestDir: "{app}\licenses"; Flags: ignoreversion recursesub Source: "opendm\*"; DestDir: "{app}\opendm"; Excludes: "__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "stages\*"; DestDir: "{app}\stages"; Excludes: "__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "SuperBuild\install\bin\*"; DestDir: "{app}\SuperBuild\install\bin"; Excludes: "__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "SuperBuild\install\lib\python3.8\*"; DestDir: "{app}\SuperBuild\install\lib\python3.8\*"; Excludes: "__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "venv\*"; DestDir: "{app}\venv"; Excludes: "__pycache__,pyvenv.cfg"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "python38\*"; DestDir: "{app}\python38"; Excludes: "__pycache__"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "console.bat"; DestDir: "{app}"; Flags: ignoreversion From 305ec0730f8bf18dd12a0a7ee5e4e86b22cd0f3c Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 16 Jan 2023 21:12:14 -0500 Subject: [PATCH 21/21] Fix weakly_canonical: The device is not ready --- win32env.bat | 1 + 1 file changed, 1 insertion(+) diff --git a/win32env.bat b/win32env.bat index d00e0192..36c35d17 100644 --- a/win32env.bat +++ b/win32env.bat @@ -14,6 +14,7 @@ set GDAL_DATA=%GDALBASE%\data\gdal set GDAL_DRIVER_PATH=%GDALBASE%\gdalplugins set OSFMBASE=%ODMBASE%SuperBuild\install\bin\opensfm\bin set SBBIN=%ODMBASE%SuperBuild\install\bin +set PDAL_DRIVER_PATH=%ODMBASE%SuperBuild\install\bin set PATH=%GDALBASE%;%SBBIN%;%OSFMBASE% set PROJ_LIB=%GDALBASE%\data\proj