regularization.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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 prepro_mask, calc_distance
  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.
  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 = prepro_mask(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 = cv2.morphologyEx(result, cv2.MORPH_OPEN,
  60. cv2.getStructuringElement(cv2.MORPH_RECT,
  61. (3, 3))) # open
  62. return result
  63. def _coarse(contour, img_shape):
  64. def _inline_check(point, shape, eps=5):
  65. x, y = point[0]
  66. iH, iW = shape
  67. if x < eps or x > iH - eps or y < eps or y > iW - eps:
  68. return False
  69. else:
  70. return True
  71. area = cv2.contourArea(contour)
  72. # S = 20
  73. if area < S: # remove polygons whose area is below a threshold S
  74. return None
  75. # D = 0.3 if area < 200 else 1.0
  76. # TD = 0.5 if area < 200 else 0.9
  77. epsilon = 0.005 * cv2.arcLength(contour, True)
  78. contour = cv2.approxPolyDP(contour, epsilon, True) # DP
  79. p_number = contour.shape[0]
  80. idx = 0
  81. while idx < p_number:
  82. last_point = contour[idx - 1]
  83. current_point = contour[idx]
  84. next_idx = (idx + 1) % p_number
  85. next_point = contour[next_idx]
  86. # remove edges whose lengths are below a given side length TD
  87. # that varies with the area of a building.
  88. distance = calc_distance(current_point, next_point)
  89. if distance < TD and not _inline_check(next_point, img_shape):
  90. contour = np.delete(contour, next_idx, axis=0)
  91. p_number -= 1
  92. continue
  93. # remove over-sharp angles with threshold α.
  94. # remove over-smooth angles with threshold β.
  95. angle = _calc_angle(last_point, current_point, next_point)
  96. if (ALPHA > angle or angle > BETA) and _inline_check(current_point,
  97. img_shape):
  98. contour = np.delete(contour, idx, axis=0)
  99. p_number -= 1
  100. continue
  101. idx += 1
  102. if p_number > 2:
  103. return contour
  104. else:
  105. return None
  106. def _fine(contour, W):
  107. # area = cv2.contourArea(contour)
  108. # W = 6 if area < 200 else 8
  109. # TD = 0.5 if area < 200 else 0.9
  110. # D = TD + 0.3
  111. nW = W
  112. p_number = contour.shape[0]
  113. distance_list = []
  114. azimuth_list = []
  115. indexs_list = []
  116. for idx in range(p_number):
  117. current_point = contour[idx]
  118. next_idx = (idx + 1) % p_number
  119. next_point = contour[next_idx]
  120. distance_list.append(calc_distance(current_point, next_point))
  121. azimuth_list.append(_calc_azimuth(current_point, next_point))
  122. indexs_list.append((idx, next_idx))
  123. # add the direction of the longest edge to the list of main direction.
  124. longest_distance_idx = np.argmax(distance_list)
  125. main_direction_list = [azimuth_list[longest_distance_idx]]
  126. max_dis = distance_list[longest_distance_idx]
  127. if max_dis <= nW:
  128. nW = max_dis - 1e-6
  129. # Add other edges’ direction to the list of main directions
  130. # according to the angle threshold δ between their directions
  131. # and directions in the list.
  132. for distance, azimuth in zip(distance_list, azimuth_list):
  133. for mdir in main_direction_list:
  134. abs_dif_ang = abs(mdir - azimuth)
  135. if distance > nW and THETA <= abs_dif_ang <= (180 - THETA):
  136. main_direction_list.append(azimuth)
  137. contour_by_lines = []
  138. md_used_list = [main_direction_list[0]]
  139. for distance, azimuth, (idx, next_idx) in zip(distance_list, azimuth_list,
  140. indexs_list):
  141. p1 = contour[idx]
  142. p2 = contour[next_idx]
  143. pm = (p1 + p2) / 2
  144. # find long edges with threshold W that varies with building’s area.
  145. if distance > nW:
  146. rotate_ang = main_direction_list[0] - azimuth
  147. for main_direction in main_direction_list:
  148. r_ang = main_direction - azimuth
  149. if abs(r_ang) < abs(rotate_ang):
  150. rotate_ang = r_ang
  151. md_used_list.append(main_direction)
  152. abs_rotate_ang = abs(rotate_ang)
  153. # adjust long edges according to the list and angles.
  154. if abs_rotate_ang < DELTA or abs_rotate_ang > (180 - DELTA):
  155. rp1 = _rotation(p1, pm, rotate_ang)
  156. rp2 = _rotation(p2, pm, rotate_ang)
  157. elif (90 - DELTA) < abs_rotate_ang < (90 + DELTA):
  158. rp1 = _rotation(p1, pm, rotate_ang - 90)
  159. rp2 = _rotation(p2, pm, rotate_ang - 90)
  160. else:
  161. rp1, rp2 = p1, p2
  162. # adjust short edges (judged by a threshold θ) according to the list and angles.
  163. else:
  164. rotate_ang = md_used_list[-1] - azimuth
  165. abs_rotate_ang = abs(rotate_ang)
  166. if abs_rotate_ang < THETA or abs_rotate_ang > (180 - THETA):
  167. rp1 = _rotation(p1, pm, rotate_ang)
  168. rp2 = _rotation(p2, pm, rotate_ang)
  169. else:
  170. rp1 = _rotation(p1, pm, rotate_ang - 90)
  171. rp2 = _rotation(p2, pm, rotate_ang - 90)
  172. # contour_by_lines.extend([rp1, rp2])
  173. contour_by_lines.append([rp1[0], rp2[0]])
  174. correct_points = np.array(contour_by_lines)
  175. # merge (or connect) parallel lines if the distance between
  176. # two lines is less than (or larger than) a threshold D.
  177. final_points = []
  178. final_points.append(correct_points[0][0].reshape([1, 2]))
  179. lp_number = correct_points.shape[0] - 1
  180. for idx in range(lp_number):
  181. next_idx = (idx + 1) if idx < lp_number else 0
  182. cur_edge_p1 = correct_points[idx][0]
  183. cur_edge_p2 = correct_points[idx][1]
  184. next_edge_p1 = correct_points[next_idx][0]
  185. next_edge_p2 = correct_points[next_idx][1]
  186. L1 = _line(cur_edge_p1, cur_edge_p2)
  187. L2 = _line(next_edge_p1, next_edge_p2)
  188. A1 = _calc_azimuth([cur_edge_p1], [cur_edge_p2])
  189. A2 = _calc_azimuth([next_edge_p1], [next_edge_p2])
  190. dif_azi = abs(A1 - A2)
  191. # find intersection point if not parallel
  192. if (90 - DELTA) < dif_azi < (90 + DELTA):
  193. point_intersection = _intersection(L1, L2)
  194. if point_intersection is not None:
  195. final_points.append(point_intersection)
  196. # move or add lines when parallel
  197. elif dif_azi < 1e-6:
  198. marg = _calc_distance_between_lines(L1, L2)
  199. if marg < D:
  200. # move
  201. point_move = _calc_project_in_line(next_edge_p1, cur_edge_p1,
  202. cur_edge_p2)
  203. final_points.append(point_move)
  204. # update next
  205. correct_points[next_idx][0] = point_move
  206. correct_points[next_idx][1] = _calc_project_in_line(
  207. next_edge_p2, cur_edge_p1, cur_edge_p2)
  208. else:
  209. # add line
  210. add_mid_point = (cur_edge_p2 + next_edge_p1) / 2
  211. rp1 = _calc_project_in_line(add_mid_point, cur_edge_p1,
  212. cur_edge_p2)
  213. rp2 = _calc_project_in_line(add_mid_point, next_edge_p1,
  214. next_edge_p2)
  215. final_points.extend([rp1, rp2])
  216. else:
  217. final_points.extend(
  218. [cur_edge_p1[np.newaxis, :], cur_edge_p2[np.newaxis, :]])
  219. final_points = np.array(final_points)
  220. return final_points
  221. def _get_priority(hierarchy):
  222. if hierarchy[3] < 0:
  223. return 1
  224. if hierarchy[2] < 0:
  225. return 2
  226. return 3
  227. def _fill(img, coarse_conts):
  228. result = np.zeros_like(img)
  229. sorted(coarse_conts, key=lambda x: x[1])
  230. for contour, priority in coarse_conts:
  231. if priority == 2:
  232. cv2.fillPoly(result, [contour.astype(np.int32)], (0, 0, 0))
  233. else:
  234. cv2.fillPoly(result, [contour.astype(np.int32)], (255, 255, 255))
  235. return result
  236. def _calc_angle(p1, vertex, p2):
  237. x1, y1 = p1[0]
  238. xv, yv = vertex[0]
  239. x2, y2 = p2[0]
  240. a = ((xv - x2) * (xv - x2) + (yv - y2) * (yv - y2))**0.5
  241. b = ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))**0.5
  242. c = ((x1 - xv) * (x1 - xv) + (y1 - yv) * (y1 - yv))**0.5
  243. return math.degrees(math.acos((b**2 - a**2 - c**2) / (-2 * a * c)))
  244. def _calc_azimuth(p1, p2):
  245. x1, y1 = p1[0]
  246. x2, y2 = p2[0]
  247. if y1 == y2:
  248. return 0.0
  249. if x1 == x2:
  250. return 90.0
  251. elif x1 < x2:
  252. if y1 < y2:
  253. ang = math.atan((y2 - y1) / (x2 - x1))
  254. return math.degrees(ang)
  255. else:
  256. ang = math.atan((y1 - y2) / (x2 - x1))
  257. return 180 - math.degrees(ang)
  258. else: # x1 > x2
  259. if y1 < y2:
  260. ang = math.atan((y2 - y1) / (x1 - x2))
  261. return 180 - math.degrees(ang)
  262. else:
  263. ang = math.atan((y1 - y2) / (x1 - x2))
  264. return math.degrees(ang)
  265. def _rotation(point, center, angle):
  266. if angle == 0:
  267. return point
  268. x, y = point[0]
  269. cx, cy = center[0]
  270. radian = math.radians(abs(angle))
  271. if angle > 0: # clockwise
  272. rx = (x - cx) * math.cos(radian) - (y - cy) * math.sin(radian) + cx
  273. ry = (x - cx) * math.sin(radian) + (y - cy) * math.cos(radian) + cy
  274. else:
  275. rx = (x - cx) * math.cos(radian) + (y - cy) * math.sin(radian) + cx
  276. ry = (y - cy) * math.cos(radian) - (x - cx) * math.sin(radian) + cy
  277. return np.array([[rx, ry]])
  278. def _line(p1, p2):
  279. A = (p1[1] - p2[1])
  280. B = (p2[0] - p1[0])
  281. C = (p1[0] * p2[1] - p2[0] * p1[1])
  282. return A, B, -C
  283. def _intersection(L1, L2):
  284. D = L1[0] * L2[1] - L1[1] * L2[0]
  285. Dx = L1[2] * L2[1] - L1[1] * L2[2]
  286. Dy = L1[0] * L2[2] - L1[2] * L2[0]
  287. if D != 0:
  288. x = Dx / D
  289. y = Dy / D
  290. return np.array([[x, y]])
  291. else:
  292. return None
  293. def _calc_distance_between_lines(L1, L2):
  294. eps = 1e-16
  295. A1, _, C1 = L1
  296. A2, B2, C2 = L2
  297. new_C1 = C1 / (A1 + eps)
  298. new_A2 = 1
  299. new_B2 = B2 / (A2 + eps)
  300. new_C2 = C2 / (A2 + eps)
  301. dist = (np.abs(new_C1 - new_C2)) / (
  302. np.sqrt(new_A2 * new_A2 + new_B2 * new_B2) + eps)
  303. return dist
  304. def _calc_project_in_line(point, line_point1, line_point2):
  305. eps = 1e-16
  306. m, n = point
  307. x1, y1 = line_point1
  308. x2, y2 = line_point2
  309. F = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
  310. x = (m * (x2 - x1) * (x2 - x1) + n * (y2 - y1) * (x2 - x1) +
  311. (x1 * y2 - x2 * y1) * (y2 - y1)) / (F + eps)
  312. y = (m * (x2 - x1) * (y2 - y1) + n * (y2 - y1) * (y2 - y1) +
  313. (x2 * y1 - x1 * y2) * (x2 - x1)) / (F + eps)
  314. return np.array([[x, y]])