kopia lustrzana https://github.com/OpenDroneMap/ODM
Refactored band detection to use XMP tags instead of filenames
rodzic
7da56c43f3
commit
30f1570e05
|
@ -314,7 +314,8 @@ void OdmOrthoPhoto::saveTIFF(const std::string &filename, GDALDataType dataType)
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
char **papszOptions = NULL;
|
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;
|
GDALRasterBandH hBand;
|
||||||
|
|
||||||
for (size_t i = 0; i < bands.size(); i++){
|
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;
|
std::cerr << "Cannot write TIFF to " << filename << std::endl;
|
||||||
exit(1);
|
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 );
|
GDALClose( hDstDS );
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ class OSFMContext:
|
||||||
exit(1)
|
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
|
Setup a OpenSfM project
|
||||||
"""
|
"""
|
||||||
|
@ -91,13 +91,22 @@ class OSFMContext:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.ODM_WARNING("Cannot set camera_models_overrides.json: %s" % str(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
|
# create config file for OpenSfM
|
||||||
config = [
|
config = [
|
||||||
"use_exif_size: no",
|
"use_exif_size: no",
|
||||||
"feature_process_size: %s" % args.resize_to,
|
"feature_process_size: %s" % args.resize_to,
|
||||||
"feature_min_frames: %s" % args.min_num_features,
|
"feature_min_frames: %s" % args.min_num_features,
|
||||||
"processes: %s" % args.max_concurrency,
|
"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,
|
"matching_gps_distance: %s" % args.matcher_distance,
|
||||||
"depthmap_method: %s" % args.opensfm_depthmap_method,
|
"depthmap_method: %s" % args.opensfm_depthmap_method,
|
||||||
"depthmap_resolution: %s" % args.depthmap_resolution,
|
"depthmap_resolution: %s" % args.depthmap_resolution,
|
||||||
|
@ -114,12 +123,16 @@ class OSFMContext:
|
||||||
|
|
||||||
if not has_gps:
|
if not has_gps:
|
||||||
log.ODM_INFO("No GPS information, using BOW matching")
|
log.ODM_INFO("No GPS information, using BOW matching")
|
||||||
|
use_bow = True
|
||||||
|
|
||||||
|
if use_bow:
|
||||||
config.append("matcher_type: WORDS")
|
config.append("matcher_type: WORDS")
|
||||||
|
|
||||||
if has_alt:
|
if has_alt:
|
||||||
log.ODM_INFO("Altitude data detected, enabling it for GPS alignment")
|
log.ODM_INFO("Altitude data detected, enabling it for GPS alignment")
|
||||||
config.append("use_altitude_tag: yes")
|
config.append("use_altitude_tag: yes")
|
||||||
|
|
||||||
|
gcp_path = reconstruction.gcp.gcp_path
|
||||||
if has_alt or gcp_path:
|
if has_alt or gcp_path:
|
||||||
config.append("align_method: auto")
|
config.append("align_method: auto")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -3,11 +3,11 @@ import exifread
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from opensfm.exif import sensor_string
|
|
||||||
from opendm import get_image_size
|
from opendm import get_image_size
|
||||||
from opendm import location
|
from opendm import location
|
||||||
from opendm.gcp import GCPFile
|
from opendm.gcp import GCPFile
|
||||||
from pyproj import CRS
|
from pyproj import CRS
|
||||||
|
import xmltodict as x2d
|
||||||
|
|
||||||
import log
|
import log
|
||||||
import io
|
import io
|
||||||
|
@ -28,10 +28,11 @@ class ODM_Photo:
|
||||||
# other attributes
|
# other attributes
|
||||||
self.camera_make = ''
|
self.camera_make = ''
|
||||||
self.camera_model = ''
|
self.camera_model = ''
|
||||||
self.make_model = ''
|
|
||||||
self.latitude = None
|
self.latitude = None
|
||||||
self.longitude = None
|
self.longitude = None
|
||||||
self.altitude = None
|
self.altitude = None
|
||||||
|
self.band_name = 'RGB'
|
||||||
|
|
||||||
# parse values from metadata
|
# parse values from metadata
|
||||||
self.parse_exif_values(path_file)
|
self.parse_exif_values(path_file)
|
||||||
|
|
||||||
|
@ -40,8 +41,9 @@ class ODM_Photo:
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} | camera: {} | dimensions: {} x {} | lat: {} | lon: {} | alt: {}'.format(
|
return '{} | camera: {} {} | dimensions: {} x {} | lat: {} | lon: {} | alt: {} | band: {}'.format(
|
||||||
self.filename, self.make_model, self.width, self.height, self.latitude, self.longitude, self.altitude)
|
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):
|
def parse_exif_values(self, _path_file):
|
||||||
# Disable exifread log
|
# Disable exifread log
|
||||||
|
@ -66,10 +68,36 @@ class ODM_Photo:
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
log.ODM_WARNING("Cannot read EXIF tags for %s: %s" % (_path_file, e.message))
|
log.ODM_WARNING("Cannot read EXIF tags for %s: %s" % (_path_file, e.message))
|
||||||
|
|
||||||
if self.camera_make and self.camera_model:
|
# Extract XMP tags
|
||||||
self.make_model = sensor_string(self.camera_make, self.camera_model)
|
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)
|
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):
|
def dms_to_decimal(self, dms, sign):
|
||||||
"""Converts dms coords to decimal degrees"""
|
"""Converts dms coords to decimal degrees"""
|
||||||
|
@ -100,45 +128,23 @@ class ODM_Reconstruction(object):
|
||||||
Looks at the reconstruction photos and determines if this
|
Looks at the reconstruction photos and determines if this
|
||||||
is a single or multi-camera setup.
|
is a single or multi-camera setup.
|
||||||
"""
|
"""
|
||||||
supported_ext_re = r"\.(" + "|".join([e[1:] for e in context.supported_extensions]) + ")$"
|
mc = {}
|
||||||
|
for p in self.photos:
|
||||||
# Match filename_1.tif, filename_2.tif, filename_3.tif, ...
|
if not p.band_name in mc:
|
||||||
#
|
mc[p.band_name] = []
|
||||||
multi_camera_patterns = {
|
mc[p.band_name].append(p)
|
||||||
'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)
|
|
||||||
|
|
||||||
# We support between 2 and 6 bands
|
bands_count = len(mc)
|
||||||
# If we matched more or less bands, we probably just
|
if bands_count >= 2 and bands_count <= 8:
|
||||||
# found filename patterns that do not match a multi-camera setup
|
# Validate that all bands have the same number of images,
|
||||||
bands_count = len(mc)
|
# otherwise this is not a multi-camera setup
|
||||||
if bands_count >= 2 and bands_count <= 6:
|
img_per_band = len(mc[p.band_name])
|
||||||
|
for band in mc:
|
||||||
# Validate that all bands have the same number of images,
|
if len(mc[band]) != img_per_band:
|
||||||
# otherwise this is not a multi-camera setup
|
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))
|
||||||
img_per_band = len(mc[band])
|
raise RuntimeError("Invalid multi-camera images")
|
||||||
valid = True
|
|
||||||
for band in mc:
|
return 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
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ODMOpenSfMStage(types.ODM_Stage):
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
octx = OSFMContext(tree.opensfm)
|
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())
|
octx.extract_metadata(self.rerun())
|
||||||
self.update_progress(20)
|
self.update_progress(20)
|
||||||
octx.feature_matching(self.rerun())
|
octx.feature_matching(self.rerun())
|
||||||
|
|
|
@ -46,7 +46,7 @@ class ODMSplitStage(types.ODM_Stage):
|
||||||
"submodel_overlap: %s" % args.split_overlap,
|
"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())
|
octx.extract_metadata(self.rerun())
|
||||||
|
|
||||||
self.update_progress(5)
|
self.update_progress(5)
|
||||||
|
|
|
@ -2,8 +2,15 @@ import unittest
|
||||||
from opendm import types
|
from opendm import types
|
||||||
|
|
||||||
class ODMPhotoMock:
|
class ODMPhotoMock:
|
||||||
def __init__(self, filename):
|
def __init__(self, filename, band_name):
|
||||||
self.filename = filename
|
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):
|
class TestTypes(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -11,36 +18,29 @@ class TestTypes(unittest.TestCase):
|
||||||
|
|
||||||
def test_reconstruction(self):
|
def test_reconstruction(self):
|
||||||
# Multi camera setup
|
# 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']
|
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'),
|
||||||
photos = [ODMPhotoMock(f) for f in micasa_redsense_files]
|
('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)
|
recon = types.ODM_Reconstruction(photos)
|
||||||
|
|
||||||
self.assertTrue(recon.multi_camera is not None)
|
self.assertTrue(recon.multi_camera is not None)
|
||||||
|
|
||||||
# Found all 5 bands
|
# 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(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
|
# 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']
|
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'),
|
||||||
photos = [ODMPhotoMock(f) for f in micasa_redsense_files]
|
('IMG_0299_2.tif', 'Green'), ('IMG_0299_3.tif', 'Blue'), ('IMG_0299_4.tif', 'NIR'), ('IMG_0299_5.tif', 'Rededge'),
|
||||||
recon = types.ODM_Reconstruction(photos)
|
('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
|
# Single camera
|
||||||
dji_files = ['DJI_0018.JPG','DJI_0019.JPG','DJI_0020.JPG','DJI_0021.JPG','DJI_0022.JPG','DJI_0023.JPG']
|
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)
|
recon = types.ODM_Reconstruction(photos)
|
||||||
self.assertTrue(recon.multi_camera is None)
|
self.assertTrue(recon.multi_camera is None)
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue