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
	
	 Piero Toffanin
						Piero Toffanin