kopia lustrzana https://github.com/OpenDroneMap/ODM
Merge pull request #1467 from pierotofy/rolling
OpenSfM Update + Rolling Shutter correctionpull/1474/head
commit
8b93e068fe
|
@ -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})
|
||||
|
|
|
@ -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-------------
|
||||
|
|
|
@ -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-------------
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
2.8.5
|
||||
2.8.6
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue