hgcalc.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import sys
  2. import numpy as np
  3. def ransac_iters(p, k, z):
  4. """ Computes the required number of iterations for RANSAC.
  5. Args:
  6. p: probability that any given correspondence is valid
  7. k: number of pairs
  8. z: total probability of success after all iterations
  9. Returns:
  10. minimum number of required iterations
  11. """
  12. def log(base, argument):
  13. """ Computes the logarithm to a given base for a given argument
  14. :param base: The base of the logarithm.
  15. :param argument: The argument of the logarithm.
  16. :return: The logarithm.
  17. """
  18. return np.log(argument) / np.log(base)
  19. """
  20. Reference: Lecture 8 Slide 23
  21. Probability that we have no outlier in an iteration: p ** k
  22. Probability that we have an outlier in an iteration: 1 - p ** k
  23. Probability that we have an outlier in each iteration: (1 - p ** k) ** number_of_iterations
  24. (1 - p ** k) ** number_of_iterations should be smaller than or equal to 1 - z
  25. So number_of_iterations must be at least the logarithm of (1 - z) to the base (1 - p ** k)
  26. We always round up to the nearest integer using numpy.ceil because we can only execute full loops.
  27. """
  28. return int(np.ceil(log(1 - p ** k, 1 - z)))
  29. def ransac(pairs, n_iters, k, threshold):
  30. """ RANSAC algorithm.
  31. Args:
  32. pairs: (l, 4) numpy array containing matched keypoint pairs
  33. n_iters: number of ransac iterations
  34. threshold: inlier detection threshold
  35. Returns:
  36. H: (3, 3) numpy array, best homography observed during RANSAC
  37. max_inliers: number of inliers N
  38. inliers: (N, 4) numpy array containing the coordinates of the inliers
  39. """
  40. H = {}
  41. max_inliers = 0
  42. inliers = {}
  43. """
  44. We need to split pairs in two arrays with two columns so that we can pass it to pick_samples.
  45. """
  46. p1, p2 = np.hsplit(pairs, 2)
  47. """
  48. We execute the ransac loop n_iters times, so that we have a good chance to have a valid homography.
  49. """
  50. for iteration in range(n_iters):
  51. print(iteration)
  52. """
  53. First, we pick a sample of k corresponding point pairs
  54. """
  55. sample1, sample2 = pick_samples(p1, p2, k)
  56. """
  57. The points in the samples then have to be conditioned, so their values are between -1 and 1.
  58. We also retrieve the condition matrices from this function.
  59. """
  60. sample1_conditioned, T_1 = condition_points(sample1)
  61. sample2_conditioned, T_2 = condition_points(sample2)
  62. """
  63. Using the conditioned coordinates, we calculate the homographies both with respect to the conditioned points
  64. and the unconditioned points.
  65. """
  66. H_current, HC = compute_homography(sample1_conditioned, sample2_conditioned, T_1, T_2)
  67. """
  68. We then use the unconditioned points (as clarified in the office hours) to compute the homography distances.
  69. """
  70. homography_distances = compute_homography_distance(H_current, p1, p2)
  71. """
  72. Using the find_inliers function, we retrieve the inliers for our given homography as well as the number
  73. of inliers we currently have.
  74. """
  75. number_inliers_current, inliers_current = find_inliers(pairs, homography_distances, threshold)
  76. """
  77. If the number of inliers is higher than for a previously computed homography (i.e. our current homography is
  78. better), we save our current homography as well as the number of inliers and the inliers themselves.
  79. """
  80. if number_inliers_current > max_inliers:
  81. H = H_current
  82. max_inliers = number_inliers_current
  83. inliers = inliers_current
  84. """
  85. We then return the best homography we found as well as the number of inliers found with it and the inliers
  86. themselves.
  87. """
  88. return H, max_inliers, inliers
  89. def recompute_homography(inliers):
  90. """ Recomputes the homography matrix based on all inliers.
  91. Args:
  92. inliers: (N, 4) numpy array containing coordinate pairs of the inlier points
  93. Returns:
  94. H: (3, 3) numpy array, recomputed homography matrix
  95. """
  96. """
  97. We split the inliers matrix into two arrays with 2 columns each (one array for the points in the left image
  98. and one for the points in the right image).
  99. """
  100. p1, p2 = np.hsplit(inliers, 2)
  101. """
  102. We then condition these points.
  103. """
  104. p1_conditioned, T_1 = condition_points(p1)
  105. p2_conditioned, T_2 = condition_points(p2)
  106. """
  107. Using the conditioned points, we recompute the homography. The new homography should give better results than
  108. the previously computed one, since we only use inliers (correct correspondences) here instead of all
  109. correspondences including the wrong ones.
  110. """
  111. H, HC = compute_homography(p1_conditioned, p2_conditioned, T_1, T_2)
  112. return H
  113. def pick_samples(p1, p2, k):
  114. """ Randomly select k corresponding point pairs.
  115. Args:
  116. p1: (n, 2) numpy array, given points in first image
  117. p2: (m, 2) numpy array, given points in second image
  118. k: number of pairs to select
  119. Returns:
  120. sample1: (k, 2) numpy array, selected k pairs in left image
  121. sample2: (k, 2) numpy array, selected k pairs in right image
  122. """
  123. """
  124. We assume that m = n (as confirmed in the office hours)
  125. """
  126. """
  127. Reference: https://numpy.org/doc/stable/reference/random/index.html
  128. We calculate the number of points n (= m) and generate k unique random integers in [0; n) (random_numbers).
  129. We then use the random numbers as indices to select points from p1 and the corresponding points from p2.
  130. """
  131. n = p1.shape[0]
  132. generator = np.random.default_rng()
  133. random_numbers = generator.choice(n, size=k, replace=False)
  134. # random_numbers = np.array([96, 93, 63, 118])
  135. sample1 = p1[random_numbers]
  136. sample2 = p2[random_numbers]
  137. return sample1, sample2
  138. def condition_points(points):
  139. """ Conditioning: Normalization of coordinates for numeric stability
  140. by substracting the mean and dividing by half of the component-wise
  141. maximum absolute value.
  142. Further, turns coordinates into homogeneous coordinates.
  143. Args:
  144. points: (l, 2) numpy array containing unnormailzed cartesian coordinates.
  145. Returns:
  146. ps: (l, 3) numpy array containing normalized points in homogeneous coordinates.
  147. T: (3, 3) numpy array, transformation matrix for conditioning
  148. """
  149. """
  150. First, we calculate half of the component-wise maximum absolute value for all points, as discussed in the
  151. office hours.
  152. """
  153. s = np.max(np.abs(points), axis=0) / 2
  154. sx = s[0]
  155. sy = s[1]
  156. """
  157. We then compute the component-wise mean of all points.
  158. """
  159. t = np.mean(points, axis=0)
  160. tx = t[0]
  161. ty = t[1]
  162. """
  163. We then calculate the transformation matrix T like on slide 18 in Lecture 8.
  164. We use the x components of s and t in the first row and the y components of s and t in the second row.
  165. """
  166. T = np.array(([1 / sx, 0, - tx / sx],
  167. [0, 1 / sy, - ty / sy],
  168. [0, 0, 1]))
  169. """
  170. We now iterate over each point and transform it into homogeneous coordinates.
  171. Then we transform the point using T and append the result to the list ps.
  172. Finally, the list ps is converted to a numpy array and returned together with the transformation matrix T.
  173. """
  174. ps = []
  175. for point in points:
  176. homogeneous_point = np.append(point, 1)
  177. homogeneous_transformed_point = np.matmul(T, homogeneous_point)
  178. ps.append(homogeneous_transformed_point)
  179. return np.array(ps), T
  180. def compute_homography(p1, p2, T1, T2):
  181. """ Estimate homography matrix from point correspondences of conditioned coordinates.
  182. Both returned matrices shoul be normalized so that the bottom right value equals 1.
  183. You may use np.linalg.svd for this function.
  184. Args:
  185. p1: (l, 3) numpy array, the conditioned homogeneous coordinates of interest points in img1
  186. p2: (l, 3) numpy array, the conditioned homogeneous coordinates of interest points in img2
  187. T1: (3,3) numpy array, conditioning matrix for p1
  188. T2: (3,3) numpy array, conditioning matrix for p2
  189. Returns:
  190. H: (3, 3) numpy array, homography matrix with respect to unconditioned coordinates
  191. HC: (3, 3) numpy array, homography matrix with respect to the conditioned coordinates
  192. """
  193. """
  194. Reference: Lecture 8 Slide 9, 13 ff.
  195. We did something similar in Assignment 1
  196. """
  197. """
  198. We calculate the number of rows in p1 (which must be the same as number of rows in p2).
  199. """
  200. N = p1.shape[0]
  201. # Create a (2*N, 9) matrix filled with zeros
  202. A = np.zeros((2 * N, 9))
  203. # We iterate from 0 to N (N is the number of correspondences)
  204. for i in range(0, N):
  205. # The point from the first image (divided by the last component so that x and y have the non-homogeneous
  206. # value. We could cut off the last component (which is now 1), but that step is not necessary here.)
  207. point1 = p1[i] / p1[i][2]
  208. # The corresponding point from the second image (also divided by the last component to get the
  209. # non-homogeneous values for x and y)
  210. point2 = p2[i] / p2[i][2]
  211. # We always calculate two rows of the matrix A in each loop iteration
  212. # Here, we calculate the first row according to the lecture slides Lecture 8 slide 14
  213. first_row = np.concatenate((np.zeros(3), point1, - point2[1] * point1))
  214. # Calculate the second row as in Lecture 8 slide 14
  215. second_row = np.concatenate((- point1, np.zeros(3), point2[0] * point1))
  216. # Calculate the row indices at matrix A, where to store the two calculated rows
  217. # When we iterate from 0 to N, we get indices 0, 2, 4, 6... etc. for the first row index
  218. first_row_index = i * 2
  219. # The second row index is simply the first row index plus one, so the rows are below each other
  220. second_row_index = first_row_index + 1
  221. """
  222. Finally, we save the values in the corresponding rows in matrix A
  223. """
  224. A[first_row_index] = first_row
  225. A[second_row_index] = second_row
  226. """
  227. Now we perform a single value decomposition on A and retrieve the last right singular vector
  228. """
  229. u, s, vt = np.linalg.svd(A)
  230. h = vt[-1]
  231. """
  232. Since we used conditioned points, we retrieve the homography matrix HC regarding the conditioned coordinates
  233. by reshaping the last right singular vector .
  234. """
  235. HC = np.reshape(h, (3, 3))
  236. """
  237. The matrix is then normalized so that the bottom right value equals 1.
  238. """
  239. HC = HC / HC[2, 2]
  240. """
  241. We calculate the homography matrix with respect to the unconditioned coordinates by using the formula from
  242. lecture 8 on slide 18.
  243. """
  244. H = np.matmul(np.linalg.inv(T2), np.matmul(HC, T1))
  245. """
  246. The homography matrix is also normalized.
  247. """
  248. H = H / H[2, 2]
  249. return H, HC
  250. def compute_homography_distance(H, p1, p2):
  251. """ Computes the pairwise symmetric homography distance.
  252. Args:
  253. H: (3, 3) numpy array, homography matrix
  254. p1: (l, 2) numpy array, interest points in img1
  255. p2: (l, 2) numpy array, interest points in img2
  256. Returns:
  257. dist: (l, ) numpy array containing the distances
  258. """
  259. """
  260. Determine the number of points in p1 (should be the same as in p2).
  261. """
  262. l = p1.shape[0]
  263. """
  264. Calculate the inverse of the homography H.
  265. """
  266. H_inverse = np.linalg.inv(H)
  267. """
  268. Create a zero-filled array which we later fill with the distances.
  269. """
  270. distances = np.zeros(l)
  271. """
  272. We calculate the transformed points for the given homography.
  273. """
  274. p1_transformed = transform_pts(np.array(p1), H)
  275. p2_transformed = transform_pts(np.array(p2), H_inverse)
  276. """
  277. We calculate the symmetric squared distances using the formula in the assignment sheet for every point pair.
  278. """
  279. for index in range(0, l):
  280. x1 = p1[index]
  281. x2 = p2[index]
  282. x1_transformed = p1_transformed[index]
  283. x2_transformed = p2_transformed[index]
  284. distances[index] = np.linalg.norm(x1_transformed - x2) ** 2 + np.linalg.norm(x1 - x2_transformed) ** 2
  285. return distances
  286. def find_inliers(pairs, dist, threshold):
  287. """ Return and count inliers based on the homography distance.
  288. Args:
  289. pairs: (l, 4) numpy array containing keypoint pairs
  290. dist: (l, ) numpy array, homography distances for k points
  291. threshold: inlier detection threshold
  292. Returns:
  293. N: number of inliers
  294. inliers: (N, 4)
  295. """
  296. inliers_list = []
  297. """
  298. We iterate over each pair and use its index in the array and its value.
  299. We then use the index to retrieve the corresponding distance for the pair in dist. If the distance is smaller
  300. than or equal to the given threshold, we add the pair to the inliers_list.
  301. """
  302. for pair_index, pair_value in enumerate(pairs):
  303. if dist[pair_index] <= threshold:
  304. inliers_list.append(pair_value)
  305. """
  306. We then get the number of inliers as the length of the list.
  307. Furthermore, we convert the inliers_list to a numpy array.
  308. Both values are then returned.
  309. """
  310. N = len(inliers_list)
  311. inliers = np.array(inliers_list)
  312. return N, inliers
  313. def transform_pts(p, H):
  314. """ Transform p through the homography matrix H.
  315. Args:
  316. p: (l, 2) numpy array, interest points
  317. H: (3, 3) numpy array, homography matrix
  318. Returns:
  319. points: (l, 2) numpy array, transformed points
  320. """
  321. """
  322. Reference: Lecture 8 Slide 8
  323. """
  324. points_list = []
  325. """
  326. Iterate over every point in p.
  327. For each point, we first append a 1 to transform the point into homogeneous coordinates.
  328. Then, we perform a matrix multiplication of the point with the homography H, as shown on slide 8 in lecture 8.
  329. The transformed point is then converted back into non-homogeneous coordinates by dividing it by the last
  330. value and then cutting off the last value (which is then 1 again).
  331. The transformed points are returned as a numpy array.
  332. """
  333. for point_index, point_value in enumerate(p):
  334. homogeneous_p = np.append(point_value, 1)
  335. homogeneous_transformed_p = np.matmul(H, homogeneous_p)
  336. normalized_homogeneous_transformed_p = homogeneous_transformed_p / homogeneous_transformed_p[2]
  337. transformed_p = normalized_homogeneous_transformed_p[0:2]
  338. points_list.append(transformed_p)
  339. return np.array(points_list)
  340. def main():
  341. if len(sys.argv) != 3:
  342. print('Usage: hgcalc <pairs_file_path> <homography_file_path>')
  343. else:
  344. pairs_file_path = sys.argv[1]
  345. homography_file_path = sys.argv[2]
  346. # RANSAC Parameters
  347. ransac_threshold = 0.02 # inlier threshold
  348. p = 0.35 # probability that any given correspondence is valid
  349. k = 4 # number of samples drawn per iteration
  350. z = 0.99 # total probability of success after all iterations
  351. pairs = np.loadtxt(pairs_file_path)
  352. n_iters = ransac_iters(p, k, z)
  353. print('Interations to be done: ', n_iters)
  354. H, num_inliers, inliers = ransac(pairs, n_iters, k, ransac_threshold)
  355. print('Number of inliers:', num_inliers)
  356. # recompute homography matrix based on inliers
  357. H = recompute_homography(inliers)
  358. print(H)
  359. np.savetxt(homography_file_path, H)
  360. if __name__ == "__main__":
  361. main()