Spaces:
Build error
Build error
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
|