|
import cv2 |
|
import logging |
|
import numpy as np |
|
|
|
from hloc.utils.read_write_model import ( |
|
read_cameras_binary, |
|
read_images_binary, |
|
read_model, |
|
write_model, |
|
qvec2rotmat, |
|
read_images_text, |
|
read_cameras_text, |
|
) |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
def scale_sfm_images(full_model, scaled_model, image_dir): |
|
"""Duplicate the provided model and scale the camera intrinsics so that |
|
they match the original image resolution - makes everything easier. |
|
""" |
|
logger.info("Scaling the COLMAP model to the original image size.") |
|
scaled_model.mkdir(exist_ok=True) |
|
cameras, images, points3D = read_model(full_model) |
|
|
|
scaled_cameras = {} |
|
for id_, image in images.items(): |
|
name = image.name |
|
img = cv2.imread(str(image_dir / name)) |
|
assert img is not None, image_dir / name |
|
h, w = img.shape[:2] |
|
|
|
cam_id = image.camera_id |
|
if cam_id in scaled_cameras: |
|
assert scaled_cameras[cam_id].width == w |
|
assert scaled_cameras[cam_id].height == h |
|
continue |
|
|
|
camera = cameras[cam_id] |
|
assert camera.model == "SIMPLE_RADIAL" |
|
sx = w / camera.width |
|
sy = h / camera.height |
|
assert sx == sy, (sx, sy) |
|
scaled_cameras[cam_id] = camera._replace( |
|
width=w, height=h, params=camera.params * np.array([sx, sx, sy, 1.0]) |
|
) |
|
|
|
write_model(scaled_cameras, images, points3D, scaled_model) |
|
|
|
|
|
def create_query_list_with_intrinsics( |
|
model, out, list_file=None, ext=".bin", image_dir=None |
|
): |
|
"""Create a list of query images with intrinsics from the colmap model.""" |
|
if ext == ".bin": |
|
images = read_images_binary(model / "images.bin") |
|
cameras = read_cameras_binary(model / "cameras.bin") |
|
else: |
|
images = read_images_text(model / "images.txt") |
|
cameras = read_cameras_text(model / "cameras.txt") |
|
|
|
name2id = {image.name: i for i, image in images.items()} |
|
if list_file is None: |
|
names = list(name2id) |
|
else: |
|
with open(list_file, "r") as f: |
|
names = f.read().rstrip().split("\n") |
|
data = [] |
|
for name in names: |
|
image = images[name2id[name]] |
|
camera = cameras[image.camera_id] |
|
w, h, params = camera.width, camera.height, camera.params |
|
|
|
if image_dir is not None: |
|
|
|
img = cv2.imread(str(image_dir / name)) |
|
assert img is not None, image_dir / name |
|
h_orig, w_orig = img.shape[:2] |
|
assert camera.model == "SIMPLE_RADIAL" |
|
sx = w_orig / w |
|
sy = h_orig / h |
|
assert sx == sy, (sx, sy) |
|
w, h = w_orig, h_orig |
|
params = params * np.array([sx, sx, sy, 1.0]) |
|
|
|
p = [name, camera.model, w, h] + params.tolist() |
|
data.append(" ".join(map(str, p))) |
|
with open(out, "w") as f: |
|
f.write("\n".join(data)) |
|
|
|
|
|
def evaluate(model, results, list_file=None, ext=".bin", only_localized=False): |
|
predictions = {} |
|
with open(results, "r") as f: |
|
for data in f.read().rstrip().split("\n"): |
|
data = data.split() |
|
name = data[0] |
|
q, t = np.split(np.array(data[1:], float), [4]) |
|
predictions[name] = (qvec2rotmat(q), t) |
|
if ext == ".bin": |
|
images = read_images_binary(model / "images.bin") |
|
else: |
|
images = read_images_text(model / "images.txt") |
|
name2id = {image.name: i for i, image in images.items()} |
|
|
|
if list_file is None: |
|
test_names = list(name2id) |
|
else: |
|
with open(list_file, "r") as f: |
|
test_names = f.read().rstrip().split("\n") |
|
|
|
errors_t = [] |
|
errors_R = [] |
|
for name in test_names: |
|
if name not in predictions: |
|
if only_localized: |
|
continue |
|
e_t = np.inf |
|
e_R = 180.0 |
|
else: |
|
image = images[name2id[name]] |
|
R_gt, t_gt = image.qvec2rotmat(), image.tvec |
|
R, t = predictions[name] |
|
e_t = np.linalg.norm(-R_gt.T @ t_gt + R.T @ t, axis=0) |
|
cos = np.clip((np.trace(np.dot(R_gt.T, R)) - 1) / 2, -1.0, 1.0) |
|
e_R = np.rad2deg(np.abs(np.arccos(cos))) |
|
errors_t.append(e_t) |
|
errors_R.append(e_R) |
|
|
|
errors_t = np.array(errors_t) |
|
errors_R = np.array(errors_R) |
|
|
|
med_t = np.median(errors_t) |
|
med_R = np.median(errors_R) |
|
out = f"Results for file {results.name}:" |
|
out += f"\nMedian errors: {med_t:.3f}m, {med_R:.3f}deg" |
|
|
|
out += "\nPercentage of test images localized within:" |
|
threshs_t = [0.01, 0.02, 0.03, 0.05, 0.25, 0.5, 5.0] |
|
threshs_R = [1.0, 2.0, 3.0, 5.0, 2.0, 5.0, 10.0] |
|
for th_t, th_R in zip(threshs_t, threshs_R): |
|
ratio = np.mean((errors_t < th_t) & (errors_R < th_R)) |
|
out += f"\n\t{th_t*100:.0f}cm, {th_R:.0f}deg : {ratio*100:.2f}%" |
|
logger.info(out) |
|
|