opensfm/large/tools.py (221 lines of code) (raw):
import itertools
import logging
from collections import namedtuple
from functools import lru_cache
import cv2
import networkx as nx
import numpy as np
import scipy.spatial as spatial
from networkx.algorithms import bipartite
from opensfm import align
from opensfm import context
from opensfm import dataset
from opensfm import geo
from opensfm import multiview
from opensfm import pybundle
from opensfm import reconstruction
logger = logging.getLogger(__name__)
PartialReconstruction = namedtuple("PartialReconstruction", ["submodel_path", "index"])
def kmeans(samples, nclusters, max_iter=100, attempts=20):
criteria = (cv2.TERM_CRITERIA_MAX_ITER, max_iter, 1.0)
flags = cv2.KMEANS_PP_CENTERS
if context.OPENCV3:
return cv2.kmeans(samples, nclusters, None, criteria, attempts, flags)
else:
return cv2.kmeans(samples, nclusters, criteria, attempts, flags)
def add_cluster_neighbors(positions, labels, centers, max_distance):
reflla = np.mean(positions, 0)
reference = geo.TopocentricConverter(reflla[0], reflla[1], 0)
topocentrics = []
for position in positions:
x, y, z = reference.to_topocentric(position[0], position[1], 0)
topocentrics.append([x, y])
topocentrics = np.array(topocentrics)
topo_tree = spatial.cKDTree(topocentrics)
clusters = []
for label in np.arange(centers.shape[0]):
cluster_indices = np.where(labels == label)[0]
neighbors = []
for i in cluster_indices:
neighbors.extend(topo_tree.query_ball_point(topocentrics[i], max_distance))
cluster = list(np.union1d(cluster_indices, neighbors))
clusters.append(cluster)
return clusters
def connected_reconstructions(reconstruction_shots):
g = nx.Graph()
for r in reconstruction_shots:
g.add_node(r, bipartite=0)
for shot_id in reconstruction_shots[r]:
g.add_node(shot_id, bipartite=1)
g.add_edge(r, shot_id)
p = bipartite.projected_graph(g, reconstruction_shots.keys())
return p.edges()
def scale_matrix(covariance):
try:
L = np.linalg.cholesky(covariance)
except Exception as e:
logger.error(
"Could not compute Cholesky of covariance matrix {}".format(covariance)
)
d = np.diag(np.diag(covariance).clip(1e-8, None))
L = np.linalg.cholesky(d)
return np.linalg.inv(L)
def invert_similarity(s, A, b):
s_inv = 1 / s
A_inv = A.T
b_inv = -s_inv * A_inv.dot(b)
return s_inv, A_inv, b_inv
def partial_reconstruction_name(key):
return str(key.submodel_path) + "_index" + str(key.index)
def add_camera_constraints_soft(ra, reconstruction_shots, reconstruction_name):
added_shots = set()
for key in reconstruction_shots:
shots = reconstruction_shots[key]
rec_name = reconstruction_name(key)
ra.add_reconstruction(rec_name, 0, 0, 0, 0, 0, 0, 1, False)
for shot_id in shots:
shot = shots[shot_id]
shot_name = str(shot_id)
R = shot.pose.rotation
t = shot.pose.translation
if shot_id not in added_shots:
ra.add_shot(shot_name, R[0], R[1], R[2], t[0], t[1], t[2], False)
gps = shot.metadata.gps_position.value
gps_sd = shot.metadata.gps_accuracy.value
ra.add_absolute_position_constraint(
shot_name, gps[0], gps[1], gps[2], gps_sd
)
added_shots.add(shot_id)
covariance = np.diag([1e-5, 1e-5, 1e-5, 1e-2, 1e-2, 1e-2])
sm = scale_matrix(covariance)
rmc = pybundle.RARelativeMotionConstraint(
rec_name, shot_name, R[0], R[1], R[2], t[0], t[1], t[2]
)
for i in range(6):
for j in range(6):
rmc.set_scale_matrix(i, j, sm[i, j])
ra.add_relative_motion_constraint(rmc)
def add_camera_constraints_hard(
ra, reconstruction_shots, reconstruction_name, add_common_camera_constraint
):
for key in reconstruction_shots:
shots = reconstruction_shots[key]
rec_name = reconstruction_name(key)
ra.add_reconstruction(rec_name, 0, 0, 0, 0, 0, 0, 1, False)
for shot_id in shots:
shot = shots[shot_id]
shot_name = rec_name + str(shot_id)
R = shot.pose.rotation
t = shot.pose.translation
ra.add_shot(shot_name, R[0], R[1], R[2], t[0], t[1], t[2], True)
gps = shot.metadata.gps_position.value
gps_sd = shot.metadata.gps_accuracy.value
ra.add_relative_absolute_position_constraint(
rec_name, shot_name, gps[0], gps[1], gps[2], gps_sd
)
if add_common_camera_constraint:
connections = connected_reconstructions(reconstruction_shots)
for connection in connections:
rec_name1 = reconstruction_name(connection[0])
rec_name2 = reconstruction_name(connection[1])
shots1 = reconstruction_shots[connection[0]]
shots2 = reconstruction_shots[connection[1]]
common_images = set(shots1.keys()).intersection(shots2.keys())
for image in common_images:
ra.add_common_camera_constraint(
rec_name1,
rec_name1 + str(image),
rec_name2,
rec_name2 + str(image),
1,
0.1,
)
@lru_cache(25)
def load_reconstruction(path, index):
d1 = dataset.DataSet(path)
r1 = d1.load_reconstruction()[index]
g1 = d1.load_tracks_manager()
return (path + ("_%s" % index)), (r1, g1)
def add_point_constraints(ra, reconstruction_shots, reconstruction_name):
connections = connected_reconstructions(reconstruction_shots)
for connection in connections:
i1, (r1, g1) = load_reconstruction(
connection[0].submodel_path, connection[0].index
)
i2, (r2, g2) = load_reconstruction(
connection[1].submodel_path, connection[1].index
)
rec_name1 = reconstruction_name(connection[0])
rec_name2 = reconstruction_name(connection[1])
scale_treshold = 1.3
treshold_in_meter = 0.3
minimum_inliers = 20
status, T, inliers = reconstruction.resect_reconstruction(
r1, r2, g1, g2, treshold_in_meter, minimum_inliers
)
if not status:
continue
s, R, t = multiview.decompose_similarity_transform(T)
if (
s > scale_treshold
or s < (1.0 / scale_treshold)
or len(inliers) < minimum_inliers
):
continue
for t1, t2 in inliers:
c1 = r1.points[t1].coordinates
c2 = r2.points[t2].coordinates
ra.add_common_point_constraint(
rec_name1, c1[0], c1[1], c1[2], rec_name2, c2[0], c2[1], c2[2], 1e-1
)
def load_reconstruction_shots(meta_data):
reconstruction_shots = {}
for submodel_path in meta_data.get_submodel_paths():
data = dataset.DataSet(submodel_path)
if not data.reconstruction_exists():
continue
reconstruction = data.load_reconstruction()
for index, partial_reconstruction in enumerate(reconstruction):
key = PartialReconstruction(submodel_path, index)
reconstruction_shots[key] = partial_reconstruction.shots
return reconstruction_shots
def align_reconstructions(
reconstruction_shots,
reconstruction_name,
use_points_constraints,
camera_constraint_type="soft_camera_constraint",
):
ra = pybundle.ReconstructionAlignment()
if camera_constraint_type == "soft_camera_constraint":
add_camera_constraints_soft(ra, reconstruction_shots, reconstruction_name)
if camera_constraint_type == "hard_camera_constraint":
add_camera_constraints_hard(ra, reconstruction_shots, reconstruction_name, True)
if use_points_constraints:
add_point_constraints(ra, reconstruction_shots, reconstruction_name)
logger.info("Running alignment")
ra.run()
logger.info(ra.brief_report())
transformations = {}
for key in reconstruction_shots:
rec_name = reconstruction_name(key)
r = ra.get_reconstruction(rec_name)
s = r.scale
A = cv2.Rodrigues(np.array([r.rx, r.ry, r.rz]))[0]
b = np.array([r.tx, r.ty, r.tz])
transformations[key] = invert_similarity(s, A, b)
return transformations
def apply_transformations(transformations):
submodels = itertools.groupby(
sorted(transformations.keys(), key=lambda key: key.submodel_path),
lambda key: key.submodel_path,
)
for submodel_path, keys in submodels:
data = dataset.DataSet(submodel_path)
if not data.reconstruction_exists():
continue
reconstruction = data.load_reconstruction()
for key in keys:
partial_reconstruction = reconstruction[key.index]
s, A, b = transformations[key]
align.apply_similarity(partial_reconstruction, s, A, b)
data.save_reconstruction(reconstruction, "reconstruction.aligned.json")