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