diff --git a/contrib/mergepreview/README.md b/contrib/mergepreview/README.md new file mode 100644 index 00000000..6255750d --- /dev/null +++ b/contrib/mergepreview/README.md @@ -0,0 +1,29 @@ +# Merge Preview + +Quickly projects drone images on a map by using georeferencing, camera angles and a global DTM. The images are then merged using ODM's split-merge algorithms. + +Quality is obviously not good, works only for nadir-only images and requires the images to have gimbal/camera angle information (not all drones provide this information). + +Usage: + +``` +# Install DDB (required for geoprojection) + +curl -fsSL https://get.dronedb.app -o get-ddb.sh +sh get-ddb.sh + +# Run + +python3 mergepreview.py -i images/*.JPG --size 25% +``` + +## Example + +![screen](https://user-images.githubusercontent.com/1951843/134249725-e178489a-e271-4244-abed-e624cd510b88.png) + + +[Sheffield Park](https://community.opendronemap.org/t/sheffield-park-1/58) images processed with this script. + +## Disclaimer + +This script is highly experimental. We welcome contributions to improve it. diff --git a/contrib/mergepreview/mergepreview.py b/contrib/mergepreview/mergepreview.py new file mode 100644 index 00000000..1e05e98c --- /dev/null +++ b/contrib/mergepreview/mergepreview.py @@ -0,0 +1,126 @@ +import argparse +import sys +sys.path.append("../../") + +import os +from opendm import orthophoto +from opendm.cutline import compute_cutline +import glob +from opendm.system import run +from opendm import log +import shutil + + +parser = argparse.ArgumentParser(description='Quick Merge Preview') +parser.add_argument('input', + metavar='', + nargs='+', + help='Path to input images or image folder') +parser.add_argument('--size', '-s', + metavar='', + type=str, + help='Size in percentage terms', + default='25%') +parser.add_argument('--force', '-f', + action='store_true', + default=False, + help="Force remove existing directories") + +args = parser.parse_args() + +try: + log.ODM_INFO("Checking for DDB...") + run("ddb --version") +except: + log.ODM_ERROR("ddb is not installed. Install it first: https://docs.dronedb.app") + +if len(args.input) == 1 and os.path.isdir(args.input[0]): + input_images = [] + for ext in ["JPG", "JPEG", "TIF", "tiff", "tif", "TIFF"]: + input_images += glob.glob(os.path.join(args.input[0], "*.%s" % ext)) +else: + input_images = args.input + +log.ODM_INFO("Processing %s images" % len(input_images)) + +if len(input_images) == 0: + log.ODM_ERROR("No images") + exit(1) + +cwd_path = os.path.dirname(input_images[0]) +tmp_path = os.path.join(cwd_path, "tmp") +if os.path.isdir(tmp_path): + if args.force: + log.ODM_INFO("Removing previous directory %s" % tmp_path) + shutil.rmtree(tmp_path) + else: + log.ODM_ERROR("%s exists. Pass --force to override." % tmp_path) + exit(1) + +os.makedirs(tmp_path) + +for f in input_images: + name, _ = os.path.splitext(os.path.basename(f)) + geojson = os.path.join(tmp_path, "%s.geojson" % name) + gpkg = os.path.join(tmp_path, "%s.gpkg" % name) + + run("ddb geoproj \"%s\" \"%s\" -s \"%s\"" % (tmp_path, f, args.size)) + + # Bounds (GPKG) + run("ddb info --format geojson --geometry polygon \"%s\" > \"%s\"" % (f, geojson)) + run("ogr2ogr \"%s\" \"%s\"" % (gpkg, geojson)) + +log.ODM_INFO("Computing cutlines") + +projected_images = glob.glob(os.path.join(tmp_path, "*.tif")) +all_orthos_and_ortho_cuts = [] + +for f in projected_images: + name, _ = os.path.splitext(os.path.basename(f)) + cutline_file = os.path.join(tmp_path, "%s_cutline.gpkg" % name) + bounds_file_path = os.path.join(tmp_path, "%s.gpkg" % name) + + compute_cutline(f, + bounds_file_path, + cutline_file, + 4, + scale=1) + + cut_raster = os.path.join(tmp_path, "%s_cut.tif" % name) + orthophoto.compute_mask_raster(f, cutline_file, + cut_raster, + blend_distance=20, only_max_coords_feature=True) + + feathered_raster = os.path.join(tmp_path, "%s_feathered.tif" % name) + + orthophoto.feather_raster(f, feathered_raster, + blend_distance=20 + ) + + all_orthos_and_ortho_cuts.append([feathered_raster, cut_raster]) + +log.ODM_INFO("Merging...") + +if len(all_orthos_and_ortho_cuts) > 1: + # TODO: histogram matching via rasterio + # currently parts have different color tones + output_file = os.path.join(cwd_path, 'mergepreview.tif') + + if os.path.isfile(output_file): + os.remove(output_file) + + orthophoto.merge(all_orthos_and_ortho_cuts, output_file, { + 'TILED': 'YES', + 'COMPRESS': 'LZW', + 'PREDICTOR': '2', + 'BIGTIFF': 'IF_SAFER', + 'BLOCKXSIZE': 512, + 'BLOCKYSIZE': 512 + }) + + + log.ODM_INFO("Wrote %s" % output_file) + shutil.rmtree(tmp_path) +else: + log.ODM_ERROR("Error: no orthos found to merge") + exit(1) \ No newline at end of file diff --git a/opendm/cutline.py b/opendm/cutline.py index 3df75b8d..6c6153ba 100644 --- a/opendm/cutline.py +++ b/opendm/cutline.py @@ -145,9 +145,12 @@ def compute_cutline(orthophoto_file, crop_area_file, destination, max_concurrenc if len(polygons) == 0: log.ODM_WARNING("No polygons, cannot compute cutline") return - + log.ODM_INFO("Merging polygons") cutline_polygons = unary_union(polygons) + if not hasattr(cutline_polygons, '__getitem__'): + cutline_polygons = [cutline_polygons] + largest_cutline = cutline_polygons[0] max_area = largest_cutline.area for p in cutline_polygons: