regularization.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import math
  15. import cv2
  16. import numpy as np
  17. from .utils import del_small_connection, calc_distance, morphological_operation
  18. S = 20
  19. TD = 3
  20. D = TD + 1
  21. ALPHA = math.degrees(math.pi / 6)
  22. BETA = math.degrees(math.pi * 17 / 18)
  23. DELTA = math.degrees(math.pi / 12)
  24. THETA = math.degrees(math.pi / 4)
  25. def building_regularization(mask: np.ndarray, W: int=32) -> np.ndarray:
  26. """
  27. Translate the mask of building into structured mask.
  28. The original article refers to
  29. Wei S, Ji S, Lu M. "Toward Automatic Building Footprint Delineation From Aerial Images Using CNN and Regularization."
  30. (https://ieeexplore.ieee.org/document/8933116).
  31. This algorithm has no public code.
  32. The implementation procedure refers to original article and this repo:
  33. https://github.com/niecongchong/RS-building-regularization
  34. The implementation is not fully consistent with the article.
  35. Args:
  36. mask (np.ndarray): Mask of building. Shape is [H, W] and values are 0 or 1.
  37. W (int, optional): Minimum threshold in main direction. Default is 32.
  38. The larger W, the more regular the image, but the worse the image detail.
  39. Returns:
  40. np.ndarray: Mask of building after regularized.
  41. """
  42. # check and pro processing
  43. mask = del_small_connection(mask)
  44. mask_shape = mask.shape
  45. # find contours
  46. contours, hierarchys = cv2.findContours(mask, cv2.RETR_TREE,
  47. cv2.CHAIN_APPROX_SIMPLE)
  48. if not contours:
  49. raise ValueError("There are no contours.")
  50. # adjust
  51. res_contours = []
  52. for contour, hierarchy in zip(contours, hierarchys[0]):
  53. contour = _coarse(contour, mask_shape) # coarse
  54. if contour is None:
  55. continue
  56. contour = _fine(contour, W) # fine
  57. res_contours.append((contour, _get_priority(hierarchy)))
  58. result = _fill(mask, res_contours) # fill
  59. result = morphological_operation(result, "open")
  60. result = np.clip(result, 0, 1)
  61. return result
  62. def _coarse(contour, img_shape):
  63. def _inline_check(point, shape, eps=5):
  64. x, y = point[0]
  65. iH, iW = shape
  66. if x < eps or x > iH - eps or y < eps or y > iW - eps:
  67. return False
  68. else:
  69. return True
  70. area = cv2.contourArea(contour)
  71. # S = 20
  72. if area < S: # remove polygons whose area is below a threshold S
  73. return None
  74. # D = 0.3 if area < 200 else 1.0
  75. # TD = 0.5 if area < 200 else 0.9
  76. epsilon = 0.005 * cv2.arcLength(contour, True)
  77. contour = cv2.approxPolyDP(contour, epsilon, True) # DP
  78. p_number = contour.shape[0]
  79. idx = 0
  80. while idx < p_number:
  81. last_point = contour[idx - 1]
  82. current_point = contour[idx]
  83. next_idx = (idx + 1) % p_number
  84. next_point = contour[next_idx]
  85. # remove edges whose lengths are below a given side length TD
  86. # that varies with the area of a building.
  87. distance = calc_distance(current_point, next_point)
  88. if distance < TD and not _inline_check(next_point, img_shape):
  89. contour = np.delete(contour, next_idx, axis=0)
  90. p_number -= 1
  91. continue
  92. # remove over-sharp angles with threshold α.
  93. # remove over-smooth angles with threshold β.
  94. angle = _calc_angle(last_point, current_point, next_point)
  95. if (ALPHA > angle or angle > BETA) and _inline_check(current_point,
  96. img_shape):
  97. contour = np.delete(contour, idx, axis=0)
  98. p_number -= 1
  99. continue
  100. idx += 1
  101. if p_number > 2:
  102. return contour
  103. else:
  104. return None
  105. def _fine(contour, W):
  106. # area = cv2.contourArea(contour)
  107. # W = 6 if area < 200 else 8
  108. # TD = 0.5 if area < 200 else 0.9
  109. # D = TD + 0.3
  110. nW = W
  111. p_number = contour.shape[0]
  112. distance_list = []
  113. azimuth_list = []
  114. indexs_list = []
  115. for idx in range(p_number):
  116. current_point = contour[idx]
  117. next_idx = (idx + 1) % p_number
  118. next_point = contour[next_idx]
  119. distance_list.append(calc_distance(current_point, next_point))
  120. azimuth_list.append(_calc_azimuth(current_point, next_point))
  121. indexs_list.append((idx, next_idx))
  122. # add the direction of the longest edge to the list of main direction.
  123. longest_distance_idx = np.argmax(distance_list)
  124. main_direction_list = [azimuth_list[longest_distance_idx]]
  125. max_dis = distance_list[longest_distance_idx]
  126. if max_dis <= nW:
  127. nW = max_dis - 1e-6
  128. # Add other edges’ direction to the list of main directions
  129. # according to the angle threshold δ between their directions
  130. # and directions in the list.
  131. for distance, azimuth in zip(distance_list, azimuth_list):
  132. for mdir in main_direction_list:
  133. abs_dif_ang = abs(mdir - azimuth)
  134. if distance > nW and THETA <= abs_dif_ang <= (180 - THETA):
  135. main_direction_list.append(azimuth)
  136. contour_by_lines = []
  137. md_used_list = [main_direction_list[0]]
  138. for distance, azimuth, (idx, next_idx) in zip(distance_list, azimuth_list,
  139. indexs_list):
  140. p1 = contour[idx]
  141. p2 = contour[next_idx]
  142. pm = (p1 + p2) / 2
  143. # find long edges with threshold W that varies with building’s area.
  144. if distance > nW:
  145. rotate_ang = main_direction_list[0] - azimuth
  146. for main_direction in main_direction_list:
  147. r_ang = main_direction - azimuth
  148. if abs(r_ang) < abs(rotate_ang):
  149. rotate_ang = r_ang
  150. md_used_list.append(main_direction)
  151. abs_rotate_ang = abs(rotate_ang)
  152. # adjust long edges according to the list and angles.
  153. if abs_rotate_ang < DELTA or abs_rotate_ang > (180 - DELTA):
  154. rp1 = _rotation(p1, pm, rotate_ang)
  155. rp2 = _rotation(p2, pm, rotate_ang)
  156. elif (90 - DELTA) < abs_rotate_ang < (90 + DELTA):
  157. rp1 = _rotation(p1, pm, rotate_ang - 90)
  158. rp2 = _rotation(p2, pm, rotate_ang - 90)
  159. else:
  160. rp1, rp2 = p1, p2
  161. # adjust short edges (judged by a threshold θ) according to the list and angles.
  162. else:
  163. rotate_ang = md_used_list[-1] - azimuth
  164. abs_rotate_ang = abs(rotate_ang)
  165. if abs_rotate_ang < THETA or abs_rotate_ang > (180 - THETA):
  166. rp1 = _rotation(p1, pm, rotate_ang)
  167. rp2 = _rotation(p2, pm, rotate_ang)
  168. else:
  169. rp1 = _rotation(p1, pm, rotate_ang - 90)
  170. rp2 = _rotation(p2, pm, rotate_ang - 90)
  171. # contour_by_lines.extend([rp1, rp2])
  172. contour_by_lines.append([rp1[0], rp2[0]])
  173. correct_points = np.array(contour_by_lines)
  174. # merge (or connect) parallel lines if the distance between
  175. # two lines is less than (or larger than) a threshold D.
  176. final_points = []
  177. final_points.append(correct_points[0][0].reshape([1, 2]))
  178. lp_number = correct_points.shape[0] - 1
  179. for idx in range(lp_number):
  180. next_idx = (idx + 1) if idx < lp_number else 0
  181. cur_edge_p1 = correct_points[idx][0]
  182. cur_edge_p2 = correct_points[idx][1]
  183. next_edge_p1 = correct_points[next_idx][0]
  184. next_edge_p2 = correct_points[next_idx][1]
  185. L1 = _line(cur_edge_p1, cur_edge_p2)
  186. L2 = _line(next_edge_p1, next_edge_p2)
  187. A1 = _calc_azimuth([cur_edge_p1], [cur_edge_p2])
  188. A2 = _calc_azimuth([next_edge_p1], [next_edge_p2])
  189. dif_azi = abs(A1 - A2)
  190. # find intersection point if not parallel
  191. if (90 - DELTA) < dif_azi < (90 + DELTA):
  192. point_intersection = _intersection(L1, L2)
  193. if point_intersection is not None:
  194. final_points.append(point_intersection)
  195. # move or add lines when parallel
  196. elif dif_azi < 1e-6:
  197. marg = _calc_distance_between_lines(L1, L2)
  198. if marg < D:
  199. # move
  200. point_move = _calc_project_in_line(next_edge_p1, cur_edge_p1,
  201. cur_edge_p2)
  202. final_points.append(point_move)
  203. # update next
  204. correct_points[next_idx][0] = point_move
  205. correct_points[next_idx][1] = _calc_project_in_line(
  206. next_edge_p2, cur_edge_p1, cur_edge_p2)
  207. else:
  208. # add line
  209. add_mid_point = (cur_edge_p2 + next_edge_p1) / 2
  210. rp1 = _calc_project_in_line(add_mid_point, cur_edge_p1,
  211. cur_edge_p2)
  212. rp2 = _calc_project_in_line(add_mid_point, next_edge_p1,
  213. next_edge_p2)
  214. final_points.extend([rp1, rp2])
  215. else:
  216. final_points.extend(
  217. [cur_edge_p1[np.newaxis, :], cur_edge_p2[np.newaxis, :]])
  218. final_points = np.array(final_points)
  219. return final_points
  220. def _get_priority(hierarchy):
  221. if hierarchy[3] < 0:
  222. return 1
  223. if hierarchy[2] < 0:
  224. return 2
  225. return 3
  226. def _fill(img, coarse_conts):
  227. result = np.zeros_like(img)
  228. sorted(coarse_conts, key=lambda x: x[1])
  229. for contour, priority in coarse_conts:
  230. if priority == 2:
  231. cv2.fillPoly(result, [contour.astype(np.int32)], (0, 0, 0))
  232. else:
  233. cv2.fillPoly(result, [contour.astype(np.int32)], (255, 255, 255))
  234. return result
  235. def _calc_angle(p1, vertex, p2):
  236. x1, y1 = p1[0]
  237. xv, yv = vertex[0]
  238. x2, y2 = p2[0]
  239. a = ((xv - x2) * (xv - x2) + (yv - y2) * (yv - y2))**0.5
  240. b = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))**0.5
  241. c = ((x1 - xv) * (x1 - xv) + (y1 - yv) * (y1 - yv))**0.5
  242. return math.degrees(math.acos((b**2 - a**2 - c**2) / (-2 * a * c)))
  243. def _calc_azimuth(p1, p2):
  244. x1, y1 = p1[0]
  245. x2, y2 = p2[0]
  246. if y1 == y2:
  247. return 0.0
  248. if x1 == x2:
  249. return 90.0
  250. elif x1 < x2:
  251. if y1 < y2:
  252. ang = math.atan((y2 - y1) / (x2 - x1))
  253. return math.degrees(ang)
  254. else:
  255. ang = math.atan((y1 - y2) / (x2 - x1))
  256. return 180 - math.degrees(ang)
  257. else: # x1 > x2
  258. if y1 < y2:
  259. ang = math.atan((y2 - y1) / (x1 - x2))
  260. return 180 - math.degrees(ang)
  261. else:
  262. ang = math.atan((y1 - y2) / (x1 - x2))
  263. return math.degrees(ang)
  264. def _rotation(point, center, angle):
  265. if angle == 0:
  266. return point
  267. x, y = point[0]
  268. cx, cy = center[0]
  269. radian = math.radians(abs(angle))
  270. if angle > 0: # clockwise
  271. rx = (x - cx) * math.cos(radian) - (y - cy) * math.sin(radian) + cx
  272. ry = (x - cx) * math.sin(radian) + (y - cy) * math.cos(radian) + cy
  273. else:
  274. rx = (x - cx) * math.cos(radian) + (y - cy) * math.sin(radian) + cx
  275. ry = (y - cy) * math.cos(radian) - (x - cx) * math.sin(radian) + cy
  276. return np.array([[rx, ry]])
  277. def _line(p1, p2):
  278. A = (p1[1] - p2[1])
  279. B = (p2[0] - p1[0])
  280. C = (p1[0] * p2[1] - p2[0] * p1[1])
  281. return A, B, -C
  282. def _intersection(L1, L2):
  283. D = L1[0] * L2[1] - L1[1] * L2[0]
  284. Dx = L1[2] * L2[1] - L1[1] * L2[2]
  285. Dy = L1[0] * L2[2] - L1[2] * L2[0]
  286. if D != 0:
  287. x = Dx / D
  288. y = Dy / D
  289. return np.array([[x, y]])
  290. else:
  291. return None
  292. def _calc_distance_between_lines(L1, L2):
  293. eps = 1e-16
  294. A1, _, C1 = L1
  295. A2, B2, C2 = L2
  296. new_C1 = C1 / (A1 + eps)
  297. new_A2 = 1
  298. new_B2 = B2 / (A2 + eps)
  299. new_C2 = C2 / (A2 + eps)
  300. dist = (np.abs(new_C1 - new_C2)) / (
  301. np.sqrt(new_A2 * new_A2 + new_B2 * new_B2) + eps)
  302. return dist
  303. def _calc_project_in_line(point, line_point1, line_point2):
  304. eps = 1e-16
  305. m, n = point
  306. x1, y1 = line_point1
  307. x2, y2 = line_point2
  308. F = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
  309. x = (m * (x2 - x1) * (x2 - x1) + n * (y2 - y1) * (x2 - x1) +
  310. (x1 * y2 - x2 * y1) * (y2 - y1)) / (F + eps)
  311. y = (m * (x2 - x1) * (y2 - y1) + n * (y2 - y1) * (y2 - y1) +
  312. (x2 * y1 - x1 * y2) * (x2 - x1)) / (F + eps)
  313. return np.array([[x, y]])