kopia lustrzana https://github.com/OpenDroneMap/ODM
Refactored band detection to use XMP tags instead of filenames
Former-commit-id: 30f1570e05
pull/1161/head
rodzic
a5522cabef
commit
494a441ef9
|
@ -314,7 +314,8 @@ void OdmOrthoPhoto::saveTIFF(const std::string &filename, GDALDataType dataType)
|
|||
exit(1);
|
||||
}
|
||||
char **papszOptions = NULL;
|
||||
GDALDatasetH hDstDS = GDALCreate( hDriver, filename.c_str(), width, height, static_cast<int>(bands.size()), dataType, papszOptions );
|
||||
GDALDatasetH hDstDS = GDALCreate( hDriver, filename.c_str(), width, height,
|
||||
static_cast<int>(bands.size()), dataType, papszOptions );
|
||||
GDALRasterBandH hBand;
|
||||
|
||||
for (size_t i = 0; i < bands.size(); i++){
|
||||
|
@ -328,10 +329,6 @@ void OdmOrthoPhoto::saveTIFF(const std::string &filename, GDALDataType dataType)
|
|||
std::cerr << "Cannot write TIFF to " << filename << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
// for (int j = 0; j < height; j++){
|
||||
// GDALRasterIO( hBand, GF_Write, 0, j, width, 1,
|
||||
// bands[i][j], width, 1, dataType, 0, 0 );
|
||||
// }
|
||||
}
|
||||
GDALClose( hDstDS );
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class OSFMContext:
|
|||
exit(1)
|
||||
|
||||
|
||||
def setup(self, args, images_path, photos, gcp_path=None, append_config = [], rerun=False):
|
||||
def setup(self, args, images_path, photos, reconstruction, append_config = [], rerun=False):
|
||||
"""
|
||||
Setup a OpenSfM project
|
||||
"""
|
||||
|
@ -91,13 +91,22 @@ class OSFMContext:
|
|||
except Exception as e:
|
||||
log.ODM_WARNING("Cannot set camera_models_overrides.json: %s" % str(e))
|
||||
|
||||
use_bow = False
|
||||
|
||||
matcher_neighbors = args.matcher_neighbors
|
||||
if matcher_neighbors != 0 and reconstruction.multi_camera is not None:
|
||||
matcher_neighbors *= len(reconstruction.multi_camera)
|
||||
log.ODM_INFO("Increasing matcher neighbors to %s to accomodate multi-camera setup" % matcher_neighbors)
|
||||
log.ODM_INFO("Multi-camera setup, using BOW matching")
|
||||
use_bow = True
|
||||
|
||||
# create config file for OpenSfM
|
||||
config = [
|
||||
"use_exif_size: no",
|
||||
"feature_process_size: %s" % args.resize_to,
|
||||
"feature_min_frames: %s" % args.min_num_features,
|
||||
"processes: %s" % args.max_concurrency,
|
||||
"matching_gps_neighbors: %s" % args.matcher_neighbors,
|
||||
"matching_gps_neighbors: %s" % matcher_neighbors,
|
||||
"matching_gps_distance: %s" % args.matcher_distance,
|
||||
"depthmap_method: %s" % args.opensfm_depthmap_method,
|
||||
"depthmap_resolution: %s" % args.depthmap_resolution,
|
||||
|
@ -114,12 +123,16 @@ class OSFMContext:
|
|||
|
||||
if not has_gps:
|
||||
log.ODM_INFO("No GPS information, using BOW matching")
|
||||
use_bow = True
|
||||
|
||||
if use_bow:
|
||||
config.append("matcher_type: WORDS")
|
||||
|
||||
if has_alt:
|
||||
log.ODM_INFO("Altitude data detected, enabling it for GPS alignment")
|
||||
config.append("use_altitude_tag: yes")
|
||||
|
||||
gcp_path = reconstruction.gcp.gcp_path
|
||||
if has_alt or gcp_path:
|
||||
config.append("align_method: auto")
|
||||
else:
|
||||
|
|
|
@ -3,11 +3,11 @@ import exifread
|
|||
import re
|
||||
import os
|
||||
from fractions import Fraction
|
||||
from opensfm.exif import sensor_string
|
||||
from opendm import get_image_size
|
||||
from opendm import location
|
||||
from opendm.gcp import GCPFile
|
||||
from pyproj import CRS
|
||||
import xmltodict as x2d
|
||||
|
||||
import log
|
||||
import io
|
||||
|
@ -28,10 +28,11 @@ class ODM_Photo:
|
|||
# other attributes
|
||||
self.camera_make = ''
|
||||
self.camera_model = ''
|
||||
self.make_model = ''
|
||||
self.latitude = None
|
||||
self.longitude = None
|
||||
self.altitude = None
|
||||
self.band_name = 'RGB'
|
||||
|
||||
# parse values from metadata
|
||||
self.parse_exif_values(path_file)
|
||||
|
||||
|
@ -40,8 +41,9 @@ class ODM_Photo:
|
|||
|
||||
|
||||
def __str__(self):
|
||||
return '{} | camera: {} | dimensions: {} x {} | lat: {} | lon: {} | alt: {}'.format(
|
||||
self.filename, self.make_model, self.width, self.height, self.latitude, self.longitude, self.altitude)
|
||||
return '{} | camera: {} {} | dimensions: {} x {} | lat: {} | lon: {} | alt: {} | band: {}'.format(
|
||||
self.filename, self.camera_make, self.camera_model, self.width, self.height,
|
||||
self.latitude, self.longitude, self.altitude, self.band_name)
|
||||
|
||||
def parse_exif_values(self, _path_file):
|
||||
# Disable exifread log
|
||||
|
@ -66,10 +68,36 @@ class ODM_Photo:
|
|||
except IndexError as e:
|
||||
log.ODM_WARNING("Cannot read EXIF tags for %s: %s" % (_path_file, e.message))
|
||||
|
||||
if self.camera_make and self.camera_model:
|
||||
self.make_model = sensor_string(self.camera_make, self.camera_model)
|
||||
# Extract XMP tags
|
||||
f.seek(0)
|
||||
xmp = self.get_xmp(f)
|
||||
|
||||
# Find band name (if available)
|
||||
for tags in xmp:
|
||||
if 'Camera:BandName' in tags:
|
||||
self.band_name = str(tags['Camera:BandName']).replace(" ", "")
|
||||
break
|
||||
|
||||
self.width, self.height = get_image_size.get_image_size(_path_file)
|
||||
|
||||
# From https://github.com/mapillary/OpenSfM/blob/master/opensfm/exif.py
|
||||
def get_xmp(self, file):
|
||||
img_str = str(file.read())
|
||||
xmp_start = img_str.find('<x:xmpmeta')
|
||||
xmp_end = img_str.find('</x:xmpmeta')
|
||||
|
||||
if xmp_start < xmp_end:
|
||||
xmp_str = img_str[xmp_start:xmp_end + 12]
|
||||
xdict = x2d.parse(xmp_str)
|
||||
xdict = xdict.get('x:xmpmeta', {})
|
||||
xdict = xdict.get('rdf:RDF', {})
|
||||
xdict = xdict.get('rdf:Description', {})
|
||||
if isinstance(xdict, list):
|
||||
return xdict
|
||||
else:
|
||||
return [xdict]
|
||||
else:
|
||||
return []
|
||||
|
||||
def dms_to_decimal(self, dms, sign):
|
||||
"""Converts dms coords to decimal degrees"""
|
||||
|
@ -100,45 +128,23 @@ class ODM_Reconstruction(object):
|
|||
Looks at the reconstruction photos and determines if this
|
||||
is a single or multi-camera setup.
|
||||
"""
|
||||
supported_ext_re = r"\.(" + "|".join([e[1:] for e in context.supported_extensions]) + ")$"
|
||||
|
||||
# Match filename_1.tif, filename_2.tif, filename_3.tif, ...
|
||||
#
|
||||
multi_camera_patterns = {
|
||||
'MicaSense RedEdge-M': r'^IMG_\d+_(?P<band>\d{1})',
|
||||
'Parrot Sequoia': r'^IMG_\d+_\d+_\d+_(?P<band>[A-Z]{3})',
|
||||
}
|
||||
|
||||
for cam, regex in multi_camera_patterns.items():
|
||||
pattern = re.compile(regex + supported_ext_re, re.IGNORECASE)
|
||||
mc = {}
|
||||
|
||||
for p in self.photos:
|
||||
matches = re.match(pattern, p.filename)
|
||||
if matches:
|
||||
band = matches.group("band")
|
||||
if not band in mc:
|
||||
mc[band] = []
|
||||
mc[band].append(p)
|
||||
mc = {}
|
||||
for p in self.photos:
|
||||
if not p.band_name in mc:
|
||||
mc[p.band_name] = []
|
||||
mc[p.band_name].append(p)
|
||||
|
||||
# We support between 2 and 6 bands
|
||||
# If we matched more or less bands, we probably just
|
||||
# found filename patterns that do not match a multi-camera setup
|
||||
bands_count = len(mc)
|
||||
if bands_count >= 2 and bands_count <= 6:
|
||||
|
||||
# Validate that all bands have the same number of images,
|
||||
# otherwise this is not a multi-camera setup
|
||||
img_per_band = len(mc[band])
|
||||
valid = True
|
||||
for band in mc:
|
||||
if len(mc[band]) != img_per_band:
|
||||
log.ODM_WARNING("This might be a multi-camera setup, but band \"%s\" (identified from \"%s\") has only %s images (instead of %s), perhaps images are missing or are corrupted." % (band, mc[band][0].filename, len(mc[band]), img_per_band))
|
||||
valid = False
|
||||
break
|
||||
|
||||
if valid:
|
||||
return mc
|
||||
bands_count = len(mc)
|
||||
if bands_count >= 2 and bands_count <= 8:
|
||||
# Validate that all bands have the same number of images,
|
||||
# otherwise this is not a multi-camera setup
|
||||
img_per_band = len(mc[p.band_name])
|
||||
for band in mc:
|
||||
if len(mc[band]) != img_per_band:
|
||||
log.ODM_ERROR("Multi-camera setup detected, but band \"%s\" (identified from \"%s\") has only %s images (instead of %s), perhaps images are missing or are corrupted. Please include all necessary files to process all bands and try again." % (band, mc[band][0].filename, len(mc[band]), img_per_band))
|
||||
raise RuntimeError("Invalid multi-camera images")
|
||||
|
||||
return mc
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class ODMOpenSfMStage(types.ODM_Stage):
|
|||
exit(1)
|
||||
|
||||
octx = OSFMContext(tree.opensfm)
|
||||
octx.setup(args, tree.dataset_raw, photos, gcp_path=reconstruction.gcp.gcp_path, rerun=self.rerun())
|
||||
octx.setup(args, tree.dataset_raw, photos, reconstruction=reconstruction, rerun=self.rerun())
|
||||
octx.extract_metadata(self.rerun())
|
||||
self.update_progress(20)
|
||||
octx.feature_matching(self.rerun())
|
||||
|
|
|
@ -46,7 +46,7 @@ class ODMSplitStage(types.ODM_Stage):
|
|||
"submodel_overlap: %s" % args.split_overlap,
|
||||
]
|
||||
|
||||
octx.setup(args, tree.dataset_raw, photos, gcp_path=reconstruction.gcp.gcp_path, append_config=config, rerun=self.rerun())
|
||||
octx.setup(args, tree.dataset_raw, photos, reconstruction=reconstruction, append_config=config, rerun=self.rerun())
|
||||
octx.extract_metadata(self.rerun())
|
||||
|
||||
self.update_progress(5)
|
||||
|
|
|
@ -2,8 +2,15 @@ import unittest
|
|||
from opendm import types
|
||||
|
||||
class ODMPhotoMock:
|
||||
def __init__(self, filename):
|
||||
def __init__(self, filename, band_name):
|
||||
self.filename = filename
|
||||
self.band_name = band_name
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%s)" % (self.filename, self.band_name)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
class TestTypes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
@ -11,36 +18,29 @@ class TestTypes(unittest.TestCase):
|
|||
|
||||
def test_reconstruction(self):
|
||||
# Multi camera setup
|
||||
micasa_redsense_files = ['IMG_0298_1.tif','IMG_0298_2.tif','IMG_0298_3.tif','IMG_0298_4.tif','IMG_0298_5.tif','IMG_0299_1.tif','IMG_0299_2.tif','IMG_0299_3.tif','IMG_0299_4.tif','IMG_0299_5.tif','IMG_0300_1.tif','IMG_0300_2.tif','IMG_0300_3.tif','IMG_0300_4.tif', 'IMG_0300_5.tif']
|
||||
photos = [ODMPhotoMock(f) for f in micasa_redsense_files]
|
||||
micasa_redsense_files = [('IMG_0298_1.tif', 'Red'), ('IMG_0298_2.tif', 'Green'), ('IMG_0298_3.tif', 'Blue'), ('IMG_0298_4.tif', 'NIR'), ('IMG_0298_5.tif', 'Rededge'),
|
||||
('IMG_0299_1.tif', 'Red'), ('IMG_0299_2.tif', 'Green'), ('IMG_0299_3.tif', 'Blue'), ('IMG_0299_4.tif', 'NIR'), ('IMG_0299_5.tif', 'Rededge'),
|
||||
('IMG_0300_1.tif', 'Red'), ('IMG_0300_2.tif', 'Green'), ('IMG_0300_3.tif', 'Blue'), ('IMG_0300_4.tif', 'NIR'), ('IMG_0300_5.tif', 'Rededge')]
|
||||
photos = [ODMPhotoMock(f, b) for f, b in micasa_redsense_files]
|
||||
recon = types.ODM_Reconstruction(photos)
|
||||
|
||||
self.assertTrue(recon.multi_camera is not None)
|
||||
|
||||
# Found all 5 bands
|
||||
for b in ["1", "2", "3", "4", "5"]:
|
||||
for b in ["Red", "Blue", "Green", "NIR", "Rededge"]:
|
||||
self.assertTrue(b in recon.multi_camera)
|
||||
self.assertTrue([p.filename for p in recon.multi_camera["1"]] == ['IMG_0298_1.tif', 'IMG_0299_1.tif', 'IMG_0300_1.tif'])
|
||||
self.assertTrue([p.filename for p in recon.multi_camera["Red"]] == ['IMG_0298_1.tif', 'IMG_0299_1.tif', 'IMG_0300_1.tif'])
|
||||
|
||||
# Missing a file
|
||||
micasa_redsense_files = ['IMG_0298_1.tif','IMG_0298_3.tif','IMG_0298_4.tif','IMG_0298_5.tif','IMG_0299_1.tif','IMG_0299_2.tif','IMG_0299_3.tif','IMG_0299_4.tif','IMG_0299_5.tif','IMG_0300_1.tif','IMG_0300_2.tif','IMG_0300_3.tif','IMG_0300_4.tif', 'IMG_0300_5.tif']
|
||||
photos = [ODMPhotoMock(f) for f in micasa_redsense_files]
|
||||
recon = types.ODM_Reconstruction(photos)
|
||||
micasa_redsense_files = [('IMG_0298_1.tif', 'Red'), ('IMG_0298_2.tif', 'Green'), ('IMG_0298_3.tif', 'Blue'), ('IMG_0298_4.tif', 'NIR'), ('IMG_0298_5.tif', 'Rededge'),
|
||||
('IMG_0299_2.tif', 'Green'), ('IMG_0299_3.tif', 'Blue'), ('IMG_0299_4.tif', 'NIR'), ('IMG_0299_5.tif', 'Rededge'),
|
||||
('IMG_0300_1.tif', 'Red'), ('IMG_0300_2.tif', 'Green'), ('IMG_0300_3.tif', 'Blue'), ('IMG_0300_4.tif', 'NIR'), ('IMG_0300_5.tif', 'Rededge')]
|
||||
photos = [ODMPhotoMock(f, b) for f,b in micasa_redsense_files]
|
||||
self.assertRaises(RuntimeError, types.ODM_Reconstruction, photos)
|
||||
|
||||
self.assertTrue(recon.multi_camera is None)
|
||||
|
||||
# Parrot Sequoia pattern
|
||||
sequoia_files = ['IMG_180822_140144_0613_GRE.TIF','IMG_180822_140144_0613_NIR.TIF','IMG_180822_140144_0613_RED.TIF','IMG_180822_140144_0613_REG.TIF','IMG_180822_140146_0614_GRE.TIF','IMG_180822_140146_0614_NIR.TIF','IMG_180822_140146_0614_RED.TIF','IMG_180822_140146_0614_REG.TIF']
|
||||
photos = [ODMPhotoMock(f) for f in sequoia_files]
|
||||
recon = types.ODM_Reconstruction(photos)
|
||||
self.assertTrue(recon.multi_camera is not None)
|
||||
|
||||
# Found 4 bands
|
||||
self.assertEqual(len(recon.multi_camera), 4)
|
||||
|
||||
# Single camera
|
||||
dji_files = ['DJI_0018.JPG','DJI_0019.JPG','DJI_0020.JPG','DJI_0021.JPG','DJI_0022.JPG','DJI_0023.JPG']
|
||||
photos = [ODMPhotoMock(f) for f in dji_files]
|
||||
photos = [ODMPhotoMock(f, 'RGB') for f in dji_files]
|
||||
recon = types.ODM_Reconstruction(photos)
|
||||
self.assertTrue(recon.multi_camera is None)
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue