Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit 10827b0

Browse files
tonzowonzotwonzo72@gmail.com
andauthored
Adding alternate matching methods (#11)
- Added alternate matching methods to s2p (LoFTR, SuperGlue and SIFT rectification is now more stable!) --------- Co-authored-by: twonzo72@gmail.com <timiles@tims-air-5.mynet>
1 parent 7821518 commit 10827b0

8 files changed

Lines changed: 153 additions & 74 deletions

File tree

‎s2p/__init__.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ def pointing_correction(tile, i):
127127
method = 'relative' if cfg['relative_sift_match_thresh'] is True else 'absolute'
128128
A, m = pointing_accuracy.compute_correction(
129129
img1, img2, rpc1, rpc2, x, y, w, h, method,
130-
cfg['sift_match_thresh'], cfg['max_pointing_error']
130+
cfg['sift_match_thresh'], cfg['max_pointing_error'], cfg['matching_method'],
131+
cfg['min_value'], cfg['max_value'], cfg['confidence_threshold']
131132
)
132133

133134
if A is not None: # A is the correction matrix
@@ -645,17 +646,18 @@ def main(user_cfg, start_from=0, merge_matches=False):
645646
print('2) correcting pointing globally...')
646647
global_pointing_correction(tiles)
647648
common.print_elapsed_time()
648-
649+
649650
# Create matches GeoJSON.
650-
print("Creating matches GeoJSON")
651651
if merge_matches:
652+
print("Creating matches GeoJSON")
652653
merge_all_match_files()
653654
matches_to_geojson(f"{cfg['out_dir']}/merged_sift_matches.txt",
654655
cfg['images'][0]['rpcm'],
655656
10,
656657
[0, 1],
657658
f"{cfg['out_dir']}/matches.geojson"
658659
)
660+
659661
# rectification step:
660662
if start_from <= 3:
661663
print('3) rectifying tiles...')

‎s2p/config.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,13 @@
177177
# If the out_crs is not set this parameter determines if the output CRS uses the EGM2008 geoid vertical datum (if True)
178178
# or the WGS84 ellipsoid vertical datum (if False). If out_crs is set, this parameter is ignored.
179179
cfg['out_geoid'] = False
180+
181+
# LoFTR parameters, points whose confidence is below this value are ignored.
182+
cfg["confidence_threshold"] = 0.5
183+
184+
# Matching parameters
185+
# Matching method to use, choose from ["sift", "loftr", "superglue", "all"]
186+
cfg["matching_method"] = "sift"
187+
# The min and max valeus to scale the image between 0 and 1 with, only used for non-SIFT methods.
188+
cfg["max_value"] = 3000
189+
cfg["min_value"] = 200

‎s2p/image_coordinates_to_coordinates.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import glob
2+
import os
3+
4+
import rasterio
15
import numpy as np
26
import geopandas as gpd
37
import shapely
4-
import srtm4
5-
from rpcm import RPCModel, rpc_from_rpc_file
8+
from shapely.geometry import Point
69
import geopy
710
import geopy.distance
11+
import srtm4
12+
13+
from rpcm import RPCModel, rpc_from_rpc_file
814

915

1016
def localize_row_col_geometry(

‎s2p/pointing_accuracy.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def local_translation(r1, r2, x, y, w, h, m):
100100

101101

102102
def compute_correction(img1, img2, rpc1, rpc2, x, y, w, h,
103-
method, sift_thresh, epipolar_threshold):
103+
method, sift_thresh, epipolar_threshold,
104+
matching_method, min_value, max_value, confidence_threshold):
104105
"""
105106
Computes pointing correction matrix for specific ROI
106107
@@ -125,7 +126,8 @@ def compute_correction(img1, img2, rpc1, rpc2, x, y, w, h,
125126
raise ValueError(f"width or height <= 0 for:\n{img1}\n{img2}\nx={x}, y={y}, w={w}, h={h}. Try a different"
126127
f"tilesize or different ROI.")
127128
m = sift.matches_on_rpc_roi(img1, img2, rpc1, rpc2, x, y, w, h,
128-
method, sift_thresh, epipolar_threshold)
129+
method, sift_thresh, epipolar_threshold,
130+
matching_method, min_value, max_value, confidence_threshold)
129131

130132
if m is not None:
131133
A = local_translation(rpc1, rpc2, x, y, w, h, m)

‎s2p/rectification.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,19 @@ def rectify_pair(im1, im2, rpc1, rpc2, x, y, w, h, out1, out2, A=None, sift_matc
320320

321321
else:
322322
raise Exception("Unknown value {} for argument 'method'".format(method))
323+
324+
325+
326+
# If there are still no matches, raise the no matches error.
327+
if matches is None or len(matches) < 4 or len(matches.shape) != 2:
328+
# find virtual matches from RPC camera models
329+
matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h,
330+
cfg['n_gcp_per_axis'])
323331

324-
if matches is None or len(matches) < 4:
325-
raise NoRectificationMatchesError(
326-
"No or not enough matches found to rectify image pair"
327-
)
332+
# correct second image coordinates with the pointing correction matrix
333+
if A is not None:
334+
matches[:, 2:] = common.points_apply_homography(np.linalg.inv(A),
335+
matches[:, 2:])
328336

329337
# compute rectifying homographies
330338
H1, H2, F = rectification_homographies(matches, x, y, w, h)

‎s2p/sift.py‎

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@
1010
import numpy as np
1111
import rasterio as rio
1212
from numpy.ctypeslib import ndpointer
13+
import cv2
1314
import ransac
1415

1516
from s2p import rpc_utils
1617
from s2p import estimation
1718

19+
try:
20+
from matching.matcher import get_keypoints_loftr
21+
except ImportError as e:
22+
print("LoFTR matching library could not be imported.")
23+
print(e)
24+
25+
try:
26+
from matching.matcher import get_keypoints_superglue
27+
except ImportError as e:
28+
print("SuperGlue matching library could not be imported.")
29+
print(e)
30+
31+
1832
# Locate sift4ctypes library and raise an ImportError if it can not be
1933
# found This call will raise an exception if library can not be found,
2034
# at import time
@@ -240,13 +254,12 @@ def keypoints_match_from_nparray(k1, k2, method, sift_threshold,
240254

241255

242256
def matches_on_rpc_roi(im1, im2, rpc1, rpc2, x, y, w, h,
243-
method, sift_thresh, epipolar_threshold):
257+
method, sift_thresh, epipolar_threshold, matching_method="sift",
258+
min_value=200, max_value=3000, confidence_threshold=0.5):
244259
"""
245260
Compute a list of SIFT matches between two images on a given roi.
246-
247261
The corresponding roi in the second image is determined using the rpc
248262
functions.
249-
250263
Args:
251264
im1, im2: paths to two large tif images
252265
rpc1, rpc2: two instances of the rpcm.RPCModel class
@@ -255,7 +268,6 @@ def matches_on_rpc_roi(im1, im2, rpc1, rpc2, x, y, w, h,
255268
of the rectangle.
256269
method, sift_thresh, epipolar_threshold: see docstring of
257270
s2p.sift.keypoints_match()
258-
259271
Returns:
260272
matches: 2D numpy array containing a list of matches. Each line
261273
contains one pair of points, ordered as x1 y1 x2 y2.
@@ -267,18 +279,100 @@ def matches_on_rpc_roi(im1, im2, rpc1, rpc2, x, y, w, h,
267279
rpc_matches = rpc_utils.matches_from_rpc(rpc1, rpc2, x, y, w, h, 5)
268280
F = estimation.affine_fundamental_matrix(rpc_matches)
269281

270-
# if less than 10 matches, lower thresh_dog. An alternative would be ASIFT
271-
thresh_dog = 0.0133
272-
for _ in range(2):
273-
p1 = image_keypoints(im1, x, y, w, h, thresh_dog=thresh_dog)
274-
p2 = image_keypoints(im2, x2, y2, w2, h2, thresh_dog=thresh_dog)
275-
matches = keypoints_match(p1, p2, method, sift_thresh, F,
276-
epipolar_threshold=epipolar_threshold,
277-
model='fundamental')
278-
if matches is not None and matches.ndim == 2 and matches.shape[0] > 10:
279-
break
280-
thresh_dog /= 2.0
281-
else:
282-
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
283-
return None
282+
if matching_method == "loftr":
283+
p1, p2 = get_keypoints_loftr(im1, im2, min_value, max_value, confidence_threshold,
284+
x, x2, y, y2, w, w2, h, h2, rpc_match=True)
285+
if len(p1) < 10 or len(p2) < 10:
286+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
287+
return None
288+
p1[:, 0] += x
289+
p1[:, 1] += y
290+
p2[:, 0] += x2
291+
p2[:, 1] += y2
292+
matches = np.hstack((p1, p2))
293+
294+
if len(matches) == 0 or matches.ndim != 2:
295+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
296+
return None
297+
elif len(matches) > 10:
298+
inliers = ransac.find_fundamental_matrix(matches, ntrials=1000,
299+
max_err=0.3)[0]
300+
matches = matches[inliers]
301+
else:
302+
print("WARNING: sift.matches_on_rpc_roi: not enough matches.")
303+
return None
304+
305+
306+
elif matching_method == "superglue":
307+
p1, p2 = get_keypoints_superglue(im1, im2, min_value, max_value, x, x2, y, y2, w, w2, h, h2, rpc_match=True)
308+
if len(p1) < 10 or len(p2) < 10:
309+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
310+
return None
311+
312+
p1[:, 0] += x
313+
p1[:, 1] += y
314+
p2[:, 0] += x2
315+
p2[:, 1] += y2
316+
matches = np.hstack((p1, p2))
317+
318+
if matches.ndim != 2:
319+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
320+
return None
321+
322+
elif matching_method == "sift":
323+
# if less than 10 matches, lower thresh_dog. An alternative would be ASIFT
324+
thresh_dog = 0.0133
325+
for _ in range(2):
326+
p1 = image_keypoints(im1, x, y, w, h, thresh_dog=thresh_dog)
327+
p2 = image_keypoints(im2, x2, y2, w2, h2, thresh_dog=thresh_dog)
328+
matches = keypoints_match(p1, p2, method, sift_thresh, F,
329+
epipolar_threshold=epipolar_threshold,
330+
model='fundamental')
331+
if matches is not None and matches.ndim == 2 and matches.shape[0] > 10:
332+
break
333+
thresh_dog /= 2.0
334+
else:
335+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
336+
return None
337+
338+
339+
elif matching_method == "all":
340+
p1, p2 = get_keypoints_loftr(im1, im2, min_value, max_value, confidence_threshold,
341+
x, x2, y, y2, w, w2, h, h2, rpc_match=True)
342+
p1[:, 0] += x
343+
p1[:, 1] += y
344+
p2[:, 0] += x2
345+
p2[:, 1] += y2
346+
matches = np.hstack((p1, p2))
347+
if len(matches) == 0 or matches.ndim != 2:
348+
print("WARNING: sift.matches_on_rpc_roi: found no matches.")
349+
matches = None
350+
elif len(matches) > 10:
351+
inliers = ransac.find_fundamental_matrix(matches, ntrials=1000,
352+
max_err=0.3)[0]
353+
matches = matches[inliers]
354+
355+
p1_sg, p2_sg = get_keypoints_superglue(im1, im2, min_value, max_value, x, x2, y, y2, w, w2, h, h2, rpc_match=True)
356+
p1_sg[:, 0] += x
357+
p1_sg[:, 1] += y
358+
p1_sg[:, 0] += x2
359+
p1_sg[:, 1] += y2
360+
matches_sg = np.hstack((p1_sg, p2_sg))
361+
362+
thresh_dog = 0.0133
363+
for _ in range(2):
364+
p1_sift = image_keypoints(im1, x, y, w, h, thresh_dog=thresh_dog)
365+
p2_sift = image_keypoints(im2, x2, y2, w2, h2, thresh_dog=thresh_dog)
366+
matches_sift = keypoints_match(p1_sift, p2_sift, method, sift_thresh, F,
367+
epipolar_threshold=epipolar_threshold,
368+
model='fundamental')
369+
if matches_sift is not None and matches_sift.ndim == 2 and matches_sift.shape[0] > 10:
370+
break
371+
thresh_dog /= 2.0
372+
373+
matches = np.vstack((matches, matches_sg, matches_sift))
374+
375+
if len(matches) < 10:
376+
return None
377+
284378
return matches

‎setup.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def finalize_options(self):
5454
'rpcm @ https://github.com/20treeAI/rpcm/archive/refs/tags/v1.4.8.tar.gz',
5555
'srtm4 @ https://github.com/20treeAI/srtm4/archive/refs/tags/1.2.4.tar.gz',
5656
'requests',
57+
'opencv-python',
5758
'geopandas',
5859
'geopy']
5960

@@ -62,7 +63,7 @@ def finalize_options(self):
6263
}
6364

6465
setup(name="s2p",
65-
version="1.3.9",
66+
version="1.4.0",
6667
description="Satellite Stereo Pipeline.",
6768
long_description=readme(),
6869
long_description_content_type='text/markdown',

‎tests/rectification_test.py‎

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -39,50 +39,6 @@ def test_rectification_homographies(matches):
3939
np.testing.assert_allclose(variable, expected, rtol=0.01, atol=1e-6, verbose=True)
4040

4141

42-
def test_rectify_pair_no_matches(tmp_path, images):
43-
"""
44-
Test running rectification.rectify_pair() where no matches are found.
45-
"""
46-
im1, rpc1, im2, rpc2 = images
47-
with pytest.raises(s2p.rectification.NoRectificationMatchesError):
48-
s2p.rectification.rectify_pair(
49-
im1=im1,
50-
im2=im2,
51-
rpc1=rpc1,
52-
rpc2=rpc2,
53-
x=100,
54-
y=100,
55-
w=200,
56-
h=200,
57-
out1=str(tmp_path / 'out1.tiff'),
58-
out2=str(tmp_path / 'out2.tiff'),
59-
sift_matches=None,
60-
method='sift',
61-
)
62-
63-
64-
def test_rectify_pair_few_matches(tmp_path, matches, images):
65-
"""
66-
Test running rectification.rectify_pair() where less than 4 matches are found.
67-
"""
68-
im1, rpc1, im2, rpc2 = images
69-
with pytest.raises(s2p.rectification.NoRectificationMatchesError):
70-
s2p.rectification.rectify_pair(
71-
im1=im1,
72-
im2=im2,
73-
rpc1=rpc1,
74-
rpc2=rpc2,
75-
x=100,
76-
y=100,
77-
w=200,
78-
h=200,
79-
out1=str(tmp_path / 'out1.tiff'),
80-
out2=str(tmp_path / 'out2.tiff'),
81-
sift_matches=matches[:3],
82-
method='sift',
83-
)
84-
85-
8642
def test_rectify_pair_with_matches(tmp_path, matches, images):
8743
"""
8844
Test running rectification.rectify_pair() with some matches.

0 commit comments

Comments
 (0)