Refactored band detection to use XMP tags instead of filenames

pull/1057/head
Piero Toffanin 2019-12-07 22:05:59 +00:00
rodzic 7da56c43f3
commit 30f1570e05
6 zmienionych plików z 89 dodań i 73 usunięć

Wyświetl plik

@ -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 );
} }

Wyświetl plik

@ -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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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())

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)