main.py 16 KB

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