diff --git a/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp b/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp index 5f206286..fd578882 100644 --- a/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp +++ b/modules/odm_orthophoto/src/OdmOrthoPhoto.cpp @@ -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(bands.size()), dataType, papszOptions ); + GDALDatasetH hDstDS = GDALCreate( hDriver, filename.c_str(), width, height, + static_cast(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 ); } diff --git a/opendm/osfm.py b/opendm/osfm.py index 4c7b1824..94659f50 100644 --- a/opendm/osfm.py +++ b/opendm/osfm.py @@ -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: diff --git a/opendm/types.py b/opendm/types.py index 88c15fb3..079a45be 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -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('\d{1})', - 'Parrot Sequoia': r'^IMG_\d+_\d+_\d+_(?P[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 diff --git a/stages/run_opensfm.py b/stages/run_opensfm.py index 010eeaa5..84e80a7b 100644 --- a/stages/run_opensfm.py +++ b/stages/run_opensfm.py @@ -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()) diff --git a/stages/splitmerge.py b/stages/splitmerge.py index 1e31d2c5..ec7cf6df 100644 --- a/stages/splitmerge.py +++ b/stages/splitmerge.py @@ -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) diff --git a/tests/test_types.py b/tests/test_types.py index 1462d3f0..6f7b6d49 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -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)