IO

Page under construction...

h5_to_db

This module contains functions to export image features and matches to a COLMAP database.

add_keypoints(db, h5_path, image_path, camera_options={})

Adds keypoints from an HDF5 file to a COLMAP database.

Reads keypoints from an HDF5 file, associates them with cameras (if necessary), and adds the image and keypoint information to the specified COLMAP database.

Parameters:
  • db (Path) –

    Path to the COLMAP database.

  • h5_path (Path) –

    Path to the HDF5 file containing keypoints.

  • image_path (Path) –

    Path to the directory containing source images.

  • camera_options (dict, default: {} ) –

    Camera configuration options (see parse_camera_options). Defaults to an empty dictionary.

Returns:
  • dict( dict ) –

    A dictionary mapping image filenames to their corresponding image IDs in the database.

Source code in src/deep_image_matching/io/h5_to_db.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def add_keypoints(
    db: Path, h5_path: Path, image_path: Path, camera_options: dict = {}
) -> dict:
    """
    Adds keypoints from an HDF5 file to a COLMAP database.

    Reads keypoints from an HDF5 file, associates them with cameras (if necessary),
    and adds the image and keypoint information to the specified COLMAP database.

    Args:
        db (Path): Path to the COLMAP database.
        h5_path (Path): Path to the HDF5 file containing keypoints.
        image_path (Path): Path to the directory containing source images.
        camera_options (dict, optional): Camera configuration options (see `parse_camera_options`).
                                         Defaults to an empty dictionary.

    Returns:
        dict: A dictionary mapping image filenames to their corresponding image IDs in the database.
    """

    grouped_images = parse_camera_options(camera_options, db, image_path)

    with h5py.File(str(h5_path), "r") as keypoint_f:
        # camera_id = None
        fname_to_id = {}
        k = 0
        for filename in tqdm(list(keypoint_f.keys())):
            keypoints = keypoint_f[filename]["keypoints"].__array__()

            path = os.path.join(image_path, filename)
            if not os.path.isfile(path):
                raise IOError(f"Invalid image path {path}")

            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"]
                    )
                elif camera_options["general"]["single_camera"] is True:
                    if k == 0:
                        camera_id = create_camera(
                            db, path, camera_options["general"]["camera_model"]
                        )
                        single_camera_id = camera_id
                        k += 1
                    elif k > 0:
                        camera_id = single_camera_id
            else:
                camera_id = grouped_images[filename]["camera_id"]
            image_id = db.add_image(filename, camera_id)
            fname_to_id[filename] = image_id
            # print('keypoints')
            # print(keypoints)
            # print('image_id', image_id)
            if len(keypoints.shape) >= 2:
                db.add_keypoints(image_id, keypoints)
            # else:
            #    keypoints =
            #    db.add_keypoints(image_id, keypoints)

    return fname_to_id

add_raw_matches(db, h5_path, fname_to_id)

Adds raw feature matches from an HDF5 file to a COLMAP database.

Reads raw matches from an HDF5 file, maps image filenames to their image IDs, and adds match information to the specified COLMAP database. Prevents duplicate matches from being added.

Parameters:
  • db (Path) –

    Path to the COLMAP database.

  • h5_path (Path) –

    Path to the HDF5 file containing raw matches.

  • fname_to_id (dict) –

    A dictionary mapping image filenames to their image IDs.

Source code in src/deep_image_matching/io/h5_to_db.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def add_raw_matches(db: Path, h5_path: Path, fname_to_id: dict):
    """
    Adds raw feature matches from an HDF5 file to a COLMAP database.

    Reads raw matches from an HDF5 file, maps image filenames to their image IDs,
    and adds match information to the specified COLMAP database. Prevents duplicate
    matches from being added.

    Args:
        db (Path): Path to the COLMAP database.
        h5_path (Path): Path to the HDF5 file containing raw matches.
        fname_to_id (dict):  A dictionary mapping image filenames to their image IDs.
    """
    match_file = h5py.File(str(h5_path), "r")

    added = set()
    n_keys = len(match_file.keys())
    n_total = (n_keys * (n_keys - 1)) // 2

    with tqdm(total=n_total) as pbar:
        for key_1 in match_file.keys():
            group = match_file[key_1]
            for key_2 in group.keys():
                id_1 = fname_to_id[key_1]
                id_2 = fname_to_id[key_2]

                pair_id = image_ids_to_pair_id(id_1, id_2)
                if pair_id in added:
                    warnings.warn(f"Pair {pair_id} ({id_1}, {id_2}) already added!")
                    continue

                matches = group[key_2][()]
                db.add_matches(id_1, id_2, matches)
                # db.add_two_view_geometry(id_1, id_2, matches)

                added.add(pair_id)

                pbar.update(1)
    match_file.close()

export_to_colmap(img_dir, feature_path, match_path, database_path='database.db', camera_options=default_camera_options)

Exports image features and matches to a COLMAP database.

Parameters:
  • img_dir (str) –

    Path to the directory containing the source images.

  • feature_path (Path) –

    Path to the feature file (in HDF5 format) containing the extracted keypoints.

  • match_path (Path) –

    Path to the match file (in HDF5 format) containing the matches between keypoints.

  • database_path (str, default: 'database.db' ) –

    Path to the COLMAP database file. Defaults to "colmap.db".

  • camera_options (dict, default: default_camera_options ) –

    Flag indicating whether to use camera options. Defaults to default_camera_options.

Returns:
  • None

Raises:
  • IOError

    If the image path is invalid.

Example

export_to_colmap( img_dir="/path/to/images", feature_path=Path("/path/to/features.h5"), match_path=Path("/path/to/matches.h5"), database_path="colmap.db", )

Source code in src/deep_image_matching/io/h5_to_db.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def export_to_colmap(
    img_dir: Path,
    feature_path: Path,
    match_path: Path,
    database_path: str = "database.db",
    camera_options: dict = default_camera_options,
):
    """
    Exports image features and matches to a COLMAP database.

    Args:
        img_dir (str): Path to the directory containing the source images.
        feature_path (Path): Path to the feature file (in HDF5 format) containing the extracted keypoints.
        match_path (Path): Path to the match file (in HDF5 format) containing the matches between keypoints.
        database_path (str, optional): Path to the COLMAP database file. Defaults to "colmap.db".
        camera_options (dict, optional): Flag indicating whether to use camera options. Defaults to default_camera_options.

    Returns:
        None

    Raises:
        IOError: If the image path is invalid.

    Warnings:
        If the database path already exists, it will be deleted and recreated.
        If a pair of images already has matches in the database, a warning will be raised.

    Example:
        export_to_colmap(
            img_dir="/path/to/images",
            feature_path=Path("/path/to/features.h5"),
            match_path=Path("/path/to/matches.h5"),
            database_path="colmap.db",
        )
    """
    database_path = Path(database_path)
    if database_path.exists():
        logger.warning(f"Database path {database_path} already exists - deleting it")
        database_path.unlink()

    db = COLMAPDatabase.connect(database_path)
    db.create_tables()
    fname_to_id = add_keypoints(db, feature_path, img_dir, camera_options)
    raw_match_path = match_path.parent / "raw_matches.h5"
    if raw_match_path.exists():
        add_raw_matches(
            db,
            raw_match_path,
            fname_to_id,
        )
    if match_path.exists():
        add_matches(
            db,
            match_path,
            fname_to_id,
        )

    db.commit()
    return

get_focal(image_path, err_on_default=False)

Get the focal length of an image.

Parameters:
  • image_path (Path) –

    The path to the image file.

  • err_on_default (bool, default: False ) –

    Whether to raise an error if the focal length cannot be determined from the image's EXIF data. Defaults to False.

Returns:
  • float( float ) –

    The focal length of the image.

Raises:
  • RuntimeError

    If the focal length cannot be determined from the image's EXIF data and err_on_default is set to True.

Note

This function calculates the focal length based on the maximum size of the image and the EXIF data. If the focal length cannot be determined from the EXIF data, it uses a default prior value.

Source code in src/deep_image_matching/io/h5_to_db.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def get_focal(image_path: Path, err_on_default: bool = False) -> float:
    """
    Get the focal length of an image.

    Parameters:
        image_path (Path): The path to the image file.
        err_on_default (bool, optional): Whether to raise an error if the focal length cannot be determined from the image's EXIF data. Defaults to False.

    Returns:
        float: The focal length of the image.

    Raises:
        RuntimeError: If the focal length cannot be determined from the image's EXIF data and `err_on_default` is set to True.

    Note:
        This function calculates the focal length based on the maximum size of the image and the EXIF data. If the focal length cannot be determined from the EXIF data, it uses a default prior value.

    """
    image = Image.open(image_path)
    max_size = max(image.size)

    exif = image.getexif()
    focal = None
    if exif is not None:
        focal_35mm = None
        # https://github.com/colmap/colmap/blob/d3a29e203ab69e91eda938d6e56e1c7339d62a99/src/util/bitmap.cc#L299
        for tag, value in exif.items():
            focal_35mm = None
            if ExifTags.TAGS.get(tag, None) == "FocalLengthIn35mmFilm":
                focal_35mm = float(value)
                break

        if focal_35mm is not None:
            focal = focal_35mm / 35.0 * max_size

    if focal is None:
        if err_on_default:
            raise RuntimeError("Failed to find focal length")

        # failed to find it in exif, use prior
        FOCAL_PRIOR = 1.2
        focal = FOCAL_PRIOR * max_size

    return focal

parse_camera_options(camera_options, db, image_path)

Parses camera options and creates camera entries in the COLMAP database.

This function groups images by camera, assigns camera IDs, and attempts to initialize camera models in the provided COLMAP database.

Parameters:
  • camera_options (dict) –

    A dictionary containing camera configuration options.

  • db (Path) –

    Path to the COLMAP database.

  • image_path (Path) –

    Path to the directory containing source images.

Returns:
  • dict( dict ) –

    A dictionary mapping image filenames to their assigned camera IDs.

Source code in src/deep_image_matching/io/h5_to_db.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def parse_camera_options(
    camera_options: dict,
    db: Path,
    image_path: Path,
) -> dict:
    """
    Parses camera options and creates camera entries in the COLMAP database.

    This function groups images by camera, assigns camera IDs, and attempts to
    initialize camera models in the provided COLMAP database.

    Args:
        camera_options (dict): A dictionary containing camera configuration options.
        db (Path): Path to the COLMAP database.
        image_path (Path): Path to the directory containing source images.

    Returns:
        dict: A dictionary mapping image filenames to their assigned camera IDs.
    """

    grouped_images = {}
    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(",")
        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"])
                except:
                    logger.warning(
                        f"Was not possible to load the first image to initialize cam{camera}"
                    )
    return grouped_images



h5_to_micmac

export_tie_points(feature_path, match_path, out_dir)

Export tie points from h5 databases containing the features on each images and the index of the matched features to text files in MicMac format.

Parameters:
  • feature_path (Path) –

    Path to the features.h5 file.

  • match_path (Path) –

    Path to the matches.h5 file.

  • out_dir (Path) –

    Path to the output directory.

Raises:
  • FileNotFoundError

    If the feature file or match file does not exist.

Returns:
  • None

    None

Source code in src/deep_image_matching/io/h5_to_micmac.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def export_tie_points(
    feature_path: Path,
    match_path: Path,
    out_dir: Path,
) -> None:
    """
    Export tie points from h5 databases containing the features on each images and the index of the matched features to text files in MicMac format.

    Args:
        feature_path (Path): Path to the features.h5 file.
        match_path (Path): Path to the matches.h5 file.
        out_dir (Path): Path to the output directory.

    Raises:
        FileNotFoundError: If the feature file or match file does not exist.

    Returns:
        None
    """

    def write_matches(file, x0y0, x1y1):
        with open(file, "w") as f:
            for x0, y0, x1, y1 in zip(x0y0[:, 0], x0y0[:, 1], x1y1[:, 0], x1y1[:, 1]):
                f.write(f"{x0:6f} {y0:6f} {x1:6f} {y1:6f} 1.000000\n")

    feature_path = Path(feature_path)
    match_path = Path(match_path)
    out_dir = Path(out_dir)

    if not feature_path.exists():
        raise FileNotFoundError(f"File {feature_path} does not exist")
    if not match_path.exists():
        raise FileNotFoundError(f"File {match_path} does not exist")
    out_dir.mkdir(exist_ok=True, parents=True)

    with h5py.File(feature_path, "r") as features:
        keys = permutations(features.keys(), 2)

    with h5py.File(match_path, "r") as matches:
        for i0, i1 in keys:
            i0_dir = out_dir / f"Pastis{i0}"
            i0_dir.mkdir(exist_ok=True, parents=True)

            # Define the file to write the matches
            file = i0_dir / (i1 + ".txt")

            # Get the matches between the two images
            if i0 in matches.keys() and i1 in matches[i0].keys():
                x0y0, x1y1 = get_matches(feature_path, match_path, i0, i1)
            else:
                x1y1, x0y0 = get_matches(feature_path, match_path, i1, i0)

            if x0y0 is None or x1y1 is None:
                continue
                # If no matches are found, write a file with a single fake match with zeros image coordinates
                # NOTE: This is a workaround to avoid MicMac from crashing when no matches are found, these matches should be discarded as outliers during the bundle adjustment
                # x0y0 = np.zeros((1, 2))
                # x1y1 = np.zeros((1, 2))

            # threading.Thread(target=lambda: write_matches(file, x0y0, x1y1)).start()
            write_matches(file, x0y0, x1y1)

    logger.info(f"Exported tie points to {out_dir}")

export_to_micmac(image_dir, features_h5, matches_h5, out_dir='micmac', img_ext=IMAGE_EXT, run_Tapas=False, micmac_path=None)

Exports image features and matches to a specified directory in a format suitable for MicMac processing. Optionally, it can also run the Tapas tool from MicMac for relative orientation.

Parameters:
  • image_dir (Path) –

    Directory containing the images.

  • features_h5 (Path) –

    Path to the HDF5 file containing the features.

  • matches_h5 (Path) –

    Path to the HDF5 file containing the matches.

  • out_dir (Path, default: 'micmac' ) –

    Output directory. Defaults to "micmac".

  • img_ext (str, default: IMAGE_EXT ) –

    Image file extension. Defaults to IMAGE_EXT.

  • run_Tapas (bool, default: False ) –

    Whether to run Tapas for relative orientation. Defaults to False.

  • micmac_path (Path, default: None ) –

    Path to the MicMac executable. If not provided, the function will try to find it. Defaults to None.

Raises:
  • FileNotFoundError

    If the image directory, feature file, or match file does not exist.

  • Exception

    If the number of images is not consistent with the number of matches.

Returns:
  • None

Source code in src/deep_image_matching/io/h5_to_micmac.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def export_to_micmac(
    image_dir: Path,
    features_h5: Path,
    matches_h5: Path,
    out_dir: Path = "micmac",
    img_ext: str = IMAGE_EXT,
    run_Tapas: bool = False,
    micmac_path: Path = None,
):
    """
    Exports image features and matches to a specified directory in a format suitable for MicMac processing. Optionally, it can also run the Tapas tool from MicMac for relative orientation.

    Args:
        image_dir (Path): Directory containing the images.
        features_h5 (Path): Path to the HDF5 file containing the features.
        matches_h5 (Path): Path to the HDF5 file containing the matches.
        out_dir (Path, optional): Output directory. Defaults to "micmac".
        img_ext (str, optional): Image file extension. Defaults to IMAGE_EXT.
        run_Tapas (bool, optional): Whether to run Tapas for relative orientation. Defaults to False.
        micmac_path (Path, optional): Path to the MicMac executable. If not provided, the function will try to find it. Defaults to None.

    Raises:
        FileNotFoundError: If the image directory, feature file, or match file does not exist.
        Exception: If the number of images is not consistent with the number of matches.

    Returns:
        None
    """
    image_dir = Path(image_dir)
    feature_path = Path(features_h5)
    match_path = Path(matches_h5)
    out_dir = Path(out_dir)

    if not image_dir.exists():
        raise FileNotFoundError(f"Image directory {image_dir} does not exist")
    if not feature_path.exists():
        raise FileNotFoundError(f"Feature file {feature_path} does not exist")
    if not match_path.exists():
        raise FileNotFoundError(f"Matches file {match_path} does not exist")
    out_dir.mkdir(exist_ok=True, parents=True)

    # Export the tie points
    homol_dir = out_dir / "Homol"
    export_tie_points(feature_path, match_path, homol_dir)

    # Check if some images have no matches and remove them
    images = sorted([e for e in image_dir.glob("*") if e.suffix in img_ext])
    for img in images:
        mtch_dir = homol_dir / f"Pastis{img.name}"
        if list(mtch_dir.glob("*.txt")):
            shutil.copyfile(img, out_dir / img.name)
        else:
            logger.info(f"No matches found for image {img.name}, removing it")
            shutil.rmtree(mtch_dir)

    # Check that the number of images is consistent with the number of matches
    images = sorted([e for e in out_dir.glob("*") if e.suffix in img_ext])
    matches = sorted([e for e in homol_dir.glob("*") if e.is_dir()])
    if len(images) != len(matches):
        raise Exception(
            f"The number of images ({len(images)}) is different from the number of matches ({len(matches)})"
        )

    # logger.info(
    #     f"Succesfully exported images and tie points ready for MICMAC processing to {out_dir}"
    # )

    if run_Tapas:
        # Try to run MicMac
        logger.info("Try to run relative orientation with MicMac...")
        try:
            # Try to find the MicMac executable
            if micmac_path is None:
                logger.info("MicMac path not specified, trying to find it...")
                micmac_path = shutil.which("mm3d")
                if not micmac_path:
                    raise FileNotFoundError("MicMac path not found")
                logger.info(f"Found MicMac executable at {micmac_path}")
                # Check if the executable is found and can be run
                subprocess.run(
                    [micmac_path],
                    check=True,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.STDOUT,
                )
        except FileNotFoundError as e:
            logger.error(
                f"Unable to find MicMac executable, skipping reconstruction.Please manually specify the path to the MicMac executable. {e}"
            )
        except subprocess.CalledProcessError as e:
            logger.error(f"Error running MicMac Tapas, skipping reconstruction.\n{e}")

        # If micmac can be run correctly, try to run Tapas
        logger.info("Running MicMac Tapas...")
        # img_list = " ".join([str(e) for e in images])
        cmd = [
            micmac_path,
            "Tapas",
            "RadialStd",
            ".*JPG",
            "Out=Calib",
            "ExpTxt=1",
        ]

        execution = execute(cmd, out_dir)
        for line in execution:
            print(line, end="")

        logger.info("Relative orientation with MicMac done!")

get_matches(feature_path, match_path, key0, key1)

Retrieve the matches between two images based on the given keys.

Parameters:
  • match_path (Path) –

    Path to the HDF5 file containing the matches.

  • features (Path) –

    Path to the HDF5 file containing the features.

  • key0 (str) –

    Name of the first image.

  • key1 (str) –

    Name of the second image.

Returns:
  • tuple( Tuple[ndarray, ndarray] ) –

    A tuple containing the coordinates of the matches in both images. The first element corresponds to the coordinates in the first image, and the second element corresponds to the coordinates in the second image. If the matches are not present, None is returned for both elements.

Source code in src/deep_image_matching/io/h5_to_micmac.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def get_matches(
    feature_path: Path, match_path: Path, key0: str, key1: str
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Retrieve the matches between two images based on the given keys.

    Args:
        match_path (Path): Path to the HDF5 file containing the matches.
        features (Path): Path to the HDF5 file containing the features.
        key0 (str): Name of the first image.
        key1 (str): Name of the second image.

    Returns:
        tuple: A tuple containing the coordinates of the matches in both images.
               The first element corresponds to the coordinates in the first image, and the second element corresponds to the coordinates in the second image.
               If the matches are not present, None is returned for both elements.
    """

    with h5py.File(str(feature_path), "r") as features, h5py.File(
        str(match_path), "r"
    ) as matches:
        # Check if the matches are present
        if key0 not in matches.keys() or key1 not in matches[key0].keys():
            return None, None

        # Get index of matches in both images
        matches0_idx = np.asarray(matches[key0][key1])[:, 0]
        matches1_idx = np.asarray(matches[key0][key1])[:, 1]

        # get index of sorted matches
        s0idx = np.argsort(matches0_idx)
        s1idx = np.argsort(matches1_idx)

        # Get coordinates of matches
        x0y0 = features[key0]["keypoints"][matches0_idx[s0idx]]
        x1y1 = features[key1]["keypoints"][matches1_idx[s1idx]]

        # Restore the original order
        x0y0 = x0y0[np.argsort(s0idx)]
        x1y1 = x1y1[np.argsort(s1idx)]

    return x0y0, x1y1

show_micmac_matches(file, image_dir, i0_name=None, i1_name=None, out=None, **kwargs)

Display the tie points between two images matched by a MicMac from the matches text file.

Parameters:
  • file (Path) –

    The path to the file containing the matches.

  • image_dir (Path) –

    The directory containing the images.

  • out (Path, default: None ) –

    The path to save the output image. Defaults to None.

Returns:
  • ndarray

    np.ndarray: The output image with the matches visualized.

Source code in src/deep_image_matching/io/h5_to_micmac.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def show_micmac_matches(
    file: Path,
    image_dir: Path,
    i0_name: Path = None,
    i1_name: Path = None,
    out: Path = None,
    **kwargs,
) -> np.ndarray:
    """
    Display the tie points between two images matched by a MicMac from the matches text file.

    Args:
        file (Path): The path to the file containing the matches.
        image_dir (Path): The directory containing the images.
        out (Path, optional): The path to save the output image. Defaults to None.

    Returns:
        np.ndarray: The output image with the matches visualized.
    """

    file = Path(file)
    if not file.exists():
        raise FileNotFoundError(f"File {file} does not exist")
    if not image_dir.exists():
        raise FileNotFoundError(f"Image directory {image_dir} does not exist")

    # Get the image names
    if i0_name is None or i1_name is None:
        i0_name = file.parent.name.replace("Pastis", "")
        i1_name = file.name.replace(".txt", "")
    if not (image_dir / i0_name).exists():
        raise FileNotFoundError(f"Image {i0_name} does not exist in {image_dir}")
    if not (image_dir / i1_name).exists():
        raise FileNotFoundError(f"Image {i1_name} does not exist in {image_dir}")

    # Read the matches
    x0y0, x1y1 = read_Homol_matches(file)

    # Read the images
    image0 = cv2.imread(str(image_dir / i0_name))
    image1 = cv2.imread(str(image_dir / i1_name))
    if image0 is None:
        raise OSError(f"Unable to read image {i0_name}")
    if image1 is None:
        raise OSError(f"Unable to read image {i1_name}")

    out = viz_matches_cv2(image0, image1, x0y0, x1y1, out, **kwargs)

    return out



h5_to_openmvg

This module contains functions to export image features, matches, and camera data to an OpenMVG project.

export_to_openmvg(img_dir, feature_path, match_path, openmvg_out_path, camera_options, openmvg_sfm_bin=None, openmvg_database=None)

Exports image features, matches, and camera data to an OpenMVG project.

This function prepares the necessary files and directories for running OpenMVG's SfM pipeline.

Parameters:
  • img_dir (Path) –

    Path to the directory containing source images.

  • feature_path (Path) –

    Path to the feature file (HDF5 format).

  • match_path (Path) –

    Path to the match file (HDF5 format).

  • openmvg_out_path (Path) –

    Path to the desired output directory for the OpenMVG project.

  • camera_options (dict) –

    Camera configuration options.

  • openmvg_sfm_bin (Path, default: None ) –

    Path to the OpenMVG SfM executable. If not provided, attempts to find it automatically (Linux only).

  • openmvg_database (Path, default: None ) –

    Path to the OpenMVG sensor width database. If not provided, downloads it to the output directory.

Source code in src/deep_image_matching/io/h5_to_openmvg.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
def export_to_openmvg(
    img_dir,
    feature_path: Path,
    match_path: Path,
    openmvg_out_path: Path,
    camera_options: dict,
    openmvg_sfm_bin: Path = None,
    openmvg_database: Path = None,
) -> None:
    """
    Exports image features, matches, and camera data to an OpenMVG project.

    This function prepares the necessary files and directories for running OpenMVG's SfM pipeline.

    Args:
        img_dir (Path): Path to the directory containing source images.
        feature_path (Path): Path to the feature file (HDF5 format).
        match_path (Path): Path to the match file (HDF5 format).
        openmvg_out_path (Path): Path to the desired output directory for the OpenMVG project.
        camera_options (dict): Camera configuration options.
        openmvg_sfm_bin (Path, optional): Path to the OpenMVG SfM executable. If not provided,
                                          attempts to find it automatically (Linux only).
        openmvg_database (Path, optional): Path to the OpenMVG sensor width database.
                                           If not provided, downloads it to the output directory.

    """
    openmvg_out_path = Path(openmvg_out_path)
    if openmvg_out_path.exists():
        logger.warning(
            f"OpenMVG output folder {openmvg_out_path} already exists - deleting it"
        )
        os.rmdir(openmvg_out_path)
    openmvg_out_path.mkdir(parents=True)

    # NOTE: this part meybe is not needed here...
    if openmvg_sfm_bin is None:
        # Try to find openMVG binaries (only on linux)
        if sys.platform == "linux":
            openmvg_sfm_bin = shutil.which("openMVG_main_SfM")
        else:
            raise FileNotFoundError(
                "openMVG binaries path is not provided and DIM is not able to find it automatically. Please provide the path to openMVG binaries."
            )
    openmvg_sfm_bin = Path(openmvg_sfm_bin)
    if not openmvg_sfm_bin.exists():
        raise FileNotFoundError(
            f"openMVG binaries path {openmvg_sfm_bin} does not exist."
        )

    if openmvg_database is not None:
        openmvg_database = Path(openmvg_database)
        if not openmvg_database.exists():
            raise FileNotFoundError(
                f"openMVG database path {openmvg_database} does not exist."
            )
    else:
        # Download openMVG sensor_width_camera_database to the openMVG output folder
        url = "https://github.com/openMVG/openMVG/blob/develop/src/openMVG/exif/sensor_width_database/sensor_width_camera_database.txt"
        openmvg_database = openmvg_out_path / "sensor_width_camera_database.txt"
        urllib.request.urlretrieve(url, openmvg_database)

    # camera_file_params = openmvg_database # Path to sensor_width_camera_database.txt file
    matches_dir = openmvg_out_path / "matches"
    matches_dir.mkdir()

    # pIntrisics = subprocess.Popen( [os.path.join(openmvg_sfm_bin, "openMVG_main_SfMInit_ImageListing"),  "-i", img_dir, "-o", matches_dir, "-d", camera_file_params] )
    # pIntrisics.wait()
    sfm_data = generate_sfm_data(img_dir, camera_options)

    with open(matches_dir / "sfm_data.json", "w") as json_file:
        json.dump(sfm_data, json_file, indent=2)

    add_keypoints(feature_path, img_dir, matches_dir)
    add_matches(match_path, openmvg_out_path / "matches" / "sfm_data.json", matches_dir)

    return None

generate_sfm_data(images_dir, camera_options)

Generates Structure-from-Motion (SfM) data in the OpenMVG format.

This function was inspired from PySfMUtils to create an OpenMVG-compatible JSON data structure, including image information, camera intrinsics, and views.

Parameters:
  • images_dir (Path) –

    Path to the directory containing source images.

  • camera_options (dict) –

    Camera configuration options from 'config/cameras.yaml'.

Returns:
  • dict

    A dictionary containing the generated SfM data structure.

Source code in src/deep_image_matching/io/h5_to_openmvg.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def generate_sfm_data(images_dir: Path, camera_options: dict):
    """
    Generates Structure-from-Motion (SfM) data in the OpenMVG format.

    This function was inspired from [PySfMUtils](https://gitlab.com/educelab/sfm-utils/-/blob/develop/sfm_utils/openmvg.py?ref_type=heads) to create an OpenMVG-compatible JSON data structure, including image information, camera intrinsics, and views.

    Args:
        images_dir (Path): Path to the directory containing source images.
        camera_options (dict): Camera configuration options from 'config/cameras.yaml'.

    Returns:
        dict: A dictionary containing the generated SfM data structure.
    """

    """
    images_dir : path to directory containing all the images
    camera_options : dictionary with all the options from config/cameras.yaml
    """
    # Emulate the Cereal pointer counter
    __ptr_cnt = 2147483649

    def open_mvg_view(
        id: int, img_name: str, images_dir: Path, images_cameras: dict
    ) -> dict:
        """
        OpenMVG View struct
        images_cameras : dictionary with image names as key and camera id as value
        """
        image_path = images_dir / img_name
        image = Image.open(image_path)
        width, height = image.size

        nonlocal __ptr_cnt
        d = {
            "key": id,
            "value": {
                "polymorphic_id": 1073741824,
                "ptr_wrapper": {
                    "id": __ptr_cnt,
                    "data": {
                        "local_path": "",
                        "filename": img_name,
                        "width": width,
                        "height": height,
                        "id_view": id,
                        "id_intrinsic": images_cameras[img_name],
                        "id_pose": id,
                    },
                },
            },
        }
        __ptr_cnt += 1
        return d

    def open_mvg_intrinsic(intrinsic: dict) -> dict:
        """
        OpenMVG Intrinsic struct
        """
        nonlocal __ptr_cnt
        d = {
            "key": intrinsic["cam_id"],
            "value": {
                "polymorphic_id": 2147483649,
                "polymorphic_name": intrinsic["camera_model"],
                "ptr_wrapper": {
                    "id": __ptr_cnt,
                    "data": {
                        "width": intrinsic["width"],
                        "height": intrinsic["height"],
                        "focal_length": intrinsic["focal"],
                        "principal_point": [
                            intrinsic["width"] / 2.0,
                            intrinsic["height"] / 2.0,
                        ],
                    },
                },
            },
        }
        __ptr_cnt += 1

        if intrinsic["dist_params"] is not None:
            dist_name = __OPENMVG_DIST_NAME_MAP[intrinsic["camera_model"]]
            d["value"]["ptr_wrapper"]["data"][dist_name] = intrinsic["dist_params"]

        return d

    def assign_intrinsics(images_dir: Path, img: str, cam: int, camera_model: str):
        image_path = images_dir / img
        focal = get_focal(image_path)
        image = Image.open(image_path)
        width, height = image.size

        if camera_model == "pinhole":
            params = None
        elif camera_model == "pinhole_radial_k3":
            params = [0.0, 0.0, 0.0]
        elif camera_model == "pinhole_brown_t2":
            params = [0.0, 0.0, 0.0, 0.0, 0.0]

        d = {
            "cam_id": cam,
            "camera_model": camera_model,
            "width": width,
            "height": height,
            "focal": focal,
            "dist_params": params,
        }

        return d

    def parse_camera_options(images_dir: Path, images: list, camera_options: dict):
        single_camera = camera_options["general"]["single_camera"]
        views_and_cameras = {}
        intrinsics = {}

        # Check assigned images exists
        for key in list(camera_options.keys()):
            if key != "general":
                imgs = camera_options[key]["images"]
                imgs = imgs.split(",")
                if imgs[0] != "":
                    for img in imgs:
                        if img not in images:
                            logger.error(
                                "Check images assigned to cameras in config/cameras.yaml. Image names or extensions are wrong."
                            )
                            quit()

        def single_main_camera():
            cam = 0
            # Assign camera defined in 'general' to all images
            for img in images:
                views_and_cameras[img] = cam
            # intrinsics[cam] = {
            #    "cam_id": cam,
            #    "camera_model": camera_options["general"]["camera_model"],
            # }
            intrinsics[cam] = assign_intrinsics(
                images_dir, img, cam, camera_options["general"]["openmvg_camera_model"]
            )
            # Assign a camera to images defined in 'camx'
            other_cam = 1
            for key in list(camera_options.keys()):
                if key != "general":
                    imgs = camera_options[key]["images"]
                    imgs = imgs.split(",")
                    if imgs[0] != "":
                        for img in imgs:
                            views_and_cameras[img] = other_cam
                            # intrinsics[other_cam] = {
                            #    "cam_id": other_cam,
                            #    "camera_model": camera_options[key]["camera_model"],
                            # }
                            intrinsics[other_cam] = assign_intrinsics(
                                images_dir,
                                img,
                                other_cam,
                                camera_options["general"]["openmvg_camera_model"],
                            )
                        other_cam += 1

            return intrinsics, views_and_cameras

        def no_main_camera():
            cam = 0
            # Assign a camera to images defined in 'camx'
            for key in list(camera_options.keys()):
                if key != "general":
                    imgs = camera_options[key]["images"]
                    imgs = imgs.split(",")
                    if imgs[0] != "":
                        for img in imgs:
                            views_and_cameras[img] = cam
                            # intrinsics[cam] = {
                            #    "cam_id": cam,
                            #    "camera_model": camera_options[key]["camera_model"],
                            # }
                            intrinsics[cam] = assign_intrinsics(
                                images_dir,
                                img,
                                cam,
                                camera_options["general"]["openmvg_camera_model"],
                            )
                        cam += 1

            # For images not defined in 'camx' assign the camera defined in 'general'
            grouped_imgs = list(views_and_cameras.keys())
            for img in images:
                if img not in grouped_imgs:
                    views_and_cameras[img] = cam
                    # intrinsics[cam] = {
                    #    "cam_id": cam,
                    #    "camera_model": camera_options["general"]["camera_model"],
                    # }
                    intrinsics[cam] = assign_intrinsics(
                        images_dir,
                        img,
                        cam,
                        camera_options["general"]["openmvg_camera_model"],
                    )
                    cam += 1
            return intrinsics, views_and_cameras

        if single_camera is True:
            intrinsics, views_and_cameras = single_main_camera()
        elif single_camera is False:
            intrinsics, views_and_cameras = no_main_camera()

        return intrinsics, views_and_cameras

    # Construct OpenMVG struct
    images = os.listdir(images_dir)
    intrinsics, views_and_cameras = parse_camera_options(
        images_dir, images, camera_options
    )

    data = {
        "sfm_data_version": "0.3",
        "root_path": str(images_dir),
        "views": [
            open_mvg_view(i, img, images_dir, views_and_cameras)
            for i, img in enumerate(images)
        ],
        "intrinsics": [
            open_mvg_intrinsic(intrinsics[c]) for c in list(intrinsics.keys())
        ],
        "extrinsics": [],
        "structure": [],
        "control_points": [],
    }

    return data