疫情期間健康碼是衡量的標準!教你用Python自動識別防疫二維碼!

語言: CN / TW / HK

這個月月初的時候,朋友興奮地和我描述著他的計劃——準備帶孩子到寧夏自駕遊。朋友感慨道,“小孩只在書本上見過黃河、見過沙漠,這樣的人生多少有一點遺憾”,可正如新冠病毒會變異為德爾塔一樣,生活裡唯一不變的變化本身,區域性地區疫情捲土重來,朋友為了孩子的健康著想,不得不取消這次計劃,因為他原本就想去寧夏看看的。回想過去這一年多,口罩和二維碼,是每天打交道最多的東西。也許,這會成為未來幾年裡的常態。在西安,不管是坐公交還是地鐵,都會有人去檢查防疫二維碼,甚至由此而創造了不少的工作崗位。每次看到那些年輕人,我都有種失落感,因為二十九歲高齡的我,已然不那麼年輕了,而這些比我更努力讀書、學歷更高的年輕人,看起來在做著和學歷/知識並不相稱的工作。也許,自卑的應該是我,因為國家剛剛給程式設計師群體定性—— 新生代農民工 。可是,我這個農民工,今天想做一點和學歷/知識相稱的事情,利用 Python 來自動識別防疫二維碼。

原理說明

對於防疫二維碼而言,靠肉眼去看的話,其實主要關注兩個顏色,即標識健康狀態的顏色和標識疫苗注射狀態的顏色。與此同時,為了追蹤人的地理位置變化,防疫/安檢人員還會關注地理位置資訊,因此,如果要自動識別防疫二維碼,核心就是讀出其中的顏色以及文字資訊。對於顏色的識別,我們可以利用 OpenCV 中的 inRange() 函式來實現,只要我們定義好對應顏色的 HSV區間即可;對於文字的識別,我們可以利用 PaddleOCR 庫來進行提取。基於以上原理,我們會通過 OpenCV 來處理攝像頭的影象,只要我們將手機二維碼對準攝像頭,即可以完成防疫二維碼的自動識別功能。考慮到檢測不到二維碼或者顏色識別不到這類問題,程式中增加了蜂鳴報警的功能。寫作本文的原因,單純是我覺得這樣好玩,我無意藉此來讓人們失業。可生而為人,說到底不能像機器一樣活著,大家不都追求有趣的靈魂嗎?下面是本文中使用到的第三方 Python 庫的清單:

  • pyzbar == 0.1.8
  • opencv-contrib-python == 4.4.0.46
  • opencv-python == 4.5.3.56
  • paddleocr == 2.2.0.2
  • paddlepaddle == 2.0.0

圖塊檢測

下面是一張從手機上擷取的防疫二維碼圖片,從這張圖片中我們看出,整個防疫二維碼,可以分為三個部分,即:上方的定位資訊圖塊,中間的二維碼資訊圖塊,以及下方的核酸檢驗資訊圖塊。

“西安一碼通” 防疫二維碼

對於二維碼的檢測,我們可以直接使用 pyzbar 這個庫來解析,可如果直接對整張圖進行解析,因為其中的干擾項實在太多,偶爾會出現明明有二維碼,結果無法進行解析的情況。所以,我們可以考慮對圖片進行切分,而切分的依據就是圖中的這三個圖塊。這裡,我們利用二值化函式 threshold() 和 輪廓提取函式 findContours() 來實現圖塊的檢測:

# 灰度化 & 二值化
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 135, 255, cv2.THRESH_BINARY)
# 檢測輪廓,獲得對應的矩形
contours = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] 
for i in range(len(contours)):
    block_rect = cv2.boundingRect(contours[i])

這裡有一個感觸頗深的地方,在檢測圖塊的過程中,博主發現中間和底部這兩個圖塊,其檢測要更為簡單一點,因為它有明顯的邊界、屬於規則的矩形,而上方的圖塊,因為帶有裝飾性的紋理,以及灰色的過渡區,二值化並不能檢測到其邊緣,如下圖所示,地鐵上使用的二維碼,相比商場裡使用的二維碼,輪廓線要更為清晰一點。所以,這裡選擇一個什麼樣的閾值賴做二值化,個人感覺是需要反覆去嘗試的。考慮到要相容這種輪廓不規則的圖塊,實際上我使用了一點小技巧,即:在得到下面兩個圖塊以後,利用高度的換算關係,人為地生成上方圖塊的矩形範圍。

“西安一碼通” 灰度化 & 二值化

那麼,這是否說明,代表美的設計,在代表絕對理性的演算法面前,其實更像是一種噪音。也許,它們各自的領域不同、觀點不同,可都一樣在為這個世界發光發熱,生活不止一種真相,世界不止一種回聲,有微小的差異,同樣有巨集大的統一。

二維碼檢測

好了,我們可以注意到,一旦完成圖塊的切分,此時,二維碼位於中間這個圖塊,檢測二維碼在這裡並不是重點,因為檢測這個二維碼是第一步,按照這個二維碼所在的矩形去檢測中心的的色彩,這是這裡的重點,因為這個二維碼解析以後就是一個 URL 地址,本身並沒有包含任何資訊,我們想要知道一個人是否健康,唯一的辦法就是檢測中間的色彩。其實,理論上剩餘兩個圖塊同樣需要檢測色彩,可考慮到三者在含義的表達上是一致的,即三者擁有相同的顏色,我們只需要處理其中一個即可。下面是利用 pyzbar 庫對二維碼區塊進行解析,獲取二維碼資訊、二維碼所在的矩形等資訊的程式碼片段:

# 檢測二維碼
def detect_qrcode(image, block):
    block_image, block_rect, _ = block
    block_x, block_y, _, _ = block_rect
    gray = cv2.cvtColor(block_image, cv2.COLOR_BGR2GRAY)
    qrcodes = decode(gray, [ZBarSymbol.QRCODE])
    if len(qrcodes) > 0:
        qrcode = qrcodes[0]
        qrcodeData = qrcode.data.decode("utf-8")
        x, y, w, h = qrcode.rect
        abs_x = block_x + x
        abs_y = block_y + y
        cv2.rectangle(image, (abs_x, abs_y), (abs_x + w, abs_y + h), color_marker, 2)
        return True, qrcodeData, (abs_x, abs_y, w, h)
    else:
        return False, None, None

可以注意到,通過 pyzbar 這個庫,我們不單單可以獲取到二維碼的資訊,同時還可以獲得二維碼在圖塊中的矩形範圍,由此我們可以推算出,二維碼在整張圖片中的矩形範圍,我們會繪製一個矩形來標識二維碼的位置,這樣使用者就可以清楚的知道,我們的的確確檢測到了二維碼。

色彩檢測

一旦我們確定了二維碼的矩形範圍,接下來的工作,就是在這個矩形範圍裡檢測顏色啦!譬如一個人如果健康狀態,二維碼的中間部分會顯示為綠色。如果一個人完成了疫苗的注射,二維碼邊上的區域會顯示為金色。所以,基於這樣的原理,我們只需要檢測對應區域是否有對應的顏色即可,這裡主要利用了 HSV 顏色模型,不同於 RGB 顏色模型, HSV 顏色模型利用色相、飽和度和亮度三個指標來描述顏色,是一種把 RGB 色彩空間中的點放在倒圓錐體上的表示方法。其中:

  • H,即 Hue,表示色相,它通過角度來度量,因此,它的取值範圍是0 到 360 度,如下圖所示,紅色對應 0 度,綠色對應 120 度,藍色對應 240 度:

HSV 顏色模型:色相

  • S,即 Saturation,表示飽和度,用 0 到 100% 之間的數值表示,如果用下面的倒圓錐體來表示,則 S 表示的是色彩點到所在圓形切面圓心的距離與該圓半徑的比值:

HSV 顏色模型:倒圓錐體

  • V,即 Value,表示亮度,同樣用 0 到 100% 之間的數值表示,參考上面的倒圓錐體,可以瞭解到,V 表示的是色彩點所在圓形切面圓心與該圓圓心在垂直距離上的比值:

此時此刻,你有沒有回想起小時候調電視機畫面時的經歷呢?

找不到合適的圖,簡單懷舊一下?

對於 HSV 顏色模型,我們可以參考下面的取值範圍:

HSV 顏色模型:參考範圍

以紅色為例,其 H 分量取值範圍為:0 到 10;S 分量取值範圍為:43 到 255;V 分量取值範圍為:46 到 255。 OpenCV 中的 inRange() 函式,可以判斷某個 HSV 陣列(此時圖片使用一個數組來表示)是否在某個給定的區間範圍內。於是,我們的思路就是:定義好目標顏色的 HSV 區間,同時提供一份 HSV 格式的圖片資料。此時,其實現邏輯如下:

# 顏色範圍定義
color_dist = {
    'red': {'Lower': np.array([0, 60, 60]), 'Upper': np.array([6, 255, 255])},
    'blue': {'Lower': np.array([100, 80, 46]), 'Upper': np.array([124, 255, 255])},
    'green': {'Lower': np.array([35, 43, 35]), 'Upper': np.array([90, 255, 255])},
    'golden': {'Lower': np.array([26, 43, 46]), 'Upper': np.array([34, 255, 255])},
}

# 檢測顏色
def detect_color(image, color):
    # gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度
    gs = cv2.GaussianBlur(image, (5, 5), 0)  # 高斯模糊
    hsv = cv2.cvtColor(gs, cv2.COLOR_BGR2HSV)  # HSV
    erode_hsv = cv2.erode(hsv, None, iterations=2) # 腐蝕
    inRange_hsv = cv2.inRange(erode_hsv, color_dist[color]['Lower'], color_dist[color]['Upper'])
    contours = cv2.findContours(inRange_hsv.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
    if len(contours) > 0:
        draw_color_area(image, contours)
        return True
    else:
        winsound.Beep(440, 5000)
        return False

這裡,我們先對圖片做了一次高斯模糊、然後將其轉換為 HSV 格式,經過侵蝕以後傳給 inRange() 函式,這樣我們就得到了所有符合這個區間範圍的點。接下來,單單找到顏色還不行,我們還需要根據這些點得到一個輪廓,此時, findContours() 函式再次登場,為了讓使用者更直觀地找到對應的顏色區域,我們這裡使用下面的方法將其“畫”出來:

# 標記顏色區域
def draw_color_area(image, contours):
    max, index = 0, -1
    for i in range(len(contours)):
        area = cv2.contourArea(contours[i])
        if area > max:
            max = area
            index = i
    if index >= 0:
        rect = cv2.minAreaRect(contours[index])
        cv2.ellipse(image, rect, color_marker, 2, 8)
        cv2.circle(image, (np.int32(rect[0][0]), np.int32(rect[0][1])), 2, color_marker, 2, 8, 0)

以中間部分的二維碼圖塊為例,此時,我們可以得到下面的結果,這是做了兩次顏色檢測得到的,第一次檢測綠色,第二次檢測金色:

“西安一碼通” 防疫二維碼:顏色檢測

OCR 識別

OCR 識別沒有太多懸念,因為我們直接使用 PaddleOCR 即可,因為我們已經完成對圖塊的切分,只需要依次對圖片進行檢驗即可:

python -m pip install paddlepaddle==2.0.0 -i https://mirror.baidu.com/pypi/simple
python -m pip install paddleocr

在安裝的過程中,可能會得到這樣的錯誤資訊: Microsoft Visual C++ 14.0 is required 。如果你安裝了 Visual Studio 依然提示錯誤,解決方案就是找到 Visual Studio 安裝包,然後勾選那些和 Microsoft Visual C++ 14.0 相關的可選的安裝項,再安裝了這些必要元件以後,重新使用pip 安裝即可。

“Microsoft Visual C++ 14.0 is required” 錯誤資訊

因為 PaddleOCR 接受的是 PIL 庫中的 Image 型別,所以,在拆分圖塊的時候,實際上是為偉哥圖塊生成了一個對應的檔案。此時,OCR 識別部分的程式碼實現如下。首先,我們需要初始化 PaddleOCR ,首次執行會自動下載訓練好的模型檔案:

# PaddleOCR
ocr = PaddleOCR()

這裡,我們通過 detect_text 來檢測每個圖塊的文字,並在原始圖片中標記出文字位置:

# 檢測文字
def detect_text(image, block):
    _, block_rect, block_file = block
    block_x, block_y, _, _ = block_rect
    result = ocr.ocr(block_file)
    for line in result:
        boxes = line[0]
        texts = line[1][0]
        x = int(boxes[0][0])
        y = int(boxes[0][1])
        w = int(boxes[2][0]) - x
        h = int(boxes[2][1]) - y
        abs_x = block_x + x
        abs_y = block_y + y
        cv2.rectangle(image, (abs_x, abs_y), (abs_x + w, abs_y + h), color_marker, 2)
        yield texts

以底部圖塊的檢測結果為例,其文字位置標記及文字識別結果如下圖所示:

通過 OCR 識別出來的文字位置

通過 OCR 識別出來的文字資訊

成品展示

到現在為止,主要的部分我們已經編寫完成,接下來,我們只需要接入攝像頭,從攝像頭捕捉影象即可。這裡,請允許在下推薦一個非常好用的軟體: iVCam ,它可以讓手機搖身一變成為攝像頭,從而可以讓我們模擬掃描二維碼的場景。使用 OpenCV 捕捉來自攝像頭的圖片非常簡單,大家可以參考我曾經的部落格: 影片是不能P的系列:OpenCV人臉檢測 ,這裡我們直接給出程式碼:

def handle_video():
    cap = cv2.VideoCapture(0)
    while True:
        ret, image = cap.read()
        if ret:
           # 檢測畫面中的圖塊
            blocks = list(detect_blocks(image))

            # 處理每個圖塊
            for block in blocks:
                image = handle_block(image, block)

            # 展示處理結果
            cv2.imshow('QRCode Detecting', image)

            # 按 Q 退出
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
        else:
            continue

    cap.release() 
    cv2.destroyAllWindows()

此時,我們就可以看到下面的結果。可以注意到,在實際應用中,通過影片採集的影象會受到環境光照、拍攝角度等因素的影響,受此影響,我們的圖塊檢測在這個環節表現不佳,它甚至把整張圖片當成了一個圖塊,這直接導致最重要的二維碼沒有檢測出來。百度的 PaddleOCR 表現倒是可圈可點,識別速度和準確性還是非常出色的。對於影片這種級別的輸入,特別是在人流量較大的商場、車站等場所,對於識別準確性、可靠性都有著非比尋常的要求,如果要考慮這個思路的落地,應該在影象採集的預處理、影象檢測的演算法上去下功夫,特別是在拆分圖塊這個環節,識別的準確性還會受到二維碼樣式的影響,而這些顯然是這篇部落格背後的故事啦!正所謂,”路漫漫其修遠兮,吾將上下而求索”,如果大家對這個專案感興趣的話,可以到 Github 上做進一步的瞭解。

通過攝像頭檢測防疫二維碼

本文小結

寫完這篇部落格的時候,我不由地會想,也許,螢幕前的某個人會在看完這篇部落格以後,一臉鄙夷地說道,就這?可這的確就是基礎性研究的現狀,即:投入了時間和精力,並不一定能得到滿意的結果。我們從小到大接受的關於成功的理念,無非都是“只要功夫深,鐵杵磨成針”、“吃得苦中苦,方為人上人”……可不知道為什麼,這種理念在被一點一點的打破,某種意義上來講,國家和個人在這個時代面對的選擇是相似的,在選擇掙快錢還是掙慢錢這個問題上。多年以前,在實驗室裡搗騰化學試劑的我,曾經一度認為做實驗、分析資料、寫報告這些事情是枯燥而無用的,因為在當時看來,這些東西距離實際應用都挺遙遠的。可是,此刻我大概不得不承認,這些基礎工作的重要性。的確,寫演算法、做模型,這些事情都是科學家去做的事情,我們普通人只要奉行“拿來主義”就好,可當 OpenCV 就放在你手裡,而你依然做不好這件事情的時候,大概還是我輸了罷,說“認真你就輸了”的人,真的真的真的認真過嗎?

⑥專案原始碼案例分享有
如果你用得到的話可以直接拿走,在我的QQ技術交流群裡群號:948351247(純技術交流和資源共享,廣告勿入)以自助拿走
點選這裡 領取