Stability improvements, bug fixes, updated OpenMVS

pull/1210/head
Piero Toffanin 2020-12-05 15:15:07 -05:00
rodzic 7c42409567
commit 92d05243b1
6 zmienionych plików z 147 dodań i 150 usunięć

Wyświetl plik

@ -20,7 +20,7 @@ ExternalProject_Add(${_proj_name}
#--Download step--------------
DOWNLOAD_DIR ${SB_DOWNLOAD_DIR}
GIT_REPOSITORY https://github.com/OpenDroneMap/openMVS
GIT_TAG 210
GIT_TAG 230
#--Update/Patch step----------
UPDATE_COMMAND ""
#--Configure step-------------

Wyświetl plik

@ -24,7 +24,7 @@ def get_max_memory_mb(minimum = 100, use_at_most = 0.5):
"""
return max(minimum, (virtual_memory().available / 1024 / 1024) * use_at_most)
def parallel_map(func, items, max_workers=1):
def parallel_map(func, items, max_workers=1, single_thread_fallback=True):
"""
Our own implementation for parallel processing
which handles gracefully CTRL+C and reverts to
@ -85,7 +85,7 @@ def parallel_map(func, items, max_workers=1):
stop_workers()
if error is not None:
if error is not None and single_thread_fallback:
# Try to reprocess using a single thread
# in case this was a memory error
log.ODM_WARNING("Failed to run process in parallel, retrying with a single thread...")

Wyświetl plik

@ -5,6 +5,7 @@ import os
from opendm import dls
import numpy as np
from opendm import log
from opendm.concurrency import parallel_map
from opensfm.io import imread
from skimage import exposure
@ -270,7 +271,7 @@ def compute_band_maps(multi_camera, primary_band):
return s2p, p2s
def compute_alignment_matrices(multi_camera, primary_band_name, images_path, s2p, p2s, max_samples=9999):
def compute_alignment_matrices(multi_camera, primary_band_name, images_path, s2p, p2s, max_concurrency=1, max_samples=30):
log.ODM_INFO("Computing band alignment")
alignment_info = {}
@ -280,93 +281,129 @@ def compute_alignment_matrices(multi_camera, primary_band_name, images_path, s2p
if band['name'] != primary_band_name:
matrices = []
# if band['name'] != "NIR":
# continue # TODO REMOVE
def parallel_compute_homography(p):
try:
if len(matrices) >= max_samples:
log.ODM_INFO("Got enough samples for %s (%s)" % (band['name'], max_samples))
return
# Find good matrix candidates for alignment
for p in band['photos']:
primary_band_photo = s2p.get(p.filename)
if primary_band_photo is None:
log.ODM_WARNING("Cannot find primary band photo for %s" % p.filename)
continue
# Find good matrix candidates for alignment
warp_matrix, score, dimension = compute_homography(os.path.join(images_path, p.filename),
os.path.join(images_path, primary_band_photo.filename))
primary_band_photo = s2p.get(p['filename'])
if primary_band_photo is None:
log.ODM_WARNING("Cannot find primary band photo for %s" % p['filename'])
return
if warp_matrix is not None:
log.ODM_INFO("%s --> %s good match (score: %s)" % (p.filename, primary_band_photo.filename, score))
matrices.append({
'warp_matrix': warp_matrix,
'score': score,
'dimension': dimension
})
else:
log.ODM_INFO("%s --> %s cannot be matched" % (p.filename, primary_band_photo.filename))
warp_matrix, dimension, algo = compute_homography(os.path.join(images_path, p['filename']),
os.path.join(images_path, primary_band_photo.filename))
if len(matrices) >= max_samples:
log.ODM_INFO("Got enough samples for %s (%s)" % (band['name'], max_samples))
break
if warp_matrix is not None:
log.ODM_INFO("%s --> %s good match" % (p['filename'], primary_band_photo.filename))
matrices.append({
'warp_matrix': warp_matrix,
'eigvals': np.linalg.eigvals(warp_matrix),
'dimension': dimension,
'algo': algo
})
else:
log.ODM_INFO("%s --> %s cannot be matched" % (p['filename'], primary_band_photo.filename))
except Exception as e:
log.ODM_WARNING("Failed to compute homography for %s: %s" % (p['filename'], str(e)))
parallel_map(parallel_compute_homography, [{'filename': p.filename} for p in band['photos']], max_concurrency, single_thread_fallback=False)
# Choose winning algorithm (doesn't seem to yield improvements)
# feat_count = 0
# ecc_count = 0
# for m in matrices:
# if m['algo'] == 'feat':
# feat_count += 1
# if m['algo'] == 'ecc':
# ecc_count += 1
# algo = 'feat' if feat_count >= ecc_count else 'ecc'
# log.ODM_INFO("Feat: %s | ECC: %s | Winner: %s" % (feat_count, ecc_count, algo))
# matrices = [m for m in matrices if m['algo'] == algo]
# Find the matrix that has the most common eigvals
# among all matrices. That should be the "best" alignment.
for m1 in matrices:
acc = np.array([0.0,0.0,0.0])
e = m1['eigvals']
for m2 in matrices:
acc += abs(e - m2['eigvals'])
m1['score'] = acc.sum()
# Sort
matrices.sort(key=lambda x: x['score'], reverse=False)
if len(matrices) > 0:
alignment_info[band['name']] = matrices[0]
print(matrices[0])
log.ODM_INFO("%s band will be aligned using warp matrix %s (score: %s)" % (band['name'], matrices[0]['warp_matrix'], matrices[0]['score']))
else:
log.ODM_WARNING("Cannot find alignment matrix for band %s, The band will likely be misaligned!" % band['name'])
return alignment_info
def compute_homography(image_filename, align_image_filename):
# try:
# Convert images to grayscale if needed
image = imread(image_filename, unchanged=True, anydepth=True)
if image.shape[2] == 3:
image_gray = to_8bit(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
else:
image_gray = to_8bit(image[:,:,0])
try:
# Convert images to grayscale if needed
image = imread(image_filename, unchanged=True, anydepth=True)
if image.shape[2] == 3:
image_gray = to_8bit(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
else:
image_gray = to_8bit(image[:,:,0])
align_image = imread(align_image_filename, unchanged=True, anydepth=True)
if align_image.shape[2] == 3:
align_image_gray = to_8bit(cv2.cvtColor(align_image, cv2.COLOR_BGR2GRAY))
else:
align_image_gray = to_8bit(align_image[:,:,0])
align_image = imread(align_image_filename, unchanged=True, anydepth=True)
if align_image.shape[2] == 3:
align_image_gray = to_8bit(cv2.cvtColor(align_image, cv2.COLOR_BGR2GRAY))
else:
align_image_gray = to_8bit(align_image[:,:,0])
def compute_using(algorithm):
h = algorithm(image_gray, align_image_gray)
if h is None:
return None, None, (None, None)
def compute_using(algorithm):
h = algorithm(image_gray, align_image_gray)
if h is None:
return None, (None, None)
det = np.linalg.det(h)
det = np.linalg.det(h)
# Check #1 homography's determinant will not be close to zero
if abs(det) < 0.25:
return None, (None, None)
# Check #2 the ratio of the first-to-last singular value is sane (not too high)
svd = np.linalg.svd(h, compute_uv=False)
if svd[-1] == 0:
return None, (None, None)
ratio = svd[0] / svd[-1]
if ratio > 100000:
return None, (None, None)
return h, (align_image_gray.shape[1], align_image_gray.shape[0])
# Check #1 homography's determinant will not be close to zero
if abs(det) < 0.25:
return None, None, (None, None)
algo = 'feat'
result = compute_using(find_features_homography)
# Check #2 the ratio of the first-to-last singular value is sane (not too high)
svd = np.linalg.svd(h, compute_uv=False)
if svd[-1] == 0:
return None, None, (None, None)
if result[0] is None:
algo = 'ecc'
log.ODM_INFO("Can't use features matching, will use ECC (this might take a bit)")
result = compute_using(find_ecc_homography)
if result[0] is None:
algo = None
ratio = svd[0] / svd[-1]
if ratio > 100000:
return None, None, (None, None)
warp_matrix, dimension = result
return warp_matrix, dimension, algo
return h, compute_alignment_score(h, image_gray, align_image_gray), (align_image_gray.shape[1], align_image_gray.shape[0])
result = compute_using(find_features_homography)
if result[0] is None:
log.ODM_INFO("Can't use features matching, will use ECC")
result = compute_using(find_ecc_homography)
return result
except Exception as e:
log.ODM_WARNING("Compute homography: %s" % str(e))
return None, None, (None, None)
# except Exception as e:
# log.ODM_WARNING("Compute homography: %s" % str(e))
# return None, None, (None, None)
def find_ecc_homography(image_gray, align_image_gray, number_of_iterations=5000, termination_eps=1e-8):
def find_ecc_homography(image_gray, align_image_gray, number_of_iterations=2500, termination_eps=1e-9):
image_gray = to_8bit(gradient(gaussian(image_gray)))
align_image_gray = to_8bit(gradient(gaussian(align_image_gray)))
@ -377,10 +414,7 @@ def find_ecc_homography(image_gray, align_image_gray, number_of_iterations=5000,
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,
number_of_iterations, termination_eps)
try:
(cc, warp_matrix) = cv2.findTransformECC (image_gray,align_image_gray,warp_matrix, cv2.MOTION_HOMOGRAPHY, criteria, inputMask=None, gaussFiltSize=1)
except:
(cc, warp_matrix) = cv2.findTransformECC (image_gray,align_image_gray,warp_matrix, cv2.MOTION_HOMOGRAPHY, criteria)
_, warp_matrix = cv2.findTransformECC (image_gray,align_image_gray,warp_matrix, cv2.MOTION_HOMOGRAPHY, criteria, inputMask=None, gaussFiltSize=9)
return warp_matrix
@ -418,37 +452,6 @@ def find_features_homography(image_gray, align_image_gray, feature_retention=0.2
h, _ = cv2.findHomography(points_image, points_align_image, cv2.RANSAC)
return h
def compute_alignment_score(warp_matrix, image_gray, align_image_gray, apply_gradient=True):
projected = align_image(image_gray, warp_matrix, (align_image_gray.shape[1], align_image_gray.shape[0]))
borders = projected==0
if apply_gradient:
image_gray = to_8bit(gradient(gaussian(image_gray)))
align_image_gray = to_8bit(gradient(gaussian(align_image_gray)))
# cv2.imwrite("/datasets/micasense/opensfm/undistorted/align_image_gray.jpg", align_image_gray)
# cv2.imwrite("/datasets/micasense/opensfm/undistorted/projected.jpg", projected)
# Threshold
align_image_gray[align_image_gray > 128] = 255
projected[projected > 128] = 255
align_image_gray[align_image_gray <= 128] = 0
projected[projected <= 128] = 0
# Mark borders
align_image_gray[borders] = 0
projected[borders] = 255
# cv2.imwrite("/datasets/micasense/opensfm/undistorted/threshold_align_image_gray.jpg", align_image_gray)
# cv2.imwrite("/datasets/micasense/opensfm/undistorted/threshold_projected.jpg", projected)
# cv2.imwrite("/datasets/micasense/opensfm/undistorted/delta.jpg", projected - align_image_gray)
# Compute delta --> the more the images overlap perfectly, the lower the score
return (projected - align_image_gray).sum()
def gradient(im, ksize=5):
im = local_normalize(im)
grad_x = cv2.Sobel(im,cv2.CV_32F,1,0,ksize=ksize)

Wyświetl plik

@ -315,11 +315,11 @@ class OSFMContext:
else:
log.ODM_INFO("Already extracted cameras")
def convert_and_undistort(self, rerun=False, imageFilter=None, image_list=None):
def convert_and_undistort(self, rerun=False, imageFilter=None, image_list=None, runId="nominal"):
log.ODM_INFO("Undistorting %s ..." % self.opensfm_project_path)
undistorted_images_path = self.path("undistorted", "images")
done_flag_file = self.path("undistorted", "%s_done.txt" % runId)
if not io.dir_exists(undistorted_images_path) or rerun:
if not io.file_exists(done_flag_file) or rerun:
ds = DataSet(self.opensfm_project_path)
if image_list is not None:
@ -327,32 +327,35 @@ class OSFMContext:
undistort.run_dataset(ds, "reconstruction.json",
0, None, "undistorted", imageFilter)
self.touch(done_flag_file)
else:
log.ODM_WARNING("Found an undistorted directory in %s" % undistorted_images_path)
log.ODM_WARNING("Already undistorted (%s)" % runId)
def restore_reconstruction_backup(self):
if os.path.exists(self.recon_backup_file()):
# This time export the actual reconstruction.json
# (containing only the primary band)
if os.path.exists(self.recon_file()):
os.remove(self.recon_file())
os.rename(self.recon_backup_file(), self.recon_file())
log.ODM_INFO("Restored reconstruction.json")
def backup_reconstruction(self):
if os.path.exists(self.recon_backup_file()):
os.remove(self.recon_backup_file())
log.ODM_INFO("Backing up reconstruction")
shutil.copyfile(self.recon_file(), self.recon_backup_file())
def recon_backup_file(self):
return self.path("reconstruction.backup.json")
def recon_file(self):
return self.path("reconstruction.json")
def add_shots_to_reconstruction(self, p2s):
recon_file = self.path("reconstruction.json")
recon_backup_file = self.path("reconstruction.backup.json")
# tracks_file = self.path("tracks.csv")
# tracks_backup_file = self.path("tracks.backup.csv")
# image_list_file = self.path("image_list.txt")
# image_list_backup_file = self.path("image_list.backup.txt")
log.ODM_INFO("Adding shots to reconstruction")
if os.path.exists(recon_backup_file):
os.remove(recon_backup_file)
# if os.path.exists(tracks_backup_file):
# os.remove(tracks_backup_file)
# if os.path.exists(image_list_backup_file):
# os.remove(image_list_backup_file)
log.ODM_INFO("Backing up reconstruction, tracks, image list")
shutil.copyfile(recon_file, recon_backup_file)
# shutil.copyfile(tracks_file, tracks_backup_file)
# shutil.copyfile(image_list_file, image_list_backup_file)
with open(recon_file) as f:
with open(self.recon_file()) as f:
reconstruction = json.loads(f.read())
# Augment reconstruction.json
@ -369,13 +372,9 @@ class OSFMContext:
for p in secondary_photos:
shots[p.filename] = shots[shot_id]
with open(recon_file, 'w') as f:
with open(self.recon_file(), 'w') as f:
f.write(json.dumps(reconstruction))
return True #(recon_file, recon_backup_file)
# (tracks_file, tracks_backup_file),
# (image_list_file, image_list_backup_file)]
def update_config(self, cfg_dict):
cfg_file = self.get_config_file_path()

Wyświetl plik

@ -23,6 +23,7 @@ PyYAML==5.1
rasterio==1.1.8
repoze.lru==0.7
scikit-learn==0.23.2
scikit-image==0.17.2
scipy==1.5.4
xmltodict==0.12.0

Wyświetl plik

@ -91,7 +91,6 @@ class ODMOpenSfMStage(types.ODM_Stage):
log.ODM_WARNING("Cannot align %s, no alignment matrix could be computed. Band alignment quality might be affected." % (shot_id))
return image
if args.radiometric_calibration != "none":
undistort_pipeline.append(radiometric_calibrate)
@ -109,16 +108,16 @@ class ODMOpenSfMStage(types.ODM_Stage):
# We finally restore the original files later
added_shots_file = octx.path('added_shots_done.txt')
if not io.file_exists(added_shots_file) or self.rerun():
primary_band_name = multispectral.get_primary_band_name(reconstruction.multi_camera, args.primary_band)
s2p, p2s = multispectral.compute_band_maps(reconstruction.multi_camera, primary_band_name)
alignment_info = multispectral.compute_alignment_matrices(reconstruction.multi_camera, primary_band_name, tree.dataset_raw, s2p, p2s)
alignment_info = multispectral.compute_alignment_matrices(reconstruction.multi_camera, primary_band_name, tree.dataset_raw, s2p, p2s, max_concurrency=args.max_concurrency)
log.ODM_INFO("Adding shots to reconstruction")
octx.backup_reconstruction()
octx.add_shots_to_reconstruction(p2s)
# TODO: what happens to reconstruction.backup.json
# if the process fails here?
octx.touch(added_shots_file)
undistort_pipeline.append(align_to_primary_band)
@ -148,16 +147,8 @@ class ODMOpenSfMStage(types.ODM_Stage):
else:
log.ODM_WARNING("Found a valid NVM file in %s for %s band" % (nvm_file, band['name']))
recon_file = octx.path("reconstruction.json")
recon_backup_file = octx.path("reconstruction.backup.json")
if os.path.exists(recon_backup_file):
# This time export the actual reconstruction.json
# (containing only the primary band)
if os.path.exists(recon_backup_file):
os.remove(recon_file)
os.rename(recon_backup_file, recon_file)
octx.convert_and_undistort(self.rerun(), undistort_callback)
octx.restore_reconstruction_backup()
octx.convert_and_undistort(self.rerun(), undistort_callback, runId='primary')
if not io.file_exists(tree.opensfm_reconstruction_nvm) or self.rerun():
octx.run('export_visualsfm --points')
@ -194,6 +185,9 @@ class ODMOpenSfMStage(types.ODM_Stage):
if args.optimize_disk_space:
os.remove(octx.path("tracks.csv"))
if io.file_exists(octx.recon_backup_file()):
os.remove(octx.recon_backup_file())
if io.dir_exists(octx.path("undistorted", "depthmaps")):
files = glob.glob(octx.path("undistorted", "depthmaps", "*.npz"))
for f in files: