hgcalc.py 17 KB

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