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 and I've tried using and adjusting the code from these two sources:
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:
Thresholded image:
Morphology processed image:
You can then clean up further by getting contours and removing small contours and very long horizontal contours.
CodePudding user response:
The key to getting this working is cleaning the image up which I have done to a good enough level to get it to work. I've done this using scikit image library.
I then look at certain squares on the image and take an average reading from that area. On the right hand-side image I've marked some of the locations with red squares.
My script I used to get this result:
import numpy as np
from pathlib import Path
import imageio.v3 as iio
import skimage.filters as skif
from skimage.color import rgb2gray
from skimage.util import img_as_ubyte
from skimage.restoration import denoise_bilateral
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import matplotlib.cm as cm
threshold = 125
digit_loc = [1600, 1300, 1000, 730, 420, 155]
size = 20
x_mid = 80
x_right = 160
y_top = 130
y_mt = 250
y_mid = 380
y_bm = 520
y_bot = 630
def img_with_threshold(orig_img):
block_size = 255
local_thresh = skif.threshold_local(
orig_img,
block_size,
method="mean",
)
binary_local = orig_img > local_thresh
u8_val = img_as_ubyte(binary_local)
return u8_val
def image_denoise(orig_img):
return denoise_bilateral(orig_img, win_size=10, bins=10, )
def plot_imgs(orig_img, mod_img):
# Display the image
fig, axes = plt.subplots(1, 2, figsize=(8, 8), sharex=True, sharey=True)
ax = axes.ravel()
ax[0].imshow(orig_img, cmap=cm.Greys_r)
ax[1].imshow(mod_img, cmap=cm.Greys_r)
# Create a Rectangle patch
for x_loc in digit_loc:
rect1 = Rectangle((x_loc x_mid, y_top), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect2 = Rectangle((x_loc, y_mt), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect3 = Rectangle((x_loc x_right, y_mt), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect4 = Rectangle((x_loc x_mid, y_mid), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect5 = Rectangle((x_loc, y_bm), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect6 = Rectangle((x_loc x_right, y_bm), size, size, linewidth=1, edgecolor='r', facecolor='none')
rect7 = Rectangle((x_loc x_mid, y_bot), size, size, linewidth=1, edgecolor='r', facecolor='none')
# Add the patch to the Axes
ax[1].add_patch(rect1)
ax[1].add_patch(rect2)
ax[1].add_patch(rect3)
ax[1].add_patch(rect4)
ax[1].add_patch(rect5)
ax[1].add_patch(rect6)
ax[1].add_patch(rect7)
plt.show()
def seg_to_digit(segments, location):
digit_values = {0b1110111: 0,
0b0010010: 1,
0b1011101: 2,
0b1011011: 3,
0b0111010: 4,
0b1101011: 5,
0b1101111: 6,
0b1110010: 7,
0b1111111: 8,
0b1111011: 9,
}
result = int("".join(["1" if i < threshold else "0" for i in segments]), 2)
# print("score:", result)
return digit_values.get(result, 0) * 10 ** location
def get_digit(location, mod_img):
"""
a
b c
d
e f
g
"""
x_loc = digit_loc[location]
m_loc = (x_loc x_mid, x_loc x_mid size)
l_loc = (x_loc, x_loc size)
r_loc = (x_loc x_right, x_loc x_right size)
seg_a = np.average(mod_img[y_top:y_top size, m_loc[0]:m_loc[1]])
seg_b = np.average(mod_img[y_mt:y_mt size, l_loc[0]:l_loc[1]])
seg_c = np.average(mod_img[y_mt:y_mt size, r_loc[0]:r_loc[1]])
seg_d = np.average(mod_img[y_mid:y_mid size, m_loc[0]:m_loc[1]])
seg_e = np.average(mod_img[y_bm:y_bm size, l_loc[0]:l_loc[1]])
seg_f = np.average(mod_img[y_bm:y_bm size, r_loc[0]:r_loc[1]])
seg_g = np.average(mod_img[y_bot:y_bot size, m_loc[0]:m_loc[1]])
segments = [seg_a, seg_b, seg_c, seg_d, seg_e, seg_f, seg_g]
# print(f"x loc: {x_loc}, digit index: {location}, segment values: {segments}")
# create an integer from the bits
# print('value:', result)
return seg_to_digit(segments, location)
def main():
data_dir = Path(__file__).parent.joinpath('data')
meter_img = data_dir.joinpath('meter_test.jpg')
img = iio.imread(meter_img)
gray_img = img_as_ubyte(rgb2gray(img))
img_result = image_denoise(gray_img)
img_result1 = img_with_threshold(img_result)
reading = 0
for dig_loc in range(6):
reading = get_digit(dig_loc, img_result1)
print(f"{reading:>21}")
print("Final reading:", reading)
plot_imgs(gray_img, img_result1)
if __name__ == '__main__':
main()
This gave the following output:
7
77
677
4677
24677
924677
Final reading: 924677