Steven Lee Steven Lee - 2 months ago 77
Python Question

Python opencv sorting contours

I am following this question:

How can I sort contours from left to right and top to bottom?

to sort contours from left-to-right and top-to-bottom. However, my contours are found using this (OpenCV 3):

im2, contours, hierarchy = cv2.findContours(threshold,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)


and they are formatted like this:

array([[[ 1, 1]],

[[ 1, 36]],

[[63, 36]],

[[64, 35]],

[[88, 35]],

[[89, 34]],

[[94, 34]],

[[94, 1]]], dtype=int32)]


When I run the code

max_width = max(contours, key=lambda r: r[0] + r[2])[0]
max_height = max(contours, key=lambda r: r[3])[3]
nearest = max_height * 1.4
contours.sort(key=lambda r: (int(nearest * round(float(r[1])/nearest)) * max_width + r[0]))


I am getting the error

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()


so I changed it to this:

max_width = max(contours, key=lambda r: np.max(r[0] + r[2]))[0]
max_height = max(contours, key=lambda r: np.max(r[3]))[3]
nearest = max_height * 1.4
contours.sort(key=lambda r: (int(nearest * round(float(r[1])/nearest)) * max_width + r[0]))


but now I am getting the error:

TypeError: only length-1 arrays can be converted to Python scalars


EDIT:

After reading the answer below I modified my code:

EDIT 2

This is the code that I use to "dilate" the characters and find the contours

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(35,35))

# dilate the image to get text
# binaryContour is just the black and white image shown below
dilation = cv2.dilate(binaryContour,kernel,iterations = 2)


END OF EDIT 2

im2, contours, hierarchy = cv2.findContours(dilation,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

myContours = []

# Process the raw contours to get bounding rectangles
for cnt in reversed(contours):

epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)

if len(approx == 4):

rectangle = cv2.boundingRect(cnt)
myContours.append(rectangle)

max_width = max(myContours, key=lambda r: r[0] + r[2])[0]
max_height = max(myContours, key=lambda r: r[3])[3]
nearest = max_height * 1.4
myContours.sort(key=lambda r: (int(nearest * round(float(r[1])/nearest)) * max_width + r[0]))

i=0
for x,y,w,h in myContours:

letter = binaryContour[y:y+h, x:x+w]
cv2.rectangle(binaryContour,(x,y),(x+w,y+h),(255,255,255),2)
cv2.imwrite("pictures/"+str(i)+'.png', letter) # save contour to file
i+=1


Contours before sorting:

[(1, 1, 94, 36), (460, 223, 914, 427), (888, 722, 739, 239), (35,723, 522, 228),
(889, 1027, 242, 417), (70, 1028, 693, 423), (1138, 1028, 567, 643),
(781, 1030, 98, 413), (497, 1527, 303, 132), (892, 1527, 168, 130),
(37, 1719, 592, 130), (676, 1721, 413, 129), (1181, 1723, 206, 128),
(30, 1925, 997, 236), (1038, 1929, 170, 129), (140, 2232, 1285, 436)]


Contours after sorting:

(NOTE: This is not the order I want the contours to be sorted in. Refer to image at the bottom)

[(1, 1, 94, 36), (460, 223, 914, 427), (35, 723, 522, 228), (70,1028, 693, 423),
(781, 1030, 98, 413), (888, 722, 739, 239), (889, 1027, 242, 417),
(1138, 1028, 567, 643), (30, 1925, 997, 236), (37, 1719, 592, 130),
(140, 2232, 1285, 436), (497, 1527, 303, 132), (676, 1721, 413, 129),
(892, 1527, 168, 130), (1038, 1929, 170, 129), (1181, 1723, 206, 128)]


Image I am working with

enter image description here

I want to find the contours in the following order:
enter image description here

Dilation image used for finding contours
enter image description here

Answer

Apart from applying a rank you should also preprocess your image a little bit more. I tried using the ranking algorithm above only on contour 1 and found that the contours were not distinct enough. Try below code:

inputImage = cv2.imread("subImage.png",0)

# preprocss image for more accurate results

# smooth out the rough edges from the thresholding 
blur = cv2.medianBlur(inputImage,7)
closing = cv2.morphologyEx(blur, cv2.MORPH_CLOSE, kernel)


myImage = closing

# Only find parent contours, ie ignore nested contours
im2, contours, hierarchy =             cv2.findContours(myImage.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)



def get_contour_precedence(contour, cols, tol):
    tolerance_factor = tol
    origin = cv2.boundingRect(contour)
    return ((origin[1] / tolerance_factor) * tolerance_factor) * cols + origin[0]


# Sort the contours from left to right, top to bottom
contours.sort(key=lambda x:get_contour_precedence(x, myImage.shape[1], 90))

myContours = []

# Process the raw contours to get bounding rectangles
for cnt in contours:

epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True)

if len(approx == 4):
    rectangle = cv2.boundingRect(cnt)
    myContours.append(rectangle)

# Draw and save contours from processed image 
i=0
for x,y,w,h in myContours:

letter = blur[y:y+h, x:x+w]
cv2.rectangle(blur,(x,y),(x+w,y+h),(255,255,255),2)
cv2.imwrite(str(i)+'A.png', letter)
_, binaryLetter = cv2.threshold(letter,127,255,cv2.THRESH_BINARY_INV)
cv2.imwrite(str(i)+'B.png', binaryLetter)
i+=1