Spaces:
Sleeping
Sleeping
Realcat
commited on
Commit
•
57c1094
1
Parent(s):
d521fb7
update: sfm
Browse files- common/app_class.py +119 -76
- common/config.yaml +8 -0
- common/sfm.py +164 -0
- hloc/colmap_from_nvm.py +21 -5
- hloc/extract_features.py +5 -1
- hloc/extractors/eigenplaces.py +57 -0
- hloc/localize_inloc.py +6 -2
- hloc/localize_sfm.py +10 -3
- hloc/match_dense.py +34 -10
- hloc/matchers/mast3r.py +5 -7
- hloc/matchers/superglue.py +3 -1
- hloc/pairs_from_exhaustive.py +3 -1
- hloc/pairs_from_poses.py +3 -1
- hloc/pairs_from_retrieval.py +6 -2
- hloc/reconstruction.py +8 -3
- hloc/triangulation.py +21 -7
- hloc/utils/viz.py +1 -1
- hloc/visualization.py +27 -8
- requirements.txt +3 -2
common/app_class.py
CHANGED
@@ -3,7 +3,10 @@ from typing import Any, Dict, Optional, Tuple
|
|
3 |
|
4 |
import gradio as gr
|
5 |
import numpy as np
|
|
|
|
|
6 |
|
|
|
7 |
from common.utils import (
|
8 |
GRADIO_VERSION,
|
9 |
gen_examples,
|
@@ -115,7 +118,7 @@ class ImageMatchingApp:
|
|
115 |
label="Match thres.",
|
116 |
value=0.1,
|
117 |
)
|
118 |
-
|
119 |
minimum=10,
|
120 |
maximum=10000,
|
121 |
step=10,
|
@@ -199,7 +202,7 @@ class ImageMatchingApp:
|
|
199 |
input_image0,
|
200 |
input_image1,
|
201 |
match_setting_threshold,
|
202 |
-
|
203 |
detect_keypoints_threshold,
|
204 |
matcher_list,
|
205 |
ransac_method,
|
@@ -314,7 +317,7 @@ class ImageMatchingApp:
|
|
314 |
input_image0,
|
315 |
input_image1,
|
316 |
match_setting_threshold,
|
317 |
-
|
318 |
detect_keypoints_threshold,
|
319 |
matcher_list,
|
320 |
input_image0,
|
@@ -378,14 +381,14 @@ class ImageMatchingApp:
|
|
378 |
outputs=[output_wrapped, geometry_result],
|
379 |
)
|
380 |
with gr.Tab("Structure from Motion(under-dev)"):
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
|
390 |
def run(self):
|
391 |
self.app.queue().launch(
|
@@ -459,7 +462,7 @@ class ImageMatchingApp:
|
|
459 |
self.cfg["defaults"][
|
460 |
"match_threshold"
|
461 |
], # matching_threshold: float
|
462 |
-
self.cfg["defaults"]["max_keypoints"], #
|
463 |
self.cfg["defaults"][
|
464 |
"keypoint_threshold"
|
465 |
], # keypoint_threshold: float
|
@@ -546,8 +549,9 @@ class ImageMatchingApp:
|
|
546 |
|
547 |
|
548 |
class AppBaseUI:
|
549 |
-
def __init__(self, cfg: Dict[str, Any] =
|
550 |
-
self.cfg = cfg
|
|
|
551 |
|
552 |
def _init_ui(self):
|
553 |
NotImplemented
|
@@ -559,9 +563,16 @@ class AppBaseUI:
|
|
559 |
class AppSfmUI(AppBaseUI):
|
560 |
def __init__(self, cfg: Dict[str, Any] = None):
|
561 |
super().__init__(cfg)
|
562 |
-
self.
|
563 |
-
self.
|
564 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
565 |
|
566 |
def _update_options(self, option):
|
567 |
if option == "sparse":
|
@@ -571,15 +582,6 @@ class AppSfmUI(AppBaseUI):
|
|
571 |
else:
|
572 |
return gr.Textbox("not set", visible=True)
|
573 |
|
574 |
-
def set_local_features(self, features):
|
575 |
-
self.features = features
|
576 |
-
|
577 |
-
def set_global_features(self, features):
|
578 |
-
self.global_features = features
|
579 |
-
|
580 |
-
def set_matchers(self, matchers):
|
581 |
-
self.matchers = matchers
|
582 |
-
|
583 |
def _on_select_custom_params(self, value: bool = False):
|
584 |
return gr.Textbox(
|
585 |
label="Camera Params",
|
@@ -592,15 +594,18 @@ class AppSfmUI(AppBaseUI):
|
|
592 |
with gr.Row():
|
593 |
# data settting and camera settings
|
594 |
with gr.Column():
|
595 |
-
input_images = gr.File(
|
596 |
-
label="SfM",
|
|
|
|
|
|
|
597 |
)
|
598 |
# camera setting
|
599 |
with gr.Accordion("Camera Settings", open=True):
|
600 |
with gr.Column():
|
601 |
with gr.Row():
|
602 |
with gr.Column():
|
603 |
-
camera_model = gr.Dropdown(
|
604 |
choices=[
|
605 |
"PINHOLE",
|
606 |
"SIMPLE_RADIAL",
|
@@ -622,7 +627,7 @@ class AppSfmUI(AppBaseUI):
|
|
622 |
interactive=True,
|
623 |
)
|
624 |
with gr.Row():
|
625 |
-
camera_params = gr.Textbox(
|
626 |
label="Camera Params",
|
627 |
value="0,0,0,0",
|
628 |
interactive=False,
|
@@ -631,30 +636,15 @@ class AppSfmUI(AppBaseUI):
|
|
631 |
camera_custom_params_cb.select(
|
632 |
fn=self._on_select_custom_params,
|
633 |
inputs=camera_custom_params_cb,
|
634 |
-
outputs=camera_params,
|
635 |
)
|
636 |
|
637 |
with gr.Accordion("Matching Settings", open=True):
|
638 |
# feature extraction and matching setting
|
639 |
with gr.Row():
|
640 |
-
feature_type = gr.Radio(
|
641 |
-
["sparse", "dense"],
|
642 |
-
label="Feature Type",
|
643 |
-
value="sparse",
|
644 |
-
interactive=True,
|
645 |
-
)
|
646 |
-
feature_details = gr.Textbox(
|
647 |
-
label="Feature Details",
|
648 |
-
visible=False,
|
649 |
-
)
|
650 |
-
# feature_type.change(
|
651 |
-
# fn=self._update_options,
|
652 |
-
# inputs=feature_type,
|
653 |
-
# outputs=feature_details,
|
654 |
-
# )
|
655 |
# matcher setting
|
656 |
-
|
657 |
-
choices=self.
|
658 |
value="disk+lightglue",
|
659 |
label="Matching Model",
|
660 |
interactive=True,
|
@@ -662,17 +652,29 @@ class AppSfmUI(AppBaseUI):
|
|
662 |
with gr.Row():
|
663 |
with gr.Accordion("Advanced Settings", open=False):
|
664 |
with gr.Column():
|
665 |
-
|
666 |
with gr.Row():
|
667 |
# matching setting
|
668 |
-
|
669 |
-
label="Max
|
670 |
minimum=100,
|
671 |
maximum=10000,
|
672 |
value=1000,
|
673 |
interactive=True,
|
674 |
)
|
675 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
676 |
label="Ransac Threshold",
|
677 |
minimum=0.01,
|
678 |
maximum=12.0,
|
@@ -682,7 +684,7 @@ class AppSfmUI(AppBaseUI):
|
|
682 |
)
|
683 |
|
684 |
with gr.Row():
|
685 |
-
ransac_confidence = gr.Slider(
|
686 |
label="Ransac Confidence",
|
687 |
minimum=0.01,
|
688 |
maximum=1.0,
|
@@ -690,7 +692,7 @@ class AppSfmUI(AppBaseUI):
|
|
690 |
step=0.0001,
|
691 |
interactive=True,
|
692 |
)
|
693 |
-
ransac_max_iter = gr.Slider(
|
694 |
label="Ransac Max Iter",
|
695 |
minimum=1,
|
696 |
maximum=100,
|
@@ -700,7 +702,7 @@ class AppSfmUI(AppBaseUI):
|
|
700 |
)
|
701 |
with gr.Accordion("Scene Graph Settings", open=True):
|
702 |
# mapping setting
|
703 |
-
scene_graph = gr.Dropdown(
|
704 |
choices=["all", "swin", "oneref"],
|
705 |
value="all",
|
706 |
label="Scene Graph",
|
@@ -708,14 +710,20 @@ class AppSfmUI(AppBaseUI):
|
|
708 |
)
|
709 |
|
710 |
# global feature setting
|
711 |
-
global_feature = gr.Dropdown(
|
712 |
-
choices=self.
|
713 |
value="netvlad",
|
714 |
label="Global features",
|
715 |
interactive=True,
|
716 |
)
|
717 |
-
|
718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
719 |
|
720 |
# mapping setting
|
721 |
with gr.Column():
|
@@ -723,26 +731,61 @@ class AppSfmUI(AppBaseUI):
|
|
723 |
with gr.Row():
|
724 |
with gr.Accordion("Buddle Settings", open=True):
|
725 |
with gr.Row():
|
726 |
-
mapper_refine_focal_length =
|
727 |
-
|
728 |
-
|
729 |
-
|
|
|
|
|
730 |
)
|
731 |
-
mapper_refine_principle_points =
|
732 |
-
|
733 |
-
|
734 |
-
|
|
|
|
|
735 |
)
|
736 |
-
mapper_refine_extra_params =
|
737 |
-
|
738 |
-
|
739 |
-
|
|
|
|
|
740 |
)
|
741 |
-
with gr.Accordion(
|
742 |
-
"Retriangluation Settings", open=True
|
743 |
-
):
|
744 |
gr.Textbox(
|
745 |
label="Retriangluation Details",
|
746 |
)
|
747 |
-
gr.Button("Run SFM", variant="primary")
|
748 |
-
model_3d = gr.Model3D(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
import gradio as gr
|
5 |
import numpy as np
|
6 |
+
from easydict import EasyDict as edict
|
7 |
+
from omegaconf import OmegaConf
|
8 |
|
9 |
+
from common.sfm import SfmEngine
|
10 |
from common.utils import (
|
11 |
GRADIO_VERSION,
|
12 |
gen_examples,
|
|
|
118 |
label="Match thres.",
|
119 |
value=0.1,
|
120 |
)
|
121 |
+
match_setting_max_keypoints = gr.Slider(
|
122 |
minimum=10,
|
123 |
maximum=10000,
|
124 |
step=10,
|
|
|
202 |
input_image0,
|
203 |
input_image1,
|
204 |
match_setting_threshold,
|
205 |
+
match_setting_max_keypoints,
|
206 |
detect_keypoints_threshold,
|
207 |
matcher_list,
|
208 |
ransac_method,
|
|
|
317 |
input_image0,
|
318 |
input_image1,
|
319 |
match_setting_threshold,
|
320 |
+
match_setting_max_keypoints,
|
321 |
detect_keypoints_threshold,
|
322 |
matcher_list,
|
323 |
input_image0,
|
|
|
381 |
outputs=[output_wrapped, geometry_result],
|
382 |
)
|
383 |
with gr.Tab("Structure from Motion(under-dev)"):
|
384 |
+
sfm_ui = AppSfmUI( # noqa: F841
|
385 |
+
{
|
386 |
+
**self.cfg,
|
387 |
+
"matcher_zoo": self.matcher_zoo,
|
388 |
+
"outputs": "experiments/sfm",
|
389 |
+
}
|
390 |
+
)
|
391 |
+
# sfm_ui.call()
|
392 |
|
393 |
def run(self):
|
394 |
self.app.queue().launch(
|
|
|
462 |
self.cfg["defaults"][
|
463 |
"match_threshold"
|
464 |
], # matching_threshold: float
|
465 |
+
self.cfg["defaults"]["max_keypoints"], # max_keypoints: int
|
466 |
self.cfg["defaults"][
|
467 |
"keypoint_threshold"
|
468 |
], # keypoint_threshold: float
|
|
|
549 |
|
550 |
|
551 |
class AppBaseUI:
|
552 |
+
def __init__(self, cfg: Dict[str, Any] = {}):
|
553 |
+
self.cfg = OmegaConf.create(cfg)
|
554 |
+
self.inputs = edict({})
|
555 |
|
556 |
def _init_ui(self):
|
557 |
NotImplemented
|
|
|
563 |
class AppSfmUI(AppBaseUI):
|
564 |
def __init__(self, cfg: Dict[str, Any] = None):
|
565 |
super().__init__(cfg)
|
566 |
+
assert "matcher_zoo" in self.cfg
|
567 |
+
self.matcher_zoo = self.cfg["matcher_zoo"]
|
568 |
+
self.sfm_engine = SfmEngine(cfg)
|
569 |
+
|
570 |
+
def init_retrieval_dropdown(self):
|
571 |
+
algos = []
|
572 |
+
for k, v in self.cfg["retrieval_zoo"].items():
|
573 |
+
if v.get("enable", True):
|
574 |
+
algos.append(k)
|
575 |
+
return algos
|
576 |
|
577 |
def _update_options(self, option):
|
578 |
if option == "sparse":
|
|
|
582 |
else:
|
583 |
return gr.Textbox("not set", visible=True)
|
584 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
585 |
def _on_select_custom_params(self, value: bool = False):
|
586 |
return gr.Textbox(
|
587 |
label="Camera Params",
|
|
|
594 |
with gr.Row():
|
595 |
# data settting and camera settings
|
596 |
with gr.Column():
|
597 |
+
self.inputs.input_images = gr.File(
|
598 |
+
label="SfM",
|
599 |
+
interactive=True,
|
600 |
+
file_count="multiple",
|
601 |
+
min_width=300,
|
602 |
)
|
603 |
# camera setting
|
604 |
with gr.Accordion("Camera Settings", open=True):
|
605 |
with gr.Column():
|
606 |
with gr.Row():
|
607 |
with gr.Column():
|
608 |
+
self.inputs.camera_model = gr.Dropdown(
|
609 |
choices=[
|
610 |
"PINHOLE",
|
611 |
"SIMPLE_RADIAL",
|
|
|
627 |
interactive=True,
|
628 |
)
|
629 |
with gr.Row():
|
630 |
+
self.inputs.camera_params = gr.Textbox(
|
631 |
label="Camera Params",
|
632 |
value="0,0,0,0",
|
633 |
interactive=False,
|
|
|
636 |
camera_custom_params_cb.select(
|
637 |
fn=self._on_select_custom_params,
|
638 |
inputs=camera_custom_params_cb,
|
639 |
+
outputs=self.inputs.camera_params,
|
640 |
)
|
641 |
|
642 |
with gr.Accordion("Matching Settings", open=True):
|
643 |
# feature extraction and matching setting
|
644 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
645 |
# matcher setting
|
646 |
+
self.inputs.matcher_key = gr.Dropdown(
|
647 |
+
choices=self.matcher_zoo.keys(),
|
648 |
value="disk+lightglue",
|
649 |
label="Matching Model",
|
650 |
interactive=True,
|
|
|
652 |
with gr.Row():
|
653 |
with gr.Accordion("Advanced Settings", open=False):
|
654 |
with gr.Column():
|
|
|
655 |
with gr.Row():
|
656 |
# matching setting
|
657 |
+
self.inputs.max_keypoints = gr.Slider(
|
658 |
+
label="Max Keypoints",
|
659 |
minimum=100,
|
660 |
maximum=10000,
|
661 |
value=1000,
|
662 |
interactive=True,
|
663 |
)
|
664 |
+
self.inputs.keypoint_threshold = gr.Slider(
|
665 |
+
label="Keypoint Threshold",
|
666 |
+
minimum=0,
|
667 |
+
maximum=1,
|
668 |
+
value=0.01,
|
669 |
+
)
|
670 |
+
with gr.Row():
|
671 |
+
self.inputs.match_threshold = gr.Slider(
|
672 |
+
label="Match Threshold",
|
673 |
+
minimum=0.01,
|
674 |
+
maximum=12.0,
|
675 |
+
value=0.2,
|
676 |
+
)
|
677 |
+
self.inputs.ransac_threshold = gr.Slider(
|
678 |
label="Ransac Threshold",
|
679 |
minimum=0.01,
|
680 |
maximum=12.0,
|
|
|
684 |
)
|
685 |
|
686 |
with gr.Row():
|
687 |
+
self.inputs.ransac_confidence = gr.Slider(
|
688 |
label="Ransac Confidence",
|
689 |
minimum=0.01,
|
690 |
maximum=1.0,
|
|
|
692 |
step=0.0001,
|
693 |
interactive=True,
|
694 |
)
|
695 |
+
self.inputs.ransac_max_iter = gr.Slider(
|
696 |
label="Ransac Max Iter",
|
697 |
minimum=1,
|
698 |
maximum=100,
|
|
|
702 |
)
|
703 |
with gr.Accordion("Scene Graph Settings", open=True):
|
704 |
# mapping setting
|
705 |
+
self.inputs.scene_graph = gr.Dropdown(
|
706 |
choices=["all", "swin", "oneref"],
|
707 |
value="all",
|
708 |
label="Scene Graph",
|
|
|
710 |
)
|
711 |
|
712 |
# global feature setting
|
713 |
+
self.inputs.global_feature = gr.Dropdown(
|
714 |
+
choices=self.init_retrieval_dropdown(),
|
715 |
value="netvlad",
|
716 |
label="Global features",
|
717 |
interactive=True,
|
718 |
)
|
719 |
+
self.inputs.top_k = gr.Slider(
|
720 |
+
label="Number of Images per Image to Match",
|
721 |
+
minimum=1,
|
722 |
+
maximum=100,
|
723 |
+
value=10,
|
724 |
+
step=1,
|
725 |
+
)
|
726 |
+
# button_match = gr.Button("Run Matching", variant="primary")
|
727 |
|
728 |
# mapping setting
|
729 |
with gr.Column():
|
|
|
731 |
with gr.Row():
|
732 |
with gr.Accordion("Buddle Settings", open=True):
|
733 |
with gr.Row():
|
734 |
+
self.inputs.mapper_refine_focal_length = (
|
735 |
+
gr.Checkbox(
|
736 |
+
label="Refine Focal Length",
|
737 |
+
value=False,
|
738 |
+
interactive=True,
|
739 |
+
)
|
740 |
)
|
741 |
+
self.inputs.mapper_refine_principle_points = (
|
742 |
+
gr.Checkbox(
|
743 |
+
label="Refine Principle Points",
|
744 |
+
value=False,
|
745 |
+
interactive=True,
|
746 |
+
)
|
747 |
)
|
748 |
+
self.inputs.mapper_refine_extra_params = (
|
749 |
+
gr.Checkbox(
|
750 |
+
label="Refine Extra Params",
|
751 |
+
value=False,
|
752 |
+
interactive=True,
|
753 |
+
)
|
754 |
)
|
755 |
+
with gr.Accordion("Retriangluation Settings", open=True):
|
|
|
|
|
756 |
gr.Textbox(
|
757 |
label="Retriangluation Details",
|
758 |
)
|
759 |
+
button_sfm = gr.Button("Run SFM", variant="primary")
|
760 |
+
model_3d = gr.Model3D(
|
761 |
+
interactive=True,
|
762 |
+
)
|
763 |
+
output_image = gr.Image(
|
764 |
+
label="SFM Visualize",
|
765 |
+
type="numpy",
|
766 |
+
image_mode="RGB",
|
767 |
+
interactive=False,
|
768 |
+
)
|
769 |
+
|
770 |
+
button_sfm.click(
|
771 |
+
fn=self.sfm_engine.call,
|
772 |
+
inputs=[
|
773 |
+
self.inputs.matcher_key,
|
774 |
+
self.inputs.input_images, # images
|
775 |
+
self.inputs.camera_model,
|
776 |
+
self.inputs.camera_params,
|
777 |
+
self.inputs.max_keypoints,
|
778 |
+
self.inputs.keypoint_threshold,
|
779 |
+
self.inputs.match_threshold,
|
780 |
+
self.inputs.ransac_threshold,
|
781 |
+
self.inputs.ransac_confidence,
|
782 |
+
self.inputs.ransac_max_iter,
|
783 |
+
self.inputs.scene_graph,
|
784 |
+
self.inputs.global_feature,
|
785 |
+
self.inputs.top_k,
|
786 |
+
self.inputs.mapper_refine_focal_length,
|
787 |
+
self.inputs.mapper_refine_principle_points,
|
788 |
+
self.inputs.mapper_refine_extra_params,
|
789 |
+
],
|
790 |
+
outputs=[model_3d, output_image],
|
791 |
+
)
|
common/config.yaml
CHANGED
@@ -403,3 +403,11 @@ matcher_zoo:
|
|
403 |
paper: https://arxiv.org/abs/2304.14845
|
404 |
project: https://feixue94.github.io/
|
405 |
display: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
paper: https://arxiv.org/abs/2304.14845
|
404 |
project: https://feixue94.github.io/
|
405 |
display: true
|
406 |
+
|
407 |
+
retrieval_zoo:
|
408 |
+
netvlad:
|
409 |
+
enable: true
|
410 |
+
openibl:
|
411 |
+
enable: true
|
412 |
+
cosplace:
|
413 |
+
enable: true
|
common/sfm.py
ADDED
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import shutil
|
2 |
+
import tempfile
|
3 |
+
from pathlib import Path
|
4 |
+
from typing import Any, Dict, List
|
5 |
+
|
6 |
+
import pycolmap
|
7 |
+
|
8 |
+
from hloc import (
|
9 |
+
extract_features,
|
10 |
+
logger,
|
11 |
+
match_features,
|
12 |
+
pairs_from_retrieval,
|
13 |
+
reconstruction,
|
14 |
+
visualization,
|
15 |
+
)
|
16 |
+
|
17 |
+
from .viz import fig2im
|
18 |
+
|
19 |
+
|
20 |
+
class SfmEngine:
|
21 |
+
def __init__(self, cfg: Dict[str, Any] = None):
|
22 |
+
self.cfg = cfg
|
23 |
+
if "outputs" in cfg and Path(cfg["outputs"]):
|
24 |
+
outputs = Path(cfg["outputs"])
|
25 |
+
outputs.mkdir(parents=True, exist_ok=True)
|
26 |
+
else:
|
27 |
+
outputs = tempfile.mkdtemp()
|
28 |
+
self.outputs = Path(outputs)
|
29 |
+
|
30 |
+
def call(
|
31 |
+
self,
|
32 |
+
key: str,
|
33 |
+
images: Path,
|
34 |
+
camera_model: str,
|
35 |
+
camera_params: List[float],
|
36 |
+
max_keypoints: int,
|
37 |
+
keypoint_threshold: float,
|
38 |
+
match_threshold: float,
|
39 |
+
ransac_threshold: int,
|
40 |
+
ransac_confidence: float,
|
41 |
+
ransac_max_iter: int,
|
42 |
+
scene_graph: bool,
|
43 |
+
global_feature: str,
|
44 |
+
top_k: int = 10,
|
45 |
+
mapper_refine_focal_length: bool = False,
|
46 |
+
mapper_refine_principle_points: bool = False,
|
47 |
+
mapper_refine_extra_params: bool = False,
|
48 |
+
):
|
49 |
+
"""
|
50 |
+
Call a list of functions to perform feature extraction, matching, and reconstruction.
|
51 |
+
|
52 |
+
Args:
|
53 |
+
key (str): The key to retrieve the matcher and feature models.
|
54 |
+
images (Path): The directory containing the images.
|
55 |
+
outputs (Path): The directory to store the outputs.
|
56 |
+
camera_model (str): The camera model.
|
57 |
+
camera_params (List[float]): The camera parameters.
|
58 |
+
max_keypoints (int): The maximum number of features.
|
59 |
+
match_threshold (float): The match threshold.
|
60 |
+
ransac_threshold (int): The RANSAC threshold.
|
61 |
+
ransac_confidence (float): The RANSAC confidence.
|
62 |
+
ransac_max_iter (int): The maximum number of RANSAC iterations.
|
63 |
+
scene_graph (bool): Whether to compute the scene graph.
|
64 |
+
global_feature (str): Whether to compute the global feature.
|
65 |
+
top_k (int): The number of image-pair to use.
|
66 |
+
mapper_refine_focal_length (bool): Whether to refine the focal length.
|
67 |
+
mapper_refine_principle_points (bool): Whether to refine the principle points.
|
68 |
+
mapper_refine_extra_params (bool): Whether to refine the extra parameters.
|
69 |
+
|
70 |
+
Returns:
|
71 |
+
Path: The directory containing the SfM results.
|
72 |
+
"""
|
73 |
+
if len(images) == 0:
|
74 |
+
logger.error(f"{images} does not exist.")
|
75 |
+
|
76 |
+
temp_images = Path(tempfile.mkdtemp())
|
77 |
+
# copy images
|
78 |
+
logger.info(f"Copying images to {temp_images}.")
|
79 |
+
for image in images:
|
80 |
+
shutil.copy(image, temp_images)
|
81 |
+
|
82 |
+
matcher_zoo = self.cfg["matcher_zoo"]
|
83 |
+
model = matcher_zoo[key]
|
84 |
+
match_conf = model["matcher"]
|
85 |
+
match_conf["model"]["max_keypoints"] = max_keypoints
|
86 |
+
match_conf["model"]["match_threshold"] = match_threshold
|
87 |
+
|
88 |
+
feature_conf = model["feature"]
|
89 |
+
feature_conf["model"]["max_keypoints"] = max_keypoints
|
90 |
+
feature_conf["model"]["keypoint_threshold"] = keypoint_threshold
|
91 |
+
|
92 |
+
# retrieval
|
93 |
+
retrieval_name = self.cfg.get("retrieval_name", "netvlad")
|
94 |
+
retrieval_conf = extract_features.confs[retrieval_name]
|
95 |
+
|
96 |
+
mapper_options = {
|
97 |
+
"ba_refine_extra_params": mapper_refine_extra_params,
|
98 |
+
"ba_refine_focal_length": mapper_refine_focal_length,
|
99 |
+
"ba_refine_principal_point": mapper_refine_principle_points,
|
100 |
+
"ba_local_max_num_iterations": 40,
|
101 |
+
"ba_local_max_refinements": 3,
|
102 |
+
"ba_global_max_num_iterations": 100,
|
103 |
+
# below 3 options are for individual/video data, for internet photos, they should be left
|
104 |
+
# default
|
105 |
+
"min_focal_length_ratio": 0.1,
|
106 |
+
"max_focal_length_ratio": 10,
|
107 |
+
"max_extra_param": 1e15,
|
108 |
+
}
|
109 |
+
|
110 |
+
sfm_dir = self.outputs / "sfm_{}".format(key)
|
111 |
+
sfm_pairs = self.outputs / "pairs-sfm.txt"
|
112 |
+
sfm_dir.mkdir(exist_ok=True, parents=True)
|
113 |
+
|
114 |
+
# extract features
|
115 |
+
retrieval_path = extract_features.main(
|
116 |
+
retrieval_conf, temp_images, self.outputs
|
117 |
+
)
|
118 |
+
pairs_from_retrieval.main(retrieval_path, sfm_pairs, num_matched=top_k)
|
119 |
+
|
120 |
+
feature_path = extract_features.main(
|
121 |
+
feature_conf, temp_images, self.outputs
|
122 |
+
)
|
123 |
+
# match features
|
124 |
+
match_path = match_features.main(
|
125 |
+
match_conf, sfm_pairs, feature_conf["output"], self.outputs
|
126 |
+
)
|
127 |
+
# reconstruction
|
128 |
+
already_sfm = False
|
129 |
+
if sfm_dir.exists():
|
130 |
+
try:
|
131 |
+
model = pycolmap.Reconstruction(str(sfm_dir))
|
132 |
+
already_sfm = True
|
133 |
+
except ValueError:
|
134 |
+
logger.info(f"sfm_dir not exists model: {sfm_dir}")
|
135 |
+
if not already_sfm:
|
136 |
+
model = reconstruction.main(
|
137 |
+
sfm_dir,
|
138 |
+
temp_images,
|
139 |
+
sfm_pairs,
|
140 |
+
feature_path,
|
141 |
+
match_path,
|
142 |
+
mapper_options=mapper_options,
|
143 |
+
)
|
144 |
+
|
145 |
+
vertices = []
|
146 |
+
for point3D_id, point3D in model.points3D.items():
|
147 |
+
vertices.append([point3D.xyz, point3D.color])
|
148 |
+
|
149 |
+
model_3d = sfm_dir / "points3D.obj"
|
150 |
+
with open(model_3d, "w") as f:
|
151 |
+
for p, c in vertices:
|
152 |
+
# Write vertex position
|
153 |
+
f.write("v {} {} {}\n".format(p[0], p[1], p[2]))
|
154 |
+
# Write vertex normal (color)
|
155 |
+
f.write(
|
156 |
+
"vn {} {} {}\n".format(
|
157 |
+
c[0] / 255.0, c[1] / 255.0, c[2] / 255.0
|
158 |
+
)
|
159 |
+
)
|
160 |
+
viz_2d = visualization.visualize_sfm_2d(
|
161 |
+
model, temp_images, color_by="visibility", n=2, dpi=300
|
162 |
+
)
|
163 |
+
|
164 |
+
return model_3d, fig2im(viz_2d) / 255.0
|
hloc/colmap_from_nvm.py
CHANGED
@@ -25,7 +25,9 @@ def recover_database_images_and_ids(database_path):
|
|
25 |
images[name] = image_id
|
26 |
cameras[name] = camera_id
|
27 |
db.close()
|
28 |
-
logger.info(
|
|
|
|
|
29 |
return images, cameras
|
30 |
|
31 |
|
@@ -34,9 +36,21 @@ def quaternion_to_rotation_matrix(qvec):
|
|
34 |
w, x, y, z = qvec
|
35 |
R = np.array(
|
36 |
[
|
37 |
-
[
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
]
|
41 |
)
|
42 |
return R
|
@@ -47,7 +61,9 @@ def camera_center_to_translation(c, qvec):
|
|
47 |
return (-1) * np.matmul(R, c)
|
48 |
|
49 |
|
50 |
-
def read_nvm_model(
|
|
|
|
|
51 |
with open(intrinsics_path, "r") as f:
|
52 |
raw_intrinsics = f.readlines()
|
53 |
|
|
|
25 |
images[name] = image_id
|
26 |
cameras[name] = camera_id
|
27 |
db.close()
|
28 |
+
logger.info(
|
29 |
+
f"Found {len(images)} images and {len(cameras)} cameras in database."
|
30 |
+
)
|
31 |
return images, cameras
|
32 |
|
33 |
|
|
|
36 |
w, x, y, z = qvec
|
37 |
R = np.array(
|
38 |
[
|
39 |
+
[
|
40 |
+
1 - 2 * y * y - 2 * z * z,
|
41 |
+
2 * x * y - 2 * z * w,
|
42 |
+
2 * x * z + 2 * y * w,
|
43 |
+
],
|
44 |
+
[
|
45 |
+
2 * x * y + 2 * z * w,
|
46 |
+
1 - 2 * x * x - 2 * z * z,
|
47 |
+
2 * y * z - 2 * x * w,
|
48 |
+
],
|
49 |
+
[
|
50 |
+
2 * x * z - 2 * y * w,
|
51 |
+
2 * y * z + 2 * x * w,
|
52 |
+
1 - 2 * x * x - 2 * y * y,
|
53 |
+
],
|
54 |
]
|
55 |
)
|
56 |
return R
|
|
|
61 |
return (-1) * np.matmul(R, c)
|
62 |
|
63 |
|
64 |
+
def read_nvm_model(
|
65 |
+
nvm_path, intrinsics_path, image_ids, camera_ids, skip_points=False
|
66 |
+
):
|
67 |
with open(intrinsics_path, "r") as f:
|
68 |
raw_intrinsics = f.readlines()
|
69 |
|
hloc/extract_features.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import argparse
|
2 |
import collections.abc as collections
|
3 |
-
import glob
|
4 |
import pprint
|
5 |
from pathlib import Path
|
6 |
from types import SimpleNamespace
|
@@ -330,6 +329,11 @@ confs = {
|
|
330 |
"model": {"name": "cosplace"},
|
331 |
"preprocessing": {"resize_max": 1024},
|
332 |
},
|
|
|
|
|
|
|
|
|
|
|
333 |
}
|
334 |
|
335 |
|
|
|
1 |
import argparse
|
2 |
import collections.abc as collections
|
|
|
3 |
import pprint
|
4 |
from pathlib import Path
|
5 |
from types import SimpleNamespace
|
|
|
329 |
"model": {"name": "cosplace"},
|
330 |
"preprocessing": {"resize_max": 1024},
|
331 |
},
|
332 |
+
"eigenplaces": {
|
333 |
+
"output": "global-feats-eigenplaces",
|
334 |
+
"model": {"name": "eigenplaces"},
|
335 |
+
"preprocessing": {"resize_max": 1024},
|
336 |
+
},
|
337 |
}
|
338 |
|
339 |
|
hloc/extractors/eigenplaces.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Code for loading models trained with EigenPlaces (or CosPlace) as a global
|
3 |
+
features extractor for geolocalization through image retrieval.
|
4 |
+
Multiple models are available with different backbones. Below is a summary of
|
5 |
+
models available (backbone : list of available output descriptors
|
6 |
+
dimensionality). For example you can use a model based on a ResNet50 with
|
7 |
+
descriptors dimensionality 1024.
|
8 |
+
|
9 |
+
EigenPlaces trained models:
|
10 |
+
ResNet18: [ 256, 512]
|
11 |
+
ResNet50: [128, 256, 512, 2048]
|
12 |
+
ResNet101: [128, 256, 512, 2048]
|
13 |
+
VGG16: [ 512]
|
14 |
+
|
15 |
+
CosPlace trained models:
|
16 |
+
ResNet18: [32, 64, 128, 256, 512]
|
17 |
+
ResNet50: [32, 64, 128, 256, 512, 1024, 2048]
|
18 |
+
ResNet101: [32, 64, 128, 256, 512, 1024, 2048]
|
19 |
+
ResNet152: [32, 64, 128, 256, 512, 1024, 2048]
|
20 |
+
VGG16: [ 64, 128, 256, 512]
|
21 |
+
|
22 |
+
EigenPlaces paper (ICCV 2023): https://arxiv.org/abs/2308.10832
|
23 |
+
CosPlace paper (CVPR 2022): https://arxiv.org/abs/2204.02287
|
24 |
+
"""
|
25 |
+
|
26 |
+
import torch
|
27 |
+
import torchvision.transforms as tvf
|
28 |
+
|
29 |
+
from ..utils.base_model import BaseModel
|
30 |
+
|
31 |
+
|
32 |
+
class EigenPlaces(BaseModel):
|
33 |
+
default_conf = {
|
34 |
+
"variant": "EigenPlaces",
|
35 |
+
"backbone": "ResNet101",
|
36 |
+
"fc_output_dim": 2048,
|
37 |
+
}
|
38 |
+
required_inputs = ["image"]
|
39 |
+
|
40 |
+
def _init(self, conf):
|
41 |
+
self.net = torch.hub.load(
|
42 |
+
"gmberton/" + conf["variant"],
|
43 |
+
"get_trained_model",
|
44 |
+
backbone=conf["backbone"],
|
45 |
+
fc_output_dim=conf["fc_output_dim"],
|
46 |
+
).eval()
|
47 |
+
|
48 |
+
mean = [0.485, 0.456, 0.406]
|
49 |
+
std = [0.229, 0.224, 0.225]
|
50 |
+
self.norm_rgb = tvf.Normalize(mean=mean, std=std)
|
51 |
+
|
52 |
+
def _forward(self, data):
|
53 |
+
image = self.norm_rgb(data["image"])
|
54 |
+
desc = self.net(image)
|
55 |
+
return {
|
56 |
+
"global_descriptor": desc,
|
57 |
+
}
|
hloc/localize_inloc.py
CHANGED
@@ -24,7 +24,9 @@ def interpolate_scan(scan, kp):
|
|
24 |
|
25 |
# To maximize the number of points that have depth:
|
26 |
# do bilinear interpolation first and then nearest for the remaining points
|
27 |
-
interp_lin = grid_sample(scan, kp, align_corners=True, mode="bilinear")[
|
|
|
|
|
28 |
interp_nn = torch.nn.functional.grid_sample(
|
29 |
scan, kp, align_corners=True, mode="nearest"
|
30 |
)[0, :, 0]
|
@@ -64,7 +66,9 @@ def get_scan_pose(dataset_dir, rpath):
|
|
64 |
return P_after_GICP
|
65 |
|
66 |
|
67 |
-
def pose_from_cluster(
|
|
|
|
|
68 |
height, width = cv2.imread(str(dataset_dir / q)).shape[:2]
|
69 |
cx = 0.5 * width
|
70 |
cy = 0.5 * height
|
|
|
24 |
|
25 |
# To maximize the number of points that have depth:
|
26 |
# do bilinear interpolation first and then nearest for the remaining points
|
27 |
+
interp_lin = grid_sample(scan, kp, align_corners=True, mode="bilinear")[
|
28 |
+
0, :, 0
|
29 |
+
]
|
30 |
interp_nn = torch.nn.functional.grid_sample(
|
31 |
scan, kp, align_corners=True, mode="nearest"
|
32 |
)[0, :, 0]
|
|
|
66 |
return P_after_GICP
|
67 |
|
68 |
|
69 |
+
def pose_from_cluster(
|
70 |
+
dataset_dir, q, retrieved, feature_file, match_file, skip=None
|
71 |
+
):
|
72 |
height, width = cv2.imread(str(dataset_dir / q)).shape[:2]
|
73 |
cx = 0.5 * width
|
74 |
cy = 0.5 * height
|
hloc/localize_sfm.py
CHANGED
@@ -40,7 +40,9 @@ def do_covisibility_clustering(
|
|
40 |
obs.image_id
|
41 |
for p2D in observed
|
42 |
if p2D.has_point3D()
|
43 |
-
for obs in reconstruction.points3D[
|
|
|
|
|
44 |
}
|
45 |
connected_frames &= set(frame_ids)
|
46 |
connected_frames -= visited
|
@@ -149,7 +151,10 @@ def main(
|
|
149 |
reference_sfm = pycolmap.Reconstruction(reference_sfm)
|
150 |
db_name_to_id = {img.name: i for i, img in reference_sfm.images.items()}
|
151 |
|
152 |
-
config = {
|
|
|
|
|
|
|
153 |
localizer = QueryLocalizer(reference_sfm, config)
|
154 |
|
155 |
cam_from_world = {}
|
@@ -162,7 +167,9 @@ def main(
|
|
162 |
logger.info("Starting localization...")
|
163 |
for qname, qcam in tqdm(queries):
|
164 |
if qname not in retrieval_dict:
|
165 |
-
logger.warning(
|
|
|
|
|
166 |
continue
|
167 |
db_names = retrieval_dict[qname]
|
168 |
db_ids = []
|
|
|
40 |
obs.image_id
|
41 |
for p2D in observed
|
42 |
if p2D.has_point3D()
|
43 |
+
for obs in reconstruction.points3D[
|
44 |
+
p2D.point3D_id
|
45 |
+
].track.elements
|
46 |
}
|
47 |
connected_frames &= set(frame_ids)
|
48 |
connected_frames -= visited
|
|
|
151 |
reference_sfm = pycolmap.Reconstruction(reference_sfm)
|
152 |
db_name_to_id = {img.name: i for i, img in reference_sfm.images.items()}
|
153 |
|
154 |
+
config = {
|
155 |
+
"estimation": {"ransac": {"max_error": ransac_thresh}},
|
156 |
+
**(config or {}),
|
157 |
+
}
|
158 |
localizer = QueryLocalizer(reference_sfm, config)
|
159 |
|
160 |
cam_from_world = {}
|
|
|
167 |
logger.info("Starting localization...")
|
168 |
for qname, qcam in tqdm(queries):
|
169 |
if qname not in retrieval_dict:
|
170 |
+
logger.warning(
|
171 |
+
f"No images retrieved for query image {qname}. Skipping..."
|
172 |
+
)
|
173 |
continue
|
174 |
db_names = retrieval_dict[qname]
|
175 |
db_ids = []
|
hloc/match_dense.py
CHANGED
@@ -13,8 +13,9 @@ import torch
|
|
13 |
import torchvision.transforms.functional as F
|
14 |
from scipy.spatial import KDTree
|
15 |
from tqdm import tqdm
|
16 |
-
|
17 |
from . import logger, matchers
|
|
|
18 |
from .match_features import find_unique_new_pairs
|
19 |
from .utils.base_model import dynamic_load
|
20 |
from .utils.io import list_h5_names
|
@@ -288,6 +289,7 @@ confs = {
|
|
288 |
},
|
289 |
}
|
290 |
|
|
|
291 |
def to_cpts(kpts, ps):
|
292 |
if ps > 0.0:
|
293 |
kpts = np.round(np.round((kpts + 0.5) / ps) * ps - 0.5, 2)
|
@@ -379,11 +381,13 @@ def kpids_to_matches0(kpt_ids0, kpt_ids1, scores):
|
|
379 |
matches, scores = get_unique_matches(matches, scores)
|
380 |
return matches_to_matches0(matches, scores)
|
381 |
|
|
|
382 |
def scale_keypoints(kpts, scale):
|
383 |
if np.any(scale != 1.0):
|
384 |
kpts *= kpts.new_tensor(scale)
|
385 |
return kpts
|
386 |
|
|
|
387 |
class ImagePairDataset(torch.utils.data.Dataset):
|
388 |
default_conf = {
|
389 |
"grayscale": True,
|
@@ -398,7 +402,9 @@ class ImagePairDataset(torch.utils.data.Dataset):
|
|
398 |
self.pairs = pairs
|
399 |
if self.conf.cache_images:
|
400 |
image_names = set(sum(pairs, ())) # unique image names in pairs
|
401 |
-
logger.info(
|
|
|
|
|
402 |
self.images = {}
|
403 |
self.scales = {}
|
404 |
for name in tqdm(image_names):
|
@@ -570,7 +576,9 @@ def aggregate_matches(
|
|
570 |
required_queries -= set(list_h5_names(feature_path))
|
571 |
|
572 |
# if an entry in cpdict is provided as np.ndarray we assume it is fixed
|
573 |
-
required_queries -= set(
|
|
|
|
|
574 |
|
575 |
# sort pairs for reduced RAM
|
576 |
pairs_per_q = Counter(list(chain(*pairs)))
|
@@ -578,7 +586,9 @@ def aggregate_matches(
|
|
578 |
pairs = [p for _, p in sorted(zip(pairs_score, pairs))]
|
579 |
|
580 |
if len(required_queries) > 0:
|
581 |
-
logger.info(
|
|
|
|
|
582 |
n_kps = 0
|
583 |
with h5py.File(str(match_path), "a") as fd:
|
584 |
for name0, name1 in tqdm(pairs, smoothing=0.1):
|
@@ -756,6 +766,7 @@ def match_and_assign(
|
|
756 |
logger.info(f'Reassign matches with max_error={conf["max_error"]}.')
|
757 |
assign_matches(pairs, match_path, cpdict, max_error=conf["max_error"])
|
758 |
|
|
|
759 |
def scale_lines(lines, scale):
|
760 |
if np.any(scale != 1.0):
|
761 |
lines *= lines.new_tensor(scale)
|
@@ -972,6 +983,7 @@ def match_images(model, image_0, image_1, conf, device="cpu"):
|
|
972 |
torch.cuda.empty_cache()
|
973 |
return ret
|
974 |
|
|
|
975 |
@torch.no_grad()
|
976 |
def main(
|
977 |
conf: Dict,
|
@@ -985,7 +997,8 @@ def main(
|
|
985 |
overwrite: bool = False,
|
986 |
) -> Path:
|
987 |
logger.info(
|
988 |
-
"Extracting semi-dense features with configuration:"
|
|
|
989 |
)
|
990 |
|
991 |
if features is None:
|
@@ -995,7 +1008,8 @@ def main(
|
|
995 |
features_q = features
|
996 |
if matches is None:
|
997 |
raise ValueError(
|
998 |
-
"Either provide both features and matches as Path"
|
|
|
999 |
)
|
1000 |
else:
|
1001 |
if export_dir is None:
|
@@ -1017,7 +1031,14 @@ def main(
|
|
1017 |
raise TypeError(str(features_ref))
|
1018 |
|
1019 |
match_and_assign(
|
1020 |
-
conf,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1021 |
)
|
1022 |
|
1023 |
return features_q, matches
|
@@ -1028,11 +1049,15 @@ if __name__ == "__main__":
|
|
1028 |
parser.add_argument("--pairs", type=Path, required=True)
|
1029 |
parser.add_argument("--image_dir", type=Path, required=True)
|
1030 |
parser.add_argument("--export_dir", type=Path, required=True)
|
1031 |
-
parser.add_argument(
|
|
|
|
|
1032 |
parser.add_argument(
|
1033 |
"--features", type=str, default="feats_" + confs["loftr"]["output"]
|
1034 |
)
|
1035 |
-
parser.add_argument(
|
|
|
|
|
1036 |
args = parser.parse_args()
|
1037 |
main(
|
1038 |
confs[args.conf],
|
@@ -1042,4 +1067,3 @@ if __name__ == "__main__":
|
|
1042 |
args.matches,
|
1043 |
args.features,
|
1044 |
)
|
1045 |
-
|
|
|
13 |
import torchvision.transforms.functional as F
|
14 |
from scipy.spatial import KDTree
|
15 |
from tqdm import tqdm
|
16 |
+
|
17 |
from . import logger, matchers
|
18 |
+
from .extract_features import read_image, resize_image
|
19 |
from .match_features import find_unique_new_pairs
|
20 |
from .utils.base_model import dynamic_load
|
21 |
from .utils.io import list_h5_names
|
|
|
289 |
},
|
290 |
}
|
291 |
|
292 |
+
|
293 |
def to_cpts(kpts, ps):
|
294 |
if ps > 0.0:
|
295 |
kpts = np.round(np.round((kpts + 0.5) / ps) * ps - 0.5, 2)
|
|
|
381 |
matches, scores = get_unique_matches(matches, scores)
|
382 |
return matches_to_matches0(matches, scores)
|
383 |
|
384 |
+
|
385 |
def scale_keypoints(kpts, scale):
|
386 |
if np.any(scale != 1.0):
|
387 |
kpts *= kpts.new_tensor(scale)
|
388 |
return kpts
|
389 |
|
390 |
+
|
391 |
class ImagePairDataset(torch.utils.data.Dataset):
|
392 |
default_conf = {
|
393 |
"grayscale": True,
|
|
|
402 |
self.pairs = pairs
|
403 |
if self.conf.cache_images:
|
404 |
image_names = set(sum(pairs, ())) # unique image names in pairs
|
405 |
+
logger.info(
|
406 |
+
f"Loading and caching {len(image_names)} unique images."
|
407 |
+
)
|
408 |
self.images = {}
|
409 |
self.scales = {}
|
410 |
for name in tqdm(image_names):
|
|
|
576 |
required_queries -= set(list_h5_names(feature_path))
|
577 |
|
578 |
# if an entry in cpdict is provided as np.ndarray we assume it is fixed
|
579 |
+
required_queries -= set(
|
580 |
+
[k for k, v in cpdict.items() if isinstance(v, np.ndarray)]
|
581 |
+
)
|
582 |
|
583 |
# sort pairs for reduced RAM
|
584 |
pairs_per_q = Counter(list(chain(*pairs)))
|
|
|
586 |
pairs = [p for _, p in sorted(zip(pairs_score, pairs))]
|
587 |
|
588 |
if len(required_queries) > 0:
|
589 |
+
logger.info(
|
590 |
+
f"Aggregating keypoints for {len(required_queries)} images."
|
591 |
+
)
|
592 |
n_kps = 0
|
593 |
with h5py.File(str(match_path), "a") as fd:
|
594 |
for name0, name1 in tqdm(pairs, smoothing=0.1):
|
|
|
766 |
logger.info(f'Reassign matches with max_error={conf["max_error"]}.')
|
767 |
assign_matches(pairs, match_path, cpdict, max_error=conf["max_error"])
|
768 |
|
769 |
+
|
770 |
def scale_lines(lines, scale):
|
771 |
if np.any(scale != 1.0):
|
772 |
lines *= lines.new_tensor(scale)
|
|
|
983 |
torch.cuda.empty_cache()
|
984 |
return ret
|
985 |
|
986 |
+
|
987 |
@torch.no_grad()
|
988 |
def main(
|
989 |
conf: Dict,
|
|
|
997 |
overwrite: bool = False,
|
998 |
) -> Path:
|
999 |
logger.info(
|
1000 |
+
"Extracting semi-dense features with configuration:"
|
1001 |
+
f"\n{pprint.pformat(conf)}"
|
1002 |
)
|
1003 |
|
1004 |
if features is None:
|
|
|
1008 |
features_q = features
|
1009 |
if matches is None:
|
1010 |
raise ValueError(
|
1011 |
+
"Either provide both features and matches as Path"
|
1012 |
+
" or both as names."
|
1013 |
)
|
1014 |
else:
|
1015 |
if export_dir is None:
|
|
|
1031 |
raise TypeError(str(features_ref))
|
1032 |
|
1033 |
match_and_assign(
|
1034 |
+
conf,
|
1035 |
+
pairs,
|
1036 |
+
image_dir,
|
1037 |
+
matches,
|
1038 |
+
features_q,
|
1039 |
+
features_ref,
|
1040 |
+
max_kps,
|
1041 |
+
overwrite,
|
1042 |
)
|
1043 |
|
1044 |
return features_q, matches
|
|
|
1049 |
parser.add_argument("--pairs", type=Path, required=True)
|
1050 |
parser.add_argument("--image_dir", type=Path, required=True)
|
1051 |
parser.add_argument("--export_dir", type=Path, required=True)
|
1052 |
+
parser.add_argument(
|
1053 |
+
"--matches", type=Path, default=confs["loftr"]["output"]
|
1054 |
+
)
|
1055 |
parser.add_argument(
|
1056 |
"--features", type=str, default="feats_" + confs["loftr"]["output"]
|
1057 |
)
|
1058 |
+
parser.add_argument(
|
1059 |
+
"--conf", type=str, default="loftr", choices=list(confs.keys())
|
1060 |
+
)
|
1061 |
args = parser.parse_args()
|
1062 |
main(
|
1063 |
confs[args.conf],
|
|
|
1067 |
args.matches,
|
1068 |
args.features,
|
1069 |
)
|
|
hloc/matchers/mast3r.py
CHANGED
@@ -8,7 +8,6 @@ import torch
|
|
8 |
import torchvision.transforms as tfm
|
9 |
|
10 |
from .. import logger
|
11 |
-
from ..utils.base_model import BaseModel
|
12 |
|
13 |
mast3r_path = Path(__file__).parent / "../../third_party/mast3r"
|
14 |
sys.path.append(str(mast3r_path))
|
@@ -16,12 +15,11 @@ sys.path.append(str(mast3r_path))
|
|
16 |
dust3r_path = Path(__file__).parent / "../../third_party/dust3r"
|
17 |
sys.path.append(str(dust3r_path))
|
18 |
|
19 |
-
from mast3r.model import AsymmetricMASt3R
|
20 |
-
from mast3r.fast_nn import fast_reciprocal_NNs
|
21 |
-
|
22 |
from dust3r.image_pairs import make_pairs
|
23 |
from dust3r.inference import inference
|
24 |
-
from
|
|
|
|
|
25 |
from hloc.matchers.duster import Duster
|
26 |
|
27 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
@@ -70,8 +68,8 @@ class Mast3r(Duster):
|
|
70 |
output = inference(pairs, self.net, device, batch_size=1)
|
71 |
|
72 |
# at this stage, you have the raw dust3r predictions
|
73 |
-
|
74 |
-
|
75 |
|
76 |
desc1, desc2 = (
|
77 |
pred1["desc"][1].squeeze(0).detach(),
|
|
|
8 |
import torchvision.transforms as tfm
|
9 |
|
10 |
from .. import logger
|
|
|
11 |
|
12 |
mast3r_path = Path(__file__).parent / "../../third_party/mast3r"
|
13 |
sys.path.append(str(mast3r_path))
|
|
|
15 |
dust3r_path = Path(__file__).parent / "../../third_party/dust3r"
|
16 |
sys.path.append(str(dust3r_path))
|
17 |
|
|
|
|
|
|
|
18 |
from dust3r.image_pairs import make_pairs
|
19 |
from dust3r.inference import inference
|
20 |
+
from mast3r.fast_nn import fast_reciprocal_NNs
|
21 |
+
from mast3r.model import AsymmetricMASt3R
|
22 |
+
|
23 |
from hloc.matchers.duster import Duster
|
24 |
|
25 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
68 |
output = inference(pairs, self.net, device, batch_size=1)
|
69 |
|
70 |
# at this stage, you have the raw dust3r predictions
|
71 |
+
_, pred1 = output["view1"], output["pred1"]
|
72 |
+
_, pred2 = output["view2"], output["pred2"]
|
73 |
|
74 |
desc1, desc2 = (
|
75 |
pred1["desc"][1].squeeze(0).detach(),
|
hloc/matchers/superglue.py
CHANGED
@@ -4,7 +4,9 @@ from pathlib import Path
|
|
4 |
from ..utils.base_model import BaseModel
|
5 |
|
6 |
sys.path.append(str(Path(__file__).parent / "../../third_party"))
|
7 |
-
from SuperGluePretrainedNetwork.models.superglue import
|
|
|
|
|
8 |
|
9 |
|
10 |
class SuperGlue(BaseModel):
|
|
|
4 |
from ..utils.base_model import BaseModel
|
5 |
|
6 |
sys.path.append(str(Path(__file__).parent / "../../third_party"))
|
7 |
+
from SuperGluePretrainedNetwork.models.superglue import ( # noqa: E402
|
8 |
+
SuperGlue as SG,
|
9 |
+
)
|
10 |
|
11 |
|
12 |
class SuperGlue(BaseModel):
|
hloc/pairs_from_exhaustive.py
CHANGED
@@ -34,7 +34,9 @@ def main(
|
|
34 |
elif isinstance(image_list, collections.Iterable):
|
35 |
names_ref = list(ref_list)
|
36 |
else:
|
37 |
-
raise ValueError(
|
|
|
|
|
38 |
elif ref_features is not None:
|
39 |
names_ref = list_h5_names(ref_features)
|
40 |
else:
|
|
|
34 |
elif isinstance(image_list, collections.Iterable):
|
35 |
names_ref = list(ref_list)
|
36 |
else:
|
37 |
+
raise ValueError(
|
38 |
+
f"Unknown type for reference image list: {ref_list}"
|
39 |
+
)
|
40 |
elif ref_features is not None:
|
41 |
names_ref = list_h5_names(ref_features)
|
42 |
else:
|
hloc/pairs_from_poses.py
CHANGED
@@ -63,6 +63,8 @@ if __name__ == "__main__":
|
|
63 |
parser.add_argument("--model", required=True, type=Path)
|
64 |
parser.add_argument("--output", required=True, type=Path)
|
65 |
parser.add_argument("--num_matched", required=True, type=int)
|
66 |
-
parser.add_argument(
|
|
|
|
|
67 |
args = parser.parse_args()
|
68 |
main(**args.__dict__)
|
|
|
63 |
parser.add_argument("--model", required=True, type=Path)
|
64 |
parser.add_argument("--output", required=True, type=Path)
|
65 |
parser.add_argument("--num_matched", required=True, type=int)
|
66 |
+
parser.add_argument(
|
67 |
+
"--rotation_threshold", default=DEFAULT_ROT_THRESH, type=float
|
68 |
+
)
|
69 |
args = parser.parse_args()
|
70 |
main(**args.__dict__)
|
hloc/pairs_from_retrieval.py
CHANGED
@@ -19,7 +19,9 @@ def parse_names(prefix, names, names_all):
|
|
19 |
prefix = tuple(prefix)
|
20 |
names = [n for n in names_all if n.startswith(prefix)]
|
21 |
if len(names) == 0:
|
22 |
-
raise ValueError(
|
|
|
|
|
23 |
elif names is not None:
|
24 |
if isinstance(names, (str, Path)):
|
25 |
names = parse_image_lists(names)
|
@@ -90,7 +92,9 @@ def main(
|
|
90 |
db_descriptors = descriptors
|
91 |
if isinstance(db_descriptors, (Path, str)):
|
92 |
db_descriptors = [db_descriptors]
|
93 |
-
name2db = {
|
|
|
|
|
94 |
db_names_h5 = list(name2db.keys())
|
95 |
query_names_h5 = list_h5_names(descriptors)
|
96 |
|
|
|
19 |
prefix = tuple(prefix)
|
20 |
names = [n for n in names_all if n.startswith(prefix)]
|
21 |
if len(names) == 0:
|
22 |
+
raise ValueError(
|
23 |
+
f"Could not find any image with the prefix `{prefix}`."
|
24 |
+
)
|
25 |
elif names is not None:
|
26 |
if isinstance(names, (str, Path)):
|
27 |
names = parse_image_lists(names)
|
|
|
92 |
db_descriptors = descriptors
|
93 |
if isinstance(db_descriptors, (Path, str)):
|
94 |
db_descriptors = [db_descriptors]
|
95 |
+
name2db = {
|
96 |
+
n: i for i, p in enumerate(db_descriptors) for n in list_h5_names(p)
|
97 |
+
}
|
98 |
db_names_h5 = list(name2db.keys())
|
99 |
query_names_h5 = list_h5_names(descriptors)
|
100 |
|
hloc/reconstruction.py
CHANGED
@@ -93,13 +93,16 @@ def run_reconstruction(
|
|
93 |
largest_num_images = num_images
|
94 |
assert largest_index is not None
|
95 |
logger.info(
|
96 |
-
f"Largest model is #{largest_index} "
|
|
|
97 |
)
|
98 |
|
99 |
for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
|
100 |
if (sfm_dir / filename).exists():
|
101 |
(sfm_dir / filename).unlink()
|
102 |
-
shutil.move(
|
|
|
|
|
103 |
return reconstructions[largest_index]
|
104 |
|
105 |
|
@@ -172,7 +175,9 @@ if __name__ == "__main__":
|
|
172 |
"--image_options",
|
173 |
nargs="+",
|
174 |
default=[],
|
175 |
-
help="List of key=value from {}".format(
|
|
|
|
|
176 |
)
|
177 |
parser.add_argument(
|
178 |
"--mapper_options",
|
|
|
93 |
largest_num_images = num_images
|
94 |
assert largest_index is not None
|
95 |
logger.info(
|
96 |
+
f"Largest model is #{largest_index} "
|
97 |
+
f"with {largest_num_images} images."
|
98 |
)
|
99 |
|
100 |
for filename in ["images.bin", "cameras.bin", "points3D.bin"]:
|
101 |
if (sfm_dir / filename).exists():
|
102 |
(sfm_dir / filename).unlink()
|
103 |
+
shutil.move(
|
104 |
+
str(models_path / str(largest_index) / filename), str(sfm_dir)
|
105 |
+
)
|
106 |
return reconstructions[largest_index]
|
107 |
|
108 |
|
|
|
175 |
"--image_options",
|
176 |
nargs="+",
|
177 |
default=[],
|
178 |
+
help="List of key=value from {}".format(
|
179 |
+
pycolmap.ImageReaderOptions().todict()
|
180 |
+
),
|
181 |
)
|
182 |
parser.add_argument(
|
183 |
"--mapper_options",
|
hloc/triangulation.py
CHANGED
@@ -118,7 +118,9 @@ def estimation_and_geometric_verification(
|
|
118 |
pycolmap.verify_matches(
|
119 |
database_path,
|
120 |
pairs_path,
|
121 |
-
options=dict(
|
|
|
|
|
122 |
)
|
123 |
|
124 |
|
@@ -142,7 +144,9 @@ def geometric_verification(
|
|
142 |
id0 = image_ids[name0]
|
143 |
image0 = reference.images[id0]
|
144 |
cam0 = reference.cameras[image0.camera_id]
|
145 |
-
kps0, noise0 = get_keypoints(
|
|
|
|
|
146 |
noise0 = 1.0 if noise0 is None else noise0
|
147 |
if len(kps0) > 0:
|
148 |
kps0 = np.stack(cam0.cam_from_img(kps0))
|
@@ -153,7 +157,9 @@ def geometric_verification(
|
|
153 |
id1 = image_ids[name1]
|
154 |
image1 = reference.images[id1]
|
155 |
cam1 = reference.cameras[image1.camera_id]
|
156 |
-
kps1, noise1 = get_keypoints(
|
|
|
|
|
157 |
noise1 = 1.0 if noise1 is None else noise1
|
158 |
if len(kps1) > 0:
|
159 |
kps1 = np.stack(cam1.cam_from_img(kps1))
|
@@ -170,7 +176,9 @@ def geometric_verification(
|
|
170 |
db.add_two_view_geometry(id0, id1, matches)
|
171 |
continue
|
172 |
|
173 |
-
cam1_from_cam0 =
|
|
|
|
|
174 |
errors0, errors1 = compute_epipolar_errors(
|
175 |
cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]]
|
176 |
)
|
@@ -209,7 +217,11 @@ def run_triangulation(
|
|
209 |
with OutputCapture(verbose):
|
210 |
with pycolmap.ostream():
|
211 |
reconstruction = pycolmap.triangulate_points(
|
212 |
-
reference_model,
|
|
|
|
|
|
|
|
|
213 |
)
|
214 |
return reconstruction
|
215 |
|
@@ -257,7 +269,8 @@ def main(
|
|
257 |
sfm_dir, database, image_dir, reference, verbose, mapper_options
|
258 |
)
|
259 |
logger.info(
|
260 |
-
"Finished the triangulation with statistics:\n%s",
|
|
|
261 |
)
|
262 |
return reconstruction
|
263 |
|
@@ -278,7 +291,8 @@ def parse_option_args(args: List[str], default_options) -> Dict[str, Any]:
|
|
278 |
target_type = type(getattr(default_options, key))
|
279 |
if not isinstance(value, target_type):
|
280 |
raise ValueError(
|
281 |
-
f'Incorrect type for option "{key}":'
|
|
|
282 |
)
|
283 |
options[key] = value
|
284 |
return options
|
|
|
118 |
pycolmap.verify_matches(
|
119 |
database_path,
|
120 |
pairs_path,
|
121 |
+
options=dict(
|
122 |
+
ransac=dict(max_num_trials=20000, min_inlier_ratio=0.1)
|
123 |
+
),
|
124 |
)
|
125 |
|
126 |
|
|
|
144 |
id0 = image_ids[name0]
|
145 |
image0 = reference.images[id0]
|
146 |
cam0 = reference.cameras[image0.camera_id]
|
147 |
+
kps0, noise0 = get_keypoints(
|
148 |
+
features_path, name0, return_uncertainty=True
|
149 |
+
)
|
150 |
noise0 = 1.0 if noise0 is None else noise0
|
151 |
if len(kps0) > 0:
|
152 |
kps0 = np.stack(cam0.cam_from_img(kps0))
|
|
|
157 |
id1 = image_ids[name1]
|
158 |
image1 = reference.images[id1]
|
159 |
cam1 = reference.cameras[image1.camera_id]
|
160 |
+
kps1, noise1 = get_keypoints(
|
161 |
+
features_path, name1, return_uncertainty=True
|
162 |
+
)
|
163 |
noise1 = 1.0 if noise1 is None else noise1
|
164 |
if len(kps1) > 0:
|
165 |
kps1 = np.stack(cam1.cam_from_img(kps1))
|
|
|
176 |
db.add_two_view_geometry(id0, id1, matches)
|
177 |
continue
|
178 |
|
179 |
+
cam1_from_cam0 = (
|
180 |
+
image1.cam_from_world * image0.cam_from_world.inverse()
|
181 |
+
)
|
182 |
errors0, errors1 = compute_epipolar_errors(
|
183 |
cam1_from_cam0, kps0[matches[:, 0]], kps1[matches[:, 1]]
|
184 |
)
|
|
|
217 |
with OutputCapture(verbose):
|
218 |
with pycolmap.ostream():
|
219 |
reconstruction = pycolmap.triangulate_points(
|
220 |
+
reference_model,
|
221 |
+
database_path,
|
222 |
+
image_dir,
|
223 |
+
model_path,
|
224 |
+
options=options,
|
225 |
)
|
226 |
return reconstruction
|
227 |
|
|
|
269 |
sfm_dir, database, image_dir, reference, verbose, mapper_options
|
270 |
)
|
271 |
logger.info(
|
272 |
+
"Finished the triangulation with statistics:\n%s",
|
273 |
+
reconstruction.summary(),
|
274 |
)
|
275 |
return reconstruction
|
276 |
|
|
|
291 |
target_type = type(getattr(default_options, key))
|
292 |
if not isinstance(value, target_type):
|
293 |
raise ValueError(
|
294 |
+
f'Incorrect type for option "{key}":'
|
295 |
+
f" {type(value)} vs {target_type}"
|
296 |
)
|
297 |
options[key] = value
|
298 |
return options
|
hloc/utils/viz.py
CHANGED
@@ -49,7 +49,7 @@ def plot_images(
|
|
49 |
if titles:
|
50 |
ax.set_title(titles[i])
|
51 |
fig.tight_layout(pad=pad)
|
52 |
-
|
53 |
|
54 |
def plot_keypoints(kpts, colors="lime", ps=4):
|
55 |
"""Plot keypoints for existing images.
|
|
|
49 |
if titles:
|
50 |
ax.set_title(titles[i])
|
51 |
fig.tight_layout(pad=pad)
|
52 |
+
return fig
|
53 |
|
54 |
def plot_keypoints(kpts, colors="lime", ps=4):
|
55 |
"""Plot keypoints for existing images.
|
hloc/visualization.py
CHANGED
@@ -6,11 +6,23 @@ import pycolmap
|
|
6 |
from matplotlib import cm
|
7 |
|
8 |
from .utils.io import read_image
|
9 |
-
from .utils.viz import
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
|
12 |
def visualize_sfm_2d(
|
13 |
-
reconstruction,
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
):
|
15 |
assert image_dir.exists()
|
16 |
if not isinstance(reconstruction, pycolmap.Reconstruction):
|
@@ -31,9 +43,11 @@ def visualize_sfm_2d(
|
|
31 |
elif color_by == "track_length":
|
32 |
tl = np.array(
|
33 |
[
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
37 |
for p in image.points2D
|
38 |
]
|
39 |
)
|
@@ -57,10 +71,11 @@ def visualize_sfm_2d(
|
|
57 |
raise NotImplementedError(f"Coloring not implemented: {color_by}.")
|
58 |
|
59 |
name = image.name
|
60 |
-
plot_images([read_image(image_dir / name)], dpi=dpi)
|
61 |
plot_keypoints([keypoints], colors=[color], ps=4)
|
62 |
add_text(0, text)
|
63 |
add_text(0, name, pos=(0.01, 0.01), fs=5, lcolor=None, va="bottom")
|
|
|
64 |
|
65 |
|
66 |
def visualize_loc(
|
@@ -121,7 +136,9 @@ def visualize_loc_from_log(
|
|
121 |
counts = np.zeros(n)
|
122 |
dbs_kp_q_db = [[] for _ in range(n)]
|
123 |
inliers_dbs = [[] for _ in range(n)]
|
124 |
-
for i, (inl, (p3D_id, db_idxs)) in enumerate(
|
|
|
|
|
125 |
track = reconstruction.points3D[p3D_id].track
|
126 |
track = {el.image_id: el.point2D_idx for el in track.elements}
|
127 |
for db_idx in db_idxs:
|
@@ -133,7 +150,9 @@ def visualize_loc_from_log(
|
|
133 |
# for inloc the database keypoints are already in the logs
|
134 |
assert "keypoints_db" in loc
|
135 |
assert "indices_db" in loc
|
136 |
-
counts = np.array(
|
|
|
|
|
137 |
|
138 |
# display the database images with the most inlier matches
|
139 |
db_sort = np.argsort(-counts)
|
|
|
6 |
from matplotlib import cm
|
7 |
|
8 |
from .utils.io import read_image
|
9 |
+
from .utils.viz import (
|
10 |
+
add_text,
|
11 |
+
cm_RdGn,
|
12 |
+
plot_images,
|
13 |
+
plot_keypoints,
|
14 |
+
plot_matches,
|
15 |
+
)
|
16 |
|
17 |
|
18 |
def visualize_sfm_2d(
|
19 |
+
reconstruction,
|
20 |
+
image_dir,
|
21 |
+
color_by="visibility",
|
22 |
+
selected=[],
|
23 |
+
n=1,
|
24 |
+
seed=0,
|
25 |
+
dpi=75,
|
26 |
):
|
27 |
assert image_dir.exists()
|
28 |
if not isinstance(reconstruction, pycolmap.Reconstruction):
|
|
|
43 |
elif color_by == "track_length":
|
44 |
tl = np.array(
|
45 |
[
|
46 |
+
(
|
47 |
+
reconstruction.points3D[p.point3D_id].track.length()
|
48 |
+
if p.has_point3D()
|
49 |
+
else 1
|
50 |
+
)
|
51 |
for p in image.points2D
|
52 |
]
|
53 |
)
|
|
|
71 |
raise NotImplementedError(f"Coloring not implemented: {color_by}.")
|
72 |
|
73 |
name = image.name
|
74 |
+
fig = plot_images([read_image(image_dir / name)], dpi=dpi)
|
75 |
plot_keypoints([keypoints], colors=[color], ps=4)
|
76 |
add_text(0, text)
|
77 |
add_text(0, name, pos=(0.01, 0.01), fs=5, lcolor=None, va="bottom")
|
78 |
+
return fig
|
79 |
|
80 |
|
81 |
def visualize_loc(
|
|
|
136 |
counts = np.zeros(n)
|
137 |
dbs_kp_q_db = [[] for _ in range(n)]
|
138 |
inliers_dbs = [[] for _ in range(n)]
|
139 |
+
for i, (inl, (p3D_id, db_idxs)) in enumerate(
|
140 |
+
zip(inliers, kp_to_3D_to_db)
|
141 |
+
):
|
142 |
track = reconstruction.points3D[p3D_id].track
|
143 |
track = {el.image_id: el.point2D_idx for el in track.elements}
|
144 |
for db_idx in db_idxs:
|
|
|
150 |
# for inloc the database keypoints are already in the logs
|
151 |
assert "keypoints_db" in loc
|
152 |
assert "indices_db" in loc
|
153 |
+
counts = np.array(
|
154 |
+
[np.sum(loc["indices_db"][inliers] == i) for i in range(n)]
|
155 |
+
)
|
156 |
|
157 |
# display the database images with the most inlier matches
|
158 |
db_sort = np.argsort(-counts)
|
requirements.txt
CHANGED
@@ -16,7 +16,7 @@ opencv-python==4.6.0.66
|
|
16 |
pandas==2.0.3
|
17 |
plotly==5.15.0
|
18 |
protobuf==4.23.2
|
19 |
-
pycolmap==0.
|
20 |
pytlsd==0.0.2
|
21 |
pytorch-lightning==1.4.9
|
22 |
PyYAML==6.0
|
@@ -34,4 +34,5 @@ onnxruntime
|
|
34 |
poselib
|
35 |
roma #dust3r
|
36 |
huggingface_hub
|
37 |
-
psutil
|
|
|
|
16 |
pandas==2.0.3
|
17 |
plotly==5.15.0
|
18 |
protobuf==4.23.2
|
19 |
+
pycolmap==0.6.0
|
20 |
pytlsd==0.0.2
|
21 |
pytorch-lightning==1.4.9
|
22 |
PyYAML==6.0
|
|
|
34 |
poselib
|
35 |
roma #dust3r
|
36 |
huggingface_hub
|
37 |
+
psutil
|
38 |
+
easydict
|