Wednesday, May 13, 2026

 Drone Survey Area reconstitution:

Problem statement:

Aerial drone images extracted from a drone video are sufficient to reconstitute the survey area with image selection to create a mosaic that fully covers the survey area. This method does away with the knowledge of flight path of the drone. Write a python implementation that places selections from the input on the tiles in a grid to increase the likelihood of match with the overall survey area.

Solution:

The following implementation assumes that the images have GPS/EXIF metadata and leverages OpenDroneMap to create a mosaic.

Usage:

pip install pyodm

docker run -p 3000:3000 opendronemap/nodeodm --test

Code:

#! /usr/bin/python

from pathlib import Path

import shutil

import sys

from pyodm import Node, exceptions

def find_images(input_folder: Path):

    exts = {".jpg", ".jpeg", ".JPG", ".JPEG"}

    images = sorted([str(p) for p in input_folder.iterdir() if p.suffix in exts])

    return images

def pick_orthomosaic_file(results_dir: Path):

    candidates = []

    for ext in ("*.tif", "*.tiff", "*.png", "*.jpg", "*.jpeg"):

        candidates.extend(results_dir.rglob(ext))

    preferred = []

    for p in candidates:

        s = str(p).lower()

        if "orthophoto" in s or "orthomosaic" in s or "odm_orthophoto" in s:

            preferred.append(p)

    if preferred:

        preferred.sort(key=lambda p: (0 if p.suffix.lower() in [".tif", ".tiff"] else 1, len(str(p))))

        return preferred[0]

    if candidates:

        candidates.sort(key=lambda p: (0 if p.suffix.lower() in [".tif", ".tiff"] else 1, len(str(p))))

        return candidates[0]

    return None

def reconstruct_mosaic(input_folder: str, node_url="localhost", node_port=3000):

    input_path = Path(input_folder).resolve()

    if not input_path.exists() or not input_path.is_dir():

        raise FileNotFoundError(f"Folder not found: {input_path}")

    images = find_images(input_path)

    if len(images) < 3:

        raise ValueError("Need at least 3 overlapping drone images for a meaningful mosaic.")

    output_dir = input_path / "odm_results"

    output_dir.mkdir(parents=True, exist_ok=True)

    node = Node(node_url, port=node_port)

    print(node.info())

    options = {

        "auto-boundary": True,

        "crop": 0,

        "fast-orthophoto": True,

        "skip-post-processing": False,

        "orthophoto-resolution": 5,

        "use-exif": True,

        "optimize-disk-space": True,

    }

    try:

        task = node.create_task(images, options)

        print("Task created:", task.info().task_id)

        task.wait_for_completion()

        task.download_assets(str(output_dir))

        orthomosaic = pick_orthomosaic_file(output_dir)

        if orthomosaic is None:

            raise FileNotFoundError("No orthomosaic file was produced by ODM.")

        final_name = input_path / f"{input_path.name}_orthomosaic{orthomosaic.suffix.lower()}"

        shutil.copy2(orthomosaic, final_name)

        print(f"Orthomosaic saved to: {final_name}")

        return str(final_name)

    except exceptions.NodeConnectionError as e:

        raise RuntimeError(f"Cannot connect to NodeODM at {node_url}:{node_port}. Error: {e}")

    except exceptions.TaskFailedError as e:

        raise RuntimeError(f"ODM task failed: {e}")

if __name__ == "__main__":

    if len(sys.argv) < 2:

        print("Usage: python odm_mosaic.py /path/to/drone_images")

        sys.exit(1)

    reconstruct_mosaic(sys.argv[1])

References: compare to previous article: 

No comments:

Post a Comment