From 762e227e9498aab1e92bdcb3ed7405c8259406d0 Mon Sep 17 00:00:00 2001 From: cauamp Date: Fri, 10 Oct 2025 22:22:31 -0300 Subject: [PATCH 1/4] feat: add the modular elf-extractor --- hloc/extract_features.py | 48 ++++++++++++++++++++++---------- hloc/extractors/elf-extractor.py | 40 ++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 hloc/extractors/elf-extractor.py diff --git a/hloc/extract_features.py b/hloc/extract_features.py index ab9456a8..4bdd07e8 100644 --- a/hloc/extract_features.py +++ b/hloc/extract_features.py @@ -125,6 +125,34 @@ "resize_max": 1024, }, }, + "dad": { + "output": "feats-dad", + "model": { + "name": "elf-extractor", + "elf_detector": "dad", + "elf_detector_conf": {}, + "elf_descriptor": "xfeat", + "elf_descriptor_conf": {}, + }, + "preprocessing": { + "grayscale": False, + }, + }, + "dedode": { + "output": "feats-dedode", + "model": { + "name": "elf-extractor", + "elf_detector": "dedode", + "elf_detector_conf": { + "detector_weights": "L-C4-v2", + }, + "elf_descriptor": "xfeat", + "elf_descriptor_conf": {}, + }, + "preprocessing": { + "grayscale": False, + }, + }, # Global descriptors "dir": { "output": "global-feats-dir", @@ -206,9 +234,7 @@ def __getitem__(self, idx): image = image.astype(np.float32) size = image.shape[:2][::-1] - if self.conf.resize_max and ( - self.conf.resize_force or max(size) > self.conf.resize_max - ): + if self.conf.resize_max and (self.conf.resize_force or max(size) > self.conf.resize_max): scale = self.conf.resize_max / max(size) size_new = tuple(int(round(x * scale)) for x in size) image = resize_image(image, size_new, self.conf.interpolation) @@ -239,17 +265,13 @@ def main( feature_path: Optional[Path] = None, overwrite: bool = False, ) -> Path: - logger.info( - "Extracting local features with configuration:" f"\n{pprint.pformat(conf)}" - ) + logger.info(f"Extracting local features with configuration:\n{pprint.pformat(conf)}") dataset = ImageDataset(image_dir, conf["preprocessing"], image_list) if feature_path is None: feature_path = Path(export_dir, conf["output"] + ".h5") feature_path.parent.mkdir(exist_ok=True, parents=True) - skip_names = set( - list_h5_names(feature_path) if feature_path.exists() and not overwrite else () - ) + skip_names = set(list_h5_names(feature_path) if feature_path.exists() and not overwrite else ()) dataset.names = [n for n in dataset.names if n not in skip_names] if len(dataset.names) == 0: logger.info("Skipping the extraction.") @@ -259,9 +281,7 @@ def main( Model = dynamic_load(extractors, conf["model"]["name"]) model = Model(conf["model"]).eval().to(device) - loader = torch.utils.data.DataLoader( - dataset, num_workers=1, shuffle=False, pin_memory=True - ) + loader = torch.utils.data.DataLoader(dataset, num_workers=1, shuffle=False, pin_memory=True) for idx, data in enumerate(tqdm(loader)): name = dataset.names[idx] pred = model({"image": data["image"].to(device, non_blocking=True)}) @@ -311,9 +331,7 @@ def main( parser = argparse.ArgumentParser() parser.add_argument("--image_dir", type=Path, required=True) parser.add_argument("--export_dir", type=Path, required=True) - parser.add_argument( - "--conf", type=str, default="superpoint_aachen", choices=list(confs.keys()) - ) + parser.add_argument("--conf", type=str, default="superpoint_aachen", choices=list(confs.keys())) parser.add_argument("--as_half", action="store_true") parser.add_argument("--image_list", type=Path) parser.add_argument("--feature_path", type=Path) diff --git a/hloc/extractors/elf-extractor.py b/hloc/extractors/elf-extractor.py new file mode 100644 index 00000000..b5d45100 --- /dev/null +++ b/hloc/extractors/elf-extractor.py @@ -0,0 +1,40 @@ +import torch +from ..utils.base_model import BaseModel +from easy_local_features import getExtractor +from easy_local_features.feature.baseline_xfeat import XFeat_baseline + + +class ElfFeatures(BaseModel): + default_conf = { + "model": { + "name": "elf-detectors", + "elf_detector": "xfeat", + "elf_detector_conf": { + "top_k": 512, + }, + "elf_descriptor": "xfeat", + "elf_descriptor_conf": {}, + }, + } + + def _init(self, conf): + print(f"{conf=}") + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.detector = getExtractor(conf["elf_detector"], conf["elf_detector_conf"]) + self.detector.to(self.device) + + self.descriptor = getExtractor(conf["elf_descriptor"], conf["elf_descriptor_conf"]) + self.descriptor.to(self.device) + + def _forward(self, data): + kps = self.detector.detect(data["image"])[0] + + desc = self.descriptor.compute(data["image"], kps).detach().squeeze(0).T + return { + "keypoints": [ + kps.cpu(), + ], + "descriptors": [ + desc.cpu(), + ], + } diff --git a/requirements.txt b/requirements.txt index 27d828dc..0d1934d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ pycolmap>=3.12.6 kornia>=0.6.11 gdown lightglue @ git+https://github.com/cvg/LightGlue +easy-local-features @ git+https://github.com/felipecadar/easy-local-features-baselines From d43cc7620c26f95ccb1a081042d44d3a69b9a31e Mon Sep 17 00:00:00 2001 From: cauamp Date: Fri, 10 Oct 2025 22:23:47 -0300 Subject: [PATCH 2/4] remove import --- hloc/extractors/elf-extractor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hloc/extractors/elf-extractor.py b/hloc/extractors/elf-extractor.py index b5d45100..ebe1c973 100644 --- a/hloc/extractors/elf-extractor.py +++ b/hloc/extractors/elf-extractor.py @@ -1,7 +1,6 @@ import torch from ..utils.base_model import BaseModel from easy_local_features import getExtractor -from easy_local_features.feature.baseline_xfeat import XFeat_baseline class ElfFeatures(BaseModel): From 8f56e08a812f8365676416dc2f44d60ae0dd17ec Mon Sep 17 00:00:00 2001 From: cauamp Date: Fri, 10 Oct 2025 22:28:10 -0300 Subject: [PATCH 3/4] format --- hloc/extract_features.py | 20 +++++++++++++++----- hloc/extractors/elf-extractor.py | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/hloc/extract_features.py b/hloc/extract_features.py index 4bdd07e8..65b1a098 100644 --- a/hloc/extract_features.py +++ b/hloc/extract_features.py @@ -234,7 +234,9 @@ def __getitem__(self, idx): image = image.astype(np.float32) size = image.shape[:2][::-1] - if self.conf.resize_max and (self.conf.resize_force or max(size) > self.conf.resize_max): + if self.conf.resize_max and ( + self.conf.resize_force or max(size) > self.conf.resize_max + ): scale = self.conf.resize_max / max(size) size_new = tuple(int(round(x * scale)) for x in size) image = resize_image(image, size_new, self.conf.interpolation) @@ -265,13 +267,17 @@ def main( feature_path: Optional[Path] = None, overwrite: bool = False, ) -> Path: - logger.info(f"Extracting local features with configuration:\n{pprint.pformat(conf)}") + logger.info( + f"Extracting local features with configuration:\n{pprint.pformat(conf)}" + ) dataset = ImageDataset(image_dir, conf["preprocessing"], image_list) if feature_path is None: feature_path = Path(export_dir, conf["output"] + ".h5") feature_path.parent.mkdir(exist_ok=True, parents=True) - skip_names = set(list_h5_names(feature_path) if feature_path.exists() and not overwrite else ()) + skip_names = set( + list_h5_names(feature_path) if feature_path.exists() and not overwrite else () + ) dataset.names = [n for n in dataset.names if n not in skip_names] if len(dataset.names) == 0: logger.info("Skipping the extraction.") @@ -281,7 +287,9 @@ def main( Model = dynamic_load(extractors, conf["model"]["name"]) model = Model(conf["model"]).eval().to(device) - loader = torch.utils.data.DataLoader(dataset, num_workers=1, shuffle=False, pin_memory=True) + loader = torch.utils.data.DataLoader( + dataset, num_workers=1, shuffle=False, pin_memory=True + ) for idx, data in enumerate(tqdm(loader)): name = dataset.names[idx] pred = model({"image": data["image"].to(device, non_blocking=True)}) @@ -331,7 +339,9 @@ def main( parser = argparse.ArgumentParser() parser.add_argument("--image_dir", type=Path, required=True) parser.add_argument("--export_dir", type=Path, required=True) - parser.add_argument("--conf", type=str, default="superpoint_aachen", choices=list(confs.keys())) + parser.add_argument( + "--conf", type=str, default="superpoint_aachen", choices=list(confs.keys()) + ) parser.add_argument("--as_half", action="store_true") parser.add_argument("--image_list", type=Path) parser.add_argument("--feature_path", type=Path) diff --git a/hloc/extractors/elf-extractor.py b/hloc/extractors/elf-extractor.py index ebe1c973..5aa83e83 100644 --- a/hloc/extractors/elf-extractor.py +++ b/hloc/extractors/elf-extractor.py @@ -22,7 +22,9 @@ def _init(self, conf): self.detector = getExtractor(conf["elf_detector"], conf["elf_detector_conf"]) self.detector.to(self.device) - self.descriptor = getExtractor(conf["elf_descriptor"], conf["elf_descriptor_conf"]) + self.descriptor = getExtractor( + conf["elf_descriptor"], conf["elf_descriptor_conf"] + ) self.descriptor.to(self.device) def _forward(self, data): From d506978d1df60c3ba87295c9fee28d840207b9da Mon Sep 17 00:00:00 2001 From: cauamp Date: Fri, 10 Oct 2025 22:32:22 -0300 Subject: [PATCH 4/4] format import --- hloc/extractors/elf-extractor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hloc/extractors/elf-extractor.py b/hloc/extractors/elf-extractor.py index 5aa83e83..00cd5118 100644 --- a/hloc/extractors/elf-extractor.py +++ b/hloc/extractors/elf-extractor.py @@ -1,7 +1,8 @@ import torch -from ..utils.base_model import BaseModel from easy_local_features import getExtractor +from ..utils.base_model import BaseModel + class ElfFeatures(BaseModel): default_conf = {