utils.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Thu Sep 9 23:11:51 2020
  5. utils
  6. @author: chineseocr
  7. """
  8. import cv2
  9. import numpy as np
  10. from skimage import measure
  11. from scipy.spatial import distance as dist
  12. from PIL import Image
  13. from scipy.ndimage import filters, interpolation
  14. from numpy import amin, amax
  15. def nms_box(boxes, scores, score_threshold=0.5, nms_threshold=0.3):
  16. # nms box
  17. boxes = np.array(boxes)
  18. scores = np.array(scores)
  19. ind = scores > score_threshold
  20. boxes = boxes[ind]
  21. scores = scores[ind]
  22. def box_to_center(box):
  23. xmin, ymin, xmax, ymax = [round(float(x), 4) for x in box]
  24. w = xmax - xmin
  25. h = ymax - ymin
  26. return [round(xmin, 4), round(ymin, 4), round(w, 4), round(h, 4)]
  27. newBoxes = [box_to_center(box) for box in boxes]
  28. newscores = [round(float(x), 6) for x in scores]
  29. index = cv2.dnn.NMSBoxes(newBoxes, newscores, score_threshold=score_threshold, nms_threshold=nms_threshold)
  30. if len(index) > 0:
  31. index = index.reshape((-1,))
  32. return boxes[index], scores[index]
  33. else:
  34. return np.array([]), np.array([])
  35. def resize_im(im, scale, max_scale=None):
  36. f = float(scale) / min(im.shape[0], im.shape[1])
  37. if max_scale is not None and f * max(im.shape[0], im.shape[1]) > max_scale:
  38. f = float(max_scale) / max(im.shape[0], im.shape[1])
  39. return cv2.resize(im, (0, 0), fx=f, fy=f)
  40. def estimate_skew_angle(raw, angleRange=[-15, 15]):
  41. """
  42. 估计图像文字偏转角度,
  43. angleRange:角度估计区间
  44. """
  45. raw = resize_im(raw, scale=600, max_scale=900)
  46. image = raw - amin(raw)
  47. image = image / amax(image)
  48. m = interpolation.zoom(image, 0.5)
  49. m = filters.percentile_filter(m, 80, size=(20, 2))
  50. m = filters.percentile_filter(m, 80, size=(2, 20))
  51. m = interpolation.zoom(m, 1.0 / 0.5)
  52. # w,h = image.shape[1],image.shape[0]
  53. w, h = min(image.shape[1], m.shape[1]), min(image.shape[0], m.shape[0])
  54. flat = np.clip(image[:h, :w] - m[:h, :w] + 1, 0, 1)
  55. d0, d1 = flat.shape
  56. o0, o1 = int(0.1 * d0), int(0.1 * d1)
  57. flat = amax(flat) - flat
  58. flat -= amin(flat)
  59. est = flat[o0:d0 - o0, o1:d1 - o1]
  60. angles = range(angleRange[0], angleRange[1])
  61. estimates = []
  62. for a in angles:
  63. roest = interpolation.rotate(est, a, order=0, mode='constant')
  64. v = np.mean(roest, axis=1)
  65. v = np.var(v)
  66. estimates.append((v, a))
  67. _, a = max(estimates)
  68. return a
  69. def eval_angle(img, angleRange=[-5, 5]):
  70. """
  71. 估计图片文字的偏移角度
  72. """
  73. im = Image.fromarray(img)
  74. degree = estimate_skew_angle(np.array(im.convert('L')), angleRange=angleRange)
  75. im = im.rotate(degree, center=(im.size[0] / 2, im.size[1] / 2), expand=1, fillcolor=(255, 255, 255))
  76. img = np.array(im)
  77. return img, degree
  78. def letterbox_image(image, size, fillValue=[128, 128, 128]):
  79. """
  80. resize image with unchanged aspect ratio using padding
  81. """
  82. image_h, image_w = image.shape[:2]
  83. w, h = size
  84. new_w = int(image_w * min(w * 1.0 / image_w, h * 1.0 / image_h))
  85. new_h = int(image_h * min(w * 1.0 / image_w, h * 1.0 / image_h))
  86. resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
  87. # cv2.imwrite('tmp/test.png', resized_image[...,::-1])
  88. if fillValue is None:
  89. # cv2.split 通道拆分
  90. fillValue = [int(x.mean()) for x in cv2.split(np.array(image))]
  91. boxed_image = np.zeros((size[1], size[0], 3), dtype=np.uint8)
  92. boxed_image[:] = fillValue
  93. boxed_image[:new_h, :new_w, :] = resized_image
  94. return boxed_image, new_w / image_w, new_h / image_h
  95. def get_table_line(binimg, axis=0, lineW=10):
  96. # 获取表格线
  97. # axis=0 竖线
  98. # axis=1 横线
  99. labels = measure.label(binimg > 0, connectivity=2) # 8连通区域标记
  100. regions = measure.regionprops(labels)
  101. if axis == 1:
  102. # 如果是横线,判断线的bbox的右下角点的x-左上角点的x,判断横线长度
  103. lineboxes = [minAreaRect(line.coords) for line in regions if line.bbox[2] - line.bbox[0] > lineW]
  104. else:
  105. # 如果是竖线,判断线的bbox的右下角点的y-左上角点的y,判断竖线长度
  106. lineboxes = [minAreaRect(line.coords) for line in regions if line.bbox[3] - line.bbox[1] > lineW]
  107. return lineboxes
  108. def sqrt(p1, p2):
  109. return np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
  110. def adjust_lines(RowsLines, ColsLines, alph=50):
  111. # 调整line
  112. nrow = len(RowsLines)
  113. ncol = len(ColsLines)
  114. newRowsLines = []
  115. newColsLines = []
  116. for i in range(nrow):
  117. x1, y1, x2, y2 = RowsLines[i]
  118. cx1, cy1 = (x1 + x2) / 2, (y1 + y2) / 2
  119. for j in range(nrow):
  120. if i != j:
  121. x3, y3, x4, y4 = RowsLines[j]
  122. cx2, cy2 = (x3 + x4) / 2, (y3 + y4) / 2
  123. if (x3 < cx1 < x4 or y3 < cy1 < y4) or (x1 < cx2 < x2 or y1 < cy2 < y2):
  124. continue
  125. else:
  126. r = sqrt((x1, y1), (x3, y3))
  127. if r < alph:
  128. newRowsLines.append([x1, y1, x3, y3])
  129. r = sqrt((x1, y1), (x4, y4))
  130. if r < alph:
  131. newRowsLines.append([x1, y1, x4, y4])
  132. r = sqrt((x2, y2), (x3, y3))
  133. if r < alph:
  134. newRowsLines.append([x2, y2, x3, y3])
  135. r = sqrt((x2, y2), (x4, y4))
  136. if r < alph:
  137. newRowsLines.append([x2, y2, x4, y4])
  138. for i in range(ncol):
  139. x1, y1, x2, y2 = ColsLines[i]
  140. cx1, cy1 = (x1 + x2) / 2, (y1 + y2) / 2
  141. for j in range(ncol):
  142. if i != j:
  143. x3, y3, x4, y4 = ColsLines[j]
  144. cx2, cy2 = (x3 + x4) / 2, (y3 + y4) / 2
  145. if (x3 < cx1 < x4 or y3 < cy1 < y4) or (x1 < cx2 < x2 or y1 < cy2 < y2):
  146. continue
  147. else:
  148. r = sqrt((x1, y1), (x3, y3))
  149. if r < alph:
  150. newColsLines.append([x1, y1, x3, y3])
  151. r = sqrt((x1, y1), (x4, y4))
  152. if r < alph:
  153. newColsLines.append([x1, y1, x4, y4])
  154. r = sqrt((x2, y2), (x3, y3))
  155. if r < alph:
  156. newColsLines.append([x2, y2, x3, y3])
  157. r = sqrt((x2, y2), (x4, y4))
  158. if r < alph:
  159. newColsLines.append([x2, y2, x4, y4])
  160. return newRowsLines, newColsLines
  161. def minAreaRect(coords):
  162. """
  163. 多边形外接矩形
  164. """
  165. rect = cv2.minAreaRect(coords[:, ::-1])
  166. # print("minAreaRect rect", rect)
  167. box = cv2.boxPoints(rect)
  168. # print("minAreaRect box", box)
  169. box = box.reshape((8,)).tolist()
  170. # print("minAreaRect box2", box)
  171. box = image_location_sort_box(box)
  172. x1, y1, x2, y2, x3, y3, x4, y4 = box
  173. degree, w, h, cx, cy = solve(box)
  174. if w < h:
  175. xmin = (x1 + x2) / 2
  176. xmax = (x3 + x4) / 2
  177. ymin = (y1 + y2) / 2
  178. ymax = (y3 + y4) / 2
  179. else:
  180. xmin = (x1 + x4) / 2
  181. xmax = (x2 + x3) / 2
  182. ymin = (y1 + y4) / 2
  183. ymax = (y2 + y3) / 2
  184. # degree,w,h,cx,cy = solve(box)
  185. # x1,y1,x2,y2,x3,y3,x4,y4 = box
  186. # return {'degree':degree,'w':w,'h':h,'cx':cx,'cy':cy}
  187. return [xmin, ymin, xmax, ymax]
  188. def fit_line(p1, p2):
  189. """A = Y2 - Y1
  190. B = X1 - X2
  191. C = X2*Y1 - X1*Y2
  192. AX+BY+C=0
  193. 直线一般方程
  194. """
  195. x1, y1 = p1
  196. x2, y2 = p2
  197. A = y2 - y1
  198. B = x1 - x2
  199. C = x2 * y1 - x1 * y2
  200. return A, B, C
  201. def point_line_cor(p, A, B, C):
  202. # 判断点与之间的位置关系
  203. # 一般式直线方程(Ax+By+c)=0
  204. x, y = p
  205. r = A * x + B * y + C
  206. return r
  207. def line_to_line(points1, points2, alpha=10):
  208. """
  209. 线段之间的距离
  210. """
  211. x1, y1, x2, y2 = points1
  212. ox1, oy1, ox2, oy2 = points2
  213. A1, B1, C1 = fit_line((x1, y1), (x2, y2))
  214. A2, B2, C2 = fit_line((ox1, oy1), (ox2, oy2))
  215. flag1 = point_line_cor([x1, y1], A2, B2, C2)
  216. flag2 = point_line_cor([x2, y2], A2, B2, C2)
  217. if (flag1 > 0 and flag2 > 0) or (flag1 < 0 and flag2 < 0):
  218. x = (B1 * C2 - B2 * C1) / (A1 * B2 - A2 * B1)
  219. y = (A2 * C1 - A1 * C2) / (A1 * B2 - A2 * B1)
  220. p = (x, y)
  221. r0 = sqrt(p, (x1, y1))
  222. r1 = sqrt(p, (x2, y2))
  223. if min(r0, r1) < alpha:
  224. if r0 < r1:
  225. points1 = [p[0], p[1], x2, y2]
  226. else:
  227. points1 = [x1, y1, p[0], p[1]]
  228. return points1
  229. def _order_points(pts):
  230. # 根据x坐标对点进行排序
  231. x_sorted = pts[np.argsort(pts[:, 0]), :]
  232. left_most = x_sorted[:2, :]
  233. right_most = x_sorted[2:, :]
  234. left_most = left_most[np.argsort(left_most[:, 1]), :]
  235. (tl, bl) = left_most
  236. distance = dist.cdist(tl[np.newaxis], right_most, "euclidean")[0]
  237. (br, tr) = right_most[np.argsort(distance)[::-1], :]
  238. return np.array([tl, tr, br, bl], dtype="float32")
  239. def image_location_sort_box(box):
  240. x1, y1, x2, y2, x3, y3, x4, y4 = box[:8]
  241. pts = (x1, y1), (x2, y2), (x3, y3), (x4, y4)
  242. pts = np.array(pts, dtype="float32")
  243. (x1, y1), (x2, y2), (x3, y3), (x4, y4) = _order_points(pts)
  244. return [x1, y1, x2, y2, x3, y3, x4, y4]
  245. def solve(box):
  246. """
  247. 绕 cx,cy点 w,h 旋转 angle 的坐标
  248. x = cx-w/2
  249. y = cy-h/2
  250. x1-cx = -w/2*cos(angle) +h/2*sin(angle)
  251. y1 -cy= -w/2*sin(angle) -h/2*cos(angle)
  252. h(x1-cx) = -wh/2*cos(angle) +hh/2*sin(angle)
  253. w(y1 -cy)= -ww/2*sin(angle) -hw/2*cos(angle)
  254. (hh+ww)/2sin(angle) = h(x1-cx)-w(y1 -cy)
  255. """
  256. x1, y1, x2, y2, x3, y3, x4, y4 = box[:8]
  257. cx = (x1 + x3 + x2 + x4) / 4.0
  258. cy = (y1 + y3 + y4 + y2) / 4.0
  259. w = (np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + np.sqrt((x3 - x4) ** 2 + (y3 - y4) ** 2)) / 2
  260. h = (np.sqrt((x2 - x3) ** 2 + (y2 - y3) ** 2) + np.sqrt((x1 - x4) ** 2 + (y1 - y4) ** 2)) / 2
  261. # x = cx-w/2
  262. # y = cy-h/2
  263. sinA = (h * (x1 - cx) - w * (y1 - cy)) * 1.0 / (h * h + w * w) * 2
  264. angle = np.arcsin(sinA)
  265. return angle, w, h, cx, cy
  266. def xy_rotate_box(cx, cy, w, h, angle=0, degree=None, **args):
  267. """
  268. 绕 cx,cy点 w,h 旋转 angle 的坐标
  269. x_new = (x-cx)*cos(angle) - (y-cy)*sin(angle)+cx
  270. y_new = (x-cx)*sin(angle) + (y-cy)*sin(angle)+cy
  271. """
  272. if degree is not None:
  273. angle = degree
  274. cx = float(cx)
  275. cy = float(cy)
  276. w = float(w)
  277. h = float(h)
  278. angle = float(angle)
  279. x1, y1 = rotate(cx - w / 2, cy - h / 2, angle, cx, cy)
  280. x2, y2 = rotate(cx + w / 2, cy - h / 2, angle, cx, cy)
  281. x3, y3 = rotate(cx + w / 2, cy + h / 2, angle, cx, cy)
  282. x4, y4 = rotate(cx - w / 2, cy + h / 2, angle, cx, cy)
  283. return x1, y1, x2, y2, x3, y3, x4, y4
  284. from numpy import cos, sin
  285. def rotate(x, y, angle, cx, cy):
  286. angle = angle # *pi/180
  287. x_new = (x - cx) * cos(angle) - (y - cy) * sin(angle) + cx
  288. y_new = (x - cx) * sin(angle) + (y - cy) * cos(angle) + cy
  289. return x_new, y_new
  290. def minAreaRectbox(regions, flag=True, W=0, H=0, filtersmall=False, adjustBox=False):
  291. """
  292. 多边形外接矩形
  293. """
  294. boxes = []
  295. for region in regions:
  296. rect = cv2.minAreaRect(region.coords[:, ::-1])
  297. box = cv2.boxPoints(rect)
  298. box = box.reshape((8,)).tolist()
  299. box = image_location_sort_box(box)
  300. x1, y1, x2, y2, x3, y3, x4, y4 = box
  301. angle, w, h, cx, cy = solve(box)
  302. if adjustBox:
  303. x1, y1, x2, y2, x3, y3, x4, y4 = xy_rotate_box(cx, cy, w + 5, h + 5, angle=0, degree=None)
  304. if w > 32 and h > 32 and flag:
  305. if abs(angle / np.pi * 180) < 20:
  306. if filtersmall and w < 10 or h < 10:
  307. continue
  308. boxes.append([x1, y1, x2, y2, x3, y3, x4, y4])
  309. else:
  310. if w * h < 0.5 * W * H:
  311. if filtersmall and w < 8 or h < 8:
  312. continue
  313. boxes.append([x1, y1, x2, y2, x3, y3, x4, y4])
  314. return boxes
  315. def rectangle(img, boxes):
  316. tmp = np.copy(img)
  317. for box in boxes:
  318. xmin, ymin, xmax, ymax = box[:4]
  319. cv2.rectangle(tmp, (int(xmin), int(ymin)), (int(xmax), int(ymax)), (0, 0, 0), 1, lineType=cv2.LINE_AA)
  320. return Image.fromarray(tmp)
  321. def draw_lines(im, bboxes, color=(0, 0, 0), lineW=3):
  322. """
  323. boxes: bounding boxes
  324. """
  325. tmp = np.copy(im)
  326. c = color
  327. h, w = im.shape[:2]
  328. for box in bboxes:
  329. x1, y1, x2, y2 = box[:4]
  330. cv2.line(tmp, (int(x1), int(y1)), (int(x2), int(y2)), c, lineW, lineType=cv2.LINE_AA)
  331. return tmp
  332. def draw_boxes(im, bboxes, color=(0, 0, 255)):
  333. """
  334. boxes: bounding boxes
  335. """
  336. tmp = np.copy(im)
  337. c = color
  338. h, w, _ = im.shape
  339. for box in bboxes:
  340. if type(box) is dict:
  341. x1, y1, x2, y2, x3, y3, x4, y4 = xy_rotate_box(**box)
  342. else:
  343. x1, y1, x2, y2, x3, y3, x4, y4 = box[:8]
  344. cv2.line(tmp, (int(x1), int(y1)), (int(x2), int(y2)), c, 1, lineType=cv2.LINE_AA)
  345. cv2.line(tmp, (int(x2), int(y2)), (int(x3), int(y3)), c, 1, lineType=cv2.LINE_AA)
  346. cv2.line(tmp, (int(x3), int(y3)), (int(x4), int(y4)), c, 1, lineType=cv2.LINE_AA)
  347. cv2.line(tmp, (int(x4), int(y4)), (int(x1), int(y1)), c, 1, lineType=cv2.LINE_AA)
  348. return tmp