1

I'm following Adrian Rosebrock's tutorial on recognising digits on an RPi, so no tesseract or whatever: https://www.pyimagesearch.com/2017/02/13/recognizing-digits-with-opencv-and-python/

But it doesn't recognise decimal points, so I've been trying really hard to create a part that would help to do that. I think I've gotten close, but I'm not sure what I've done wrong.

This is my image after preprocessing

enter image description here

and this is what happens after the attempted recognising part

enter image description here

As you can see, I'm doing something wrong somewhere. Already tried tuning param1 and param2 in the houghCircles

More examples:

enter image description here

enter image description here

Can anyone guide me on what I should do? I'm really lost here

================================================================

The images i'm using enter image description here

enter image description here

The code I'm using

from imutils.perspective import four_point_transform
from imutils import contours
import imutils
import cv2
import numpy

DIGITS_LOOKUP = {
        # Old Library
    #(1, 1, 1, 0, 1, 1, 1): 0, # same as new 8
    (0, 0, 1, 0, 0, 1, 0): 1,
    (1, 0, 1, 1, 1, 1, 0): 2,
    (1, 0, 1, 1, 0, 1, 1): 3,
    (0, 1, 1, 1, 0, 1, 0): 4,
    (1, 1, 0, 1, 0, 1, 1): 5,
    #(1, 1, 0, 1, 1, 1, 1): 6,
    (1, 0, 1, 0, 0, 1, 0): 7,
    (1, 1, 1, 1, 1, 1, 1): 8,
    (1, 1, 1, 1, 0, 1, 1): 9,

    # New Digital Library
        (0, 0, 1, 1, 1, 0, 1): 0,
        (1, 0, 1, 0, 0, 1, 1): 2,

        (0, 0, 1, 1, 0, 1, 1): 4,
        (0, 0, 0, 0, 0, 1, 1): 4,

        (1, 1, 0, 0, 0, 1, 1): 5,
        (1, 1, 0, 1, 1, 0, 1): 5,
        (1, 0, 0, 0, 0, 1, 1): 5,

        (1, 1, 1, 0, 0, 0, 0): 7,

        (1, 1, 0, 1, 1, 1, 1): 8,
        (1, 1, 1, 0, 1, 1, 1): 8
}

image = cv2.imread("10.jpg")

image = imutils.resize(image, height=100)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(blurred, 120, 255, 1)
cv2.imshow("1", edged)

cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
displayCnt = None

for c in cnts:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)

    if len(approx) == 4:
        displayCnt = approx
        break

warped = four_point_transform(gray, displayCnt.reshape(4, 2))
output = four_point_transform(image, displayCnt.reshape(4, 2))

thresh = cv2.threshold(warped, 0, 255,
    cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv2.imshow("2", thresh)
print(thresh.shape)

circles = cv2.HoughCircles(warped, cv2.HOUGH_GRADIENT, 7, 14, param1=0.1, param2=20, minRadius=3, maxRadius=7)

# ensure at least some circles were found
if circles is not None:
    circles = numpy.round(circles[0, :]).astype("int")

    for (x, y, r) in circles:
        cv2.circle(output, (x, y), r, (0, 255, 0), 4)
        cv2.rectangle(output, (x - 5, y - 5), (x + 5, y + 5), (0, 128, 255), -1)


    # show the output image
    cv2.imshow("test", output)
    cv2.waitKey(0)
Aesreal
  • 131
  • 1
  • 12
  • First thing I notice is that HoughCircles should be applied on binary edge image, not `warped`. So you would need to run edge detection, i.e. Canny, on `thresh` and apply HoughCircles on the output. Second, HoughCircles might not be what you want because the dot is square, not circle, in the original picture. A `findContours` followed by thresholding the contour size would work better. – Quang Hoang Apr 05 '19 at 14:21
  • You need to be detecting the smallest connected components in the image, rather than circles or other specific shapes. You know there's exactly 9 pixels in the dot, seems fairly trivial to detect. – Cris Luengo Apr 05 '19 at 14:26
  • i've used connectedComponentsWithStats, but I get noise, pixels that are even smaller than the decimal and yet don't matter – Aesreal Apr 05 '19 at 17:10
  • 1
    Another option could be using color thresholding and then determine the smallest contour using `findContours` – nathancy Apr 05 '19 at 19:48

2 Answers2

1

Since the decimal may be a square instead of a circle, using cv2.HoughCircles() may not be the best option. Additionally, since you may have background noise, trying to find connected components may give you false positive results.

Here's a method to detect the decimal using cv2.boundingRect() and cv2.contourArea(). We could set threshold min and max areas so it will only detect the decimal but also avoid detecting noise.

Attempting to detect on images

enter image description here enter image description here

enter image description here enter image description here

from imutils.perspective import four_point_transform
from imutils import contours
import imutils
import cv2
import numpy

DIGITS_LOOKUP = {
    (1, 1, 1, 0, 1, 1, 1): 0,
    (0, 0, 1, 0, 0, 1, 0): 1,
    (1, 0, 1, 1, 1, 1, 0): 2,
    (1, 0, 1, 1, 0, 1, 1): 3,
    (0, 1, 1, 1, 0, 1, 0): 4,
    (1, 1, 0, 1, 0, 1, 1): 5,
    (1, 1, 0, 1, 1, 1, 1): 6,
    (1, 0, 1, 0, 0, 1, 0): 7,
    (1, 1, 1, 1, 1, 1, 1): 8,
    (1, 1, 1, 1, 0, 1, 1): 9
}

image = cv2.imread("10.jpg")

image = imutils.resize(image, height=100)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(blurred, 120, 255, 1)
cv2.imshow("1", edged)

cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
displayCnt = None

for c in cnts:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)

    if len(approx) == 4:
        displayCnt = approx
        break

warped = four_point_transform(gray, displayCnt.reshape(4, 2))

thresh = cv2.threshold(warped, 0, 255,
    cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv2.imshow("2", thresh)

digit_cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
digit_cnts = imutils.grab_contours(digit_cnts)

threshold_max_area = 25
threshold_min_area = 5
contour_image = thresh.copy()

for c in digit_cnts:
    (x,y,w,h) = cv2.boundingRect(c)
    area = cv2.contourArea(c) 
    if area < threshold_max_area and area > threshold_min_area:
        cv2.drawContours(contour_image,[c], 0, (100,5,10), 3)

cv2.imshow("detect decimal", contour_image)
cv2.waitKey(0)
nathancy
  • 42,661
  • 14
  • 115
  • 137
  • Thanks for your help! Your solution is way more dynamic than the one I have. Would you recommend to use boundingRect over HoughCircles when attempting to detect circles? Or it depends on its size – Aesreal Apr 06 '19 at 07:24
  • 1
    Hard to say, I think it really depends on the application. Since your decimal points are not 100% circles, boundingrect seems better but if you know beforehand if you have real circles (like the ones in MS paint) then hough circles may be better – nathancy Apr 06 '19 at 22:26
1

Worked on it, referring to this: How to remove small connected objects using OpenCV

got these as results enter image description here enter image description here

But they are not very good or stable for use. If the dots are suddenly larger than it is before (eg more than 9 pixels, or the image is resized), then it can no longer be used, so this is not a dynamic answer, which is bad. But, i'll just leave this here if anyone is interested

Code

#find all your connected components (white blobs in your image)
nb_components, dotput, stats, centroids = cv2.connectedComponentsWithStats(thresh, connectivity=8)
#connectedComponentswithStats yields every seperated component with information on each of them, such as size
#the following part is just taking out the background which is also considered a component, but most of the time we don't want that.
sizes = stats[1:, -1]; nb_components = nb_components - 1

# minimum size of particles we want to keep (number of pixels)
#here, it's a fixed value, but you can set it as you want, eg the mean of the sizes or whatever
min_size = 50

#your answer image
img2 = numpy.zeros((dotput.shape))
#for every component in the image, you keep it only if it's above min_size
#thresh[output == 5 + 1] = 0
dots = []
for i in range(0, nb_components):
    if sizes[i] < min_size:
        dots.append(centroids[i])

#print(dots)
if dots:
    dots.sort(key = lambda x: abs(x[1]-digitCenY))
    print(dots)
    pDot = -1

    for i in range(len(digitCenX)):
        if (dots[0][0] <= digitCenX[i]) and (i > 0):
            pDot = 0
            break
        elif (digitCenX[i] <= dots[0][0]) and (i != len(digitCenX)-1):
            pDot = 0
            break
        else:
            pDot = 1

    cv2.rectangle(output, (int(dots[pDot][0]), int(dots[pDot][1])), (int(dots[pDot][0]) + 3, int(dots[pDot][1]) + 3), (0, 255, 0), 1)
Aesreal
  • 131
  • 1
  • 12