panel_finder.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. from pathlib import Path
  2. import sys
  3. import cv2 as cv
  4. import numpy as np
  5. from PIL import Image
  6. KERNEL_SIZE = 7
  7. BORDER_SIZE = 10
  8. def panel_process_image(img: Image.Image):
  9. """Preprocesses an image to make it easier to find panels.
  10. Args:
  11. img: The image to preprocess.
  12. Returns:
  13. The preprocessed image.
  14. """
  15. img_gray = cv.cvtColor(np.array(img), cv.COLOR_BGR2GRAY)
  16. img_gray = cv.GaussianBlur(img_gray, (KERNEL_SIZE, KERNEL_SIZE), 0)
  17. img_gray = cv.threshold(img_gray, 200, 255, cv.THRESH_BINARY)[1]
  18. # Add black border to image, to help with finding contours
  19. img_gray = cv.copyMakeBorder(
  20. img_gray,
  21. BORDER_SIZE,
  22. BORDER_SIZE,
  23. BORDER_SIZE,
  24. BORDER_SIZE,
  25. cv.BORDER_CONSTANT,
  26. value=255,
  27. )
  28. # Invert image
  29. img_gray = cv.bitwise_not(img_gray)
  30. return img_gray
  31. def remove_contained_contours(polygons):
  32. """Removes polygons from a list if any completely contain the other.
  33. Args:
  34. polygons: A list of polygons.
  35. Returns:
  36. A list of polygons with any contained polygons removed.
  37. """
  38. # Create a new list to store the filtered polygons.
  39. filtered_polygons = []
  40. # Iterate over the polygons.
  41. for polygon in polygons:
  42. # Check if the polygon contains any of the other polygons.
  43. contains = False
  44. for other_polygon in polygons:
  45. # Check if the polygon contains the other polygon and that the polygons
  46. if np.array_equal(other_polygon, polygon):
  47. continue
  48. rect1 = cv.boundingRect(other_polygon)
  49. rect2 = cv.boundingRect(polygon)
  50. # Check if rect2 is completely within rect1
  51. if (
  52. rect2[0] >= rect1[0]
  53. and rect2[1] >= rect1[1]
  54. and rect2[0] + rect2[2] <= rect1[0] + rect1[2]
  55. and rect2[1] + rect2[3] <= rect1[1] + rect1[3]
  56. ):
  57. contains = True
  58. break
  59. # If the polygon does not contain any of the other polygons, add it to the
  60. # filtered list.
  61. if not contains:
  62. filtered_polygons.append(polygon)
  63. return filtered_polygons
  64. def calc_panel_contours(im: Image.Image):
  65. img_gray = panel_process_image(im)
  66. contours_raw, hierarchy = cv.findContours(
  67. img_gray, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE
  68. )
  69. contours = contours_raw
  70. min_area = 10000
  71. contours = [i for i in contours if cv.contourArea(i) > min_area]
  72. contours = [cv.convexHull(i) for i in contours]
  73. contours = remove_contained_contours(contours)
  74. # Remap the contours to the original image
  75. contours = [i + np.array([[-BORDER_SIZE, -BORDER_SIZE]]) for i in contours]
  76. # Sort the contours by their y-coordinate.
  77. contours = order_panels(contours, img_gray)
  78. return contours
  79. def determine_panel_order_from_contours(contours):
  80. """
  81. build a tree of regions that are determined vertically
  82. order by an n like pattern
  83. """
  84. def draw_contours(im, contours):
  85. """Debugging, draws the contours on the image."""
  86. colors = [
  87. (255, 0, 0),
  88. (0, 255, 0),
  89. (0, 0, 255),
  90. ]
  91. im_contour = np.array(im)
  92. for i, contour in enumerate(range(len(contours))):
  93. color = colors[i % len(colors)]
  94. im_contour = cv.drawContours(im_contour, contours, i, color, 4, cv.LINE_AA)
  95. # Draw a number at the top left of contour
  96. x, y, _, _ = cv.boundingRect(contours[i])
  97. cv.putText(
  98. im_contour,
  99. str(i),
  100. (x + 50, y + 50),
  101. cv.FONT_HERSHEY_SIMPLEX,
  102. 1,
  103. color,
  104. 2,
  105. cv.LINE_AA,
  106. )
  107. img = Image.fromarray(im_contour)
  108. return img
  109. def save_draw_contours(pth: Path | str):
  110. if str:
  111. pth = Path(pth)
  112. pth_out = pth.parent / (pth.stem + "-contours")
  113. if not pth_out.exists():
  114. pth_out.mkdir()
  115. # Glob get all images in folder
  116. pths = [i for i in pth.iterdir() if i.suffix in [".png", ".jpg", ".jpeg"]]
  117. for t in pths:
  118. print(t)
  119. im = Image.open(t)
  120. contours = calc_panel_contours(im)
  121. img_panels = draw_contours(im, contours)
  122. f_name = t.stem + t.suffix
  123. img_panels.save(pth_out / f_name)
  124. def order_panels(contours, img_gray):
  125. """Orders the panels in a comic book page.
  126. Args:
  127. contours: A list of contours, where each contour is a list of points.
  128. Returns:
  129. A list of contours, where each contour is a list of points, ordered by
  130. their vertical position.
  131. """
  132. # Get the bounding boxes for each contour.
  133. bounding_boxes = [cv.boundingRect(contour) for contour in contours]
  134. # Generate groups of vertically overlapping bounding boxes.
  135. groups_indices = generate_vertical_bounding_box_groups_indices(bounding_boxes)
  136. c = []
  137. for group in groups_indices:
  138. # Reorder contours based on reverse z-order,
  139. cs = [bounding_boxes[i] for i in group]
  140. ymax, xmax = img_gray.shape
  141. order_scores = [1 * (ymax - i[1]) + i[0] * 1 for i in cs]
  142. # Sort the list based on the location score value
  143. combined_list = list(zip(group, order_scores))
  144. sorted_list = sorted(combined_list, key=lambda x: x[1], reverse=True)
  145. c.extend(sorted_list)
  146. ordered_contours = [contours[i[0]] for i in c]
  147. return ordered_contours
  148. def generate_vertical_bounding_box_groups_indices(bounding_boxes):
  149. """Generates groups of vertically overlapping bounding boxes.
  150. Args:
  151. bounding_boxes: A list of bounding boxes, where each bounding box is a tuple
  152. of (x, y, width, height).
  153. Returns:
  154. A list of groups, where each group is a list of bounding boxes that overlap
  155. vertically.
  156. """
  157. # Operate on indices Sort the bounding boxes by their y-coordinate.
  158. bbox_inds = np.argsort([i[1] for i in bounding_boxes])
  159. # generate groups of vertically overlapping bounding boxes
  160. groups = [[bbox_inds[0]]]
  161. for i in bbox_inds[1:]:
  162. is_old_group = False
  163. bbox = bounding_boxes[i]
  164. start1 = bbox[1]
  165. end1 = bbox[1] + bbox[3]
  166. for n, group in enumerate(groups):
  167. for ind in group:
  168. _bbox = bounding_boxes[ind]
  169. start2 = _bbox[1]
  170. end2 = _bbox[1] + _bbox[3]
  171. # Check for any partial overlapping
  172. if check_overlap((start1, end1), (start2, end2)):
  173. groups[n] = group + [i]
  174. is_old_group = True
  175. break
  176. if is_old_group:
  177. break
  178. else:
  179. groups.append([i])
  180. return groups
  181. def check_overlap(range1, range2):
  182. # Check if range1 is before range2
  183. if range1[1] < range2[0]:
  184. return False
  185. # Check if range1 is after range2
  186. elif range1[0] > range2[1]:
  187. return False
  188. # If neither of the above conditions are met, the ranges must overlap
  189. else:
  190. return True
  191. if __name__ == "__main__":
  192. save_draw_contours(sys.argv[1])