Dependencies¶

In [1]:
!python -m pip install --no-deps /kaggle/input/dependencies-imc/pycolmap/pycolmap-0.4.0-cp310-cp310-manylinux2014_x86_64.whl
!python -m pip install --no-deps /kaggle/input/dependencies-imc/safetensors/safetensors-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
!python -m pip install --no-index --find-links=/kaggle/input/dependencies-imc/transformers/ transformers > /dev/null
!python -m pip install  --no-deps /kaggle/input/imc2024-packages-lightglue-rerun-kornia/lightglue-0.0-py3-none-any.whl

# dkm
!python -m pip install --no-index --find-links=/kaggle/input/dkm-dependencies/packages einops > /dev/null
Processing /kaggle/input/dependencies-imc/pycolmap/pycolmap-0.4.0-cp310-cp310-manylinux2014_x86_64.whl

Installing collected packages: pycolmap

Successfully installed pycolmap-0.4.0

Processing /kaggle/input/dependencies-imc/safetensors/safetensors-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Installing collected packages: safetensors

  Attempting uninstall: safetensors

    Found existing installation: safetensors 0.4.3

    Uninstalling safetensors-0.4.3:

      Successfully uninstalled safetensors-0.4.3

Successfully installed safetensors-0.4.1

Processing /kaggle/input/imc2024-packages-lightglue-rerun-kornia/lightglue-0.0-py3-none-any.whl

Installing collected packages: lightglue

Successfully installed lightglue-0.0

In [2]:
# lightglue models
!mkdir -p /root/.cache/torch/hub/checkpoints
!cp /kaggle/input/aliked/pytorch/aliked-n16/1/* /root/.cache/torch/hub/checkpoints/
!cp /kaggle/input/lightglue/pytorch/aliked/1/* /root/.cache/torch/hub/checkpoints/
!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/aliked_lightglue_v0-1_arxiv-pth
!cp /kaggle/input/pytorch-lightglue-models/* /root/.cache/torch/hub/checkpoints/

# dkm model
!mkdir -p /root/.cache/torch/hub/checkpoints
!cp /kaggle/input/dkm-dependencies/DKMv3_outdoor.pth /root/.cache/torch/hub/checkpoints/
In [3]:
%matplotlib inline
In [4]:
# General utilities
import os
from tqdm import tqdm
from time import time
from fastprogress import progress_bar
import gc
import numpy as np
import pandas as pd
import h5py
from IPython.display import clear_output
from collections import defaultdict
from copy import deepcopy

# CV/ML
import cv2
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import kornia as K
import kornia.feature as KF
from PIL import Image
import timm
from timm.data import resolve_data_config
from timm.data.transforms_factory import create_transform

import torchvision

# 3D reconstruction
import pycolmap

import glob
import matplotlib
from matplotlib import pyplot as plt

# dkm
import sys
sys.path.append('/kaggle/input/dkm-dependencies/DKM/')
from dkm.utils.utils import tensor_to_pil, get_tuple_transform_ops
from dkm import DKMv3_outdoor

# LoFTR
from kornia.feature import LoFTR
In [5]:
from lightglue import match_pair
from lightglue import ALIKED, SuperPoint, DoGHardNet, LightGlue
from lightglue.utils import load_image, rbd
from kornia.feature import LoFTR
In [6]:
print('Kornia version', K.__version__)
print('Pycolmap version', pycolmap.__version__)
Kornia version 0.7.2
Pycolmap version 0.4.0

Configurations¶

In [7]:
class CONFIG:
    use_aliked_lightglue = True
    use_doghardnet_lightglue = False
    use_superpoint_lightglue = True
    use_loftr = False
    use_dkm = False
    use_superglue = False
    
    params_aliked_lightglue = {
        "num_features" : 8192,
        "detection_threshold" : 0.005,
        "min_matches" : 100,
        "resize_to" : 1280,
    }
    
    params_doghardnet_lightglue = {
        "num_features" : 8192,
        "detection_threshold" : 0.001,
        "min_matches" : 15,
        "resize_to" : 1024,
    }
    
    params_superpoint_lightglue = {
        "num_features" : 4096,
        "detection_threshold" : 0.005,
        "min_matches" : 15,
        "resize_to" : 1280,
    }
    
    params_loftr = {
        "resize_small_edge_to" : 750,
        "min_matches" : 15,
    }
    
    params_dkm = {
        "num_features" : 2048,
        "detection_threshold" : 0.4,
        "min_matches" : 15,
        "resize_to" : (540, 720),    
    }
    
    params_sg = {
        "sg_config" : 
        {
            "superpoint": {
                "nms_radius": 4, 
                "keypoint_threshold": 0.0001,
                "max_keypoints": 4096
            },
            "superglue": {
                "weights": "outdoor",
                "sinkhorn_iterations": 10,
                "match_threshold": 0.2,
            },
        },
        "resize_to": 1240,
        "min_matches": 15,
    }
In [8]:
device=torch.device('cuda')

COLMAP utilities¶

In [9]:
# Code to manipulate a colmap database.
# Forked from https://github.com/colmap/colmap/blob/dev/scripts/python/database.py

# Copyright (c) 2018, ETH Zurich and UNC Chapel Hill.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#
#     * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
#       its contributors may be used to endorse or promote products derived
#       from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

# This script is based on an original implementation by True Price.

import sys
import sqlite3
import numpy as np


IS_PYTHON3 = sys.version_info[0] >= 3

MAX_IMAGE_ID = 2**31 - 1

CREATE_CAMERAS_TABLE = """CREATE TABLE IF NOT EXISTS cameras (
    camera_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    model INTEGER NOT NULL,
    width INTEGER NOT NULL,
    height INTEGER NOT NULL,
    params BLOB,
    prior_focal_length INTEGER NOT NULL)"""

CREATE_DESCRIPTORS_TABLE = """CREATE TABLE IF NOT EXISTS descriptors (
    image_id INTEGER PRIMARY KEY NOT NULL,
    rows INTEGER NOT NULL,
    cols INTEGER NOT NULL,
    data BLOB,
    FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)"""

CREATE_IMAGES_TABLE = """CREATE TABLE IF NOT EXISTS images (
    image_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    name TEXT NOT NULL UNIQUE,
    camera_id INTEGER NOT NULL,
    prior_qw REAL,
    prior_qx REAL,
    prior_qy REAL,
    prior_qz REAL,
    prior_tx REAL,
    prior_ty REAL,
    prior_tz REAL,
    CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}),
    FOREIGN KEY(camera_id) REFERENCES cameras(camera_id))
""".format(MAX_IMAGE_ID)

CREATE_TWO_VIEW_GEOMETRIES_TABLE = """
CREATE TABLE IF NOT EXISTS two_view_geometries (
    pair_id INTEGER PRIMARY KEY NOT NULL,
    rows INTEGER NOT NULL,
    cols INTEGER NOT NULL,
    data BLOB,
    config INTEGER NOT NULL,
    F BLOB,
    E BLOB,
    H BLOB)
"""

CREATE_KEYPOINTS_TABLE = """CREATE TABLE IF NOT EXISTS keypoints (
    image_id INTEGER PRIMARY KEY NOT NULL,
    rows INTEGER NOT NULL,
    cols INTEGER NOT NULL,
    data BLOB,
    FOREIGN KEY(image_id) REFERENCES images(image_id) ON DELETE CASCADE)
"""

CREATE_MATCHES_TABLE = """CREATE TABLE IF NOT EXISTS matches (
    pair_id INTEGER PRIMARY KEY NOT NULL,
    rows INTEGER NOT NULL,
    cols INTEGER NOT NULL,
    data BLOB)"""

CREATE_NAME_INDEX = \
    "CREATE UNIQUE INDEX IF NOT EXISTS index_name ON images(name)"

CREATE_ALL = "; ".join([
    CREATE_CAMERAS_TABLE,
    CREATE_IMAGES_TABLE,
    CREATE_KEYPOINTS_TABLE,
    CREATE_DESCRIPTORS_TABLE,
    CREATE_MATCHES_TABLE,
    CREATE_TWO_VIEW_GEOMETRIES_TABLE,
    CREATE_NAME_INDEX
])


def image_ids_to_pair_id(image_id1, image_id2):
    if image_id1 > image_id2:
        image_id1, image_id2 = image_id2, image_id1
    return image_id1 * MAX_IMAGE_ID + image_id2


def pair_id_to_image_ids(pair_id):
    image_id2 = pair_id % MAX_IMAGE_ID
    image_id1 = (pair_id - image_id2) / MAX_IMAGE_ID
    return image_id1, image_id2


def array_to_blob(array):
    if IS_PYTHON3:
        return array.tostring()
    else:
        return np.getbuffer(array)


def blob_to_array(blob, dtype, shape=(-1,)):
    if IS_PYTHON3:
        return np.fromstring(blob, dtype=dtype).reshape(*shape)
    else:
        return np.frombuffer(blob, dtype=dtype).reshape(*shape)


class COLMAPDatabase(sqlite3.Connection):

    @staticmethod
    def connect(database_path):
        return sqlite3.connect(database_path, factory=COLMAPDatabase)


    def __init__(self, *args, **kwargs):
        super(COLMAPDatabase, self).__init__(*args, **kwargs)

        self.create_tables = lambda: self.executescript(CREATE_ALL)
        self.create_cameras_table = \
            lambda: self.executescript(CREATE_CAMERAS_TABLE)
        self.create_descriptors_table = \
            lambda: self.executescript(CREATE_DESCRIPTORS_TABLE)
        self.create_images_table = \
            lambda: self.executescript(CREATE_IMAGES_TABLE)
        self.create_two_view_geometries_table = \
            lambda: self.executescript(CREATE_TWO_VIEW_GEOMETRIES_TABLE)
        self.create_keypoints_table = \
            lambda: self.executescript(CREATE_KEYPOINTS_TABLE)
        self.create_matches_table = \
            lambda: self.executescript(CREATE_MATCHES_TABLE)
        self.create_name_index = lambda: self.executescript(CREATE_NAME_INDEX)

    def add_camera(self, model, width, height, params,
                   prior_focal_length=False, camera_id=None):
        params = np.asarray(params, np.float64)
        cursor = self.execute(
            "INSERT INTO cameras VALUES (?, ?, ?, ?, ?, ?)",
            (camera_id, model, width, height, array_to_blob(params),
             prior_focal_length))
        return cursor.lastrowid

    def add_image(self, name, camera_id,
                  prior_q=np.zeros(4), prior_t=np.zeros(3), image_id=None):
        cursor = self.execute(
            "INSERT INTO images VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (image_id, name, camera_id, prior_q[0], prior_q[1], prior_q[2],
             prior_q[3], prior_t[0], prior_t[1], prior_t[2]))
        return cursor.lastrowid

    def add_keypoints(self, image_id, keypoints):
        assert(len(keypoints.shape) == 2)
        assert(keypoints.shape[1] in [2, 4, 6])

        keypoints = np.asarray(keypoints, np.float32)
        self.execute(
            "INSERT INTO keypoints VALUES (?, ?, ?, ?)",
            (image_id,) + keypoints.shape + (array_to_blob(keypoints),))

    def add_descriptors(self, image_id, descriptors):
        descriptors = np.ascontiguousarray(descriptors, np.uint8)
        self.execute(
            "INSERT INTO descriptors VALUES (?, ?, ?, ?)",
            (image_id,) + descriptors.shape + (array_to_blob(descriptors),))

    def add_matches(self, image_id1, image_id2, matches):
        assert(len(matches.shape) == 2)
        assert(matches.shape[1] == 2)

        if image_id1 > image_id2:
            matches = matches[:,::-1]

        pair_id = image_ids_to_pair_id(image_id1, image_id2)
        matches = np.asarray(matches, np.uint32)
        self.execute(
            "INSERT INTO matches VALUES (?, ?, ?, ?)",
            (pair_id,) + matches.shape + (array_to_blob(matches),))

    def add_two_view_geometry(self, image_id1, image_id2, matches,
                              F=np.eye(3), E=np.eye(3), H=np.eye(3), config=2):
        assert(len(matches.shape) == 2)
        assert(matches.shape[1] == 2)

        if image_id1 > image_id2:
            matches = matches[:,::-1]

        pair_id = image_ids_to_pair_id(image_id1, image_id2)
        matches = np.asarray(matches, np.uint32)
        F = np.asarray(F, dtype=np.float64)
        E = np.asarray(E, dtype=np.float64)
        H = np.asarray(H, dtype=np.float64)
        self.execute(
            "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            (pair_id,) + matches.shape + (array_to_blob(matches), config,
             array_to_blob(F), array_to_blob(E), array_to_blob(H)))

h5 to colmap db¶

In [10]:
# Code to interface DISK with Colmap.
# Forked from https://github.com/cvlab-epfl/disk/blob/37f1f7e971cea3055bb5ccfc4cf28bfd643fa339/colmap/h5_to_db.py

#  Copyright [2020] [Michał Tyszkiewicz, Pascal Fua, Eduard Trulls]
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import os, argparse, h5py, warnings
import numpy as np
from tqdm import tqdm
from PIL import Image, ExifTags


def get_focal(image_path, err_on_default=False):
    image         = Image.open(image_path)
    max_size      = max(image.size)

    exif = image.getexif()
    focal = None
    if exif is not None:
        focal_35mm = None
        # https://github.com/colmap/colmap/blob/d3a29e203ab69e91eda938d6e56e1c7339d62a99/src/util/bitmap.cc#L299
        for tag, value in exif.items():
            focal_35mm = None
            if ExifTags.TAGS.get(tag, None) == 'FocalLengthIn35mmFilm':
                focal_35mm = float(value)
                break

        if focal_35mm is not None:
            focal = focal_35mm / 35. * max_size
    
    if focal is None:
        if err_on_default:
            raise RuntimeError("Failed to find focal length")

        # failed to find it in exif, use prior
        FOCAL_PRIOR = 1.2
        focal = FOCAL_PRIOR * max_size

    return focal

def create_camera(db, image_path, camera_model):
    image         = Image.open(image_path)
    width, height = image.size

    focal = get_focal(image_path)

    if camera_model == 'simple-pinhole':
        model = 0 # simple pinhole
        param_arr = np.array([focal, width / 2, height / 2])
    if camera_model == 'pinhole':
        model = 1 # pinhole
        param_arr = np.array([focal, focal, width / 2, height / 2])
    elif camera_model == 'simple-radial':
        model = 2 # simple radial
        param_arr = np.array([focal, width / 2, height / 2, 0.1])
    elif camera_model == 'opencv':
        model = 4 # opencv
        param_arr = np.array([focal, focal, width / 2, height / 2, 0., 0., 0., 0.])
         
    return db.add_camera(model, width, height, param_arr)


def add_keypoints(db, h5_path, image_path, img_ext, camera_model, single_camera = True):
    keypoint_f = h5py.File(os.path.join(h5_path, 'keypoints.h5'), 'r')

    camera_id = None
    fname_to_id = {}
    for filename in tqdm(list(keypoint_f.keys())):
        keypoints = keypoint_f[filename][()]

        fname_with_ext = filename# + img_ext
        path = os.path.join(image_path, fname_with_ext)
        if not os.path.isfile(path):
            raise IOError(f'Invalid image path {path}')

        if camera_id is None or not single_camera:
            camera_id = create_camera(db, path, camera_model)
        image_id = db.add_image(fname_with_ext, camera_id)
        fname_to_id[filename] = image_id

        db.add_keypoints(image_id, keypoints)

    return fname_to_id

def add_matches(db, h5_path, fname_to_id):
    match_file = h5py.File(os.path.join(h5_path, 'matches.h5'), 'r')
    
    added = set()
    n_keys = len(match_file.keys())
    n_total = (n_keys * (n_keys - 1)) // 2

    with tqdm(total=n_total) as pbar:
        for key_1 in match_file.keys():
            group = match_file[key_1]
            for key_2 in group.keys():
                id_1 = fname_to_id[key_1]
                id_2 = fname_to_id[key_2]

                pair_id = image_ids_to_pair_id(id_1, id_2)
                if pair_id in added:
                    warnings.warn(f'Pair {pair_id} ({id_1}, {id_2}) already added!')
                    continue
            
                matches = group[key_2][()]
                db.add_matches(id_1, id_2, matches)

                added.add(pair_id)

                pbar.update(1)
                
def import_into_colmap(img_dir,
                       feature_dir ='.featureout',
                       database_path = 'colmap.db',
                       img_ext='.jpg'):
    db = COLMAPDatabase.connect(database_path)
    db.create_tables()
    single_camera = False
    fname_to_id = add_keypoints(db, feature_dir, img_dir, img_ext, 'simple-radial', single_camera)
    add_matches(
        db,
        feature_dir,
        fname_to_id,
    )

    db.commit()
    return

Image Pairs¶

In [11]:
# We will use ViT global descriptor to get matching shortlists.
def get_global_desc(fnames, model,
                    device =  torch.device('cpu')):
    model = model.eval()
    model= model.to(device)
    config = resolve_data_config({}, model=model)
    transform = create_transform(**config)
    global_descs_convnext=[]
    for i, img_fname_full in tqdm(enumerate(fnames),total= len(fnames)):
        key = os.path.splitext(os.path.basename(img_fname_full))[0]
        img = Image.open(img_fname_full).convert('RGB')
        timg = transform(img).unsqueeze(0).to(device)
        with torch.no_grad(), torch.cuda.amp.autocast():
            desc = model.forward_features(timg.to(device)).mean(dim=(-1,2))#
            #print (desc.shape)
            desc = desc.view(1, -1)
            desc_norm = F.normalize(desc, dim=1, p=2)
        #print (desc_norm)
        global_descs_convnext.append(desc_norm.detach().cpu())
    global_descs_all = torch.cat(global_descs_convnext, dim=0)
    return global_descs_all.to(torch.float32)

def convert_1d_to_2d(idx, num_images):
    idx1 = idx // num_images
    idx2 = idx % num_images
    return (idx1, idx2)

def get_pairs_from_distancematrix(mat):
    pairs = [ convert_1d_to_2d(idx, mat.shape[0]) for idx in np.argsort(mat.flatten())]
    pairs = [ pair for pair in pairs if pair[0] < pair[1] ]
    return pairs

def get_img_pairs_exhaustive(img_fnames, model, device):
    #index_pairs = []
    #for i in range(len(img_fnames)):
    #    for j in range(i+1, len(img_fnames)):
    #        index_pairs.append((i,j))
    #return index_pairs
    descs = get_global_desc(img_fnames, model, device=device)
    dm = torch.cdist(descs, descs, p=2).detach().cpu().numpy()
    matching_list = get_pairs_from_distancematrix(dm)
    return matching_list


def get_image_pairs_shortlist(fnames,
                              sim_th = 0.6, # should be strict
                              min_pairs = 20,
                              exhaustive_if_less = 20,
                              device=torch.device('cpu')):
    num_imgs = len(fnames)

    model = timm.create_model('tf_efficientnet_b7',
                              checkpoint_path='/kaggle/input/tf-efficientnet/pytorch/tf-efficientnet-b7/1/tf_efficientnet_b7_ra-6c08e654.pth')
    model.eval()
    descs = get_global_desc(fnames, model, device=device)

    if num_imgs <= exhaustive_if_less:
        return get_img_pairs_exhaustive(fnames, model, device)
    
    dm = torch.cdist(descs, descs, p=2).detach().cpu().numpy()
    # removing half
    mask = dm <= sim_th
    total = 0
    matching_list = []
    ar = np.arange(num_imgs)
    already_there_set = []
    for st_idx in range(num_imgs-1):
        mask_idx = mask[st_idx]
        to_match = ar[mask_idx]
        if len(to_match) < min_pairs:
            to_match = np.argsort(dm[st_idx])[:min_pairs]  
        for idx in to_match:
            if st_idx == idx:
                continue
            if dm[st_idx, idx] < 1000:
                matching_list.append(tuple(sorted((st_idx, idx.item()))))
                total+=1
    matching_list = sorted(list(set(matching_list)))
    return matching_list

Keypoints: LightGlue series¶

In [12]:
def load_torch_image(fname, device=torch.device('cpu')):
    img = K.io.load_image(fname, K.io.ImageLoadType.RGB32, device=device)[None, ...]
    return img

def detect_common(img_fnames,
                  model_name,
                  feature_dir = '.featureout',
                  num_features = 4096,
                  resize_to = 1024,
                  detection_threshold = 0.01,
                  device=torch.device('cpu')):
    
    dict_model = {
        "aliked" : ALIKED,
        "superpoint" : SuperPoint,
        "doghardnet" : DoGHardNet,
    }
    extractor_class = dict_model[model_name]
    
    dtype = torch.float32 # ALIKED has issues with float16
    extractor = extractor_class(
        max_num_keypoints=num_features, detection_threshold=detection_threshold, resize=resize_to
    ).eval().to(device, dtype)
    if not os.path.isdir(feature_dir):
        os.makedirs(feature_dir)
    with h5py.File(f'{feature_dir}/keypoints_{model_name}.h5', mode='w') as f_kp, \
         h5py.File(f'{feature_dir}/descriptors_{model_name}.h5', mode='w') as f_desc:
        for img_path in tqdm(img_fnames):
            img_fname = img_path.split('/')[-1]
            key = img_fname
            with torch.inference_mode():
                image0 = load_torch_image(img_path, device=device).to(dtype)
                feats0 = extractor.extract(image0)  # auto-resize the image, disable with resize=None
                kpts = feats0['keypoints'].reshape(-1, 2).detach().cpu().numpy()
                descs = feats0['descriptors'].reshape(len(kpts), -1).detach().cpu().numpy()
                f_kp[key] = kpts
                f_desc[key] = descs
                print(f"{model_name} > kpts.shape={kpts.shape}, descs.shape={descs.shape}")
    return

def match_with_lightglue_common(img_fnames, model_name,
                   index_pairs, file_keypoints,
                   feature_dir = '.featureout',
                   device=torch.device('cpu'),
                   min_matches=15,verbose=True):
    lg_matcher = KF.LightGlueMatcher(model_name, {"width_confidence": -1,
                                                "depth_confidence": -1,
                                                 "mp": True if 'cuda' in str(device) else False}).eval().to(device)
    
    cnt_pairs = 0
    with h5py.File(f'{feature_dir}/keypoints_{model_name}.h5', mode='r') as f_kp, \
        h5py.File(f'{feature_dir}/descriptors_{model_name}.h5', mode='r') as f_desc, \
        h5py.File(file_keypoints, mode='w') as f_match:
        for pair_idx in tqdm(index_pairs):
            idx1, idx2 = pair_idx
            fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
            
            key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]
            kp1 = torch.from_numpy(f_kp[key1][...]).to(device)
            kp2 = torch.from_numpy(f_kp[key2][...]).to(device)
            desc1 = torch.from_numpy(f_desc[key1][...]).to(device)
            desc2 = torch.from_numpy(f_desc[key2][...]).to(device)
            with torch.inference_mode():
                dists, idxs = lg_matcher(desc1,
                                         desc2,
                                         KF.laf_from_center_scale_ori(kp1[None]),
                                         KF.laf_from_center_scale_ori(kp2[None]))
            if len(idxs)  == 0:
                continue
            n_matches = len(idxs)
            kp1 = kp1[idxs[:,0], :].cpu().numpy().reshape(-1, 2).astype(np.float32)
            kp2 = kp2[idxs[:,1], :].cpu().numpy().reshape(-1, 2).astype(np.float32)
            group  = f_match.require_group(key1)
            if n_matches >= min_matches:
                group.create_dataset(key2, data=np.concatenate([kp1, kp2], axis=1))
                cnt_pairs+=1
                print (f'{key1}-{key2}: {n_matches} matches @ {cnt_pairs}th pair({model_name}+lightglue)')            
            else:
                print (f'{key1}-{key2}: {n_matches} matches --> skipped')            
    return

def detect_lightglue_common(
    img_fnames, model_name, index_pairs, feature_dir, device, file_keypoints,
    resize_to=1024,
    detection_threshold=0.01, 
    num_features=4096, 
    min_matches=15,
):
    t=time()
    detect_common(
        img_fnames, model_name, feature_dir, 
        resize_to=resize_to,
        num_features=num_features, 
        detection_threshold=detection_threshold, 
        device=device
    )
    gc.collect()
    match_with_lightglue_common(
        img_fnames, model_name, index_pairs, file_keypoints,
        feature_dir=feature_dir,
        min_matches=min_matches,
        device=device
    )
    t=time() -t 
    print(f'Features matched in  {t:.4f} sec ({model_name}+LightGlue)')
    return t

Keypoints: SuperGlue¶

In [13]:
import sys
sys.path.append("../input/super-glue-pretrained-network")
from models.matching import Matching
from models.utils import (compute_pose_error, compute_epipolar_error,
                          estimate_pose, make_matching_plot,
                          error_colormap, AverageTimer, pose_auc, read_image,
                          process_resize, frame2tensor,
                          rotate_intrinsics, rotate_pose_inplane,
                          scale_intrinsics)

from torch.nn import functional as torchF  # For resizing tensor

def sg_imread(path):
    image = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
    return image

# Preprocess
def sg_read_image(image, device, resize):
    w, h = image.shape[1], image.shape[0]
    w_new, h_new = process_resize(w, h, [resize,])
    
    unit_shape = 8
    w_new = w_new // unit_shape * unit_shape
    h_new = h_new // unit_shape * unit_shape
    
    scales = (float(w) / float(w_new), float(h) / float(h_new))
    image = cv2.resize(image.astype('float32'), (w_new, h_new))

    inp = frame2tensor(image, "cpu")
    return image, inp, scales, (h, w)

class SGDataset(Dataset):
    def __init__(self, fnames1, fnames2, resize_to, device):
        self.fnames1 = fnames1
        self.fnames2 = fnames2
        self.resize_to = resize_to
        self.device = device
        
    def __len__(self):
        return len(self.fnames1)
    
    def __getitem__(self, idx):
        fname1 = self.fnames1[idx]
        fname2 = self.fnames2[idx]
                
        im1, im2 = cv2.imread(fname1, cv2.IMREAD_GRAYSCALE), cv2.imread(fname2, cv2.IMREAD_GRAYSCALE)
        _, image1, scale1, ori_shape1 = sg_read_image(im1, self.device, self.resize_to)
        _, image2, scale2, ori_shape2 = sg_read_image(im2, self.device, self.resize_to)
        return image1, image2, torch.tensor([idx]), torch.tensor(ori_shape1), torch.tensor(ori_shape2)

def get_superglue_dataloader(images1, images2, resize_to, device, batch_size=1):
    dataset = SGDataset(images1, images2, resize_to, device)
    dataloader = DataLoader(
        dataset=dataset,
        shuffle=False,
        batch_size=batch_size,
        pin_memory=True,
        num_workers=2,
        drop_last=False
    )
    return dataloader

def detect_superglue(
    img_fnames, index_pairs, feature_dir, device, sg_config, file_keypoints, 
    resize_to=750, min_matches=15
):    
    t=time()

    matcher_superglue = Matching(sg_config).eval().to(device)

    fnames1, fnames2, idxs1, idxs2 = [], [], [], []
    for pair_idx in progress_bar(index_pairs):
        idx1, idx2 = pair_idx
        fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
        fnames1.append(fname1)
        fnames2.append(fname2)
        idxs1.append(idx1)
        idxs2.append(idx2)
        
    dataloader = get_superglue_dataloader( fnames1, fnames2, resize_to, device)

    cnt_pairs = 0

    with h5py.File(file_keypoints, mode='w') as f_match:
        for X in dataloader:
            image1, image2, idx, ori_shape_1, ori_shape_2 = X

            fname1, fname2 = fnames1[idx], fnames2[idx]
            key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]
            
            pred = matcher_superglue({"image0": image1[0].to(device), "image1": image2[0].to(device)})
            pred = {k: v[0].detach().cpu().numpy().copy() for k, v in pred.items()}
            mkpts1, mkpts2 = pred["keypoints0"], pred["keypoints1"]
            matches, conf = pred["matches0"], pred["matching_scores0"]

            valid = matches > -1
            mkpts1 = mkpts1[valid]
            mkpts2 = mkpts2[matches[valid]]
            mconf = conf[valid]

            ori_shape_1 = ori_shape_1[0].numpy()
            ori_shape_2 = ori_shape_2[0].numpy()
            
            # Scaling coords
            mkpts1[:,0] = mkpts1[:,0] * ori_shape_1[1] / image1[0].shape[3]   # X
            mkpts1[:,1] = mkpts1[:,1] * ori_shape_1[0] / image1[0].shape[2]   # Y
            mkpts2[:,0] = mkpts2[:,0] * ori_shape_2[1] / image2[0].shape[3]   # X
            mkpts2[:,1] = mkpts2[:,1] * ori_shape_2[0] / image2[0].shape[2]   # Y  
            
            n_matches = mconf.shape[0]
            
            group  = f_match.require_group(key1)
            if n_matches >= min_matches:
                group.create_dataset(key2, data=np.concatenate([mkpts1, mkpts2], axis=1).astype(np.float32))
                cnt_pairs+=1
                print (f'{key1}-{key2}: {n_matches} matches @ {cnt_pairs}th pair(superglue)')            
            else:
                print (f'{key1}-{key2}: {n_matches} matches --> skipped')            

    gc.collect()
    t=time() -t 
    print(f'Features matched in  {t:.4f} sec')
    return t

Keypoints: DKM¶

In [14]:
class DKMDataset(Dataset):
    def __init__(self, fnames1, fnames2, resize_to, device):
        self.fnames1 = fnames1
        self.fnames2 = fnames2
        self.resize_to = resize_to
        self.device = device
        self.test_transform = get_tuple_transform_ops(
            resize=self.resize_to, normalize=True
        )

        
    def __len__(self):
        return len(self.fnames1)
    
    def __getitem__(self, idx):
        fname1 = self.fnames1[idx]
        fname2 = self.fnames2[idx]
                
        im1, im2 = Image.open(fname1), Image.open(fname2)
        ori_shape_1 = im1.size
        ori_shape_2 = im2.size
        image1, image2 = self.test_transform((im1, im2))
        return image1, image2, torch.tensor([idx]), torch.tensor(ori_shape_1), torch.tensor(ori_shape_2)

def get_dkm_dataloader(images1, images2, resize_to, device, batch_size=4):
    dataset = DKMDataset(images1, images2, resize_to, device)
    dataloader = DataLoader(
        dataset=dataset,
        shuffle=False,
        batch_size=batch_size,
        pin_memory=True,
        num_workers=2,
        drop_last=False
    )
    return dataloader

def get_dkm_mkpts(dkm_model, bimgs1, bimgs2, shapes1, shapes2, detection_threshold=0.5, num_features = 2000, min_matches=15):
    dense_matches, dense_certainty = dkm_model.match(bimgs1, bimgs2, batched=True)
    print("***", dense_matches.shape, dense_certainty.shape)

    store_mkpts1, store_mkpts2, store_mconf = [], [], []
    # drop low confidence pairs
    for b in range(dense_matches.shape[0]):
        u_dense_matches = dense_matches[b, dense_certainty[b,...].sqrt() >= detection_threshold, :]
        u_dense_certainty = dense_certainty[b, dense_certainty[b,...].sqrt() >= detection_threshold]
    
        if u_dense_matches.shape[0] > num_features:
            u_dense_matches, u_dense_certainty = dkm_model.sample( u_dense_matches, u_dense_certainty, num=num_features)
        
        u_dense_matches = u_dense_matches.reshape((-1, 4))
        u_dense_certainty = u_dense_certainty.reshape((-1,))
    
        mkpts1 = u_dense_matches[:, :2]
        mkpts2 = u_dense_matches[:, 2:]
        
        w1, h1 = shapes1[b, :]
        w2, h2 = shapes2[b, :]

        mkpts1[:, 0] = ((mkpts1[:, 0] + 1)/2) * w1
        mkpts1[:, 1] = ((mkpts1[:, 1] + 1)/2) * h1

        mkpts2[:, 0] = ((mkpts2[:, 0] + 1)/2) * w2
        mkpts2[:, 1] = ((mkpts2[:, 1] + 1)/2) * h2

        mkpts1 = mkpts1.cpu().detach().numpy()
        mkpts2 = mkpts2.cpu().detach().numpy()
        mconf  = u_dense_certainty.sqrt().cpu().detach().numpy()

        
        if mconf.shape[0] > min_matches:
            try:
                # calc Fundamental matrix from keypoints
                F, inliers = cv2.findFundamentalMat(mkpts1, mkpts2, cv2.USAC_MAGSAC, 0.200, 0.999, 2000)
                inliers = inliers > 0
                mkpts1 = mkpts1[inliers[:,0]]
                mkpts2 = mkpts2[inliers[:,0]]
                mconf  = mconf[inliers[:,0]]
                #print("---", mconf.shape)
                if mconf.shape[0] > 3000:
                    rand_idx = np.random.choice(range(mconf.shape[0]), 3000, replace=False)
                    mkpts1 = mkpts1[rand_idx, :]
                    mkpts2 = mkpts2[rand_idx, :]
                    mconf  = mconf[rand_idx]
            except:
                mkpts1 = np.empty((0,2))
                mkpts2 = np.empty((0,2))
                mconf = np.empty((0,))
        
        store_mkpts1.append(mkpts1)
        store_mkpts2.append(mkpts2)
        store_mconf.append(mconf)
    return store_mkpts1, store_mkpts2, store_mconf

def detect_dkm(
    img_fnames, index_pairs, feature_dir, device, 
    resize_to=(540, 720), 
    detection_threshold=0.4, 
    num_features=2000, 
    min_matches=15,
):
    t=time()
    dkm_model = DKMv3_outdoor(device=device)
    dkm_model.upsample_preds=False

    fnames1, fnames2 = [], []
    for pair_idx in progress_bar(index_pairs):
        idx1, idx2 = pair_idx
        fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
        fnames1.append(fname1)
        fnames2.append(fname2)
        
    cnt_pairs = 0
    with h5py.File(f'{feature_dir}/matches_dkm.h5', mode='w') as f_match:    
        dataloader = get_dkm_dataloader(fnames1, fnames2, resize_to, device, batch_size=4)
        for X in tqdm(dataloader):
            images1, images2, idxs, shapes1, shapes2 = X
            store_mkpts1, store_mkpts2, store_mconf = get_dkm_mkpts(
                dkm_model, images1.to(device), images2.to(device), shapes1, shapes2, 
                detection_threshold=detection_threshold, num_features = num_features, min_matches=min_matches,
            )
            
            for b in range(images1.shape[0]):
                mkpts1 = store_mkpts1[b]
                mkpts2 = store_mkpts2[b]
                mconf = store_mconf[b]
                file1 = fnames1[idxs[b]]
                file2 = fnames2[idxs[b]]
                key1, key2 = file1.split('/')[-1], file2.split('/')[-1]
            
                n_matches = mconf.shape[0]
                print (f'{key1}-{key2}: {n_matches} matches @ {cnt_pairs}th pair(dkm)')            

                group  = f_match.require_group(key1)
                if n_matches >= min_matches:
                    group.create_dataset(key2, data=np.concatenate([mkpts1, mkpts2], axis=1).astype(np.float32))
                    cnt_pairs+=1
    gc.collect()
    t=time() -t 
    print(f'Features matched in  {t:.4f} sec')
    return t

Keypoints: LoFTR¶

In [15]:
class LoFTRDataset(Dataset):
    def __init__(self, fnames1, fnames2, idxs1, idxs2, resize_small_edge_to, device):
        self.fnames1 = fnames1
        self.fnames2 = fnames2
        self.keys1 = [ fname.split('/')[-1] for fname in fnames1 ]
        self.keys2 = [ fname.split('/')[-1] for fname in fnames2 ]
        self.idxs1 = idxs1
        self.idxs2 = idxs2
        self.resize_small_edge_to = resize_small_edge_to
        self.device = device
        self.round_unit = 16
        
    def __len__(self):
        return len(self.images1)

    def load_torch_image(self, fname, device):
        img = cv2.imread(fname)
        original_shape = img.shape
        ratio = self.resize_small_edge_to / min([img.shape[0], img.shape[1]])
        w = int(img.shape[1] * ratio) # int( (img.shape[1] * ratio) // self.round_unit * self.round_unit )
        h = int(img.shape[0] * ratio) # int( (img.shape[0] * ratio) // self.round_unit * self.round_unit )
        img_resized = cv2.resize(img, (w, h))
        img_resized = K.image_to_tensor(img_resized, False).float() /255.
        img_resized = K.color.bgr_to_rgb(img_resized)
        img_resized = K.color.rgb_to_grayscale(img_resized)
        return img_resized.to(device), original_shape
    
    def __getitem__(self, idx):
        fname1 = self.fnames1[idx]
        fname2 = self.fnames2[idx]
        image1, ori_shape_1 = self.load_torch_image(fname1, device)
        image2, ori_shape_2 = self.load_torch_image(fname2, device)

        return image1, image2, self.keys1[idx], self.keys2[idx], self.idxs1[idx], self.idxs2[idx], ori_shape_1, ori_shape_2

def get_loftr_dataloader(images1, images2, idxs1, idxs2, resize_small_edge_to, device, batch_size=1):
    dataset = LoFTRDataset(images1, images2, idxs1, idxs2, resize_small_edge_to, device)
    dataloader = DataLoader(
        dataset=dataset,
        shuffle=False,
        batch_size=batch_size,
        pin_memory=True,
        num_workers=2,
        drop_last=False
    )
    return dataset
    
def detect_loftr(img_fnames, index_pairs, feature_dir, device, file_keypoints, resize_small_edge_to=750, min_matches=15):
    t=time()

    matcher = LoFTR(pretrained=None)
    matcher.load_state_dict(torch.load("../input/loftr/pytorch/outdoor/1/loftr_outdoor.ckpt")['state_dict'])
    matcher = matcher.to(device).eval()

    fnames1, fnames2, idxs1, idxs2 = [], [], [], []
    for pair_idx in progress_bar(index_pairs):
        idx1, idx2 = pair_idx
        fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
        fnames1.append(fname1)
        fnames2.append(fname2)
        idxs1.append(idx1)
        idxs2.append(idx2)
        
        
    dataloader = get_loftr_dataloader( fnames1, fnames2, idxs1, idxs2, resize_small_edge_to, device)

    cnt_pairs = 0

    with h5py.File(file_keypoints, mode='w') as f_match:    
        store_mkpts = {}
        for X in tqdm(dataloader):
            image1, image2, key1, key2, idx1, idx2, ori_shape_1, ori_shape_2 = X
            fname1, fname2 = img_fnames[idx1], img_fnames[idx2]

            with torch.no_grad():
                correspondences = matcher( {"image0": image1.to(device),"image1": image2.to(device)} )
                mkpts1 = correspondences['keypoints0'].cpu().numpy()
                mkpts2 = correspondences['keypoints1'].cpu().numpy()
                mconf  = correspondences['confidence'].cpu().numpy()

            mkpts1[:,0] *= (float(ori_shape_1[1]) / float(image1.shape[3]))
            mkpts1[:,1] *= (float(ori_shape_1[0]) / float(image1.shape[2]))

            mkpts2[:,0] *= (float(ori_shape_2[1]) / float(image2.shape[3]))
            mkpts2[:,1] *= (float(ori_shape_2[0]) / float(image2.shape[2]))
            
            n_matches = mconf.shape[0]
            
            group  = f_match.require_group(key1)
            if n_matches >= min_matches:
                group.create_dataset(key2, data=np.concatenate([mkpts1, mkpts2], axis=1).astype(np.float32))
                cnt_pairs+=1
                print (f'{key1}-{key2}: {n_matches} matches @ {cnt_pairs}th pair(loftr)')
            else:
                print (f'{key1}-{key2}: {n_matches} matches --> skipped')
    gc.collect()
    t=time() -t 
    print(f'Features matched in  {t:.4f} sec')
    return t

Keypoints: DKM¶

In [16]:
class DKMDataset(Dataset):
    def __init__(self, fnames1, fnames2, resize_to, device):
        self.fnames1 = fnames1
        self.fnames2 = fnames2
        self.resize_to = resize_to
        self.device = device
        self.test_transform = get_tuple_transform_ops(
            resize=self.resize_to, normalize=True
        )

        
    def __len__(self):
        return len(self.fnames1)
    
    def __getitem__(self, idx):
        fname1 = self.fnames1[idx]
        fname2 = self.fnames2[idx]
                
        im1, im2 = Image.open(fname1), Image.open(fname2)
        ori_shape_1 = im1.size
        ori_shape_2 = im2.size
        image1, image2 = self.test_transform((im1, im2))
        return image1, image2, torch.tensor([idx]), torch.tensor(ori_shape_1), torch.tensor(ori_shape_2)

def get_dkm_dataloader(images1, images2, resize_to, device, batch_size=4):
    dataset = DKMDataset(images1, images2, resize_to, device)
    dataloader = DataLoader(
        dataset=dataset,
        shuffle=False,
        batch_size=batch_size,
        pin_memory=True,
        num_workers=2,
        drop_last=False
    )
    return dataloader

def get_dkm_mkpts(dkm_model, bimgs1, bimgs2, shapes1, shapes2, detection_threshold=0.5, num_features = 2000, min_matches=15):
    dense_matches, dense_certainty = dkm_model.match(bimgs1, bimgs2, batched=True)

    store_mkpts1, store_mkpts2, store_mconf = [], [], []
    # drop low confidence pairs
    for b in range(dense_matches.shape[0]):
        u_dense_matches = dense_matches[b, dense_certainty[b,...].sqrt() >= detection_threshold, :]
        u_dense_certainty = dense_certainty[b, dense_certainty[b,...].sqrt() >= detection_threshold]
    
        if u_dense_matches.shape[0] > num_features:
            u_dense_matches, u_dense_certainty = dkm_model.sample( u_dense_matches, u_dense_certainty, num=num_features)
        
        u_dense_matches = u_dense_matches.reshape((-1, 4))
        u_dense_certainty = u_dense_certainty.reshape((-1,))
    
        mkpts1 = u_dense_matches[:, :2]
        mkpts2 = u_dense_matches[:, 2:]
        
        w1, h1 = shapes1[b, :]
        w2, h2 = shapes2[b, :]

        mkpts1[:, 0] = ((mkpts1[:, 0] + 1)/2) * w1
        mkpts1[:, 1] = ((mkpts1[:, 1] + 1)/2) * h1

        mkpts2[:, 0] = ((mkpts2[:, 0] + 1)/2) * w2
        mkpts2[:, 1] = ((mkpts2[:, 1] + 1)/2) * h2

        mkpts1 = mkpts1.cpu().detach().numpy()
        mkpts2 = mkpts2.cpu().detach().numpy()
        mconf  = u_dense_certainty.sqrt().cpu().detach().numpy()

        if mconf.shape[0] > min_matches:
            try:
                # calc Fundamental matrix from keypoints
                F, inliers = cv2.findFundamentalMat(mkpts1, mkpts2, cv2.USAC_MAGSAC, 0.200, 0.999, 2000)
                inliers = inliers > 0
                mkpts1 = mkpts1[inliers[:,0]]
                mkpts2 = mkpts2[inliers[:,0]]
                mconf  = mconf[inliers[:,0]]
            except:
                pass
        store_mkpts1.append(mkpts1)
        store_mkpts2.append(mkpts2)
        store_mconf.append(mconf)
    return store_mkpts1, store_mkpts2, store_mconf

def detect_dkm(
    img_fnames, index_pairs, feature_dir, device, file_keypoints,
    resize_to=(540, 720), 
    detection_threshold=0.4, 
    num_features=2000, 
    min_matches=15
):
    t=time()
    dkm_model = DKMv3_outdoor(device=device)
    dkm_model.upsample_preds=False

    fnames1, fnames2 = [], []
    for pair_idx in progress_bar(index_pairs):
        idx1, idx2 = pair_idx
        fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
        fnames1.append(fname1)
        fnames2.append(fname2)
        
    cnt_pairs = 0
    with h5py.File(file_keypoints, mode='w') as f_match:    
        dataloader = get_dkm_dataloader(fnames1, fnames2, resize_to, device, batch_size=4)
        for X in tqdm(dataloader):
            images1, images2, idxs, shapes1, shapes2 = X
            store_mkpts1, store_mkpts2, store_mconf = get_dkm_mkpts(
                dkm_model, images1.to(device), images2.to(device), shapes1, shapes2, 
                detection_threshold=detection_threshold, num_features = num_features, min_matches=min_matches,
            )
            
            for b in range(images1.shape[0]):
                mkpts1 = store_mkpts1[b]
                mkpts2 = store_mkpts2[b]
                mconf = store_mconf[b]
                file1 = fnames1[idxs[b]]
                file2 = fnames2[idxs[b]]
                key1, key2 = file1.split('/')[-1], file2.split('/')[-1]
            
                n_matches = mconf.shape[0]

                group  = f_match.require_group(key1)
                if n_matches >= min_matches:
                    group.create_dataset(key2, data=np.concatenate([mkpts1, mkpts2], axis=1).astype(np.float32))
                    cnt_pairs+=1
                    print (f'{key1}-{key2}: {n_matches} matches @ {cnt_pairs}th pair(dkm)')
                else:
                    print (f'{key1}-{key2}: {n_matches} matches --> skipped')

    gc.collect()
    t=time() -t 
    print(f'Features matched in  {t:.4f} sec')
    return t

Keypoints merger¶

In [17]:
def get_unique_idxs(A, dim=0):
    # https://stackoverflow.com/questions/72001505/how-to-get-unique-elements-and-their-firstly-appeared-indices-of-a-pytorch-tenso
    unique, idx, counts = torch.unique(A, dim=dim, sorted=True, return_inverse=True, return_counts=True)
    _, ind_sorted = torch.sort(idx, stable=True)
    cum_sum = counts.cumsum(0)
    cum_sum = torch.cat((torch.tensor([0],device=cum_sum.device), cum_sum[:-1]))
    first_indices = ind_sorted[cum_sum]
    return first_indices

def get_keypoint_from_h5(fp, key1, key2):
    rc = -1
    try:
        kpts = np.array(fp[key1][key2])
        rc = 0
        return (rc, kpts)
    except:
        return (rc, None)

def get_keypoint_from_multi_h5(fps, key1, key2):
    list_mkpts = []
    for fp in fps:
        rc, mkpts = get_keypoint_from_h5(fp, key1, key2)
        if rc == 0:
            list_mkpts.append(mkpts)
    if len(list_mkpts) > 0:
        list_mkpts = np.concatenate(list_mkpts, axis=0)
    else:
        list_mkpts = None
    return list_mkpts

def keypoints_merger(
    img_fnames,
    index_pairs,
    files_keypoints,
    feature_dir = 'featureout',
):
    # open h5 files
    fps = [ h5py.File(file, mode="r") for file in files_keypoints ]

    # temprary file
    with h5py.File(f'{feature_dir}/merge_tmp.h5', mode='w') as f_match:
        counter = 0
        for pair_idx in progress_bar(index_pairs):
            idx1, idx2 = pair_idx
            fname1, fname2 = img_fnames[idx1], img_fnames[idx2]
            key1, key2 = fname1.split('/')[-1], fname2.split('/')[-1]

            # extract keypoints
            mkpts = get_keypoint_from_multi_h5(fps, key1, key2)
            if mkpts is None:
                print(f"skipped key1={key1}, key2={key2}")
                continue
                
            print (f'{key1}-{key2}: {mkpts.shape[0]} matches')            
            # regist tmp file
            group  = f_match.require_group(key1)
            group.create_dataset(key2, data=mkpts)
            counter += 1
    print( f"Ensembled pairs : {counter} pairs" )
    for fp in fps:
        fp.close()
    
    # Let's find unique loftr pixels and group them together.
    kpts = defaultdict(list)
    match_indexes = defaultdict(dict)
    total_kpts=defaultdict(int)
    with h5py.File(f'{feature_dir}/merge_tmp.h5', mode='r') as f_match:
        for k1 in f_match.keys():
            group  = f_match[k1]
            for k2 in group.keys():
                matches = group[k2][...]
                total_kpts[k1]
                kpts[k1].append(matches[:, :2])
                kpts[k2].append(matches[:, 2:])
                current_match = torch.arange(len(matches)).reshape(-1, 1).repeat(1, 2)
                current_match[:, 0]+=total_kpts[k1]
                current_match[:, 1]+=total_kpts[k2]
                total_kpts[k1]+=len(matches)
                total_kpts[k2]+=len(matches)
                match_indexes[k1][k2]=current_match

    for k in kpts.keys():
        kpts[k] = np.round(np.concatenate(kpts[k], axis=0))
    unique_kpts = {}
    unique_match_idxs = {}
    out_match = defaultdict(dict)
    for k in kpts.keys():
        uniq_kps, uniq_reverse_idxs = torch.unique(torch.from_numpy(kpts[k]),dim=0, return_inverse=True)
        unique_match_idxs[k] = uniq_reverse_idxs
        unique_kpts[k] = uniq_kps.numpy()
    for k1, group in match_indexes.items():
        for k2, m in group.items():
            m2 = deepcopy(m)
            m2[:,0] = unique_match_idxs[k1][m2[:,0]]
            m2[:,1] = unique_match_idxs[k2][m2[:,1]]
            mkpts = np.concatenate([unique_kpts[k1][ m2[:,0]],
                                    unique_kpts[k2][  m2[:,1]],
                                   ],
                                   axis=1)
            unique_idxs_current = get_unique_idxs(torch.from_numpy(mkpts), dim=0)
            m2_semiclean = m2[unique_idxs_current]
            unique_idxs_current1 = get_unique_idxs(m2_semiclean[:, 0], dim=0)
            m2_semiclean = m2_semiclean[unique_idxs_current1]
            unique_idxs_current2 = get_unique_idxs(m2_semiclean[:, 1], dim=0)
            m2_semiclean2 = m2_semiclean[unique_idxs_current2]
            out_match[k1][k2] = m2_semiclean2.numpy()
    with h5py.File(f'{feature_dir}/keypoints.h5', mode='w') as f_kp:
        for k, kpts1 in unique_kpts.items():
            f_kp[k] = kpts1
    
    with h5py.File(f'{feature_dir}/matches.h5', mode='w') as f_match:
        for k1, gr in out_match.items():
            group  = f_match.require_group(k1)
            for k2, match in gr.items():
                group[k2] = match
    return

Submission utilities¶

In [18]:
def arr_to_str(a):
    return ';'.join([str(x) for x in a.reshape(-1)])

# Function to create a submission file.
def create_submission(out_results, data_dict):
    with open(f'submission.csv', 'w') as f:
        f.write('image_path,dataset,scene,rotation_matrix,translation_vector\n')
        for dataset in data_dict:
            if dataset in out_results:
                res = out_results[dataset]
            else:
                res = {}
            for scene in data_dict[dataset]:
                if scene in res:
                    scene_res = res[scene]
                else:
                    scene_res = {"R":{}, "t":{}}
                for image in data_dict[dataset][scene]:
                    if image in scene_res:
                        print (image)
                        R = scene_res[image]['R'].reshape(-1)
                        T = scene_res[image]['t'].reshape(-1)
                    else:
                        R = np.eye(3).reshape(-1)
                        T = np.zeros((3))
                    f.write(f'{image},{dataset},{scene},{arr_to_str(R)},{arr_to_str(T)}\n')

Main¶

In [19]:
src = '/kaggle/input/image-matching-challenge-2024'
DEBUG = False
DUMP = False

# Get data from csv.
data_dict = {}
with open(f'{src}/sample_submission.csv', 'r') as f:
    for i, l in enumerate(f):
        # Skip header.
        if l and i > 0:
            image, dataset, scene, _, _ = l.strip().split(',')
            if dataset not in data_dict:
                data_dict[dataset] = {}
            if scene not in data_dict[dataset]:
                data_dict[dataset][scene] = []
            data_dict[dataset][scene].append(image)
            
            #if len(data_dict[dataset][scene]) == 21:
            #    break
In [20]:
for dataset in data_dict:
    for scene in data_dict[dataset]:
        print(f'{dataset} / {scene} -> {len(data_dict[dataset][scene])} images')
church / church -> 41 images
In [21]:
out_results = {}
timings = {"shortlisting":[],
           "feature_detection": [],
           "feature_matching":[],
           "RANSAC": [],
           "Reconstruction": []}
In [22]:
gc.collect()
datasets = []
for dataset in data_dict:
    datasets.append(dataset)

for dataset in datasets:
    print(dataset)
    if dataset not in out_results:
        out_results[dataset] = {}
    for scene in data_dict[dataset]:
        print(scene)
        # Fail gently if the notebook has not been submitted and the test data is not populated.
        # You may want to run this on the training data in that case?
        img_dir = f'{src}/test/{dataset}/images'
        if not os.path.exists(img_dir):
            continue
        # Wrap the meaty part in a try-except block.
        try:
            out_results[dataset][scene] = {}
            img_fnames = [f'{src}/{x}' for x in data_dict[dataset][scene]]
            print (f"Got {len(img_fnames)} images")
            feature_dir = f'featureout/{dataset}_{scene}'
            if not os.path.isdir(feature_dir):
                os.makedirs(feature_dir, exist_ok=True)

            #############################################################
            # get image pairs
            #############################################################
            t=time()
            index_pairs = get_image_pairs_shortlist(img_fnames,
                                  sim_th = 1.0, # should be strict
                                  min_pairs = 50, # we select at least min_pairs PER IMAGE with biggest similarity
                                  exhaustive_if_less = 50,
                                  device=device)
            t=time() -t 
            timings['shortlisting'].append(t)
            print (f'{len(index_pairs)}, pairs to match, {t:.4f} sec')
            gc.collect()
            
            #############################################################
            # get keypoints
            #############################################################
            files_keypoints = []
            if CONFIG.use_superglue:
                resize_to = CONFIG.params_sg["resize_to"]
                file_keypoints = f"{feature_dir}/matches_superglue_{resize_to}pix.h5"
                !rm -rf {file_keypoints}
                t = detect_superglue(
                    img_fnames, index_pairs, feature_dir, device, 
                    CONFIG.params_sg["sg_config"], file_keypoints,
                    resize_to=CONFIG.params_sg["resize_to"], 
                    min_matches=CONFIG.params_sg["min_matches"],
                )
                gc.collect()
                files_keypoints.append( file_keypoints )
                timings['feature_matching'].append(t)

            if CONFIG.use_aliked_lightglue:
                model_name = "aliked"
                file_keypoints = f'{feature_dir}/matches_lightglue_{model_name}.h5'
                t = detect_lightglue_common(
                    img_fnames, model_name, index_pairs, feature_dir, device, file_keypoints,
                    resize_to=CONFIG.params_aliked_lightglue["resize_to"],
                    detection_threshold=CONFIG.params_aliked_lightglue["detection_threshold"],
                    num_features=CONFIG.params_aliked_lightglue["num_features"],
                    min_matches=CONFIG.params_aliked_lightglue["min_matches"],
                )
                gc.collect()
                files_keypoints.append(file_keypoints)
                timings['feature_matching'].append(t)
                
            if CONFIG.use_doghardnet_lightglue:
                model_name = "doghardnet"
                file_keypoints = f'{feature_dir}/matches_lightglue_{model_name}.h5'
                t = detect_lightglue_common(
                    img_fnames, model_name, index_pairs, feature_dir, device, file_keypoints,
                    resize_to=CONFIG.params_doghardnet_lightglue["resize_to"],
                    detection_threshold=CONFIG.params_doghardnet_lightglue["detection_threshold"],
                    num_features=CONFIG.params_doghardnet_lightglue["num_features"],
                    min_matches=CONFIG.params_doghardnet_lightglue["min_matches"],
                )
                gc.collect()
                files_keypoints.append(file_keypoints)
                timings['feature_matching'].append(t)
                
            if CONFIG.use_superpoint_lightglue:
                model_name = "superpoint"
                file_keypoints = f'{feature_dir}/matches_lightglue_{model_name}.h5'
                t = detect_lightglue_common(
                    img_fnames, model_name, index_pairs, feature_dir, device, file_keypoints,
                    resize_to=CONFIG.params_superpoint_lightglue["resize_to"],
                    detection_threshold=CONFIG.params_superpoint_lightglue["detection_threshold"],
                    num_features=CONFIG.params_superpoint_lightglue["num_features"],
                    min_matches=CONFIG.params_superpoint_lightglue["min_matches"],
                )
                gc.collect()
                files_keypoints.append(file_keypoints)
                timings['feature_matching'].append(t)
                
            if CONFIG.use_loftr:
                file_keypoints = f'{feature_dir}/matches_loftr_{CONFIG.params_loftr["resize_small_edge_to"]}pix.h5'
                t = detect_loftr(
                    img_fnames, index_pairs, feature_dir, device, file_keypoints,
                    resize_small_edge_to=CONFIG.params_loftr["resize_small_edge_to"],
                    min_matches=CONFIG.params_loftr["min_matches"],
                )
                gc.collect()
                files_keypoints.append( file_keypoints )
                timings['feature_matching'].append(t)
                
            if CONFIG.use_dkm:
                file_keypoints = f'{feature_dir}/matches_dkm.h5'
                t = detect_dkm(
                    img_fnames, index_pairs, feature_dir, device, file_keypoints,
                    resize_to=CONFIG.params_dkm["resize_to"], 
                    detection_threshold=CONFIG.params_dkm["detection_threshold"], 
                    num_features=CONFIG.params_dkm["num_features"], 
                    min_matches=CONFIG.params_dkm["min_matches"]
                )
                gc.collect()
                files_keypoints.append(file_keypoints)
                timings['feature_matching'].append(t)
                
            #############################################################
            # merge keypoints
            #############################################################
            keypoints_merger(
                img_fnames,
                index_pairs,
                files_keypoints,
                feature_dir = feature_dir,
            )
            
            #############################################################
            # regist keypoints from h5 into colmap db
            #############################################################
            database_path = f'{feature_dir}/colmap.db'
            if os.path.isfile(database_path):
                os.remove(database_path)
            gc.collect()
            import_into_colmap(img_dir, feature_dir=feature_dir,database_path=database_path)
            output_path = f'{feature_dir}/colmap_rec'

            #############################################################
            # Calculate fundamental matrix with colmap api
            #############################################################
            t=time()
            options = pycolmap.SiftMatchingOptions()
            options.confidence = 0.9999
            options.max_num_trials = 20000
            pycolmap.match_exhaustive(database_path, sift_options=options)
            t=time() - t 
            timings['RANSAC'].append(t)
            print(f'RANSAC in  {t:.4f} sec')

            #############################################################
            # Execute bundle adjustmnet with colmap api
            # --> Bundle adjustment Calcs Camera matrix, R and t
            #############################################################
            t=time()
            # By default colmap does not generate a reconstruction if less than 10 images are registered. Lower it to 3.
            mapper_options = pycolmap.IncrementalMapperOptions()
            mapper_options.min_model_size = 3
            os.makedirs(output_path, exist_ok=True)
            maps = pycolmap.incremental_mapping(database_path=database_path, image_path=img_dir, output_path=output_path, options=mapper_options)
            print(maps)
            clear_output(wait=False)
            t=time() - t
            timings['Reconstruction'].append(t)
            print(f'Reconstruction done in  {t:.4f} sec')

            #############################################################
            # Extract R,t from maps 
            #############################################################            
            imgs_registered  = 0
            best_idx = None
            list_num_images = []            
            print ("Looking for the best reconstruction")
            if isinstance(maps, dict):
                for idx1, rec in maps.items():
                    print (idx1, rec.summary())
                    list_num_images.append( len(rec.images) )
                    if len(rec.images) > imgs_registered:
                        imgs_registered = len(rec.images)
                        best_idx = idx1
            list_num_images = np.array(list_num_images)
            print(f"list_num_images = {list_num_images}")
            if best_idx is not None:
                print (maps[best_idx].summary())
                for k, im in maps[best_idx].images.items():
                    key1 = f'test/{dataset}/images/{im.name}'
                    out_results[dataset][scene][key1] = {}
                    out_results[dataset][scene][key1]["R"] = deepcopy(im.rotmat())
                    out_results[dataset][scene][key1]["t"] = deepcopy(np.array(im.tvec))
                
            print(f'Registered: {dataset} / {scene} -> {len(out_results[dataset][scene])} images')
            print(f'Total: {dataset} / {scene} -> {len(data_dict[dataset][scene])} images')
            create_submission(out_results, data_dict)
            gc.collect()
        except:
            pass
Reconstruction done in  331.3675 sec
Looking for the best reconstruction
0 Reconstruction:
	num_reg_images = 40
	num_cameras = 40
	num_points3D = 29091
	num_observations = 164313
	mean_track_length = 5.64824
	mean_observations_per_image = 4107.82
	mean_reprojection_error = 0.963606
list_num_images = [40]
Reconstruction:
	num_reg_images = 40
	num_cameras = 40
	num_points3D = 29091
	num_observations = 164313
	mean_track_length = 5.64824
	mean_observations_per_image = 4107.82
	mean_reprojection_error = 0.963606
Registered: church / church -> 40 images
Total: church / church -> 41 images
test/church/images/00046.png
test/church/images/00090.png
test/church/images/00092.png
test/church/images/00087.png
test/church/images/00050.png
test/church/images/00068.png
test/church/images/00083.png
test/church/images/00096.png
test/church/images/00069.png
test/church/images/00081.png
test/church/images/00042.png
test/church/images/00018.png
test/church/images/00030.png
test/church/images/00024.png
test/church/images/00032.png
test/church/images/00026.png
test/church/images/00037.png
test/church/images/00008.png
test/church/images/00035.png
test/church/images/00021.png
test/church/images/00010.png
test/church/images/00039.png
test/church/images/00011.png
test/church/images/00013.png
test/church/images/00006.png
test/church/images/00012.png
test/church/images/00029.png
test/church/images/00001.png
test/church/images/00098.png
test/church/images/00072.png
test/church/images/00066.png
test/church/images/00058.png
test/church/images/00059.png
test/church/images/00111.png
test/church/images/00061.png
test/church/images/00060.png
test/church/images/00074.png
test/church/images/00102.png
test/church/images/00076.png
test/church/images/00063.png

Result¶

In [23]:
!cat submission.csv
image_path,dataset,scene,rotation_matrix,translation_vector

test/church/images/00046.png,church,church,-0.19045505245638727;-0.3205655736895829;-0.9278817736968995;0.3810124792406934;0.8469522530229538;-0.3708117740339467;0.9047410478455676;-0.4241575109135888;-0.03916685178685886,5.4489788089748705;4.733225577664254;10.512054854114277

test/church/images/00090.png,church,church,0.9995787801824203;-0.0024292498045050577;-0.028919905850676064;-0.00236521167013138;0.9863569513383538;-0.1646036765089527;0.02892521361684525;0.16460274387717777;0.9859357325532553,-0.01124905218176241;0.022314701446219835;-0.5090553736532376

test/church/images/00092.png,church,church,0.9474947439618744;-0.11968791349365804;-0.2965274245801842;0.1437975977599753;0.9877379746628613;0.06079427840980734;0.2856150574516031;-0.10024219058083006;0.9530873738458998,0.08742134929926888;-0.059116058263106515;-0.501689606321542

test/church/images/00087.png,church,church,0.8923582292613469;-0.15707227252578382;-0.4231135685287573;0.18365597898498331;0.9827326728825436;0.022516106062083194;0.412270872185534;-0.0977997691854;0.9057968497928559,0.776907744820194;-0.017032735640488773;-0.5614159015006596

test/church/images/00050.png,church,church,0.923066557363316;0.17227239672352865;0.343904568164727;-0.20180558337651247;0.9780589675871709;0.05172197251055717;-0.3274486787200425;-0.11714468510969038;0.9375790556294705,-2.8187925847691986;0.4350366265410224;0.996373302305946

test/church/images/00068.png,church,church,0.5750216444127919;-0.2057560163985181;-0.7918425160173086;0.5254450568784128;0.8347411036693562;0.16466566748082856;0.6271025439741498;-0.5107560587606161;0.5881076838304969,4.618447806045479;0.41551386307216553;0.6780916886668167

test/church/images/00083.png,church,church,0.9350598692932851;-0.1418081133598155;-0.32488998110522616;0.17671136238701293;0.9809722713520954;0.0804145337711824;0.30730462938353253;-0.13260415462435768;0.9423269087401723,1.401318948731229;-0.039184509409423716;-0.5373441894441385

test/church/images/00096.png,church,church,0.9432617456998709;0.08764688046173384;0.32027379450176496;-0.012517512560910146;0.9732348836071537;-0.2294715084917024;-0.3318140909884927;0.21244266444265975;0.9191123562143988,-0.9779618710924005;0.0031846118198715296;-0.9447097838212319

test/church/images/00069.png,church,church,0.5874407755809463;-0.20225814997283464;-0.7835846960950823;0.34713440575753696;0.9376384075385067;0.018218700502916685;0.7310342259595086;-0.2827116153931769;0.6210177960393426,4.647343668529042;0.20291130261573695;0.7878911028475933

test/church/images/00081.png,church,church,0.8898344548709913;-0.1293904859141845;-0.43755313400698775;0.16108871391860252;0.9862852670107054;0.03594159603679402;0.42690170902972074;-0.10246694214483358;0.8984739598869802,1.4512251768099498;0.022205189057821026;-0.28184928393565006

test/church/images/00042.png,church,church,0.7257352096283635;0.19482885694072782;0.6598106713359861;-0.2522028140055299;0.9676386060303515;-0.008322783628714442;-0.6400797966764515;-0.16036597090033694;0.7513857925618669,-4.737008904827578;2.552398919778121;6.942950586544505

test/church/images/00018.png,church,church,0.9846400584465941;-0.058868736137131755;-0.16437282989565105;0.08297938069748165;0.9861064503311586;0.14390445091915852;0.15361763466916906;-0.15533364259199886;0.9758448041557382,0.22504822565620958;1.2253019590648935;2.895676780419565

test/church/images/00030.png,church,church,0.9461184830777511;0.07337675344797576;0.31539763479152794;-0.15586641810425836;0.9569288050081982;0.24493452564497778;-0.28384058146432056;-0.2808969814701054;0.9168050011398169,-2.7495653305029197;0.24870714085630966;0.77309550933949

test/church/images/00024.png,church,church,0.8116517566335592;0.2093626190919364;0.5453335856891839;-0.2284651045849356;0.9729754424003588;-0.03350349941156062;-0.5376105671772571;-0.09739652053739273;0.837549279652581,0.01656199335221601;0.5600915352511748;2.0590611904068665

test/church/images/00032.png,church,church,0.974368683566628;0.08190772597735088;0.20951561495472268;-0.1048823050371904;0.9893434958210527;0.10099083803328043;-0.19901098104055;-0.12037679054451375;0.972575990719224,-1.0397352240973365;0.5630160710685964;1.6466550646027849

test/church/images/00026.png,church,church,0.9645595065717133;0.13794593203481662;0.22493527539534278;-0.1836096707501054;0.9631187334262391;0.19669721434345472;-0.18950579698038486;-0.23102645986657638;0.9543135374458157,-2.0960475995847574;0.6241056188291473;1.0916923035203818

test/church/images/00037.png,church,church,-0.7833926937623681;0.19383919034274505;0.590527099841328;-0.08524557184166648;0.9076339827554167;-0.41101550558212185;-0.6156533763743427;-0.37232636441005496;-0.6945098980749398,-0.5498955513208535;0.39540352642523857;3.445370393280146

test/church/images/00008.png,church,church,0.9999663360810358;-3.671735215840853e-05;0.008205203014252484;3.257310880761173e-05;0.9999998718518661;0.0005052081193885731;-0.008205220512675462;-0.0005049238431328101;0.9999662091332142,-0.8165602834576889;1.40543727750834;5.236422421239763

test/church/images/00035.png,church,church,-0.46598581912900716;0.20839141048401089;0.859901294572321;-0.3476321634129135;0.8505917821827548;-0.394519326578588;-0.8136394135804426;-0.4827697589116883;-0.3239201515016079,-0.5873677336060524;1.1614687929503975;3.3914736523908604

test/church/images/00021.png,church,church,0.9701923961118828;0.135210790060731;0.20110881824981344;-0.15090852449855452;0.9864203691862958;0.06481876648040251;-0.18961363811802343;-0.09323570939036561;0.9774220023787691,-1.7729476122270293;0.7843715128413354;2.0506093348389154

test/church/images/00010.png,church,church,0.9972112072516602;-0.01832211034328201;-0.07234713818980666;0.0135552952386382;0.997738259709579;-0.06583782410358144;0.07338979563045352;0.06467352923932151;0.9952041361012578,-0.5734624092951723;0.9745563778467372;4.923625934742633

test/church/images/00039.png,church,church,0.5831532835806126;0.24359678883725197;0.7749792592820333;-0.33556421521695085;0.9410223708132004;-0.043284582647120234;-0.7398168052396361;-0.23481376045130478;0.6305026507380723,-1.829182753366879;0.7865097238012043;2.0321684566132165

test/church/images/00011.png,church,church,0.9994551117785063;0.012242638142110896;0.030652852251213263;-0.012596838515874561;0.9998557960322672;0.011388889326281213;-0.0305090019374341;-0.011768812684490922;0.9994652049215015,0.28341244360331325;1.1662618882249485;4.263132829032265

test/church/images/00013.png,church,church,0.9940336264060289;0.01964442266883493;0.10729047596169707;-0.007513566524384699;0.993643729690009;-0.1123195653740338;-0.1088149617109843;0.11084329075652875;0.9878628796559271,0.6255139958988624;0.6256048301002536;4.357217437117637

test/church/images/00006.png,church,church,0.9708504514702527;-0.07323870466337691;0.22822246387951634;0.08839036236110172;0.9944606830322252;-0.056877884496113744;-0.2227926047284326;0.0753925861229788;0.9719461987353053,0.21229212682102747;1.0498449266991632;5.369542790522304

test/church/images/00012.png,church,church,0.9991012811326345;0.007436090407237434;-0.04172930143896225;-0.00973617324526048;0.9984284283258488;-0.0551894957515806;0.04125332677090006;0.0555461796186817;0.9976035209240706,0.1924046884539358;0.5375674160432287;2.849938089625637

test/church/images/00029.png,church,church,0.9979896123192642;-0.006893506521869631;0.06300169260168842;0.005725737604948875;0.9998087950969622;0.018697303967150834;-0.06311853635600223;-0.018298983977087385;0.9978382622217301,-0.9528981159312442;0.440762069959487;1.9988132085507897

test/church/images/00001.png,church,church,0.9463787001104561;0.10436304102719092;0.30573797874127506;-0.12205650793517793;0.9917454846271424;0.03928234453726395;-0.299114634961102;-0.07449328418878662;0.951304990927124,1.2873380829775245;1.6632030111866922;5.103387733384806

test/church/images/00098.png,church,church,0.9038236995107594;-0.13240994329670666;-0.4069033387904891;0.19778793206815684;0.9725141135631343;0.12286672799709969;0.3794504633425644;-0.1915304305761203;0.9051703928169763,1.9477375218833204;0.0910709111924087;-2.3442233609059167

test/church/images/00072.png,church,church,0.7487643290140215;-0.1762424581745275;-0.6389761932441476;0.32779061044740293;0.936330315949211;0.12585251343127685;0.5761122245697459;-0.3036842692189549;0.7588613637089768,3.26361746312355;0.3792559743742919;0.511732945298637

test/church/images/00066.png,church,church,0.5442941323428014;-0.23222972878851966;-0.8061099494262601;0.34880983607972094;0.9365659301217268;-0.03429222636783558;0.7629387889937406;-0.2625140217246219;0.5907713539489956,4.61155466425232;0.3739192917697607;0.9662541634298554

test/church/images/00104.png,church,church,1.0;0.0;0.0;0.0;1.0;0.0;0.0;0.0;1.0,0.0;0.0;0.0

test/church/images/00058.png,church,church,0.9066177453298658;0.2624931349378865;0.33036588498734426;0.0418004065316796;0.7232215745772292;-0.6893498966996511;-0.41987735098094653;0.6387862773858827;0.6447131935649442,0.5072940621740217;-0.47331259997475866;0.5547865777888344

test/church/images/00059.png,church,church,-0.1436521234773558;-0.26745417054384335;-0.9528023583509605;0.570081739311648;0.7646283301453589;-0.30058331165007535;0.8089319365211475;-0.5863546566693445;0.04263025543346266,5.810319328034658;2.604848300412115;4.068512011759298

test/church/images/00111.png,church,church,0.8642097406340425;0.18053174892195056;0.46962731162531707;-0.26954798658849854;0.9542828496568607;0.12918252891497362;-0.4248357413393175;-0.23822789610363265;0.8733625034306828,-3.536845878846759;0.9764794128668803;1.4898229049307894

test/church/images/00061.png,church,church,0.08378663348420157;0.31069077230048553;0.9468109864469847;-0.42490358537712675;0.870583313047108;-0.24807587181663598;-0.9013527296142432;-0.38151794066760364;0.2049568680618047,-5.493133205891144;2.2413042189542396;4.410691435946078

test/church/images/00060.png,church,church,0.9783354004235938;-0.049097470084256314;-0.20112007037919397;0.039475236719436854;0.9978883751901195;-0.051579999480105596;0.20322782773023682;0.04252327705796939;0.9782076573735765,0.38278234329129657;0.4712520337543369;1.00560341172369

test/church/images/00074.png,church,church,0.8547530873297045;-0.163773701014919;-0.4925193748028748;0.15668625942170772;0.9860616360786036;-0.05596307677764591;0.49481974072259083;-0.02933640587841841;0.8685003163392427,3.3742017847774814;0.12922666611352002;0.2531314341511051

test/church/images/00102.png,church,church,0.9374558054716234;-0.06774781858399104;-0.3414481598495743;0.021835397234206418;0.9903915353740851;-0.13655702869858702;0.3474188080907166;0.12056052312624123;0.9299275950569232,-0.437281090399856;0.5275633621010443;2.480261933989846

test/church/images/00076.png,church,church,0.46404609326404866;-0.24221723130857326;-0.8520516628606407;0.31010442239641073;0.945441710580714;-0.09987601866515208;0.8297568743533026;-0.21787791249096924;0.5138411668132626,4.404216774065016;1.1504250101446147;3.1897923353882205

test/church/images/00063.png,church,church,0.8835200606906524;-0.12094085246348368;-0.4525103452547708;0.39336657335510855;0.7160418789154879;0.5766686800980594;0.25427355608487096;-0.6875007911592421;0.6802114530286668,0.1696906735204117;0.2717531921150028;-0.06247921888739553

In [24]:
pd.read_csv('submission.csv').rotation_matrix.value_counts()
Out[24]:
rotation_matrix
-0.19045505245638727;-0.3205655736895829;-0.9278817736968995;0.3810124792406934;0.8469522530229538;-0.3708117740339467;0.9047410478455676;-0.4241575109135888;-0.03916685178685886               1
0.9995787801824203;-0.0024292498045050577;-0.028919905850676064;-0.00236521167013138;0.9863569513383538;-0.1646036765089527;0.02892521361684525;0.16460274387717777;0.9859357325532553           1
0.9474947439618744;-0.11968791349365804;-0.2965274245801842;0.1437975977599753;0.9877379746628613;0.06079427840980734;0.2856150574516031;-0.10024219058083006;0.9530873738458998                 1
0.8923582292613469;-0.15707227252578382;-0.4231135685287573;0.18365597898498331;0.9827326728825436;0.022516106062083194;0.412270872185534;-0.0977997691854;0.9057968497928559                    1
0.923066557363316;0.17227239672352865;0.343904568164727;-0.20180558337651247;0.9780589675871709;0.05172197251055717;-0.3274486787200425;-0.11714468510969038;0.9375790556294705                  1
0.5750216444127919;-0.2057560163985181;-0.7918425160173086;0.5254450568784128;0.8347411036693562;0.16466566748082856;0.6271025439741498;-0.5107560587606161;0.5881076838304969                   1
0.9350598692932851;-0.1418081133598155;-0.32488998110522616;0.17671136238701293;0.9809722713520954;0.0804145337711824;0.30730462938353253;-0.13260415462435768;0.9423269087401723                1
0.9432617456998709;0.08764688046173384;0.32027379450176496;-0.012517512560910146;0.9732348836071537;-0.2294715084917024;-0.3318140909884927;0.21244266444265975;0.9191123562143988               1
0.5874407755809463;-0.20225814997283464;-0.7835846960950823;0.34713440575753696;0.9376384075385067;0.018218700502916685;0.7310342259595086;-0.2827116153931769;0.6210177960393426                1
0.8898344548709913;-0.1293904859141845;-0.43755313400698775;0.16108871391860252;0.9862852670107054;0.03594159603679402;0.42690170902972074;-0.10246694214483358;0.8984739598869802               1
0.7257352096283635;0.19482885694072782;0.6598106713359861;-0.2522028140055299;0.9676386060303515;-0.008322783628714442;-0.6400797966764515;-0.16036597090033694;0.7513857925618669               1
0.9846400584465941;-0.058868736137131755;-0.16437282989565105;0.08297938069748165;0.9861064503311586;0.14390445091915852;0.15361763466916906;-0.15533364259199886;0.9758448041557382             1
0.9461184830777511;0.07337675344797576;0.31539763479152794;-0.15586641810425836;0.9569288050081982;0.24493452564497778;-0.28384058146432056;-0.2808969814701054;0.9168050011398169               1
0.8116517566335592;0.2093626190919364;0.5453335856891839;-0.2284651045849356;0.9729754424003588;-0.03350349941156062;-0.5376105671772571;-0.09739652053739273;0.837549279652581                  1
0.974368683566628;0.08190772597735088;0.20951561495472268;-0.1048823050371904;0.9893434958210527;0.10099083803328043;-0.19901098104055;-0.12037679054451375;0.972575990719224                    1
0.9645595065717133;0.13794593203481662;0.22493527539534278;-0.1836096707501054;0.9631187334262391;0.19669721434345472;-0.18950579698038486;-0.23102645986657638;0.9543135374458157               1
-0.7833926937623681;0.19383919034274505;0.590527099841328;-0.08524557184166648;0.9076339827554167;-0.41101550558212185;-0.6156533763743427;-0.37232636441005496;-0.6945098980749398              1
0.9999663360810358;-3.671735215840853e-05;0.008205203014252484;3.257310880761173e-05;0.9999998718518661;0.0005052081193885731;-0.008205220512675462;-0.0005049238431328101;0.9999662091332142    1
-0.46598581912900716;0.20839141048401089;0.859901294572321;-0.3476321634129135;0.8505917821827548;-0.394519326578588;-0.8136394135804426;-0.4827697589116883;-0.3239201515016079                 1
0.9701923961118828;0.135210790060731;0.20110881824981344;-0.15090852449855452;0.9864203691862958;0.06481876648040251;-0.18961363811802343;-0.09323570939036561;0.9774220023787691                1
0.9972112072516602;-0.01832211034328201;-0.07234713818980666;0.0135552952386382;0.997738259709579;-0.06583782410358144;0.07338979563045352;0.06467352923932151;0.9952041361012578                1
0.5831532835806126;0.24359678883725197;0.7749792592820333;-0.33556421521695085;0.9410223708132004;-0.043284582647120234;-0.7398168052396361;-0.23481376045130478;0.6305026507380723              1
0.9994551117785063;0.012242638142110896;0.030652852251213263;-0.012596838515874561;0.9998557960322672;0.011388889326281213;-0.0305090019374341;-0.011768812684490922;0.9994652049215015          1
0.9940336264060289;0.01964442266883493;0.10729047596169707;-0.007513566524384699;0.993643729690009;-0.1123195653740338;-0.1088149617109843;0.11084329075652875;0.9878628796559271                1
0.9708504514702527;-0.07323870466337691;0.22822246387951634;0.08839036236110172;0.9944606830322252;-0.056877884496113744;-0.2227926047284326;0.0753925861229788;0.9719461987353053               1
0.9991012811326345;0.007436090407237434;-0.04172930143896225;-0.00973617324526048;0.9984284283258488;-0.0551894957515806;0.04125332677090006;0.0555461796186817;0.9976035209240706               1
0.9979896123192642;-0.006893506521869631;0.06300169260168842;0.005725737604948875;0.9998087950969622;0.018697303967150834;-0.06311853635600223;-0.018298983977087385;0.9978382622217301          1
0.9463787001104561;0.10436304102719092;0.30573797874127506;-0.12205650793517793;0.9917454846271424;0.03928234453726395;-0.299114634961102;-0.07449328418878662;0.951304990927124                 1
0.9038236995107594;-0.13240994329670666;-0.4069033387904891;0.19778793206815684;0.9725141135631343;0.12286672799709969;0.3794504633425644;-0.1915304305761203;0.9051703928169763                 1
0.7487643290140215;-0.1762424581745275;-0.6389761932441476;0.32779061044740293;0.936330315949211;0.12585251343127685;0.5761122245697459;-0.3036842692189549;0.7588613637089768                   1
0.5442941323428014;-0.23222972878851966;-0.8061099494262601;0.34880983607972094;0.9365659301217268;-0.03429222636783558;0.7629387889937406;-0.2625140217246219;0.5907713539489956                1
1.0;0.0;0.0;0.0;1.0;0.0;0.0;0.0;1.0                                                                                                                                                              1
0.9066177453298658;0.2624931349378865;0.33036588498734426;0.0418004065316796;0.7232215745772292;-0.6893498966996511;-0.41987735098094653;0.6387862773858827;0.6447131935649442                   1
-0.1436521234773558;-0.26745417054384335;-0.9528023583509605;0.570081739311648;0.7646283301453589;-0.30058331165007535;0.8089319365211475;-0.5863546566693445;0.04263025543346266                1
0.8642097406340425;0.18053174892195056;0.46962731162531707;-0.26954798658849854;0.9542828496568607;0.12918252891497362;-0.4248357413393175;-0.23822789610363265;0.8733625034306828               1
0.08378663348420157;0.31069077230048553;0.9468109864469847;-0.42490358537712675;0.870583313047108;-0.24807587181663598;-0.9013527296142432;-0.38151794066760364;0.2049568680618047               1
0.9783354004235938;-0.049097470084256314;-0.20112007037919397;0.039475236719436854;0.9978883751901195;-0.051579999480105596;0.20322782773023682;0.04252327705796939;0.9782076573735765           1
0.8547530873297045;-0.163773701014919;-0.4925193748028748;0.15668625942170772;0.9860616360786036;-0.05596307677764591;0.49481974072259083;-0.02933640587841841;0.8685003163392427                1
0.9374558054716234;-0.06774781858399104;-0.3414481598495743;0.021835397234206418;0.9903915353740851;-0.13655702869858702;0.3474188080907166;0.12056052312624123;0.9299275950569232               1
0.46404609326404866;-0.24221723130857326;-0.8520516628606407;0.31010442239641073;0.945441710580714;-0.09987601866515208;0.8297568743533026;-0.21787791249096924;0.5138411668132626               1
0.8835200606906524;-0.12094085246348368;-0.4525103452547708;0.39336657335510855;0.7160418789154879;0.5766686800980594;0.25427355608487096;-0.6875007911592421;0.6802114530286668                 1
Name: count, dtype: int64
In [25]:
12min in a run
  Cell In[25], line 1
    12min in a run
     ^
SyntaxError: invalid decimal literal