diff --git a/Dockerfile b/Dockerfile index 6aa14161..d6cec366 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ RUN python3 -m pip install --upgrade pip RUN pip3 install setuptools RUN pip3 install torch torchvision RUN pip3 install -e . -RUN pip3 install pycolmap +RUN pip3 install pycolmap==0.6.1 # Running the tests: RUN python -m pytest diff --git a/config/cameras.yaml b/config/cameras.yaml index b89e7a8e..764e3952 100644 --- a/config/cameras.yaml +++ b/config/cameras.yaml @@ -2,11 +2,19 @@ general: camera_model: "pinhole" # ["simple-pinhole", "pinhole", "simple-radial", "opencv"] openmvg_camera_model: "pinhole_radial_k3" # ["pinhole", "pinhole_radial_k3", "pinhole_brown_t2"] single_camera: True + intrinsics: ~ # None cam0: camera_model: "pinhole" - images : "" + intrinsics: [ + 481.14, 478.43, 481.44, 383.72 + ] + images : "cam0_*.jpg" cam1: - camera_model: "pinhole" - images : "DSC_6468.jpg,DSC_6468.jpg" \ No newline at end of file + camera_model: "opencv" + intrinsics: [ + 481.91, 482.20, 482.70, 384.33, + 0.0, 0.0, 0.0, 0.0 + ] + images : "cam1_*.jpg" diff --git a/docs/camera_models.md b/docs/camera_models.md index bb1d60ec..c125d8a1 100644 --- a/docs/camera_models.md +++ b/docs/camera_models.md @@ -4,14 +4,19 @@ For the COLMAP database, by default, DIM assigns camera models to images based o For images not assigned to specific `cam` camera groups, the options specified under `general` are applied. The `camera_model` can be selected from `["simple-pinhole", "pinhole", "simple-radial", "opencv"]`. It's worth noting that it's easily possible to extend this to include all the classical COLMAP camera models. Cameras can either be shared among all images (`single_camera == True`), or each camera can have a different camera model (`single_camera == False`). -A subset of images can share intrinsics using `cam` key, by specifying the `camera_model` along with the names of the images separated by commas. -Note that you must specify the full image name, including the extension. +A subset of images can share intrinsics using `cam` key, by specifying the `camera_model` along with the names of the images separated by commas, and the `intrinsics` corresponding to the `camera_model`. + +Note that you must specify the full image name, including the extension. Image name supports globbing, so you can use `*` to match multiple images. + +If you want to read intrinsics from the EXIF data, you can set the `intrinsics` to `~` (null). + For instance: ```python cam0: camera_model: "pinhole" - images: "DSC_6468.jpg,DSC_6469.jpg" + images: "DSC_64*.jpg,DSC_65*.jpg" + intrinsics: [481.14, 478.43, 481.44, 383.72] ``` There's no limit to the number of `cam` entries you can use, just add them following the provided format. @@ -23,14 +28,22 @@ general: camera_model: "pinhole" # ["simple-pinhole", "pinhole", "simple-radial", "opencv"] openmvg_camera_model: "pinhole_radial_k3" # ["pinhole", "pinhole_radial_k3", "pinhole_brown_t2"] single_camera: True + intrinsics: ~ cam0: camera_model: "pinhole" - images : "" + intrinsics: [ + 481.14, 478.43, 481.44, 383.72 + ] + images : "cam0_*.jpg" cam1: - camera_model: "pinhole" - images : "DSC_6468.jpg,DSC_6468.jpg" + camera_model: "opencv" + intrinsics: [ + 481.91, 482.20, 482.70, 384.33, + 0.0, 0.0, 0.0, 0.0 + ] + images : "cam1_*.jpg" ``` For OpenMVG and MICMAC, refer to their respective sections. \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index 516e3375..6c6ce465 100644 --- a/docs/config.md +++ b/docs/config.md @@ -31,19 +31,10 @@ matcher: th: 0.85 ``` -### Default configuration - -Note that all the defaults configurations of DIM, including local features, matchers and geometric verification, are in `src/deep_image_matching/config.py`. - - - - - diff --git a/docs/getting_started.md b/docs/getting_started.md index 5f61ebea..87db4480 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -19,7 +19,7 @@ Example: `python main.py --dir ./assets/example_cyprus --pipeline superpoint+lig Other optional parameters are: -- `--config_file` `-c`: the path to the YAML configuration file containing the custom configuration. See the [Advanced configuration](#advanced-configuration) section (default: `None`, so default configuration is used) +- `--config_file` `-c`: the path to the YAML configuration file containing the custom configuration. See the [Advanced configuration](./advanced_configuration.md) section (default: `None`, so default configuration is used) - `--strategy` `-s`: the strategy to use for matching the images. It can be `matching_lowres`, `bruteforce`, `sequential`, `retrieval`, `custom_pairs`. See [Matching strategies](#matching-strategies) section (default: `matching_lowres`) - `--quality` `-q`: the quality of the images to be matched. It can be `lowest`, `low`, `medium`, `high` or `highest`. See [Quality](#quality) section (default: `high`). - `tiling` `-t`: if passed, the images are tiled in 4 parts and each part is matched separately. This is useful for high-resolution images if you do not want to resize them. See [Tiling](#tiling) section (default: `None`). @@ -54,7 +54,6 @@ The GUI loads the available configurations from [`config.py`](https://github.com If you want to use Deep_Image_Matching from a Jupyter notebook, you can check the examples in the [`notebooks`](https://github.com/3DOM-FBK/deep-image-matching/tree/master/notebooks) folder. - ## Pipelines The `pipeline` parameter defines the combination of local feature extractor and matcher to be used for the matching is is defined by the `--pipeline` option in the CLI. @@ -130,4 +129,3 @@ If you want to run the matching by tile, you can choose different approaches for - `exhaustive`: the images are divided into a regular grid of size 2400x2000 px to extract the features. The matching is carried out by matching all the possible combinations of tiles (brute-force). This method can be very slow for large images or in combination with the `highest` quality option and, in some cases, it may lead to error in the geometric verification if too many wrong matches are detected. To control the tile size and the tile overlap, refer to the [Advanced configuration](./advanced_configuration.md) section. - diff --git a/src/deep_image_matching/hloc/utils/database.py b/src/deep_image_matching/hloc/utils/database.py index 65f59e77..b1f45757 100644 --- a/src/deep_image_matching/hloc/utils/database.py +++ b/src/deep_image_matching/hloc/utils/database.py @@ -31,12 +31,9 @@ # This script is based on an original implementation by True Price. -import sys import sqlite3 -import numpy as np - -IS_PYTHON3 = sys.version_info[0] >= 3 +import numpy as np MAX_IMAGE_ID = 2**31 - 1 @@ -68,9 +65,7 @@ prior_tz REAL, CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}), FOREIGN KEY(camera_id) REFERENCES cameras(camera_id)) -""".format( - MAX_IMAGE_ID -) +""".format(MAX_IMAGE_ID) CREATE_TWO_VIEW_GEOMETRIES_TABLE = """ CREATE TABLE IF NOT EXISTS two_view_geometries ( @@ -128,17 +123,11 @@ def pair_id_to_image_ids(pair_id): def array_to_blob(array): - if IS_PYTHON3: - return array.tobytes() - else: - return np.getbuffer(array) + return array.tobytes() def blob_to_array(blob, dtype, shape=(-1,)): - if IS_PYTHON3: - return np.fromstring(blob, dtype=dtype).reshape(*shape) - else: - return np.frombuffer(blob, dtype=dtype).reshape(*shape) + return np.frombuffer(blob, dtype=dtype).reshape(*shape) class COLMAPDatabase(sqlite3.Connection): @@ -183,8 +172,8 @@ def add_image( self, name, camera_id, - prior_q=np.full(4, np.NaN), - prior_t=np.full(3, np.NaN), + prior_q=np.full(4, np.nan), + prior_t=np.full(3, np.nan), image_id=None, ): cursor = self.execute( @@ -277,8 +266,8 @@ def add_two_view_geometry( def example_usage(): - import os import argparse + import os parser = argparse.ArgumentParser() parser.add_argument("--database_path", default="database.db") diff --git a/src/deep_image_matching/io/h5_to_db.py b/src/deep_image_matching/io/h5_to_db.py index d60ddad0..fca14a74 100644 --- a/src/deep_image_matching/io/h5_to_db.py +++ b/src/deep_image_matching/io/h5_to_db.py @@ -144,7 +144,9 @@ def get_focal(image_path: Path, err_on_default: bool = False) -> float: return focal -def create_camera(db: Path, image_path: Path, camera_model: str): +def create_camera( + db: Path, image_path: Path, camera_model: str, param_arr: np.array = None +): image = Image.open(image_path) width, height = image.size @@ -152,16 +154,22 @@ def create_camera(db: Path, image_path: Path, camera_model: str): if camera_model == "simple-pinhole": model = 0 # simple pinhole - param_arr = np.array([focal, width / 2, height / 2]) + if param_arr is None: + param_arr = np.array([focal, width / 2, height / 2]) elif camera_model == "pinhole": model = 1 # pinhole - param_arr = np.array([focal, focal, width / 2, height / 2]) + if param_arr is None: + param_arr = np.array([focal, focal, width / 2, height / 2]) elif camera_model == "simple-radial": model = 2 # simple radial - param_arr = np.array([focal, width / 2, height / 2, 0.1]) + if param_arr is None: + param_arr = np.array([focal, width / 2, height / 2, 0.1]) elif camera_model == "opencv": model = 4 # opencv - param_arr = np.array([focal, focal, width / 2, height / 2, 0.0, 0.0, 0.0, 0.0]) + if param_arr is None: + param_arr = np.array( + [focal, focal, width / 2, height / 2, 0.0, 0.0, 0.0, 0.0] + ) else: raise RuntimeError(f"Invalid camera model {camera_model}") @@ -192,13 +200,22 @@ def parse_camera_options( n_cameras = len(camera_options.keys()) - 1 for camera in range(n_cameras): cam_opt = camera_options[f"cam{camera}"] - images = cam_opt["images"].split(",") + # images = cam_opt["images"].split(",") + # use glob pattern to find images + patterns = cam_opt["images"].split(",") + images = [] + for pattern in patterns: + images.extend(img.name for img in Path(image_path).glob(pattern)) + images = sorted(images) + for i, img in enumerate(images): grouped_images[img] = {"camera_id": camera + 1} if i == 0: path = os.path.join(image_path, img) try: - create_camera(db, path, cam_opt["camera_model"]) + create_camera( + db, path, cam_opt["camera_model"], cam_opt["intrinsics"] + ) except: logger.warning( f"Was not possible to load the first image to initialize cam{camera}" @@ -242,12 +259,18 @@ def add_keypoints( if filename not in list(grouped_images.keys()): if camera_options["general"]["single_camera"] is False: camera_id = create_camera( - db, path, camera_options["general"]["camera_model"] + db, + path, + camera_options["general"]["camera_model"], + camera_options["general"]["intrinsics"], ) elif camera_options["general"]["single_camera"] is True: if k == 0: camera_id = create_camera( - db, path, camera_options["general"]["camera_model"] + db, + path, + camera_options["general"]["camera_model"], + camera_options["general"]["intrinsics"], ) single_camera_id = camera_id k += 1 diff --git a/src/deep_image_matching/utils/database.py b/src/deep_image_matching/utils/database.py index d3098bd8..6da392fe 100644 --- a/src/deep_image_matching/utils/database.py +++ b/src/deep_image_matching/utils/database.py @@ -32,12 +32,9 @@ # This script is based on an original implementation by True Price. import sqlite3 -import sys import numpy as np -IS_PYTHON3 = sys.version_info[0] >= 3 - MAX_IMAGE_ID = 2**31 - 1 CREATE_CAMERAS_TABLE = """CREATE TABLE IF NOT EXISTS cameras ( @@ -68,9 +65,7 @@ prior_tz REAL, CONSTRAINT image_id_check CHECK(image_id >= 0 and image_id < {}), FOREIGN KEY(camera_id) REFERENCES cameras(camera_id)) -""".format( - MAX_IMAGE_ID -) +""".format(MAX_IMAGE_ID) CREATE_TWO_VIEW_GEOMETRIES_TABLE = """ CREATE TABLE IF NOT EXISTS two_view_geometries ( @@ -126,17 +121,11 @@ def pair_id_to_image_ids(pair_id): def array_to_blob(array): - if IS_PYTHON3: - return array.tostring() - else: - return np.getbuffer(array) + return array.tobytes() def blob_to_array(blob, dtype, shape=(-1,)): - if IS_PYTHON3: - return np.fromstring(blob, dtype=dtype).reshape(*shape) - else: - return np.frombuffer(blob, dtype=dtype).reshape(*shape) + return np.frombuffer(blob, dtype=dtype).reshape(*shape) class COLMAPDatabase(sqlite3.Connection): @@ -295,11 +284,9 @@ def example_usage(): args = parser.parse_args() if os.path.exists(args.database_path): - print("ERROR: database path already exists -- will not modify it.") - return + os.remove(args.database_path) # Open the database. - db = COLMAPDatabase.connect(args.database_path) # For convenience, try creating all the tables upfront. @@ -414,6 +401,8 @@ def example_usage(): if os.path.exists(args.database_path): os.remove(args.database_path) + print("All tests passed.") + if __name__ == "__main__": example_usage()