Search code examples
pythonopencvdigitsimage-thresholdingimutils

Masking problems in 6 digit recognition using OpenCV with lighted meter


I am trying to recognize six digits from a meter using python-OpenCV. It's surprising how incredibly hard it is to set morphological operations working in the right way, given the time I have spent adjusting the focus/distance of my raspberry pi camera to the meter screen and I even have bought a separate led lamp to have as much uniform light as possible. This is a template image enter image description here and I've tried using and adjusting the code from these two sources: enter link description here and enter link description here reproduced below without any progress. I got stuck right at the start when setting the thresholding options. Thank you in advance for any help.

# Code 1
import cv2
import numpy as np
import pytesseract

# Load the image
img = cv2.imread("test.jpg")

# Color-segmentation to get binary mask
lwr = np.array([43, 0, 71])
upr = np.array([103, 255, 130])
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
msk = cv2.inRange(hsv, lwr, upr)
cv2.imwrite("msk.png", msk)

# Extract digits
krn = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 3))
dlt = cv2.dilate(msk, krn, iterations=5)
res = 255 - cv2.bitwise_and(dlt, msk)
cv2.imwrite("res.png", res)

# Displaying digits and OCR
txt = pytesseract.image_to_string(res, config="--psm 6 digits")
print(''.join(t for t in txt if t.isalnum()))
cv2.imshow("res", res)
cv2.waitKey(0)
cv2.destroyAllWindows()
# code 2
# https://pyimagesearch.com/2017/02/13/recognizing-digits-with-opencv-and-python/
# import the necessary packages
# from imutils.perspective import four_point_transform
from imutils import contours
import imutils
import cv2
import numpy as np
from numpy.linalg import norm

# define the dictionary of digit segments so we can identify
# each digit on the thermostat
DIGITS_LOOKUP = {
        (1, 1, 1, 0, 1, 1, 1): 0,
        (1, 0, 1, 0, 1, 0, 1): 1,
        (1, 0, 1, 1, 1, 0, 1): 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, 1, 1, 0, 0, 1, 0): 7,
        (1, 1, 1, 1, 1, 1, 1): 8,
        (1, 1, 1, 1, 0, 1, 1): 9
}

images = 'test.jpg'
image = cv2.imread(images, 1)
# pre-process the image by resizing it, converting it to
# graycale, blurring it, and computing an edge map
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
# gray = cv2.medianBlur(blurred, 1)

# threshold the warped image, then apply a series of morphological
# operations to cleanup the thresholded image
(T, thresh) = cv2.threshold(blurred, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

cv2.imshow('thresh', thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()

mask = np.zeros((image.shape[0] + 2, image.shape[1] + 2), np.uint8)
cv2.floodFill(thresh, mask, (0, 0), 0)
cv2.floodFill(thresh, mask, (image.shape[1]-1, 0), 0)
cv2.floodFill(thresh, mask, (round(image.shape[1]/2.4), 0), 0)
cv2.floodFill(thresh, mask, (image.shape[1]//2, 0), 0)
cv2.floodFill(thresh, mask, (0, image.shape[0]-1), 0)
cv2.floodFill(thresh, mask, (image.shape[1]-1, image.shape[0]-1), 0)

kernel = np.ones((2, 2), np.uint8)
thresh = cv2.erode(thresh, kernel, iterations=2)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 13))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)

# cv2.imshow('thresh', thresh)
# cv2.waitKey(0)
# cv2.destroyAllWindows()


# find contours in the thresholded image, then initialize the
# digit contours lists
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
                        cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
digitCnts = []
# loop over the digit area candidates
for c in cnts:
        # compute the bounding box of the contour
        (x, y, w, h) = cv2.boundingRect(c)
        # if the contour is sufficiently large, it must be a digit
        if w <= 300 and (h >= 130 and h <= 300):
            digitCnts.append(c)
            cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)

# cv2.imshow('image', image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()

# sort the contours from left-to-right, then initialize the
# actual digits themselves
digitCnts = contours.sort_contours(digitCnts, method="left-to-right")[0]
digits = []

clao = 0
# loop over each of the digits
for c in digitCnts:
        clao = clao + 1
        # extract the digit ROI
        (x, y, w, h) = cv2.boundingRect(c)
        roi = thresh[y:y + h, x:x + w]
        # compute the width and height of each of the 7 segments
        # we are going to examine
        (roiH, roiW) = roi.shape
        (dW, dH) = (int(roiW * 0.25), int(roiH * 0.15))
        dHC = int(roiH * 0.05)
        # define the set of 7 segments
        segments = [
                ((0, 0), (w, dH)),                           # top
                ((0, 0), (dW, h // 2)),                      # top-left
                ((w - dW, 0), (w, h // 2)),                  # top-right
                ((0, (h // 2) - dHC), (w, (h // 2) + dHC)),  # center
                ((0, h // 2), (dW, h)),                      # bottom-left
                ((w - dW, h // 2), (w, h)),                  # bottom-right
                ((0, h - dH), (w, h))                        # bottom
        ]
        on = [0] * len(segments)

        # loop over the segments
        for (i, ((xA, yA), (xB, yB))) in enumerate(segments):
                #  extract the segment ROI, count the total number of
                #  thresholded pixels in the segment, and then compute
                #  the area of the segment
                segROI = roi[yA:yB, xA:xB]
                total = cv2.countNonZero(segROI)
                area = (xB - xA) * (yB - yA)
                # if the total number of non-zero pixels is greater than
                # 50% of the area, mark the segment as "on"
                if clao == 1:
                        if total / float(area) > 0.34:
                                if area < 1500:
                                        on = [1, 0, 1, 0, 1, 0, 1]
                                else:
                                        on[i] = 1
                else:
                        if total / float(area) > 0.39:
                                if area < 1500:
                                        on = [1, 0, 1, 0, 1, 0, 1]
                                else:
                                        on[i] = 1

        # lookup the digit and draw it on the image
        digit = DIGITS_LOOKUP.get(tuple(on)) or DIGITS_LOOKUP[
                min(DIGITS_LOOKUP.keys(), key=lambda key: norm(np.array(key)-np.array(on)))]
        # digit = DIGITS_LOOKUP[tuple(on)]
        digits.append(digit)
        # print(digits)
        cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 1)
        cv2.putText(image, str(digit), (x - 10, y - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2)

# display the digits
print(digits)
cv2.imshow("Input", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

Update

Apologies for my late reply but I have been quite busy with work.

I have captured 22 images throughout the day and used @fmw42 code (with some amendments) to apply thresholding and morphological operations. I am making the images available here and the code that I am using is available below. Overall the performance is quite robust, although 1s and sometimes 8s get mixed up with 2s. I am happy to accept a code that provides improved performance. Note: I think that one problem is that the vertical lines of the numbers are slightly slanted? Thank you in advance.

import cv2
import numpy as np
from numpy.linalg import norm
from imutils import contours
import imutils
import os

# define the dictionary of digit segments so we can identify
# each digit on the thermostat
DIGITS_LOOKUP = {
        (1, 1, 1, 0, 1, 1, 1): 0,
        (1, 0, 1, 0, 1, 0, 1): 1,
        (1, 0, 1, 1, 1, 0, 1): 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, 1, 1, 0, 0, 1, 0): 7,
        (1, 1, 1, 1, 1, 1, 1): 8,
        (1, 1, 1, 1, 0, 1, 1): 9
}

path_of_the_directory = "/home/myusername/mypathdirectory"
ext = ('.jpg')
for files in os.listdir(path_of_the_directory):
    if files.endswith(ext):
        # load image
        print(files)
        img = cv2.imread(path_of_the_directory+files)

        # convert to grayscale
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # blur
        blur = cv2.GaussianBlur(gray, (0,0), sigmaX=51, sigmaY=51)

        # divide
        divide = cv2.divide(gray, blur, scale=255)

        # threshold  
        thresh = cv2.threshold(divide, 235, 255, cv2.THRESH_BINARY)[1]

        # apply morphology
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (41,41))
        morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (41,41))
        morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
        morph = cv2.bitwise_not(morph)  # reverse
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (1, 70))
        morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)

        # write result to disk
        cv2.imwrite("digits_division.jpg", divide)
        cv2.imwrite("digits_threshold.jpg", thresh)
        cv2.imwrite("digits_morph.jpg", morph)

        # display it
        cv2.imshow("divide", divide)
        cv2.imshow("thresh", thresh)
        cv2.imshow("morph", morph)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

        # find contours in the thresholded image, then initialize the
        # digit contours lists
        cnts = cv2.findContours(morph.copy(), cv2.RETR_EXTERNAL,
                                cv2.CHAIN_APPROX_SIMPLE)
        cnts = imutils.grab_contours(cnts)
        digitCnts = []

        # loop over the digit area candidates
        for c in cnts:
                # compute the bounding box of the contour
                (x, y, w, h) = cv2.boundingRect(c)
                # if the contour is sufficiently large, it must be a digit
                if w >= 60 and (h >= 300 and h <= 800):
                    digitCnts.append(c)
                    cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)

        cv2.imshow('image', img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

        # sort the contours from left-to-right, then initialize the
        # actual digits themselves
        digitCnts = contours.sort_contours(digitCnts, method="left-to-right")[0]
        digits = []

        clao = 0
        # loop over each of the digits
        for c in digitCnts:
                clao = clao + 1
                # extract the digit ROI
                (x, y, w, h) = cv2.boundingRect(c)
                roi = morph[y:y + h, x:x + w]
                # compute the width and height of each of the 7 segments
                # we are going to examine
                (roiH, roiW) = roi.shape
                (dW, dH) = (int(roiW * 0.25), int(roiH * 0.15))
                dHC = int(roiH * 0.05)
                # define the set of 7 segments
                segments = [
                        ((0, 0), (w, dH)),                           # top
                        ((0, 0), (dW, h // 2)),                      # top-left
                        ((w - dW, 0), (w, h // 2)),                  # top-right
                        ((0, (h // 2) - dHC), (w, (h // 2) + dHC)),  # center
                        ((0, h // 2), (dW, h)),                      # bottom-left
                        ((w - dW, h // 2), (w, h)),                  # bottom-right
                        ((0, h - dH), (w, h))                        # bottom
                ]
                on = [0] * len(segments)
                
                # loop over the segments
                for (i, ((xA, yA), (xB, yB))) in enumerate(segments):
                        #  extract the segment ROI, count the total number of
                        #  thresholded pixels in the segment, and then compute
                        #  the area of the segment
                        segROI = roi[yA:yB, xA:xB]
                        total = cv2.countNonZero(segROI)
                        area = (xB - xA) * (yB - yA)
                        # if the total number of non-zero pixels is greater than
                        # 50% of the area, mark the segment as "on"
                        if clao == 1:
                                if total / float(area) > 0.34:
                                        if area < 1500:
                                                on = [1, 0, 1, 0, 1, 0, 1]
                                        else:
                                                on[i] = 1
                        else:
                                if total / float(area) > 0.42:
                                        if area < 1500:
                                                on = [1, 0, 1, 0, 1, 0, 1]
                                        else:
                                                on[i] = 1
                                                
                # lookup the digit andq draw it on the image
                digit = DIGITS_LOOKUP.get(tuple(on)) or DIGITS_LOOKUP[
                        min(DIGITS_LOOKUP.keys(), key=lambda key: norm(np.array(key)-np.array(on)))]
                # digit = DIGITS_LOOKUP[tuple(on)]
                digits.append(digit)
                # print(digits)
                cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 1)
                cv2.putText(img, str(digit), (x - 10, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2)

        # display the digits
        print(digits)
        cv2.imshow("Input", img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()        
    else:
        continue

Solution

  • Perhaps this will help you using division normalization in Python/OpenCV.

    Input:

    enter image description here

    import cv2
    import numpy as np
    
    # load image
    img = cv2.imread("digits.jpg")
    
    # convert to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # blur
    blur = cv2.GaussianBlur(gray, (0,0), sigmaX=51, sigmaY=51)
    
    # divide
    divide = cv2.divide(gray, blur, scale=255)
    
    # threshold  
    thresh = cv2.threshold(divide, 235, 255, cv2.THRESH_BINARY)[1]
    
    # apply morphology
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11,11))
    morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11,11))
    morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
    
    # write result to disk
    cv2.imwrite("digits_division.jpg", divide)
    cv2.imwrite("digits_threshold.jpg", thresh)
    cv2.imwrite("digits_morph.jpg", morph)
    
    # display it
    cv2.imshow("divide", divide)
    cv2.imshow("thresh", thresh)
    cv2.imshow("morph", morph)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Division normalized image:

    enter image description here

    Thresholded image:

    enter image description here

    Morphology processed image:

    enter image description here

    You can then clean up further by getting contours and removing small contours and very long horizontal contours.