main.py 16 KB

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