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 is a visual survey approximation, not a georeferenced orthomosaic. Without GPS/EXIF or camera poses from the previous example, the script cannot know the true ground positions, so the grid is an informed montage rather than a mathematically correct map.
Usage:
pip install pyodm
docker run -p 3000:3000 opendronemap/nodeodm --test
Code:
#! /usr/bin/python
from pathlib import Path
import cv2
import numpy as np
import math
import shutil
import sys
def list_images(folder):
exts = {".jpg", ".jpeg", ".JPG", ".JPEG"}
files = [p for p in Path(folder).iterdir() if p.suffix in exts]
return sorted(files, key=lambda p: p.name)
def make_detector():
try:
return cv2.SIFT_create()
except Exception:
return cv2.ORB_create(4000)
def detect(detector, img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
return detector.detectAndCompute(gray, None)
def match_score(des1, des2, use_sift=True):
if des1 is None or des2 is None:
return 0
if use_sift:
matcher = cv2.FlannBasedMatcher(dict(algorithm=1, trees=5), dict(checks=40))
matches = matcher.knnMatch(des1, des2, k=2)
else:
matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
matches = matcher.knnMatch(des1, des2, k=2)
good = 0
for pair in matches:
if len(pair) < 2:
continue
m, n = pair
if m.distance < 0.75 * n.distance:
good += 1
return good
def overlap_score(img1, img2, detector):
kp1, des1 = detect(detector, img1)
kp2, des2 = detect(detector, img2)
use_sift = hasattr(cv2, "SIFT_create") and detector.__class__.__name__.lower().find("sift") >= 0
return match_score(des1, des2, use_sift=use_sift)
def choose_grid(n, aspect=1.0):
best = None
for rows in range(1, n + 1):
cols = math.ceil(n / rows)
score = abs((cols / rows) - aspect)
waste = rows * cols - n
cand = (score, waste, abs(rows - cols), rows, cols)
if best is None or cand < best:
best = cand
return best[3], best[4]
def fit_tile(img, tile_w, tile_h, pad=8, bg=(255, 255, 255)):
h, w = img.shape[:2]
scale = min((tile_w - 2 * pad) / w, (tile_h - 2 * pad) / h)
nw, nh = max(1, int(round(w * scale))), max(1, int(round(h * scale)))
resized = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA)
canvas = np.full((tile_h, tile_w, 3), bg, dtype=np.uint8)
x = (tile_w - nw) // 2
y = (tile_h - nh) // 2
canvas[y:y+nh, x:x+nw] = resized
return canvas
def build_montage(folder, max_tiles=30, tile_w=360, tile_h=240, pad=8):
folder = Path(folder).resolve()
files = list_images(folder)
if not files:
raise ValueError("No JPG images found.")
imgs = []
for p in files:
im = cv2.imread(str(p))
if im is not None:
imgs.append((p, im))
if not imgs:
raise ValueError("Could not read any images.")
detector = make_detector()
n = min(len(imgs), max_tiles)
used = imgs[:n]
scores = np.zeros((n, n), dtype=int)
for i in range(n):
for j in range(i + 1, n):
s = overlap_score(used[i][1], used[j][1], detector)
scores[i, j] = scores[j, i] = s
remaining = set(range(1, n))
order = [0]
while remaining:
last = order[-1]
nxt = max(remaining, key=lambda j: (scores[last, j], -j))
order.append(nxt)
remaining.remove(nxt)
rows, cols = choose_grid(n, aspect=1.0)
while len(order) < rows * cols:
order.append(None)
montage = np.full((rows * tile_h, cols * tile_w, 3), 255, dtype=np.uint8)
for idx in range(rows * cols):
r = idx // cols
c = idx % cols
x0, y0 = c * tile_w, r * tile_h
cv2.rectangle(montage, (x0, y0), (x0 + tile_w - 1, y0 + tile_h - 1), (230, 230, 230), 1)
item_idx = order[idx]
if item_idx is None:
continue
p, img = used[item_idx]
tile = fit_tile(img, tile_w, tile_h, pad=pad)
montage[y0:y0 + tile_h, x0:x0 + tile_w] = tile
label = p.stem[:34]
cv2.putText(
montage,
label,
(x0 + 10, y0 + tile_h - 12),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(20, 20, 20),
1,
cv2.LINE_AA,
)
out_dir = folder / "montage_output"
out_dir.mkdir(exist_ok=True)
out_path = out_dir / f"{folder.name}_grid_montage.png"
cv2.imwrite(str(out_path), montage)
same_folder_copy = folder / out_path.name
shutil.copy2(out_path, same_folder_copy)
return str(same_folder_copy)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python grid_montage.py /path/to/folder")
sys.exit(1)
print(build_montage(sys.argv[1]))
No comments:
Post a Comment