WINDOWS上で7セグLED数字を判定(python+OPENCV+PYPYLON)

概要

 過去にラズパイでの7セグLED読み取りについて投稿していますが、今回はWindows10環境で検証します。
 今回の主な試みは下記です。
 ①空白(桁数が少ない),少数点に対応
 ②python から basler 社カメラを使ったプログラム作成
  → カメラと表示器の距離を少し離して読み取る確認
 ③プログラムのマイナーチェンジ(機能改善)

7セグLED数字判定状況

 判定状況の動画です。動画は容量抑制の為、4倍速で再生しています。プログラム内にも確認目的で遅延処理やLED表示変更等の処理が入っているので、最大速度という訳ではありません。感覚的には十分実用に堪えるレベルと思います。

 ラズパイ+USBカメラ検証時と基本は同じですが、下図の通り、未表示部は “*” で表示しています。ドッド表示(小数点)にも対応しています。
 ランダム表示切替と画像解析を同じプログラムで行っていますので、表示内容と解析結果が同じかの判定が出来ます。判定結果は画面左上に、“合格回数/判定回数(合格率)”の様に表示しています。約2000(回/時間)の頻度で表示更新するので、下図は約4時間経過後100%を維持していることを示します。

  

カメラ選定

 LED表示器とカメラの位置関係です。カメラとレンズの組み合わせで距離を離すことが出来ます。
 表示器への人の視線を遮らず安全な位置、また、計測中に人がカメラの前を横切らないことを想定し、次の配置で検証しています。実際の取り付けには問題もありそうですが・・・。

 今回は、BASLERの“acA2500-14um” というカメラを使います。
 BASLER社レンズセルクターで、カメラ型式,撮影距離,視野高さを設定すると対象レンズを選定出来ます。この選定結果に従い、焦点距離25mmのレンズを使用しましたが、必ずしもこの選択が正しい訳ではありません。

BASLER 社レンズセレクター
https://www.baslerweb.com/jp/products/tools/lens-selector/

プログラム

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

 下記の様なテンプレートマッチングを行う為のテンプレート画像を作成します。前の投稿では1回づつ作成するテンプレートの文字をプログラム内で書き換えていましたが、今回は連続的に生成できる様に少し改善しました。ドット表示用も作成します。
 空白箇所(未表示)については、真っ白なテンプレートも作成しましたが、表示に関係なくスコアが全て100%になってしまう為、一旦保留しました。最大確定スコアが基準値より小さい場合は、空白(未表示)と判定することにしています。

 以下、プログラムです。

'''
Camera : Basler acA2500-14um (USB3)
Lens   : C125-2522-5M
WORK_DISTANCE : 1200 mm , ANGLE : 48 degree ([Horizontal]800mm , [Vertical]900mm)  
'''

from pypylon import pylon
import numpy as np
import cv2
import serial
import time
import sys

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

    ser.write(b"\r")
    time.sleep(0.1)
    ser.close

# conecting to the first available camera
camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateFirstDevice())

# Grabing Continusely (video) with minimal delay
camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly) 
converter = pylon.ImageFormatConverter()

# converting to opencv bgr format
converter.OutputPixelFormat = pylon.PixelType_BGR8packed
converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned

# 変数設定
dst_w = 1120                # 変換幅
dst_h = 680                 # 変換高

P_LU=[700,950]              # [左上] 射影変換
P_RU=[1820,950]             # [右上] 射影変換
P_LD=[760,1390]             # [左下]  射影変換
P_RD=[1830,1390]            # [右下]  射影変換

SEG_X=58                    # X : SEG-LOCATION-X
SEG_Y=51                    # Y : SEG-LOCATION-Y
SEG_W=113                   # W : SEG-WIDTH
SEG_H=162                   # H : SEG-HEIGHT
S_S_X=125.6                 # SEG_SPAN_X
S_S_Y=1.6                   # SEG_SPAN_Y

for i in range(21):
    if i < 10:
        DISP_CHR = str(i) + ""    
    elif i < 20:
        DISP_CHR = str( i - 10 ) + "."
    else:
        DISP_CHR = " "

    
    DISP_7SEG(DISP_CHR)  # 7SEG表示

    tmp_no = ("00" + str(i))[-2:]
    file_name = "std_" + tmp_no + ".jpg"

    while camera.IsGrabbing():
        grabResult = camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException)

        if grabResult.GrabSucceeded():
            # Access the image data
            image = converter.Convert(grabResult)
            img = image.GetArray()
        
            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),5)

            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[ 310 : 560 , 10 : 1100 ]
            img_gry = cv2.cvtColor(img_crp, cv2.COLOR_BGR2GRAY)
            ret, img_bin = cv2.threshold(img_gry , 127 , 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==5:
                    tmp_img = img_bin[ tmp_Y : tmp_Y + SEG_H , tmp_X : tmp_X+SEG_W ]
                    # cv2.imshow('4_tmp_img', tmp_img)
                    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.imshow('3_img_bin', img_bin)

            k = cv2.waitKey(1)
            if k == 27:
                break
            elif k == ord('q'):
                sys.exit()

        grabResult.Release()
    
# Releasing the resource    
camera.StopGrabbing()
cv2.destroyAllWindows()

②解析プログラム

 7セグLED数字判定プログラムです。

''' 
Camera : Basler acA2500-14um (USB3) 
Lens : C125-2522-5M
WORK_DISTANCE : 1200 mm , ANGLE : 48 degree ([Horizontal]800mm , [Vertical]900mm)   
'''

from pypylon import pylon
import numpy as np
import cv2
import serial
import time
import random


def DISP_7SEG_1CHR( disp_str ):
    ser = serial.Serial("COM5",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():
    disp_chr = ""
    dot_pos=random.randint(4, 6)
    emp_pos=random.randint(0, 3)

    ser = serial.Serial("COM5", 9600)
    for i in range(8):
        tmp = random.randint(0, 9)

        if i >= emp_pos:
            disp_chr = disp_chr + str(tmp)
        
            if i == dot_pos:
                disp_chr = disp_chr + '.'
        else:
            #disp_chr = disp_chr + str(tmp)
            disp_chr = disp_chr + ' '


    ser.write(bytes(disp_chr,"UTF-8"))

    ser.write(b"\r")
    time.sleep(0.3)
    ser.close

    return disp_chr

# conecting to the first available camera
camera = pylon.InstantCamera(pylon.TlFactory.GetInstance().CreateFirstDevice())

# Grabing Continusely (video) with minimal delay
camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly) 
converter = pylon.ImageFormatConverter()

# converting to opencv bgr format
converter.OutputPixelFormat = pylon.PixelType_BGR8packed
converter.OutputBitAlignment = pylon.OutputBitAlignment_MsbAligned

# 変数設定
dst_w = 1120                # 変換幅
dst_h = 680                 # 変換高

P_LU=[700,950]              # [左上] 射影変換
P_RU=[1820,950]             # [右上] 射影変換
P_LD=[760,1390]             # [左下]  射影変換
P_RD=[1830,1390]            # [右下]  射影変換

SEG_X=75                    # X : SEG-LOCATION-X
SEG_Y=80                    # Y : SEG-LOCATION-Y
SEG_W=100                   # W : SEG-WIDTH
SEG_H=155                   # H : SEG-HEIGHT
S_S_X=124.5                 # SEG_SPAN_X
S_S_Y=0                     # SEG_SPAN_Y
s_mgn=55                    # マージン

# DISP_7SEG_1CHR("8.")       # 7SEG表示
dsp_str = DISP_7SEG_RANDAM()
chk_cnt=0
OK_CNT=0
NG_CNT=0
OK_RATIO=0

while camera.IsGrabbing():

    grabResult = camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException)

    if grabResult.GrabSucceeded():
        # Access the image data
        image = converter.Convert(grabResult)
        img = image.GetArray()
        
        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),5)

        # 射影変換座標 [[左上],[右上],[左下],[右下]]
        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=0
        crp_Y=280
        crp_W=1120
        crp_H=320
        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 , 127 , 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.65           # 低い値だと空白の誤差が増える
            num_dsp = -1

            # cv2.imshow('tmp_img', tmp_img)
            for j in range(20):
                i_tmpl=cv2.imread("./template_7seg/std_" +("00" + str(j))[-2:] + ".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

                    if j<10:
                        getVal[i] = str(num_dsp)
                    elif j<20:
                        getVal[i] = str(num_dsp-10) + "."
                    else:
                        getVal[i] = "!"

                    getScr[i] = "Pos" + str(i)+" : "+str(int(100*max_val))+" %"


            cv2.putText(img, getScr[i] , (2050, 900+i*80), cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 0, 0), 5, cv2.LINE_AA)
            if( i % 2 == 0 ):
                cv2.rectangle(img_bin, (tmp_X - s_mgn, tmp_Y - s_mgn), (tmp_X + SEG_W + s_mgn, tmp_Y + SEG_H + s_mgn),(0, 0, 255), thickness=6)
            else:
                cv2.rectangle(img_bin, (tmp_X - s_mgn, tmp_Y - s_mgn), (tmp_X + SEG_W + s_mgn, tmp_Y + SEG_H + s_mgn),(0, 0, 80), thickness=2)


        img[ 1450 : 1450 + crp_H , 550 : 550 + crp_W] = cv2.cvtColor(img_bin, cv2.COLOR_GRAY2BGR)

        cv2.putText(img, str(OK_CNT) + "/" + str(OK_CNT+NG_CNT) + '  (' + str(OK_RATIO) + ' %)' , (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 3, (0, 0, 255), 3, cv2.LINE_AA)

        dspText = getVal[0]+getVal[1]+getVal[2]+getVal[3]+getVal[4]+getVal[5]+getVal[6]+getVal[7]
        cv2.putText(img,dspText,(280,700),cv2.FONT_HERSHEY_SIMPLEX,10,(0,0,255),20,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)

        # 10回ごとに表示変更
        if chk_cnt > 3:
            if dsp_str == dspText.replace('*', ' ') :
                OK_CNT += 1
            else :
                NG_CNT += 1
                # x = input("""読み取りエラーです。""")

            OK_RATIO = int(100 * OK_CNT / (OK_CNT+NG_CNT))

            dsp_str = DISP_7SEG_RANDAM()
            chk_cnt = 0

        chk_cnt += 1

        k = cv2.waitKey(1)
        if k == 27:
            break
        elif k==ord('c'):
            dsp_str = DISP_7SEG_RANDAM()

    grabResult.Release()
    
# Releasing the resource    
camera.StopGrabbing()
cv2.destroyAllWindows()

焦点距離12mmレンズでの確認

 今回の検証では、焦点距離 25mm のレンズを使いましたが、参考までに焦点距離12mmレンズを使って確認しました。
 7セグLEDとカメラの位置関係は同じなので、視野が大きくなっています。カメラ本体は同じですので、視野が大きくなると解像度は低くなります。
 約3時間 連続動作させて合格率は100%なので、焦点距離が 12mmでも良いかもしれません。

 BASLER社レンズセレクターで、焦点距離(12mm)と撮影距離(1200mm)を入力すると視野の大きさ(幅564mmX高さ424mm)が判ります。

BASLER 社レンズセレクター
https://www.baslerweb.com/jp/products/tools/lens-selector/

 反対にBASLER社レンズセレクターで、焦点距離(25mm)と視野の大きさ(幅564mmX高さ424mm)を入力すると、撮影距離は2499mmと設定され、 焦点距離(25mm)レンズの撮影距離の1つの目安になります。

BASLER 社レンズセレクター
https://www.baslerweb.com/jp/products/tools/lens-selector/

まとめ

 表示器の色、明るさ、サイズ等によっても結果は異なると思いますが、基本検証はある程度出来たのではないかと思います。
 python , OpenCV 等、使ったことがありませんでしたが、私の様な時代遅れの人間はとても驚かされます。