From 898cfc2616b7602596c03bd5fd55c535c9de5ec3 Mon Sep 17 00:00:00 2001 From: olga Date: Fri, 27 Jun 2025 15:56:41 +0200 Subject: [PATCH 1/2] added new run scripts --- src/python/matching_scripts.py | 168 ++++++++++++++++++ src/python/run_matching.py | 212 ----------------------- src/python/run_matching_from_config.py | 37 ++++ src/python/run_matching_from_features.py | 84 +++++++++ src/python/run_matching_from_images.py | 124 +++++++++++++ 5 files changed, 413 insertions(+), 212 deletions(-) create mode 100644 src/python/matching_scripts.py delete mode 100644 src/python/run_matching.py create mode 100644 src/python/run_matching_from_config.py create mode 100644 src/python/run_matching_from_features.py create mode 100644 src/python/run_matching_from_images.py diff --git a/src/python/matching_scripts.py b/src/python/matching_scripts.py new file mode 100644 index 0000000..bf45780 --- /dev/null +++ b/src/python/matching_scripts.py @@ -0,0 +1,168 @@ +from pathlib import Path +import os +from dataclasses import dataclass, fields, asdict, replace + + +@dataclass +class RunParameters: + path2query_images: str = None + path2ref_images: str = None + path2qu: str = None + path2ref: str = None + costMatrix: str = None + matchingResult: str = None + matchingResultImage: str = None + expansionRate: float = 0.3 + fanOut: int = 5 + nonMatchCost: float = 3.7 + querySize: int = None + bufferSize: int = 100 + + +def initializeFromDict(params, params_as_dict): + init_obj = replace(params, **params_as_dict) + return init_obj + + +def convertToDictWithoutNoneEntries(params): + # Iterate over the fields and check if any are None + for field in fields(params): + field_value = getattr(params, field.name) # Get the value of the field + if field_value is None: + print(f"Field '{field.name}' is None") + + # Remove None entries + params_as_dict = asdict(params) + dict_without_none = { + key: value for key, value in params_as_dict.items() if value is not None + } + return dict_without_none + + +def convertNumpyFeaturesToProtoAndWrite( + np_features, output_folder, dataset_name, feature_type +): + params = "--filename {features_file} ".format(features_file=np_features) + params += "--feature_type {feature_type} ".format(feature_type=feature_type) + params += "--output_folder {output_folder} ".format(output_folder=output_folder) + params += "--output_file_prefix {output_file_prefix} ".format( + output_file_prefix=dataset_name + ) + + command = "python convert_numpy_features_to_protos.py " + params + print("Calling:", command) + os.system(command) + + +def computeNetVLADFeatures(image_dir, output_folder): + netvlad_weights = Path("netvlad/data/Pitts30K_struct.mat") + if not netvlad_weights.exists(): + print( + "ERROR: Can't find netvlad weights. Please download to netvlad/data/ from https://cvg-data.inf.ethz.ch/hloc/netvlad/Pitts30K_struct.mat" + ) + exit(1) + + feature_name_prefix = "NetVLAD" + params = "--data_dir {image_dir} ".format(image_dir=image_dir) + np_features_file = output_folder / (feature_name_prefix + "-features.txt") + params += "--output_file {output_file} ".format(output_file=np_features_file) + params += "--netvlad_weights_file {weights} ".format(weights=netvlad_weights) + + command = "python netvlad/netvlad_extractor.py " + params + print("Calling:", command) + os.system(command) + return np_features_file + + +def computeFeatures(image_dir, output_folder, dataset_name, feature_type): + if output_folder.exists(): + print( + "WARNING: Feature folder {folder_name} exists. Skipping feature computation.".format( + folder_name=output_folder + ) + ) + return + output_folder.mkdir() + + if feature_type == "NetVLAD": + print("Computing NetVLAD features") + np_features_file = computeNetVLADFeatures(image_dir, output_folder) + else: + print("ERROR. Unrecognized feature type.") + exit(1) + + convertNumpyFeaturesToProtoAndWrite( + np_features_file, output_folder, dataset_name, feature_type + ) + + +def computeCostMatrix(run_params): + params = "--query_features {query_features} ".format( + query_features=run_params.path2qu + ) + params += "--db_features {reference_features} ".format( + reference_features=run_params.path2ref + ) + params += "--cost_matrix_file {cost_matrix_file} ".format( + cost_matrix_file=run_params.costMatrix + ) + command = "python compute_cost_matrix.py " + params + print("Calling:", command) + os.system(command) + + +def runMatching(config_yaml_file): + binary = "../../build/src/apps/cost_matrix_based_matching/online_localizer_lsh" + command = binary + " " + str(config_yaml_file) + print("Calling:", command) + os.system(command) + + +def runLocalizationResultVisualization(run_params): + params = "--cost_matrix {cost_matrix} ".format(cost_matrix=run_params.costMatrix) + params += "--matching_result {matching_result} ".format( + matching_result=run_params.matchingResult + ) + params += "--image_name {image_name} ".format( + image_name=run_params.matchingResultImage + ) + command = "python visualize_localization_result.py " + params + print("Calling:", command) + os.system(command) + + +def runStoreImageMatches(run_params, output_dir): + params = "--matching_result {matching_result} ".format( + matching_result=run_params.matchingResult + ) + params += "--query_images {query_images} ".format( + query_images=run_params.path2query_images + ) + params += "--reference_images {ref_images} ".format( + ref_images=run_params.path2ref_images + ) + params += "--output_dir {output_dir}/matched_images ".format(output_dir=output_dir) + + command = "python store_image_matches.py " + params + print("Calling:", command) + os.system(command) + + +def linkImages(image_dir, output_folder): + if output_folder.exists(): + print( + "Output {output_folder} exists. Skipping...".format( + output_folder=output_folder + ) + ) + return + output_folder.mkdir() + images = image_dir.glob("*") + for image in images: + image_link_name = output_folder / image.name + command = "ln {image} {image_link_name}".format( + image=image, image_link_name=image_link_name + ) + print(command) + os.system(command) + print("Linked images from", image_dir) diff --git a/src/python/run_matching.py b/src/python/run_matching.py deleted file mode 100644 index f9320c7..0000000 --- a/src/python/run_matching.py +++ /dev/null @@ -1,212 +0,0 @@ -import argparse -from pathlib import Path -import os -import yaml - - -def parseParams(): - parser = argparse.ArgumentParser(description="Run image matching.") - parser.add_argument( - "--query_images", - type=Path, - required=True, - help="Path to the directory with images in .jpg or .png format", - ) - parser.add_argument( - "--reference_images", - type=Path, - required=True, - help="Path to the directory with images in .jpg or .png format", - ) - parser.add_argument( - "--dataset_name", - type=str, - required=True, - help="The name of the dataset.", - ) - parser.add_argument( - "--output_dir", - type=Path, - required=True, - help="Path to output directory to store results.", - ) - parser.add_argument( - "--write_image_matches", - action="store_true", - help="Creates and writes the pair of matching images.", - ) - parser.add_argument( - "--link_images", - action="store_true", - help="Creates hard link for images in the result folder.", - ) - return parser.parse_args() - - -def setDictParam(args, query_features_dir, reference_features_dir): - params = dict() - params["path2query_images"] = str(args.query_images) - params["path2ref_images"] = str(args.reference_images) - params["path2qu"] = str(query_features_dir) - params["path2ref"] = str(reference_features_dir) - params["costMatrix"] = str(args.output_dir / (args.dataset_name + ".CostMatrix.pb")) - params["matchingResult"] = str( - args.output_dir / (args.dataset_name + ".MatchingResult.pb") - ) - params["matchingResultImage"] = str( - args.output_dir / (args.dataset_name + "_result.png") - ) - params["expansionRate"] = 0.3 - params["fanOut"] = 5 - params["nonMatchCost"] = 3.7 - params["bufferSize"] = 100 - - queriesNum = len(list(query_features_dir.glob("*Feature.pb"))) - params["querySize"] = queriesNum - return params - - -def computeFeatures(image_dir, output_folder, feature_name_prefix): - if output_folder.exists(): - print( - "WARNING: Feature folder {folder_name} exists. Skipping feature computation.".format( - folder_name=output_folder - ) - ) - return - output_folder.mkdir() - # Extract features. - netvlad_weights = Path("netvlad/data/Pitts30K_struct.mat") - if not netvlad_weights.exists(): - print( - "ERROR: Can't find netvlad weights. Please download to netvlad/data/ from https://cvg-data.inf.ethz.ch/hloc/netvlad/Pitts30K_struct.mat" - ) - exit(1) - - params = "--data_dir {image_dir} ".format(image_dir=image_dir) - np_features = output_folder / (feature_name_prefix + "-features.txt") - params += "--output_file {output_file} ".format(output_file=np_features) - params += "--netvlad_weights_file {weights} ".format(weights=netvlad_weights) - - command = "python netvlad/netvlad_extractor.py " + params - print("Calling:", command) - os.system(command) - - # Convert from np to protos. - params = "--filename {features_file} ".format(features_file=np_features) - params += "--feature_type NetVLAD " - params += "--output_folder {output_folder} ".format(output_folder=output_folder) - params += "--output_file_prefix {output_file_prefix} ".format( - output_file_prefix=feature_name_prefix - ) - - command = "python convert_numpy_features_to_protos.py " + params - print("Calling:", command) - os.system(command) - - -def computeCostMatrix(config): - params = "--query_features {query_features} ".format( - query_features=config["path2qu"] - ) - params += "--db_features {reference_features} ".format( - reference_features=config["path2ref"] - ) - params += "--cost_matrix_file {cost_matrix_file} ".format( - cost_matrix_file=config["costMatrix"] - ) - command = "python compute_cost_matrix.py " + params - print("Calling:", command) - os.system(command) - - -def runMatching(config_yaml_file): - binary = "../../build/src/apps/cost_matrix_based_matching/online_localizer_lsh" - command = binary + " " + str(config_yaml_file) - print("Calling:", command) - os.system(command) - - -def runResultVisualization(config): - params = "--cost_matrix {cost_matrix} ".format(cost_matrix=config["costMatrix"]) - params += "--matching_result {matching_result} ".format( - matching_result=config["matchingResult"] - ) - params += "--image_name {image_name} ".format( - image_name=config["matchingResultImage"] - ) - command = "python visualize_localization_result.py " + params - print("Calling:", command) - os.system(command) - - -def runMatchingResultVisualization(config, output_dir): - params = "--matching_result {matching_result} ".format( - matching_result=config["matchingResult"] - ) - params += "--query_images {query_images} ".format( - query_images=config["path2query_images"] - ) - params += "--reference_images {ref_images} ".format( - ref_images=config["path2ref_images"] - ) - params += "--output_dir {output_dir}/matched_images ".format(output_dir=output_dir) - - command = "python visualize_matching_result.py " + params - print("Calling:", command) - os.system(command) - - -def linkImages(image_dir, output_folder): - if output_folder.exists(): - print( - "Output {output_folder} exists. Skipping...".format( - output_folder=output_folder - ) - ) - return - output_folder.mkdir() - images = image_dir.glob("*") - for image in images: - image_link_name = output_folder / image.name - command = "ln {image} {image_link_name}".format( - image=image, image_link_name=image_link_name - ) - print(command) - os.system(command) - print("Linked images from", image_dir) - - -def main(): - args = parseParams() - - if args.output_dir.exists(): - print("WARNING: output_dir exists. Overwritting the results") - else: - args.output_dir.mkdir() - - if args.link_images: - linkImages(args.query_images, args.output_dir / "query_images") - linkImages(args.reference_images, args.output_dir / "reference_images") - - # Compute query features. - query_features_dir = args.output_dir / "query_features" - computeFeatures(args.query_images, query_features_dir, args.dataset_name) - - # Compute reference features. - reference_features_dir = args.output_dir / "reference_features" - computeFeatures(args.reference_images, reference_features_dir, args.dataset_name) - - yaml_config = setDictParam(args, query_features_dir, reference_features_dir) - yaml_config_file = args.output_dir / (args.dataset_name + "_config.yml") - with open(yaml_config_file, "w") as file: - yaml.dump(yaml_config, file) - computeCostMatrix(yaml_config) - runMatching(yaml_config_file) - runResultVisualization(yaml_config) - if args.write_image_matches: - runMatchingResultVisualization(yaml_config, args.output_dir) - - -if __name__ == "__main__": - main() diff --git a/src/python/run_matching_from_config.py b/src/python/run_matching_from_config.py new file mode 100644 index 0000000..bd997b1 --- /dev/null +++ b/src/python/run_matching_from_config.py @@ -0,0 +1,37 @@ +import matching_scripts as matching + +import yaml + +import argparse + + +def parseParams(): + parser = argparse.ArgumentParser( + description="Run image matching starting from config yaml file." + ) + + parser.add_argument( + "--yaml_config_file", + type=str, + required=True, + help="The yaml config file.", + ) + return parser.parse_args() + + +def main(): + args = parseParams() + + with open(args.yaml_config_file, "r") as file: + config = yaml.load(file, Loader=yaml.SafeLoader) + + run_params = matching.RunParameters() + run_params = matching.initializeFromDict(run_params, config) + + matching.computeCostMatrix(run_params) + matching.runMatching(args.yaml_config_file) + matching.runLocalizationResultVisualization(run_params) + + +if __name__ == "__main__": + main() diff --git a/src/python/run_matching_from_features.py b/src/python/run_matching_from_features.py new file mode 100644 index 0000000..e522423 --- /dev/null +++ b/src/python/run_matching_from_features.py @@ -0,0 +1,84 @@ +import matching_scripts as matching + +import yaml + +import argparse +from pathlib import Path + + +def parseParams(): + parser = argparse.ArgumentParser( + description="Run image matching starting from features." + ) + parser.add_argument( + "--query_features", + type=Path, + required=True, + help="Path to the directory with features, e.g., .NetVLAD.Feature.pb", + ) + parser.add_argument( + "--reference_features", + type=Path, + required=True, + help="Path to the directory with features, e.g., .NetVLAD.Feature.pb", + ) + parser.add_argument( + "--dataset_name", + type=str, + required=True, + help="The name of the dataset.", + ) + parser.add_argument( + "--output_dir", + type=Path, + required=True, + help="Path to output directory to store results.", + ) + return parser.parse_args() + + +def setRunParameters(args): + run_parameters = matching.RunParameters() + run_parameters.path2qu = args.query_features.as_posix() + run_parameters.path2ref = args.reference_features.as_posix() + run_parameters.costMatrix = ( + args.output_dir / (args.dataset_name + ".CostMatrix.pb") + ).as_posix() + run_parameters.matchingResult = ( + args.output_dir / (args.dataset_name + ".MatchingResult.pb") + ).as_posix() + run_parameters.matchingResultImage = ( + args.output_dir / (args.dataset_name + "_result.png") + ).as_posix() + run_parameters.debugProto = ( + args.output_dir / (args.dataset_name + ".OnlineLocalizerDebug.pb") + ).as_posix() + + queriesNum = len(list(args.query_features.glob("*Feature.pb"))) + run_parameters.querySize = queriesNum + + return run_parameters + + +def main(): + args = parseParams() + + if args.output_dir.exists(): + print("WARNING: output_dir exists. Overwritting the results") + else: + args.output_dir.mkdir() + + run_params = setRunParameters(args) + param_as_dict = matching.convertToDictWithoutNoneEntries(run_params) + + yaml_config_file = args.output_dir / (args.dataset_name + "_config.yml") + with open(yaml_config_file, "w") as file: + yaml.dump(param_as_dict, file) + + matching.computeCostMatrix(run_params) + matching.runMatching(yaml_config_file) + matching.runLocalizationResultVisualization(run_params) + + +if __name__ == "__main__": + main() diff --git a/src/python/run_matching_from_images.py b/src/python/run_matching_from_images.py new file mode 100644 index 0000000..af7e617 --- /dev/null +++ b/src/python/run_matching_from_images.py @@ -0,0 +1,124 @@ +import matching_scripts as matching + +import yaml + +import argparse +from pathlib import Path + + +def parseParams(): + parser = argparse.ArgumentParser( + description="Run image matching starting from images." + ) + parser.add_argument( + "--query_images", + type=Path, + required=True, + help="Path to the directory with images in .jpg or .png format", + ) + parser.add_argument( + "--reference_images", + type=Path, + required=True, + help="Path to the directory with images in .jpg or .png format", + ) + parser.add_argument( + "--dataset_name", + type=str, + required=True, + help="The name of the dataset.", + ) + parser.add_argument( + "--output_dir", + type=Path, + required=True, + help="Path to output directory to store results.", + ) + parser.add_argument( + "--feature_type", + type=str, + choices=("NetVLAD", "Other"), + default="NetVLAD", + help="Feature type to be extracted from images.", + ) + parser.add_argument( + "--write_image_matches", + action="store_true", + help="Creates and writes the pair of matching images.", + ) + parser.add_argument( + "--link_images", + action="store_true", + help="Creates hard link for images in the result folder.", + ) + return parser.parse_args() + + +def setRunParameters(args): + run_parameters = matching.RunParameters() + run_parameters.path2query_images = args.query_images.as_posix() + run_parameters.path2ref_images = args.reference_images.as_posix() + run_parameters.path2qu = (args.output_dir / "query_features").as_posix() + run_parameters.path2ref = (args.output_dir / "reference_features").as_posix() + run_parameters.costMatrix = ( + args.output_dir / (args.dataset_name + ".CostMatrix.pb") + ).as_posix() + run_parameters.matchingResult = ( + args.output_dir / (args.dataset_name + ".MatchingResult.pb") + ).as_posix() + run_parameters.matchingResultImage = ( + args.output_dir / (args.dataset_name + "_result.png") + ).as_posix() + run_parameters.debugProto = ( + args.output_dir / (args.dataset_name + ".OnlineLocalizerDebug.png") + ).as_posix() + + queriesNum = len(list(Path(run_parameters.path2qu).glob("*Feature.pb"))) + run_parameters.querySize = queriesNum + + return run_parameters + + +def main(): + args = parseParams() + + if args.output_dir.exists(): + print("WARNING: output_dir exists. Overwritting the results") + else: + args.output_dir.mkdir() + + if args.link_images: + matching.linkImages(args.query_images, args.output_dir / "query_images") + matching.linkImages(args.reference_images, args.output_dir / "reference_images") + + # Compute query features. + query_features_dir = args.output_dir / "query_features" + matching.computeFeatures( + args.query_images, query_features_dir, args.dataset_name, args.feature_type + ) + + # Compute reference features. + reference_features_dir = args.output_dir / "reference_features" + matching.computeFeatures( + args.reference_images, + reference_features_dir, + args.dataset_name, + args.feature_type, + ) + + run_params = setRunParameters(args) + params_as_dict = matching.convertToDictWithoutNoneEntries(run_params) + + yaml_config_file = args.output_dir / (args.dataset_name + "_config.yml") + with open(yaml_config_file, "w") as file: + yaml.dump(params_as_dict, file) + + matching.computeCostMatrix(run_params) + matching.runMatching(yaml_config_file) + matching.runLocalizationResultVisualization(run_params) + if args.write_image_matches: + matching.runStoreImageMatches(run_params, args.output_dir) + + +if __name__ == "__main__": + main() From 5789c06040eda76b5b90d4022e253f503fe97e8f Mon Sep 17 00:00:00 2001 From: Olga Date: Mon, 30 Jun 2025 13:01:24 +0200 Subject: [PATCH 2/2] updated readme --- readme.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 8bf1f63..6d74761 100644 --- a/readme.md +++ b/readme.md @@ -41,11 +41,11 @@ Requires `Python 3.8+`. ## Usage -The code is under continuous development but at any point in time you should be able to run the matching procedure through: +If you have images sequences, start matching procedure by calling: ```bash cd src/python -python run_matching.py \ +python run_matching_from_images.py \ --query_images \ --reference_images \ --dataset_name \ @@ -53,11 +53,23 @@ python run_matching.py \ --write_image_matches ``` +If you have pre-computed features already: + +```bash +python run_matching_from_features.py \ + --query_features \ + --reference_features \ + --dataset_name \ + --output_dir +``` + +\*\* Make sure the features are stored as a correct proto message `.Feature.pb`, check [localization_protos.proto](src/localization_protos.proto) for format details. + The framework assumes that there is a _query_ image sequence, for every image of which the user wants to find the corresponding image in the _reference_ image sequence. -The `run_matching.py` script stores all the results in the user-provided `output_dir`. The user also needs to specify the name of the dataset, for example, "my_awesome_dataset". +The scripts store all the results in the user-provided `output_dir`. The user also needs to specify the name of the dataset, for example, "my_awesome_dataset". -For more details about the parameters, please use `python run_matching.py --help`. +For more details about the parameters, please use `python run_matching_from_*.py --help`. For more details about the underlying method and the interpretation of the results, please have a look at [paper](http://www.ipb.uni-bonn.de/pdfs/vysotska16ral-icra.pdf). Here is a sketch of what roughly is happening for those who don't like to read much ![](doc/cost_matrix_view.png)