--radiometric-calibration option, refactoring

pull/1082/head
Piero Toffanin 2020-02-26 22:33:08 +00:00
rodzic 850500ac0d
commit d4800c93bc
6 zmienionych plików z 262 dodań i 39 usunięć

Wyświetl plik

@ -160,6 +160,17 @@ def config():
'set to one of: [auto, perspective, brown, fisheye, spherical]. Default: '
'%(default)s'))
parser.add_argument('--radiometric-calibration',
metavar='<string>',
default='none',
choices=['none', 'camera', 'sun', 'sunangle'],
help=('Set the radiometric calibration to perform on images. '
'When processing multispectral images you should set this value '
'to obtain reflectance value (otherwise orthophotos will contain Digital Number (DN) values). '
'Can be set to one of: [none, camera, sun, sunangle]. Default: '
'%(default)s'))
parser.add_argument('--max-concurrency',
metavar='<positive integer>',
default=context.num_cores,

Wyświetl plik

@ -1,9 +1,10 @@
def dn_to_radiance(photo, img):
def dn_to_radiance(photo, image):
"""
Convert Digital Number values to Radiance values
:param photo ODM_Photo
:param img numpy array containing image data
:param image numpy array containing image data
:return numpy array with radiance image values
"""
# TODO!
return img
print("HI")
return image

Wyświetl plik

@ -234,14 +234,12 @@ class OSFMContext:
else:
log.ODM_INFO("Already extracted cameras")
def convert_and_undistort(self, rerun=False):
def convert_and_undistort(self, rerun=False, imageFilter=None):
log.ODM_INFO("Undistorting %s ..." % self.opensfm_project_path)
undistorted_images_path = self.path("undistorted", "images")
if not io.dir_exists(undistorted_images_path) or rerun:
def test(image):
return image
cmd = undistort.Command(test)
cmd = undistort.Command(imageFilter)
parser = argparse.ArgumentParser()
cmd.add_arguments(parser)
cmd.run(parser.parse_args([self.opensfm_project_path]))

204
opendm/photo.py 100644
Wyświetl plik

@ -0,0 +1,204 @@
import io
import logging
import re
import exifread
from six import string_types
import log
import system
import xmltodict as x2d
from opendm import get_image_size
class ODM_Photo:
""" ODMPhoto - a class for ODMPhotos
"""
def __init__(self, path_file):
# Standard tags (virtually all photos have these)
self.filename = io.extract_file_from_path_file(path_file)
self.width = None
self.height = None
self.camera_make = ''
self.camera_model = ''
# Geo tags
self.latitude = None
self.longitude = None
self.altitude = None
# Multi-band fields
self.band_name = 'RGB'
self.band_index = 0
# Multi-spectral fields (not all cameras implement all these values)
self.radiometric_calibration = None
self.black_level = None
# Capture info (most cameras implement these)
self.exposure_time = None
self.iso_speed = None
self.bits_per_sample = None
self.vignetting_center = None
self.vignetting_polynomial = None
# parse values from metadata
self.parse_exif_values(path_file)
# print log message
log.ODM_DEBUG('Loaded {}'.format(self))
def __str__(self):
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, self.band_index)
def parse_exif_values(self, _path_file):
# Disable exifread log
logging.getLogger('exifread').setLevel(logging.CRITICAL)
with open(_path_file, 'rb') as f:
tags = exifread.process_file(f, details=False)
try:
if 'Image Make' in tags:
self.camera_make = tags['Image Make'].values.encode('utf8')
if 'Image Model' in tags:
self.camera_model = tags['Image Model'].values.encode('utf8')
if 'GPS GPSAltitude' in tags:
self.altitude = self.float_value(tags['GPS GPSAltitude'])
if 'GPS GPSAltitudeRef' in tags and self.int_value(tags['GPS GPSAltitudeRef']) > 0:
self.altitude *= -1
if 'GPS GPSLatitude' in tags and 'GPS GPSLatitudeRef' in tags:
self.latitude = self.dms_to_decimal(tags['GPS GPSLatitude'], tags['GPS GPSLatitudeRef'])
if 'GPS GPSLongitude' in tags and 'GPS GPSLongitudeRef' in tags:
self.longitude = self.dms_to_decimal(tags['GPS GPSLongitude'], tags['GPS GPSLongitudeRef'])
# if 'BlackLevel' in tags:
if 'Image Tag 0xC61A' in tags:
self.black_level = self.list_values(tags['Image Tag 0xC61A'])
if 'EXIF ExposureTime' in tags:
self.exposure_time = self.float_value(tags['EXIF ExposureTime'])
if 'EXIF ISOSpeed' in tags:
self.iso_speed = self.int_value(tags['EXIF ISOSpeed'])
if 'Image BitsPerSample' in tags:
self.bits_per_sample = self.int_value(tags['Image BitsPerSample'])
except IndexError as e:
log.ODM_WARNING("Cannot read EXIF tags for %s: %s" % (_path_file, e.message))
# Extract XMP tags
f.seek(0)
xmp = self.get_xmp(f)
for tags in xmp:
band_name = self.get_xmp_tag(tags, 'Camera:BandName')
if band_name is not None:
self.band_name = band_name.replace(" ", "")
self.set_attr_from_xmp_tag('band_index', tags, [
'DLS:SensorId', # Micasense RedEdge
'@Camera:RigCameraIndex', # Parrot Sequoia, Sentera 21244-00_3.2MP-GS-0001
'Camera:RigCameraIndex', # MicaSense Altum
])
self.set_attr_from_xmp_tag('radiometric_calibration', tags, [
'MicaSense:RadiometricCalibration',
])
self.set_attr_from_xmp_tag('vignetting_center', tags, [
'Camera:VignettingCenter',
'Sentera:VignettingCenter',
])
self.set_attr_from_xmp_tag('vignetting_polynomial', tags, [
'Camera:VignettingPolynomial',
'Sentera:VignettingPolynomial',
])
# print(self.band_name)
# print(self.band_index)
# print(self.radiometric_calibration)
# print(self.black_level)
# print(self.exposure_time)
# print(self.iso_speed)
# print(self.bits_per_sample)
# print(self.vignetting_center)
# print(self.vignetting_polynomial)
self.width, self.height = get_image_size.get_image_size(_path_file)
# Sanitize band name since we use it in folder paths
self.band_name = re.sub('[^A-Za-z0-9]+', '', self.band_name)
def set_attr_from_xmp_tag(self, attr, xmp_tags, tags):
v = self.get_xmp_tag(xmp_tags, tags)
if v is not None:
setattr(self, attr, v)
def get_xmp_tag(self, xmp_tags, tags):
if isinstance(tags, str):
tags = [tags]
for tag in tags:
if tag in xmp_tags:
t = xmp_tags[tag]
if isinstance(t, string_types):
return str(t)
elif isinstance(t, dict):
items = t.get('rdf:Seq', {}).get('rdf:li', {})
if items:
return " ".join(items)
elif isinstance(t, int) or isinstance(t, float):
return t
# 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"""
degrees, minutes, seconds = self.float_values(dms)
return (-1 if sign.values[0] in 'SWsw' else 1) * (
degrees +
minutes / 60 +
seconds / 3600
)
def float_values(self, tag):
return map(lambda v: float(v.num) / float(v.den), tag.values)
def float_value(self, tag):
v = self.float_values(tag)
if len(v) > 0:
return v[0]
def int_values(self, tag):
return map(int, tag.values)
def int_value(self, tag):
v = self.int_values(tag)
if len(v) > 0:
return v[0]
def list_values(self, tag):
return " ".join(map(str, tag.values))

Wyświetl plik

@ -127,10 +127,11 @@ class ODM_Reconstruction(object):
with open(file, 'w') as f:
f.write(self.georef.proj4())
def get_photo(filename):
def get_photo(self, filename):
for p in self.photos:
if p.filename == filename:
return p
class ODM_GeoRef(object):
@staticmethod

Wyświetl plik

@ -9,6 +9,7 @@ from opendm import gsd
from opendm import point_cloud
from opendm import types
from opendm.osfm import OSFMContext
from opendm.multispectral import dn_to_radiance
class ODMOpenSfMStage(types.ODM_Stage):
def process(self, args, outputs):
@ -21,43 +22,50 @@ class ODMOpenSfMStage(types.ODM_Stage):
exit(1)
octx = OSFMContext(tree.opensfm)
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())
self.update_progress(30)
octx.reconstruct(self.rerun())
octx.extract_cameras(tree.path("cameras.json"), self.rerun())
self.update_progress(70)
# 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())
# self.update_progress(30)
# octx.reconstruct(self.rerun())
# octx.extract_cameras(tree.path("cameras.json"), self.rerun())
# self.update_progress(70)
# If we find a special flag file for split/merge we stop right here
if os.path.exists(octx.path("split_merge_stop_at_reconstruction.txt")):
log.ODM_INFO("Stopping OpenSfM early because we found: %s" % octx.path("split_merge_stop_at_reconstruction.txt"))
self.next_stage = None
return
# # If we find a special flag file for split/merge we stop right here
# if os.path.exists(octx.path("split_merge_stop_at_reconstruction.txt")):
# log.ODM_INFO("Stopping OpenSfM early because we found: %s" % octx.path("split_merge_stop_at_reconstruction.txt"))
# self.next_stage = None
# return
if args.fast_orthophoto:
output_file = octx.path('reconstruction.ply')
elif args.use_opensfm_dense:
output_file = tree.opensfm_model
else:
output_file = tree.opensfm_reconstruction
# if args.fast_orthophoto:
# output_file = octx.path('reconstruction.ply')
# elif args.use_opensfm_dense:
# output_file = tree.opensfm_model
# else:
# output_file = tree.opensfm_reconstruction
updated_config_flag_file = octx.path('updated_config.txt')
# updated_config_flag_file = octx.path('updated_config.txt')
# Make sure it's capped by the depthmap-resolution arg,
# since the undistorted images are used for MVS
outputs['undist_image_max_size'] = max(
gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd, has_gcp=reconstruction.has_gcp()),
args.depthmap_resolution
)
# # Make sure it's capped by the depthmap-resolution arg,
# # since the undistorted images are used for MVS
# outputs['undist_image_max_size'] = max(
# gsd.image_max_size(photos, args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd, has_gcp=reconstruction.has_gcp()),
# args.depthmap_resolution
# )
if not io.file_exists(updated_config_flag_file) or self.rerun():
octx.update_config({'undistorted_image_max_size': outputs['undist_image_max_size']})
octx.touch(updated_config_flag_file)
# if not io.file_exists(updated_config_flag_file) or self.rerun():
# octx.update_config({'undistorted_image_max_size': outputs['undist_image_max_size']})
# octx.touch(updated_config_flag_file)
# These will be used for texturing / MVS
octx.convert_and_undistort(self.rerun())
if args.radiometric_calibration == "none":
octx.convert_and_undistort(self.rerun())
else:
def radiometric_calibrate(shot_id, image):
photo = reconstruction.get_photo(shot_id)
return dn_to_radiance(photo, image)
octx.convert_and_undistort(self.rerun(), radiometric_calibrate)
self.update_progress(80)