Reputation: 115
I am trying to segment a web page into a header, footer, left panel, right panel, etc. (get coordinates) using an image processing tool(OpenCV), which is not giving satisfactory results.
I want to get something like this:
But, all I got
import cv2
import numpy
from google.colab.patches import cv2_imshow
img = cv2.imread("test.png")
blue, green, red = cv2.split(img)
def medianCanny(img, thresh1, thresh2):
median = numpy.median(img)
img = cv2.Canny(img, int(thresh1 * median), int(thresh2 * median))
return img
blue_edges = medianCanny(blue, 0, 1)
green_edges = medianCanny(green, 0, 1)
red_edges = medianCanny(red, 0, 1)
edges = blue_edges | green_edges | red_edges
contours,hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL ,cv2.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy[0]
for component in zip(contours, hierarchy):
currentContour = component[0]
currentHierarchy = component[1]
x,y,w,h = cv2.boundingRect(currentContour)
if currentHierarchy[3] < 0:
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),1)
cv2_imshow(img)
cv2.waitKey(0)
cv2.destroyAllWindows()
And I want to connect nearby boxes, but I don't understand how best to do it.
Upvotes: 6
Views: 12914
Reputation: 3143
I started with your code as a base to get the green rectangles. I filtered the boxes by size to get rid of the big ones that contained large chunks of the image (there's even one that goes around the entire image). From there I iteratively merged nearby boxes until there were no more overlapping boxes. I used the merge_margin variable to set how close two boxes needed to be before they counted as "overlapping".
Each Step (I highlighted the last merged box and the points it found inside) (This gif is heavily compressed so you'll see some artifacting)
Final Image
import cv2
import numpy as np
# tuplify
def tup(point):
return (point[0], point[1]);
# returns true if the two boxes overlap
def overlap(source, target):
# unpack points
tl1, br1 = source;
tl2, br2 = target;
# checks
if (tl1[0] >= br2[0] or tl2[0] >= br1[0]):
return False;
if (tl1[1] >= br2[1] or tl2[1] >= br1[1]):
return False;
return True;
# returns all overlapping boxes
def getAllOverlaps(boxes, bounds, index):
overlaps = [];
for a in range(len(boxes)):
if a != index:
if overlap(bounds, boxes[a]):
overlaps.append(a);
return overlaps;
img = cv2.imread("test.png")
orig = np.copy(img);
blue, green, red = cv2.split(img)
def medianCanny(img, thresh1, thresh2):
median = np.median(img)
img = cv2.Canny(img, int(thresh1 * median), int(thresh2 * median))
return img
blue_edges = medianCanny(blue, 0, 1)
green_edges = medianCanny(green, 0, 1)
red_edges = medianCanny(red, 0, 1)
edges = blue_edges | green_edges | red_edges
# I'm using OpenCV 3.4. This returns (contours, hierarchy) in OpenCV 2 and 4
_, contours,hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL ,cv2.CHAIN_APPROX_SIMPLE)
# go through the contours and save the box edges
boxes = []; # each element is [[top-left], [bottom-right]];
hierarchy = hierarchy[0]
for component in zip(contours, hierarchy):
currentContour = component[0]
currentHierarchy = component[1]
x,y,w,h = cv2.boundingRect(currentContour)
if currentHierarchy[3] < 0:
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),1)
boxes.append([[x,y], [x+w, y+h]]);
# filter out excessively large boxes
filtered = [];
max_area = 30000;
for box in boxes:
w = box[1][0] - box[0][0];
h = box[1][1] - box[0][1];
if w*h < max_area:
filtered.append(box);
boxes = filtered;
# go through the boxes and start merging
merge_margin = 20;
# this is gonna take a long time
finished = False;
highlight = [[0,0], [1,1]];
points = [[[0,0]]];
while not finished:
# set end con
finished = True;
# check progress
print("Len Boxes: " + str(len(boxes)));
# draw boxes # comment this section out to run faster
copy = np.copy(orig);
for box in boxes:
cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0,200,0), 1);
cv2.rectangle(copy, tup(highlight[0]), tup(highlight[1]), (0,0,255), 2);
for point in points:
point = point[0];
cv2.circle(copy, tup(point), 4, (255,0,0), -1);
cv2.imshow("Copy", copy);
key = cv2.waitKey(1);
if key == ord('q'):
break;
# loop through boxes
index = 0;
while index < len(boxes):
# grab current box
curr = boxes[index];
# add margin
tl = curr[0][:];
br = curr[1][:];
tl[0] -= merge_margin;
tl[1] -= merge_margin;
br[0] += merge_margin;
br[1] += merge_margin;
# get matching boxes
overlaps = getAllOverlaps(boxes, [tl, br], index);
# check if empty
if len(overlaps) > 0:
# combine boxes
# convert to a contour
con = [];
overlaps.append(index);
for ind in overlaps:
tl, br = boxes[ind];
con.append([tl]);
con.append([br]);
con = np.array(con);
# get bounding rect
x,y,w,h = cv2.boundingRect(con);
# stop growing
w -= 1;
h -= 1;
merged = [[x,y], [x+w, y+h]];
# highlights
highlight = merged[:];
points = con;
# remove boxes from list
overlaps.sort(reverse = True);
for ind in overlaps:
del boxes[ind];
boxes.append(merged);
# set flag
finished = False;
break;
# increment
index += 1;
cv2.destroyAllWindows();
# show final
copy = np.copy(orig);
for box in boxes:
cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0,200,0), 1);
cv2.imshow("Final", copy);
cv2.waitKey(0);
Edit: The inefficiency of this bothered me a bit. The order that the boxes gets merged in doesn't really make sense. You can see that there are a lot of steps where little boxes are merging into a big box, rather than a big box eating everything inside of itself and growing. Turns out this was a really easy code fix. Since the new merged boxes are appended to the end of the boxes list, we can just index in reverse to make it so that we go from big to small.
I changed the merge_margin to 15 since I think that's closer to the target solution in the question.
import cv2
import numpy as np
# tuplify
def tup(point):
return (point[0], point[1]);
# returns true if the two boxes overlap
def overlap(source, target):
# unpack points
tl1, br1 = source;
tl2, br2 = target;
# checks
if (tl1[0] >= br2[0] or tl2[0] >= br1[0]):
return False;
if (tl1[1] >= br2[1] or tl2[1] >= br1[1]):
return False;
return True;
# returns all overlapping boxes
def getAllOverlaps(boxes, bounds, index):
overlaps = [];
for a in range(len(boxes)):
if a != index:
if overlap(bounds, boxes[a]):
overlaps.append(a);
return overlaps;
img = cv2.imread("test.png")
orig = np.copy(img);
blue, green, red = cv2.split(img)
def medianCanny(img, thresh1, thresh2):
median = np.median(img)
img = cv2.Canny(img, int(thresh1 * median), int(thresh2 * median))
return img
blue_edges = medianCanny(blue, 0, 1)
green_edges = medianCanny(green, 0, 1)
red_edges = medianCanny(red, 0, 1)
edges = blue_edges | green_edges | red_edges
# I'm using OpenCV 3.4. This returns (contours, hierarchy) in OpenCV 2 and 4
_, contours,hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL ,cv2.CHAIN_APPROX_SIMPLE)
# go through the contours and save the box edges
boxes = []; # each element is [[top-left], [bottom-right]];
hierarchy = hierarchy[0]
for component in zip(contours, hierarchy):
currentContour = component[0]
currentHierarchy = component[1]
x,y,w,h = cv2.boundingRect(currentContour)
if currentHierarchy[3] < 0:
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),1)
boxes.append([[x,y], [x+w, y+h]]);
# filter out excessively large boxes
filtered = [];
max_area = 30000;
for box in boxes:
w = box[1][0] - box[0][0];
h = box[1][1] - box[0][1];
if w*h < max_area:
filtered.append(box);
boxes = filtered;
# go through the boxes and start merging
merge_margin = 15;
# this is gonna take a long time
finished = False;
highlight = [[0,0], [1,1]];
points = [[[0,0]]];
while not finished:
# set end con
finished = True;
# check progress
print("Len Boxes: " + str(len(boxes)));
# draw boxes # comment this section out to run faster
copy = np.copy(orig);
for box in boxes:
cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0,200,0), 1);
cv2.rectangle(copy, tup(highlight[0]), tup(highlight[1]), (0,0,255), 2);
for point in points:
point = point[0];
cv2.circle(copy, tup(point), 4, (255,0,0), -1);
cv2.imshow("Copy", copy);
key = cv2.waitKey(1);
if key == ord('q'):
break;
# loop through boxes
index = len(boxes) - 1;
while index >= 0:
# grab current box
curr = boxes[index];
# add margin
tl = curr[0][:];
br = curr[1][:];
tl[0] -= merge_margin;
tl[1] -= merge_margin;
br[0] += merge_margin;
br[1] += merge_margin;
# get matching boxes
overlaps = getAllOverlaps(boxes, [tl, br], index);
# check if empty
if len(overlaps) > 0:
# combine boxes
# convert to a contour
con = [];
overlaps.append(index);
for ind in overlaps:
tl, br = boxes[ind];
con.append([tl]);
con.append([br]);
con = np.array(con);
# get bounding rect
x,y,w,h = cv2.boundingRect(con);
# stop growing
w -= 1;
h -= 1;
merged = [[x,y], [x+w, y+h]];
# highlights
highlight = merged[:];
points = con;
# remove boxes from list
overlaps.sort(reverse = True);
for ind in overlaps:
del boxes[ind];
boxes.append(merged);
# set flag
finished = False;
break;
# increment
index -= 1;
cv2.destroyAllWindows();
# show final
copy = np.copy(orig);
for box in boxes:
cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0,200,0), 1);
cv2.imshow("Final", copy);
cv2.waitKey(0);
Upvotes: 24