Write exif data directly

268
Piero Toffanin 2021-12-14 14:47:48 -05:00
rodzic fe37770c52
commit b544ca2464
4 zmienionych plików z 155 dodań i 25 usunięć

Wyświetl plik

@ -120,30 +120,10 @@ class OSFMContext:
# GPSDOP override if we have GPS accuracy information (such as RTK)
if 'gps_accuracy_is_set' in args:
log.ODM_INFO("Forcing GPS DOP to %s for all images" % args.gps_accuracy)
log.ODM_INFO("Writing exif overrides")
exif_overrides = {}
for p in photos:
if 'gps_accuracy_is_set' in args:
dop = args.gps_accuracy
elif p.get_gps_dop() is not None:
dop = p.get_gps_dop()
else:
dop = args.gps_accuracy # default value
for p in photos:
p.override_gps_dop(args.gps_accuracy)
if p.latitude is not None and p.longitude is not None:
exif_overrides[p.filename] = {
'gps': {
'latitude': p.latitude,
'longitude': p.longitude,
'altitude': p.altitude if p.altitude is not None else 0,
'dop': dop,
}
}
with open(os.path.join(self.opensfm_project_path, "exif_overrides.json"), 'w') as f:
f.write(json.dumps(exif_overrides))
# Check image masks
masks = []
@ -287,6 +267,17 @@ class OSFMContext:
metadata_dir = self.path("exif")
if not io.dir_exists(metadata_dir) or rerun:
self.run('extract_metadata')
def photos_to_metadata(self, photos, rerun=False):
metadata_dir = self.path("exif")
if io.dir_exists(metadata_dir) and rerun:
shutil.rmtree(metadata_dir)
os.makedirs(metadata_dir)
for p in photos:
with open(os.path.join(metadata_dir, "%s.exif" % p.filename), 'w') as f:
f.write(json.dumps(p.to_opensfm_exif()))
def is_feature_matching_done(self):
features_dir = self.path("features")

Wyświetl plik

@ -14,6 +14,9 @@ from opendm import system
import xmltodict as x2d
from opendm import get_image_size
from xml.parsers.expat import ExpatError
from opensfm.sensors import sensor_data
projections = ['perspective', 'fisheye', 'brown', 'dual', 'equirectangular', 'spherical']
def find_largest_photo(photos):
max_photo = None
@ -36,6 +39,27 @@ def find_largest_photo_dim(photos):
return max_dim
def get_mm_per_unit(resolution_unit):
"""Length of a resolution unit in millimeters.
Uses the values from the EXIF specs in
https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html
Args:
resolution_unit: the resolution unit value given in the EXIF
"""
if resolution_unit == 2: # inch
return 25.4
elif resolution_unit == 3: # cm
return 10
elif resolution_unit == 4: # mm
return 1
elif resolution_unit == 5: # um
return 0.001
else:
log.ODM_WARNING("Unknown EXIF resolution unit value: {}".format(resolution_unit))
return None
class PhotoCorruptedException(Exception):
pass
@ -51,6 +75,7 @@ class ODM_Photo:
self.height = None
self.camera_make = ''
self.camera_model = ''
self.orientation = 1
# Geo tags
self.latitude = None
@ -91,6 +116,10 @@ class ODM_Photo:
self.gps_xy_stddev = None # Dilution of Precision X/Y
self.gps_z_stddev = None # Dilution of Precision Z
# Misc SFM
self.camera_projection = 'brown'
self.focal_ratio = 0.0
# parse values from metadata
self.parse_exif_values(path_file)
@ -143,9 +172,16 @@ class ODM_Photo:
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 'Image Orientation' in tags:
self.orientation = self.int_value(tags['Image Orientation'])
except (IndexError, ValueError) as e:
log.ODM_WARNING("Cannot read basic EXIF tags for %s: %s" % (_path_file, str(e)))
try:
self.focal_ratio = self.extract_focal(self.camera_make, self.camera_model, tags)
except (IndexError, ValueError) as e:
log.ODM_WARNING("Cannot extract focal ratio for %s: %s" % (_path_file, str(e)))
try:
if 'Image Tag 0xC61A' in tags:
self.black_level = self.list_values(tags['Image Tag 0xC61A'])
@ -241,6 +277,17 @@ class ODM_Photo:
'@Camera:ImageUniqueID', # sentera 6x
])
# DJI GPS tags
self.set_attr_from_xmp_tag('longitude', tags, [
'@drone-dji:Longitude'
], float)
self.set_attr_from_xmp_tag('latitude', tags, [
'@drone-dji:Latitude'
], float)
self.set_attr_from_xmp_tag('altitude', tags, [
'@drone-dji:AbsoluteAltitude'
], float)
# Phantom 4 RTK
if '@drone-dji:RtkStdLon' in tags:
y = float(self.get_xmp_tag(tags, '@drone-dji:RtkStdLon'))
@ -263,6 +310,13 @@ class ODM_Photo:
self.set_attr_from_xmp_tag('dls_yaw', tags, ['DLS:Yaw'], float)
self.set_attr_from_xmp_tag('dls_pitch', tags, ['DLS:Pitch'], float)
self.set_attr_from_xmp_tag('dls_roll', tags, ['DLS:Roll'], float)
camera_projection = self.get_xmp_tag(tags, ['@Camera:ModelType', 'Camera:ModelType'])
if camera_projection is not None:
camera_projection = camera_projection.lower()
if camera_projection in projections:
self.camera_projection = camera_projection
except Exception as e:
log.ODM_WARNING("Cannot read XMP tags for %s: %s" % (_path_file, str(e)))
@ -281,6 +335,45 @@ class ODM_Photo:
# Sanitize band name since we use it in folder paths
self.band_name = re.sub('[^A-Za-z0-9]+', '', self.band_name)
def extract_focal(self, make, model, tags):
if make != "unknown":
# remove duplicate 'make' information in 'model'
model = model.replace(make, "")
sensor_string = (make.strip() + " " + model.strip()).strip().lower()
sensor_width = None
if ("EXIF FocalPlaneResolutionUnit" in tags and "EXIF FocalPlaneXResolution" in tags):
resolution_unit = self.float_value(tags["EXIF FocalPlaneResolutionUnit"])
mm_per_unit = get_mm_per_unit(resolution_unit)
if mm_per_unit:
pixels_per_unit = self.float_value(tags["EXIF FocalPlaneXResolution"])
if pixels_per_unit <= 0 and "EXIF FocalPlaneYResolution" in tags:
pixels_per_unit = self.float_value(tags["EXIF FocalPlaneYResolution"])
if pixels_per_unit > 0 and self.width is not None:
units_per_pixel = 1 / pixels_per_unit
sensor_width = self.width * units_per_pixel * mm_per_unit
focal_35 = None
focal = None
if "EXIF FocalLengthIn35mmFilm" in tags:
focal_35 = self.float_value(tags["EXIF FocalLengthIn35mmFilm"])
if "EXIF FocalLength" in tags:
focal = self.float_value(tags["EXIF FocalLength"])
if focal_35 is not None and focal_35 > 0:
focal_ratio = focal_35 / 36.0 # 35mm film produces 36x24mm pictures.
else:
if not sensor_width:
sensor_width = sensor_data().get(sensor_string, None)
if sensor_width and focal:
focal_ratio = focal / sensor_width
else:
focal_ratio = 0.0
return focal_ratio
def set_attr_from_xmp_tag(self, attr, xmp_tags, tags, cast=None):
v = self.get_xmp_tag(xmp_tags, tags)
if v is not None:
@ -487,5 +580,51 @@ class ODM_Photo:
return None
def override_gps_dop(self, dop):
self.gps_xy_stddev = self.gps_z_stddev = dop
def is_thermal(self):
return self.band_name.upper() in ["LWIR"] # TODO: more?
return self.band_name.upper() in ["LWIR"] # TODO: more?
def to_opensfm_exif(self):
capture_time = 0.0
if self.utc_time is not None:
capture_time = self.utc_time / 1000.0
gps = {}
if self.latitude is not None and self.longitude is not None:
gps['latitude'] = self.latitude
gps['longitude'] = self.longitude
if self.altitude is not None:
gps['altitude'] = self.altitude
else:
gps['altitude'] = 0.0
dop = self.get_gps_dop()
if dop is None:
dop = 10.0 # Default
gps['dop'] = dop
return {
"make": self.camera_make,
"model": self.camera_model,
"width": self.width,
"height": self.height,
"projection_type": self.camera_projection,
"focal_ratio": self.focal_ratio,
"orientation": self.orientation,
"capture_time": capture_time,
"gps": gps,
"camera": " ".join(
[
"v2",
self.camera_make.strip(),
self.camera_model.strip(),
str(int(self.width)),
str(int(self.height)),
self.camera_projection,
str(float(self.focal_ratio))[:6],
]
).lower()
}

Wyświetl plik

@ -30,7 +30,7 @@ class ODMOpenSfMStage(types.ODM_Stage):
octx = OSFMContext(tree.opensfm)
octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, rerun=self.rerun())
octx.extract_metadata(self.rerun())
octx.photos_to_metadata(photos, self.rerun())
self.update_progress(20)
octx.feature_matching(self.rerun())
self.update_progress(30)

Wyświetl plik

@ -52,7 +52,7 @@ class ODMSplitStage(types.ODM_Stage):
]
octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, append_config=config, rerun=self.rerun())
octx.extract_metadata(self.rerun())
octx.photos_to_metadata(photos, self.rerun())
self.update_progress(5)