ラズパイ4 7セグLED数字をテンプレートマッチングで判定

機能

 ラズパイに接続したUSBカメラで、7セグLEDを斜め上方向から撮影し、OpenCVのテンプレートマッチングを使って、数字を認識します。
 大まかな流れとしては、先ず、斜め上から撮影した画像対象領域を正面から見た様な画像に射影変換し、二値化処理します。
 予めプログラム上で設定している全8文字の各検索領域を1箇所づつ順番にテンプレートマッチングで、[0]~[9] のテンプレート画像と照合して、最もスコアの高い数字を、各領域に表示されている数字と判定しています。
 下のプログラム実行時に表示するフォームには、撮影画像の上に射影変換・二値化処理画像、最終変換結果、各文字検索領域の判定時のスコア値、射影変換対象領域枠を表示しています。
 スコア値があまりに低い場合や他数字とスコア値が近い場合などは判定が誤っている可能性もあるので判定条件に考量する必要があります。

 次の写真は対象領域を射影変換した直後の画像です。上方から撮影した画像は下部(カメラから見て遠い位置)は狭くなっていますが、射影変換後は補正されています。
 今回の目的では必ずしも射影変換は必要なかったのかもしれませんが、個人的には勉強になりました。

 下図は対象領域を限定し、二値化処理後の画像です。

 今回の確認のUSBカメラと7セグLEDの位置関係です。
 7セグLEDはもともと人が確認する為のものなので、人の視線を出来るだけ遮らない様にと思い、上方から撮影します。下図では三脚が視線を遮っていますが、実験ですのでご了承下さい。
 上方から撮影することによって、室内光等による光の反射の影響を受けにくくする効果があると感じました。また、LED前に有色透明プラスチック板でカバーすると文字が鮮明になります。

  [0]~[9] の テンプレート画像です。



プログラム

①テンプレート作成プログラム

 テンプレート画像作成用プログラムです。対象数字をプログラム内で変更して、 [0]~[9] の テンプレート画像 を作成する必要があります。

import numpy as np
import cv2
import serial
import time

def DISP_7SEG( disp_str ):
    ser = serial.Serial('/dev/ttyUSB0',9600)
    for i in range(8):
        ser.write(bytes( disp_str , "UTF-8" ))

    ser.write(b"\r")
    time.sleep(0.1)
    ser.close
    
    
# 変数設定
dst_w = 374                 # 変換幅 1120
dst_h = 226                 # 変換高 680

P_LU=[100,220]              # [左上] 射影変換
P_RU=[530,220]              # [右上] 射影変換
P_LD=[150,332]              # [左下]  射影変換
P_RD=[493,334]              # [右下]  射影変換

SEG_X=30                    # X : SEG-LOCATION-X
SEG_Y=21                    # Y : SEG-LOCATION-Y
SEG_W=29                    # W : SEG-WIDTH
SEG_H=50                    # H : SEG-HEIGHT
S_S_X=30.2                  # SEG_SPAN_X
S_S_Y=0.8                   # SEG_SPAN_Y

DISP_CHR="8"
file_name = "std_" + DISP_CHR + ".jpg"
DISP_7SEG( DISP_CHR )       # 7SEG表示

capture=cv2.VideoCapture(0)

while(True):
    ret,img = capture.read()
    
    pts = np.array([P_LU,P_RU,P_RD,P_LD])
    pts = pts.reshape((-1,1,2))
    cv2.polylines(img,[pts],True,(0,0,255),2)

    cv2.namedWindow('1_img_org', cv2.WINDOW_NORMAL)
    cv2.imshow('1_img_org', img)
    
    # 射影変換座標 [[左上],[右上],[左下],[右下]]
    pts_src = np.float32([ P_LU , P_RU , P_LD , P_RD ])
    pts_dst = np.float32([[0,0],[dst_w,0],[0,dst_h],[dst_w,dst_h]])

    # 射影変換
    M = cv2.getPerspectiveTransform( pts_src , pts_dst )
    img_trn = cv2.warpPerspective( img , M , ( dst_w , dst_h ))
    cv2.namedWindow('2_img_trn', cv2.WINDOW_AUTOSIZE)
    cv2.imshow('2_img_trn',img_trn)
    
    # 対象領域を絞り込む
    img_crp = img_trn[ 100 : 200 , 80 : 400]
    img_gry = cv2.cvtColor(img_crp, cv2.COLOR_BGR2GRAY)
    ret, img_bin = cv2.threshold(img_gry , 130 , 255 , cv2.THRESH_BINARY_INV )
    
    # セグメント領域処理
    for i in range(8):
        tmp_X = int(SEG_X + i * S_S_X)
        tmp_Y = int(SEG_Y + i * S_S_Y)

        if i==4:
            tmp_img = img_bin[ tmp_Y : tmp_Y + SEG_H , tmp_X : tmp_X+SEG_W ]
            cv2.imwrite("./template_7seg/"+file_name,tmp_img)

        cv2.rectangle(img_bin, (tmp_X, tmp_Y), (tmp_X + SEG_W , tmp_Y + SEG_H), (0, 0, 255))

    cv2.namedWindow('3_img_bin', cv2.WINDOW_AUTOSIZE)
    cv2.imshow('3_img_bin',img_bin)    
    
    k = cv2.waitKey(1)
    if k == 27:
        break
    
capture.release()
cv2.destroyAllWindows()

②数字判定プログラム

 数字判定プログラムです。射影変換やテンプレートマッチング領域等、プログラム内で座標設定しています。
 初期設定として、カメラと7セグLEDの位置調整やプログラム内での座標調整が必要です。

import numpy as np
import cv2
import serial
import time
import random

def DISP_7SEG( disp_str ):
    ser = serial.Serial('/dev/ttyUSB0',9600)
    for i in range(8):
        ser.write(bytes( disp_str , "UTF-8" ))

    ser.write(b"\r")
    time.sleep(0.1)
    ser.close
    
def DISP_7SEG_RANDAM():
    ser = serial.Serial("/dev/ttyUSB0", 9600)
    for i in range(8):
        tmp = random.randint(0, 9)
        ser.write(bytes(str(tmp), "UTF-8"))
    ser.write(b"\r")
    time.sleep(0.3)
    ser.close
    
    
# 変数設定
dst_w = 374                 # 変換幅 1120
dst_h = 226                 # 変換高 680

P_LU=[100,220]              # [左上] 射影変換
P_RU=[530,220]              # [右上] 射影変換
P_LD=[150,332]              # [左下]  射影変換
P_RD=[493,334]              # [右下]  射影変換

SEG_X=30                    # X : SEG-LOCATION-X
SEG_Y=21                    # Y : SEG-LOCATION-Y
SEG_W=29                    # W : SEG-WIDTH
SEG_H=50                    # H : SEG-HEIGHT
S_S_X=30.2                  # SEG_SPAN_X
S_S_Y=0.8                   # SEG_SPAN_Y
s_mgn=20                    # マージン

DISP_7SEG_RANDAM()       # 7SEG表示

capture=cv2.VideoCapture(0)

while(True):
    ret,img = capture.read()
    
    pts = np.array([P_LU,P_RU,P_RD,P_LD])
    pts = pts.reshape((-1,1,2))
    cv2.polylines(img,[pts],True,(0,0,255),2)

    cv2.namedWindow('1_img_org', cv2.WINDOW_NORMAL)
    cv2.imshow('1_img_org', img)
    
    # 射影変換座標 [[左上],[右上],[左下],[右下]]
    pts_src = np.float32([ P_LU , P_RU , P_LD , P_RD ])
    pts_dst = np.float32([[0,0],[dst_w,0],[0,dst_h],[dst_w,dst_h]])

    # 射影変換
    M = cv2.getPerspectiveTransform( pts_src , pts_dst )
    img_trn = cv2.warpPerspective( img , M , ( dst_w , dst_h ))
    cv2.namedWindow('2_img_trn', cv2.WINDOW_AUTOSIZE)
    cv2.imshow('2_img_trn',img_trn)
    
    # 対象領域を絞り込む
    crp_X=80
    crp_Y=100
    crp_W=294
    crp_H=100
    img_crp = img_trn[ crp_Y : crp_Y + crp_H , crp_X : crp_X+crp_W ]
    img_gry = cv2.cvtColor(img_crp, cv2.COLOR_BGR2GRAY)
    ret, img_bin = cv2.threshold(img_gry , 130 , 255 , cv2.THRESH_BINARY_INV )
    
    getVal = [ "*", "*", "*", "*", "*", "*", "*", "*" ]
    getScr = ["*", "*", "*", "*", "*", "*", "*", "*"]

    # セグメント領域処理
    for i in range(8):
        tmp_X = int(SEG_X + i * S_S_X)
        tmp_Y = int(SEG_Y + i * S_S_Y)
        tmp_img = img_bin[tmp_Y-s_mgn: tmp_Y + SEG_H+s_mgn , tmp_X - s_mgn: tmp_X + SEG_W + s_mgn]

        maxVal_All = 0.4
        num_dsp = -1

        # cv2.imshow('tmp_img', tmp_img)
        for j in range(10):
            i_tmpl=cv2.imread("./template_7seg/std_" + str(j) + ".jpg",0)
            result = cv2.matchTemplate(tmp_img,i_tmpl,cv2.TM_CCOEFF_NORMED)
            min_val , max_val , min_loc , max_loc = cv2.minMaxLoc(result)
            # print(str(j)+" : "+str(max_val))

            if max_val > maxVal_All:
                num_dsp = j
                maxVal_All = max_val

                getVal[i] = str(num_dsp)
                getScr[i] = "P" + str(i)+": "+str(int(100*max_val))+" %"
        
        if i<4:
            cv2.putText(img, getScr[i] , (20, 370+i*30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)
        else:
            cv2.putText(img, getScr[i] , (220, 370+(i-4)*30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)   

    img[ 0 : crp_H , 0 : crp_W] = cv2.cvtColor(img_bin, cv2.COLOR_GRAY2BGR)

    dspText = getVal[0]+getVal[1]+getVal[2]+getVal[3]+getVal[4]+getVal[5]+getVal[6]+getVal[7]
    cv2.putText(img,dspText,(80,200),cv2.FONT_HERSHEY_SIMPLEX,3,(0,0,255),5,cv2.LINE_AA)
    cv2.namedWindow('1_img_org', cv2.WINDOW_NORMAL)
    cv2.imshow('1_img_org', img)
    # cv2.namedWindow('3_img_bin', cv2.WINDOW_NORMAL)
    # cv2.imshow('3_img_bin', img_bin)
        
    print(dspText)
    time.sleep(0.3)
        
    k = cv2.waitKey(1)
    if k == 27:
        break
    elif k==ord('c'):
        DISP_7SEG_RANDAM()
    
capture.release()
cv2.destroyAllWindows()

まとめ

 今回対象にしている赤色の7セグLEDとは別に黄色の7セグLEDも評価しています。赤色の方は比較的安定していますが、黄色の方は周囲の照明,自然光等のちょっとした変化に影響され非常に不安定なので、評価を中断しています。
 ラズパイでは、一応連続的に動作しますが、結構重たい感じです。“c” ボタンを押すと表示内容を切り換えする様にしていますが、平均1~2秒くらい、長い場合は10秒程度待つことがあります。
 一方、7セグLEDに接続しているシリアルポート名を変更すれば、同じプログラムで python + OpenCV をインストールしたWindows10 PC上で動作することを確認しました。もともと私のパソコンは今回の様な実験を行うには非力と常々感じているのですが、感覚的に少し遅れると感じる程度で動作しています。



  

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です