使用 OpenCV 和 Python 識別數字

語言: CN / TW / HK

這是我參與11月更文挑戰的第1天,活動詳情檢視:2021最後一次更文挑戰

使用 OpenCV 和 Python 識別數字

本文演示如何使用 OpenCV 和 Python 識別影象中的數字。

在本教程的第一部分,我們將討論什麼是七段顯示器,以及我們如何應用計算機視覺和影象處理操作來識別這些型別的數字(不需要機器學習!)

七段顯示

您可能已經熟悉七段顯示器,即使您不認識特定術語。 這種顯示的一個很好的例子是您的經典數字鬧鐘:

image-20211108135630394 鬧鐘上的每個數字都由一個七段元件表示,如下所示:

image-20211108140852713

七段顯示器總共可以呈現 128 種可能的狀態:

image-20211108140904381

我們只對其中的 10 個感興趣——數字 0 到 9:

image-20211108140913319

我們的目標是編寫 OpenCV 和 Python 程式碼來識別影象中的這十個數字狀態中的每一個。

設計OpenCV 數字識別器

我們將使用恆溫器影象作為輸入:

image-20211108140921872

識別的步驟:

步驟 1:定位恆溫器上的 LCD。 這可以使用邊緣檢測來完成,因為塑料外殼和 LCD 之間有足夠的對比度。

步驟2:提取 LCD。 給定一個輸入邊緣圖,我可以找到輪廓並尋找矩形的輪廓——最大的矩形區域應該對應於 LCD。 透視變換會給我一個很好的 LCD 提取。

步驟3:提取數字區域。 一旦我有了 LCD 本身,我就可以專注於提取數字。 由於數字區域和 LCD 背景之間似乎存在對比,我相信閾值和形態操作可以實現這一點。

步驟4:識別數字。 使用 OpenCV 識別實際數字將涉及將數字 ROI 劃分為七個部分。 從那裡我可以在閾值影象上應用畫素計數來確定給定的片段是“開”還是“關”。

所以看看我們如何使用 OpenCV 和 Python 完成這個四步過程來進行數字識別,繼續閱讀。

使用計算機視覺和 OpenCV 識別數字

讓我們繼續開始這個例子。新建一個檔案,將其命名為 identify_digits.py ,並插入以下程式碼:

```

import the necessary packages

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

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, (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 } ```

匯入我們所需的 Python 包。 引入mutils,這是我的一系列便利函式,可以更輕鬆地使用 OpenCV + Python。 如果您還沒有安裝 imutils,現在應該花一點時間使用 pip 在您的系統上安裝該軟體包: 使用 OpenCV 和 Python 識別數字

pip install imutils

定義一個名為 DIGITS_LOOKUP 的 Python 字典。 他們對錶的關鍵是七段陣列。 陣列中的 1 表示給定的段已開啟,零表示該段已關閉。 該值是實際數字本身:0-9。

一旦我們識別了恆溫器顯示器中的段,我們就可以將陣列傳遞到我們的 DIGITS_LOOKUP 表中並獲得數字值。 作為參考,該詞典使用與上面圖 2 中相同的段順序。 讓我們繼續我們的例子:

```

load the example image

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

pre-process the image by resizing it, converting it to

graycale, blurring it, and computing an edge map

image = imutils.resize(image, height=500) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) edged = cv2.Canny(blurred, 50, 200, 255) ```

載入我們的影象。

然後我們通過以下方式預處理影象:-

  • 調整大小。
  • 將影象轉換為灰度。
  • 使用 5×5 核心應用高斯模糊以減少高頻噪聲。
  • 通過 Canny 邊緣檢測器計算邊緣圖。

應用這些預處理步驟後,我們的邊緣圖如下所示:

image-20211108140933252 注意 LCD 的輪廓是如何清晰可見的——這完成了步驟 #1。 我們現在可以繼續第 2 步,提取 LCD 本身:

```

find contours in the edge map, then sort them by their

size in descending order

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

loop over the contours

for c in cnts: # approximate the contour peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) # if the contour has four vertices, then we have found # the thermostat display if len(approx) == 4: displayCnt = approx break ```

為了找到 LCD 區域,我們需要提取邊緣圖中區域的輪廓(即輪廓)。

然後我們按面積對等高線進行排序,確保將面積較大的等高線放在列表的前面。

給定我們排序的輪廓列表,逐個迴圈它們並應用輪廓近似。

如果我們的近似輪廓有四個頂點,那麼我們假設我們已經找到了恆溫器顯示。 這是一個合理的假設,因為我們輸入影象中最大的矩形區域應該是 LCD 本身。

獲得四個頂點後,我們可以通過四點透視變換提取 LCD:

```

extract the thermostat display, apply a perspective transform

to it

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

應用這種透視變換為我們提供了一個自上而下的 LCD 鳥瞰圖:

image-20211108140948248

獲得 LCD 的這個檢視滿足第 2 步——我們現在準備從 LCD 中提取數字:

```

threshold the warped image, then apply a series of morphological

operations to cleanup the thresholded image

thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (1, 5)) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) ```

為了獲得數字本身,我們需要對扭曲影象進行閾值處理,以在較亮的背景(即 LCD 顯示屏的背景)中顯示暗區(即數字):

image-20211108140956034 然後我們應用一系列形態學操作來清理閾值影象:

image-20211108141005262

現在我們有一個很好的分割影象,我們再次需要應用輪廓過濾,只是這次我們正在尋找實際的數字:

```

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 >= 15 and (h >= 30 and h <= 40): digitCnts.append(c) ```

為此,我們在閾值影象中找到輪廓。 初始化digitsCnts 列表——這個列表將儲存數字本身的輪廓。

在每個輪廓上迴圈。

對於每個輪廓,我們計算邊界框,確保寬度和高度是可接受的大小,如果是,則更新digitsCnts 列表。

如果我們迴圈遍歷digitsCnts內部的輪廓並在影象上繪製邊界框,結果將如下所示:

image-20211108141017066

果然,我們在液晶顯示屏上找到了數字! 最後一步是實際識別每個數字:

```

sort the contours from left-to-right, then initialize the

actual digits themselves

digitCnts = contours.sort_contours(digitCnts, method="left-to-right")[0] digits = [] ```

在這裡,我們只是根據 (x, y) 座標從左到右對數字輪廓進行排序。

這個排序步驟是必要的,因為不能保證輪廓已經從左到右排序(與我們讀取數字的方向相同)。

接下來是實際的數字識別過程:

```

loop over each of the digits

for c in digitCnts: # 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) ```

遍歷每個數字輪廓。 對於這些區域中的每一個,我們計算邊界框並提取數字 ROI。

我在下面包含了每個數字 ROI 的 GIF 動畫:

Figure 11: Extracting each individual digit ROI by computing the bounding box and applying NumPy array slicing.

給定數字 ROI,我們現在需要定位和提取數字顯示的七個部分。

根據 ROI 尺寸計算每個段的大致寬度和高度。 然後我們定義一個 (x, y) 座標列表,這些座標對應七個線段。 此列表遵循與上面圖 2 相同的段順序。 這是一個示例 GIF 動畫,它在正在調查的當前片段上繪製一個綠色框:

Figure 12: An example of drawing the segment ROI for each of the seven segments of the digit.

最後,初始化我們的 on 列表——該列表中的值 1 表示給定的段是“開啟”的,而值為零表示該段是“關閉的”。 給定七個顯示段的 (x, y) 座標,識別一個段是開啟還是關閉是相當容易的: 最後,初始化我們的 on 列表——該列表中的值 1 表示給定的段是“開啟”的,而值為零表示該段是“關閉的”。 給定七個顯示段的 (x, y) 座標,識別一個段是開啟還是關閉是相當容易的:

```

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 total / float(area) > 0.5:
        on[i]= 1
# lookup the digit and draw it on the image
digit = DIGITS_LOOKUP[tuple(on)]
digits.append(digit)
cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 1)
cv2.putText(output, str(digit), (x - 10, y - 10),
    cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2)

```

我們開始迴圈遍歷每個線段的 (x, y) 座標。

我們提取片段 ROI,然後計算非零畫素數(即片段中“開啟”的畫素數)。

如果非零畫素與段總面積的比率大於 50%,那麼我們可以假設該段是“on”並相應地更新我們的 on 列表。 在迴圈七段之後,我們可以將列表傳遞給 DIGITS_LOOKUP 以獲取數字本身。

然後我們在數字周圍繪製一個邊界框並在輸出影象上顯示數字。 最後,我們的最後一個程式碼塊將數字列印到我們的螢幕上並顯示輸出影象:

```

display the digits

print(u"{}{}.{} \u00b0C".format(*digits)) cv2.imshow("Input", image) cv2.imshow("Output", output) cv2.waitKey(0) ```

請注意我們如何使用 Python 和 OpenCV 正確識別 LCD 螢幕上的數字:

image-20211108141044971

總結

在今天的部落格文章中,我演示瞭如何利用 OpenCV 和 Python 來識別影象中的數字。

這種方法專門用於七段顯示器(即您通常會在數字鬧鐘上看到的數字顯示器)。

通過提取七個段中的每一個並應用基本的閾值和形態學操作,我們可以確定哪些段是“開”的,哪些是“關”的。

從那裡,我們可以在 Python 字典資料結構中查詢開/關段以快速確定實際數字——無需機器學習!

正如我在這篇博文的開頭提到的,應用計算機視覺來識別恆溫器影象中的數字往往會使問題本身過於複雜——使用資料記錄溫度計會更可靠,並且需要的工作量要少得多。

我希望你喜歡今天的博文!