Refactored band detection to use XMP tags instead of filenames

Former-commit-id: 30f1570e05
pull/1161/head
Piero Toffanin 2019-12-07 22:05:59 +00:00
rodzic a5522cabef
commit 494a441ef9
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);
}
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 );
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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