Merge pull request #1467 from pierotofy/rolling

OpenSfM Update + Rolling Shutter correction
pull/1474/head
Piero Toffanin 2022-06-17 16:02:58 -04:00 zatwierdzone przez GitHub
commit 8b93e068fe
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 156 dodań i 24 usunięć

Wyświetl plik

@ -119,7 +119,7 @@ SETUP_EXTERNAL_PROJECT(GFlags ${ODM_GFlags_Version} ${ODM_BUILD_GFlags})
# ---------------------------------------------------------------------------------------------
# Ceres Solver
#
set(ODM_Ceres_Version 1.10.0)
set(ODM_Ceres_Version 2.0.0)
option(ODM_BUILD_Ceres "Force to build Ceres library" OFF)
SETUP_EXTERNAL_PROJECT(Ceres ${ODM_Ceres_Version} ${ODM_BUILD_Ceres})

Wyświetl plik

@ -8,7 +8,7 @@ ExternalProject_Add(${_proj_name}
STAMP_DIR ${_SB_BINARY_DIR}/stamp
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
URL http://ceres-solver.org/ceres-solver-1.14.0.tar.gz
URL http://ceres-solver.org/ceres-solver-2.0.0.tar.gz
#--Update/Patch step----------
UPDATE_COMMAND ""
#--Configure step-------------

Wyświetl plik

@ -19,7 +19,7 @@ ExternalProject_Add(${_proj_name}
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
GIT_REPOSITORY https://github.com/OpenDroneMap/OpenSfM/
GIT_TAG 284
GIT_TAG 286
#--Update/Patch step----------
UPDATE_COMMAND git submodule update --init --recursive
#--Configure step-------------

Wyświetl plik

@ -1 +1 @@
2.8.5
2.8.6

Wyświetl plik

@ -536,7 +536,7 @@ def config(argv=None, parser=None):
action=StoreValue,
type=float,
default=5,
help='DSM/DTM resolution in cm / pixel. Note: This value is locked to at least 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.'
help='DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.'
' Default: %(default)s')
parser.add_argument('--dem-decimation',
@ -620,6 +620,24 @@ def config(argv=None, parser=None):
default=False,
help='Generate OGC 3D Tiles outputs. Default: %(default)s')
parser.add_argument('--rolling-shutter',
action=StoreTrue,
nargs=0,
default=False,
help='Turn on rolling shutter correction. If the camera '
'has a rolling shutter and the images were taken in motion, you can turn on this option '
'to improve the accuracy of the results. See also --rolling-shutter-readout. '
'Default: %(default)s')
parser.add_argument('--rolling-shutter-readout',
type=float,
action=StoreValue,
metavar='<positive integer>',
default=0,
help='Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. '
'Note that not all cameras are present in the database. Set to 0 to use the database value. '
'Default: %(default)s')
parser.add_argument('--build-overviews',
action=StoreTrue,
nargs=0,

Wyświetl plik

@ -120,7 +120,7 @@ def opensfm_reconstruction_average_gsd(reconstruction_json, use_all_shots=False)
gsds = []
for shotImage in reconstruction['shots']:
shot = reconstruction['shots'][shotImage]
if use_all_shots or shot['gps_dop'] < 999999:
if use_all_shots or shot.get('gps_dop', 999999) < 999999:
camera = reconstruction['cameras'][shot['camera']]
shot_origin = get_origin(shot)
shot_height = shot_origin[2]

Wyświetl plik

@ -40,15 +40,17 @@ class OSFMContext:
return io.file_exists(tracks_file) and io.file_exists(reconstruction_file)
def reconstruct(self, rerun=False):
def create_tracks(self, rerun=False):
tracks_file = os.path.join(self.opensfm_project_path, 'tracks.csv')
reconstruction_file = os.path.join(self.opensfm_project_path, 'reconstruction.json')
rs_file = self.path('rs_done.txt')
if not io.file_exists(tracks_file) or rerun:
self.run('create_tracks')
else:
log.ODM_WARNING('Found a valid OpenSfM tracks file in: %s' % tracks_file)
def reconstruct(self, rolling_shutter_correct=False, rerun=False):
reconstruction_file = os.path.join(self.opensfm_project_path, 'reconstruction.json')
if not io.file_exists(reconstruction_file) or rerun:
self.run('reconstruct')
self.check_merge_partial_reconstructions()
@ -64,6 +66,22 @@ class OSFMContext:
"You could also try to increase the --min-num-features parameter."
"The program will now exit.")
if rolling_shutter_correct:
rs_file = self.path('rs_done.txt')
if not io.file_exists(rs_file) or rerun:
self.run('rs_correct')
log.ODM_INFO("Re-running the reconstruction pipeline")
self.match_features(True)
self.create_tracks(True)
self.reconstruct(rolling_shutter_correct=False, rerun=True)
self.touch(rs_file)
else:
log.ODM_WARNING("Rolling shutter correction already applied")
def check_merge_partial_reconstructions(self):
if self.reconstructed():
data = DataSet(self.opensfm_project_path)
@ -215,7 +233,7 @@ class OSFMContext:
"matching_gps_neighbors: %s" % matcher_neighbors,
"matching_gps_distance: 0",
"matching_graph_rounds: %s" % matcher_graph_rounds,
"optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params or args.cameras else 'yes'),
"optimize_camera_parameters: %s" % ('no' if args.use_fixed_camera_params else 'yes'),
"reconstruction_algorithm: %s" % (args.sfm_algorithm),
"undistorted_image_format: tif",
"bundle_outlier_filtering_type: AUTO",
@ -323,7 +341,7 @@ class OSFMContext:
if not io.dir_exists(metadata_dir) or rerun:
self.run('extract_metadata')
def photos_to_metadata(self, photos, rerun=False):
def photos_to_metadata(self, photos, rolling_shutter, rolling_shutter_readout, rerun=False):
metadata_dir = self.path("exif")
if io.dir_exists(metadata_dir) and not rerun:
@ -339,7 +357,7 @@ class OSFMContext:
data = DataSet(self.opensfm_project_path)
for p in photos:
d = p.to_opensfm_exif()
d = p.to_opensfm_exif(rolling_shutter, rolling_shutter_readout)
with open(os.path.join(metadata_dir, "%s.exif" % p.filename), 'w') as f:
f.write(json.dumps(d, indent=4))
@ -368,7 +386,6 @@ class OSFMContext:
def feature_matching(self, rerun=False):
features_dir = self.path("features")
matches_dir = self.path("matches")
if not io.dir_exists(features_dir) or rerun:
try:
@ -386,6 +403,10 @@ class OSFMContext:
else:
log.ODM_WARNING('Detect features already done: %s exists' % features_dir)
self.match_features(rerun)
def match_features(self, rerun=False):
matches_dir = self.path("matches")
if not io.dir_exists(matches_dir) or rerun:
self.run('match_features')
else:

Wyświetl plik

@ -12,6 +12,7 @@ import pytz
from opendm import io
from opendm import log
from opendm import system
from opendm.rollingshutter import get_rolling_shutter_readout
import xmltodict as x2d
from opendm import get_image_size
from xml.parsers.expat import ExpatError
@ -140,6 +141,11 @@ class ODM_Photo:
self.dls_pitch = None
self.dls_roll = None
# Aircraft speed
self.speedX = None
self.speedY = None
self.speedZ = None
# self.center_wavelength = None
# self.bandwidth = None
@ -192,7 +198,7 @@ class ODM_Photo:
xtags = {}
with open(_path_file, 'rb') as f:
tags = exifread.process_file(f, details=False)
tags = exifread.process_file(f, details=True, extract_thumbnail=False)
try:
if 'Image Make' in tags:
try:
@ -266,6 +272,14 @@ class ODM_Photo:
timezone = pytz.timezone('UTC')
epoch = timezone.localize(datetime.utcfromtimestamp(0))
self.utc_time = (timezone.localize(utc_time) - epoch).total_seconds() * 1000.0
if 'MakerNote SpeedX' in tags and \
'MakerNote SpeedY' in tags and \
'MakerNote SpeedZ' in tags:
self.speedX = self.float_value(tags['MakerNote SpeedX'])
self.speedY = self.float_value(tags['MakerNote SpeedY'])
self.speedZ = self.float_value(tags['MakerNote SpeedZ'])
except Exception as e:
log.ODM_WARNING("Cannot read extended EXIF tags for %s: %s" % (self.filename, str(e)))
@ -366,6 +380,20 @@ class ODM_Photo:
'GPSZAccuracy'
], float)
# DJI Speed tags
if '@drone-dji:FlightXSpeed' in xtags and \
'@drone-dji:FlightYSpeed' in xtags and \
'@drone-dji:FlightZSpeed' in xtags:
self.set_attr_from_xmp_tag('speedX', xtags, [
'@drone-dji:FlightXSpeed'
], float)
self.set_attr_from_xmp_tag('speedY', xtags, [
'@drone-dji:FlightYSpeed',
], float)
self.set_attr_from_xmp_tag('speedZ', xtags, [
'@drone-dji:FlightZSpeed',
], float)
# Account for over-estimation
if self.gps_xy_stddev is not None:
self.gps_xy_stddev *= 2.0
@ -542,6 +570,8 @@ class ODM_Photo:
for v in tag.values:
if isinstance(v, int):
result.append(float(v))
elif isinstance(v, tuple) and len(v) == 1 and isinstance(v[0], float):
result.append(v[0])
elif v.den != 0:
result.append(float(v.num) / float(v.den))
else:
@ -701,13 +731,14 @@ class ODM_Photo:
]
).lower()
def to_opensfm_exif(self):
def to_opensfm_exif(self, rolling_shutter = False, rolling_shutter_readout = 0):
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:
has_gps = self.latitude is not None and self.longitude is not None
if has_gps:
gps['latitude'] = self.latitude
gps['longitude'] = self.longitude
if self.altitude is not None:
@ -741,6 +772,13 @@ class ODM_Photo:
'kappa': self.kappa
}
# Speed is not useful without GPS
if self.has_speed() and has_gps:
d['speed'] = [self.speedY, self.speedX, self.speedZ]
if rolling_shutter:
d['rolling_shutter'] = get_rolling_shutter_readout(self.camera_make, self.camera_model, rolling_shutter_readout)
return d
def has_ypr(self):
@ -752,6 +790,11 @@ class ODM_Photo:
return self.omega is not None and \
self.phi is not None and \
self.kappa is not None
def has_speed(self):
return self.speedX is not None and \
self.speedY is not None and \
self.speedZ is not None
def has_geo(self):
return self.latitude is not None and \

Wyświetl plik

@ -28,11 +28,12 @@ class LocalRemoteExecutor:
to use the processing power of the current machine as well as offloading tasks to a
network node.
"""
def __init__(self, nodeUrl, rerun = False):
def __init__(self, nodeUrl, rolling_shutter = False, rerun = False):
self.node = Node.from_url(nodeUrl)
self.params = {
'tasks': [],
'threads': [],
'rolling_shutter': rolling_shutter,
'rerun': rerun
}
self.node_online = True
@ -446,7 +447,8 @@ class ReconstructionTask(Task):
log.ODM_INFO("Local Reconstruction %s" % octx.name())
log.ODM_INFO("==================================")
octx.feature_matching(self.params['rerun'])
octx.reconstruct(self.params['rerun'])
octx.create_tracks(self.params['rerun'])
octx.reconstruct(self.params['rolling_shutter'], self.params['rerun'])
def process_remote(self, done):
octx = OSFMContext(self.path("opensfm"))

Wyświetl plik

@ -0,0 +1,45 @@
from opendm import log
# Make Model (lowercase) --> readout time (ms)
RS_DATABASE = {
'dji phantom vision fc200': 74, # Phantom 2
'dji fc300s': 33, # Phantom 3 Advanced
'dji fc300c': 33, # Phantom 3 Standard
'dji fc300x': 33, # Phantom 3 Professional
'dji fc330': 33, # Phantom 4
'dji fc6310': 33, # Phantom 4 Professional
'dji fc7203': 20, # Mavic Mini v1
'dji fc350': 30, # Inspire 1
'gopro hero4 black': 30, # GoPro Hero 4 Black
'gopro hero8 black': 17 # GoPro Hero 8 Black
# Help us add more!
# See: https://github.com/OpenDroneMap/RSCalibration for instructions
}
DEFAULT_RS_READOUT = 30 # Just a guess
def make_model_key(make, model):
return ("%s %s" % (make.strip(), model.strip())).lower().strip()
warn_db_missing = {}
def get_rolling_shutter_readout(make, model, override_value=0):
global warn_db_missing
if override_value > 0:
return override_value
key = make_model_key(make, model)
if key in RS_DATABASE:
return float(RS_DATABASE[key])
else:
# Warn once
if not key in warn_db_missing:
log.ODM_WARNING("Rolling shutter readout time for \"%s %s\" is not in our database, using default of %sms which might be incorrect. Use --rolling-shutter-readout to set an actual value (see https://github.com/OpenDroneMap/RSCalibration for instructions on how to calculate this value)" % (make, model, DEFAULT_RS_READOUT))
warn_db_missing[key] = True
return float(DEFAULT_RS_READOUT)

Wyświetl plik

@ -3,7 +3,7 @@ attrs==20.3.0
beautifulsoup4==4.9.3
cloudpickle==1.6.0
edt==2.0.2
ExifRead==2.3.2
ODMExifRead==3.0.3
Fiona==1.8.17 ; sys_platform == 'linux' or sys_platform == 'darwin'
https://github.com/OpenDroneMap/windows-deps/raw/main/Fiona-1.8.19-cp38-cp38-win_amd64.whl ; sys_platform == 'win32'
joblib==0.17.0

Wyświetl plik

@ -85,7 +85,7 @@ class ODMOpenMVSStage(types.ODM_Stage):
if not args.pc_geometric:
config.append("--geometric-iters 0")
sharp = args.pc_filter > 0
sharp = args.pc_geometric
with open(densify_ini_file, 'w+') as f:
f.write("Optimize = %s\n" % (7 if sharp else 3))

Wyświetl plik

@ -30,11 +30,12 @@ class ODMOpenSfMStage(types.ODM_Stage):
octx = OSFMContext(tree.opensfm)
octx.setup(args, tree.dataset_raw, reconstruction=reconstruction, rerun=self.rerun())
octx.photos_to_metadata(photos, self.rerun())
octx.photos_to_metadata(photos, args.rolling_shutter, args.rolling_shutter_readout, self.rerun())
self.update_progress(20)
octx.feature_matching(self.rerun())
self.update_progress(30)
octx.reconstruct(self.rerun())
octx.create_tracks(self.rerun())
octx.reconstruct(args.rolling_shutter, self.rerun())
octx.extract_cameras(tree.path("cameras.json"), self.rerun())
self.update_progress(70)

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.photos_to_metadata(photos, self.rerun())
octx.photos_to_metadata(photos, args.rolling_shutter, args.rolling_shutter_readout, self.rerun())
self.update_progress(5)
@ -103,9 +103,11 @@ class ODMSplitStage(types.ODM_Stage):
if local_workflow:
for sp in submodel_paths:
log.ODM_INFO("Reconstructing %s" % sp)
OSFMContext(sp).reconstruct(self.rerun())
local_sp_octx = OSFMContext(sp)
local_sp_octx.create_tracks(self.rerun())
local_sp_octx.reconstruct(args.rolling_shutter, self.rerun())
else:
lre = LocalRemoteExecutor(args.sm_cluster, self.rerun())
lre = LocalRemoteExecutor(args.sm_cluster, args.rolling_shutter, self.rerun())
lre.set_projects([os.path.abspath(os.path.join(p, "..")) for p in submodel_paths])
lre.run_reconstruction()