ラズパイでディープラーニング:交通標識認識プログラム例

標識認識

前回のブログでは、ラズパイが持っているフォント数字を学習させることで、かなり高い精度でパソコンのフォント印字数字を認識する学習プログラムを紹介した。今度は、交通標識を機械学習することを試みた。交通標識は、形状が、円、四角、三角に分けられ、同じ形状でも、色が異なり、数字が記されていることもある。フォント数字の学習方法で使った白黒の二値画像ではなく、RGBのカラー画像で学習する方が、認識率が高まるはず。今回、カラーの交通標識を機械学習して、交通標識を認識するプログラムを作成したので、その手順を記録に残すことにした。
この結果は、ラズマウスが交通標識を認識し、その指示に従って走行するプログラムに応用できる。

●完成したラズパイで学習した交通標識認識事例

ラズパイにWEBカメラを接続して、「D」キーを押した時にキャプチャした画像を認識プログラムで交通標識を認識する。この事例は、CNN(Convolution Neural Network)モデルで学習したプログラムで認識したもの。標識は25mm(W) x 25mm(H)で、交通標識画像をプリンターで印刷し、その標識を抽出(ブルーで外形輪郭)して、CNNモデルで標識を認識して、その結果を表示した。以下に、交通標識の選択、標識サンプルの作成、学習モデルの作成(CNN)、認識プログラム事例について記載する。

標識認識前後

●ラズパイのプログラム環境

深層学習(ディープラーニング)のプログラムを作る時に必要なモジュールをインストールする必要があるが、今回はRaspberry Pi 4BにフルセットでRaspberry Pi OSをインストールし、必要なモジュールTensorflow, Keras, sklearn, OpenCVをインストールした。最新のTensorflowにはKerasが含まれているが、ここでは単独Kerasをインストールして使った。
<使用したラズパイ>
              Raspberry Pi 4B(4GB) :MicroSDHC(32GB)、Raspberry Pi OS

<各モジュールのバージョン>
              Tensorflow : 1.14.0 (tensorflow.keras : 2.2.4 tf)
              Keras : 2.3.1
              OpenCV : 3.2.0

●交通標識認識の学習ステップ

 (1)WEBサイトから認識に使える交通標識画像を選択
 (2)学習用交通標識の作成
 (3)学習モデルの構築と保存
 (4)学習モデルを使った交通標識の認識

●WEBサイトから認識に使える交通標識画像を選択

WEBサイトから適当な交通標識画像をダウンロード。ダウンロードした11枚の画像のサイズは異なっているので、50 x 50 pixelのサイズにそろえ、標準の交通標識画像とした。フォントと異なり形状の異なる交通標識はないので、縦と横をそれぞれ100%~50%に圧縮し変形した標準画像を作成。

<11種類の50 x 50 pixelの標準の交通標識画像>
11の標準標識
<標準交通標識を縦横に圧縮した標識のサンプル>
標準変形画像

これで231の標準サンプル画像が得られた(./pictureDXフォルダーに保存)

●学習用交通標識の作成

50 x 50 pixelの学習用の交通標識画像データを作る。標準サンプル画像はが231枚しかないので、それぞれを回転、拡大・縮小の画像処理を施して、学習データを水増しし、データを”./image/SignImage.npz”に保存。サンプルデータ数は19404となった。

<水増ししたサンプル標識画像>ランダムにピックアップ
水増し画像の一部
<プログラム例>RasSignSampleCreate.py
# -*- coding: utf-8 -*-
# 基本図形の交通標識: ./pictureSTD/*.png   11枚(50 x 50 pix)
# 基本図形の道路標識を、横方向に圧縮、縦方向に圧縮した変形図形を作成
# 画像サンプルを多くするため、回転、拡大で水増しする
# サンプル画像を出力するフォルダは、"./image/sign"(ランダムに抜き取り)
# データは、"./image/RasSignImg.npz"に保存
import os, glob
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import cv2, random
import time

def transDX(signList):
    # 変形した標準画像を出力するフォルダ
    if not os.path.exists("./pictureDX"):
        os.makedirs("./pictureDX")
    for no, fname in enumerate(signList):
        signName = os.path.basename(fname)		# フォルダー名無しにする
        signName = signName[:-4]	# 拡張子除き
        classNo = int(signName[:2])	# 先頭2文字がクラス番号
        signImg = cv2.imread(fname)		#BGR/ndarray		ファイル名はフルネームで指定
        baseH, baseW = signImg.shape[:2]		#入力画像のサイズ
        fname = "./pictureDX/{}-ORG.png".format(signName)	# 標準図形も./pictureDX/に保存
        cv2.imwrite(fname, signImg)	# BGR(ndarray)
        # 標準図形の標識の変形(横、縦を圧縮)
        for sc in range(5, 55, 5):		#0% ~ 50%圧縮
            signImgDX = cv2.resize(signImg, (int(baseW * (100 - sc) / 100), baseH))	# ndarray
            signH, signW = signImgDX.shape[:2]
            wx = (baseW - signW) // 2
            signImgX = np.full((baseH, baseW, 3), 255, dtype=np.uint8)		# 白の正方形(H=ww,W=ww, color=3)画像をつくる。
            signImgX[:, wx:wx+signW] = signImgDX # 中央に標識画像をコピー
            fname = "./pictureDX/{}-X{}.png".format(signName, sc)
            cv2.imwrite(fname, signImgX)		# BGR(ndarray)
            signImgDY = cv2.resize(signImg, (baseW, int(baseH * (100 - sc) / 100)))
            signH, sjgnw = signImgDY.shape[:2]
            wy = (baseH - signH) // 2
            signImgY = np.full((baseH, baseW, 3), 255, dtype=np.uint8)		# 白の正方形(H=ww,W=ww, color=3)画像をつくる。
            signImgY[wy:wy+signH, :] = signImgDY # 中央に標識画像をコピー
            fname = "./pictureDX/{}-Y{}.png".format(signName, sc)
            cv2.imwrite(fname, signImgY)	# BGR(ndarray)

def sampleCreate(signDxList, imageSize):
    # 水増し変形標識画像の作成
    X, Y = [], []
    if not os.path.exists("./image/sign"):	# ランダムに選んだサンプル画像を保存
        os.makedirs("./image/sign")
    seqNo = list(range(len(signDxList)))
    random.shuffle(seqNo)		# 変形した図形の並びをシャッフル
    for no, picNo in enumerate(seqNo):
        fname = signDxList[picNo]
        signName = os.path.basename(fname)
        classNo = int(signName[:2]) 
        signImg = cv2.imread(fname)		# ndarray(GBR)
        # 回転、縮小/拡大する:PIL画像で行うため、ndarray->PILに変換
        baseImg = Image.fromarray(np.uint8(signImg))	# numpy 配列画像を、PIL画像に変換
        for ang in range(-20, 22, 2):	# 回転-20, -18,....0, ...18, 20deg
            subImg = baseImg.rotate(ang, fillcolor=(255, 255, 255))	#回転後の隙間を白にする
            data = np.asarray(subImg)	# asarrayなので、data = subImgとなる
            X.append(data)
            Y.append(classNo)
            w = imageSize
            for ratio in range(8, 15, 3):	# 縮小・拡大する(70%, 100%, 130% , 160%)
                size = round((ratio/10) * imageSize)
                img2 = cv2.resize(data, (size, size), cv2.INTER_AREA)
                data2 = np.asarray(img2)
                if imageSize > size:	# 変形画像が小さい時は空白画像の中心にコピー
                    x = (imageSize - size) // 2
                    data = np.full((imageSize, imageSize, 3), 255, dtype=np.uint8)		# 白の正方形(H=ww,W=ww, color=3)画像をつくる。
                    data[x:x+size, x:x+size] = data2
                else:							# 変形画像が大きい時は、変形画像から定型サイズを切り抜く
                    x = (size - imageSize) // 2
                    data = data2[x:x+w, x:x+w]
                X.append(data)
                Y.append(classNo)
                # 参考にサンプリングで画像データを保存(400回に1回)
                if random.randint(0, 400) == 0:
                    fname = "image/sign/{0}({1})({2})({3}).png".format(signName[:-4], classNo, ang, ratio)
                    cv2.imwrite(fname, data)
    return X, Y

"""メイン関数"""
if __name__ == '__main__':
    # サイズの指定
    imageSize = 50 # 50x50
    # 標準標識の画像(50x50pixel)ファイルリスト。ファイル名={クラス番号}{標識名}-STD.png
    signList = glob.glob("./pictureSTD/*.png") 
    transDX(signList)	# 縦・横を#0% ~ 50%圧縮
    # 変形を含めた図形を、回転、縮小・拡大で水増しして、X、Yに加える
    signDxList = glob.glob("./pictureDX/*.png")		# 変形した図形 リスト
    X, Y = sampleCreate(signDxList, imageSize)
    X = np.array(X)
    Y = np.array(Y)
    np.savez("./image/RasSignImg.npz", x=X, y=Y)
    print("ok,", len(Y))

 

●学習モデルの構築と保存

CNNの学習モデルを構築し、モデルを“RasSignModelCNN_1.h5”のファイル名で保存した。このモデル構築にあたって、「Pythonで動かして学ぶ!あたらしい機械学習の教科書(伊藤真著)翔泳社」を参考にした。

交通標識認識CNN図

1層目、2層目は畳み込み層、3層目はマックスプーリング層、4層目は畳み込み層、5層目でマックスプーリング層を入れ、6層目はFlattenで6400個に平坦化、7層目で128個の全結合、最後の8層目は出力が11個の全結合層となる。データは訓練用14553個、検証用4851個
ラズパイではこの計算に約30分かかった。同じプログラムをWindows10 PCで走らせると、古いPC(Intel-i7:4core)でも3分程度で完了した。エポックが5で収束し正答率は99.96%となった。保存された次の学習モデルファイルを使って、文字認識を実行する。
(CNN文字認識学習モデルファイル)RasSignModelCNN_1.h5

<プログラム例>RasLearnSignCNN.py
# -*- coding: utf-8 -*-
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten, Activation
from keras.layers import Conv2D, MaxPooling2D
from keras.optimizers import SGD, Adam, RMSprop
from keras.utils import np_utils
from sklearn import model_selection
import numpy as np
import time
import os
imgW, imgH = 50, 50
nClass = 11

def buildModelCNN():
    # CNNのモデルを構築
    model = Sequential()
    model.add(Conv2D(32, (3, 3), input_shape=(imgW, imgH, 3), activation='relu'))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(64, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Flatten())
    model.add(Dense(128, activation='relu'))
    model.add(Dropout(0.25))
    model.add(Dense(nClass, activation='softmax'))
    model.compile(loss='categorical_crossentropy',
        optimizer=Adam(),
        metrics=['accuracy'])
    return model

if __name__ == '__main__':
    import matplotlib.pyplot as plt
    signFileName = "./image/RasSignImg.npz"
    if not os.path.exists(signFileName):
        print("Sign File does not exist")
        exit()
    saveModelFile = "./RasSignModelCNN_1.h5"
    sTime = time.time()
    # フォント画像のデータを読む
    xy = np.load(signFileName)
    X = xy["x"]
    Y = xy["y"]
    # データを正規化
    X = X.reshape(X.shape[0], imgW, imgH, 3).astype('float32')
    X /= 255
    Y = np_utils.to_categorical(Y, nClass)
    # 訓練データとテストデータに分割
    X_train, X_test, y_train, y_test = model_selection.train_test_split(X, Y)
    # モデルを構築
    model = buildModelCNN()
    history = model.fit(X_train, y_train,
                                    batch_size=128, epochs=5, verbose=1,
                                    validation_data=(X_test, y_test))
    # モデルを保存
    model.save(saveModelFile)
    model.summary()
    # モデルを評価
    score = model.evaluate(X_test, y_test, verbose=0)
    print("Test Loss = {},  Test Accuracy = {}".format(score[0], score[1]))
    print('Conputation Time = ', time.time() - sTime)
    # 学習過程をグラフ化
    plt.figure(1, figsize = (12, 3))
    plt.subplots_adjust(wspace=0.5)
    plt.subplot(1,2,1)
    plt.plot(history.history['loss'], 'black', label='training')
    plt.plot(history.history['val_loss'], 'red', label='test')
    plt.legend()
    plt.subplot(1,2,2)
    plt.plot(history.history['accuracy'], 'black', label='training')
    plt.plot(history.history['val_accuracy'], 'red', label='test')
    plt.legend()
    plt.show()

 

<実行結果>
Train on 14553 samples, validate on 4851 samples
Epoch 1/5
14553/14553 [==============================] - 360s 25ms/step - loss: 0.3371 - accuracy: 0.8861 - val_loss: 0.0059 - val_accuracy: 0.9988
Epoch 2/5
14553/14553 [==============================] - 375s 26ms/step - loss: 0.0093 - accuracy: 0.9977 - val_loss: 0.0011 - val_accuracy: 0.9998
Epoch 3/5
14553/14553 [==============================] - 376s 26ms/step - loss: 0.0023 - accuracy: 0.9995 - val_loss: 1.2406e-04 - val_accuracy: 1.0000
Epoch 4/5
14553/14553 [==============================] - 376s 26ms/step - loss: 0.0052 - accuracy: 0.9990 - val_loss: 4.1104e-04 - val_accuracy: 1.0000
Epoch 5/5
14553/14553 [==============================] - 376s 26ms/step - loss: 0.0026 - accuracy: 0.9993 - val_loss: 0.0018 - val_accuracy: 0.9996
Test Loss = 0.0017822985765753151,  Test Accuracy = 0.9995877146720886
Conputation Time =  1899.2608473300934

 

<Model Summary>
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 48, 48, 32)        896
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 46, 46, 32)        9248
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 23, 23, 32)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 21, 21, 64)        18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 10, 10, 64)        0
_________________________________________________________________
dropout (Dropout)            (None, 10, 10, 64)        0
_________________________________________________________________
flatten (Flatten)            (None, 6400)              0
_________________________________________________________________
dense (Dense)                (None, 128)               819328
_________________________________________________________________
dropout_1 (Dropout)          (None, 128)               0
_________________________________________________________________
dense_1 (Dense)              (None, 11)                1419
=================================================================
Total params: 849,387
Trainable params: 849,387
Non-trainable params: 0

 

RasCNN-Result

●CNN学習モデルを使った標識認識

 ラズパイにUSB接続WEBカメラ(BSWHD06M:バッファロー製, 120万画素)を取付けOpenCVのビデオキャプチャで画像を取り込んだ。取り込んだ画像は、640 x 480 pixel。認識する標識は、25mm(W) x 25mm(H)の白い紙にプリンターでカラー印刷。問題は標識の外側輪郭をOpenCVで抽出する方法である。
<プログラムの構成>
(1)keras.modelsのload_modelでCNN学習モデルファイルを読み込む
              RasSignModelCNN_1.h5
(2)カメラでキャプチャした画像から標識外形輪郭を抽出
(3)キャプチャ画像の中から標識の輪郭を抽出し、標識学習モデルで標識名を認識し表示

<プログラム例>RasRecogSign.py
# -*- coding: utf-8 -*-
import numpy as np
import cv2
from operator import itemgetter		# sortのため
from keras.models import load_model
import os, glob

saveModelFile = "./RasSignModelCNN_1.h5"
model = load_model(saveModelFile)
font = cv2.FONT_HERSHEY_SIMPLEX
cap = cv2.VideoCapture(0)

def transImage(imgColor):
    imgGray = cv2.cvtColor(imgColor, cv2.COLOR_BGR2GRAY)	# 入力画像をグレースケール化
    imgGray = cv2.GaussianBlur(imgGray, (5, 5), 0)	# 5x5のgaussianフィルターでノイズ抑制
    imgThresh = cv2.adaptiveThreshold(imgGray, 255,  cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY, 11, 2)	# アダプティブ二値化
    return imgGray, imgThresh

def detectSignFromThresh(imgThresh):
    imgR, imgC = imgThresh.shape[:2]
    imgThresh, contours, hierarchy = cv2.findContours(imgThresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    wjMin, hjMin = 50, 50	#最小文字サイズ 
    signCont=[]		# parentがなく、childがあるcontoursのリスト
    signRectList = []		# 選択された標識外形座標リスト
    if len(contours) > 0:
        for i in range(len(contours)):
            parentNo = hierarchy[0][i][3]	# 0:Next, 1:Prev, 2:Child, 3:Parent
            childNo = hierarchy[0][i][2]
            if parentNo != -1 and childNo > 0:	# parentがなく、childがある輪郭を選択
                signCont.append([contours[i], hierarchy[0][i]])
        for i in range(len(signCont)):
            cont = signCont[i][0]
            (xj, yj, wj, hj) = cv2.boundingRect(cont)		#(x, y , w, h) = 外接矩形の座標を示すタプル。 座標のデータの型はint
            if wj >= wjMin and  hj >= hjMin:	# エリアがwjMin x hjMin より大きいならOK(小さなノイズを除去)
                ptj1 = (xj, yj)
                ptj2 = (xj + wj, yj + hj)
                signRectList.append([i, [ptj1, ptj2], cont])
        noRect = len(signRectList)
        rectDelNo = []
        statJ = False
        for i in range(noRect):		# 入れ子になっている輪郭は除外
            statJ = False
            no1, [pti1, pti2], cont = signRectList[i]
            dx = (pti2[0] - pti1[0]) * 0.3
            dy = (pti2[1] - pti1[1]) * 0.3
            for j in range(noRect):
                if i == j:
                    continue
                no2, [ptj1, ptj2], cont = signRectList[j]
                statJ = False
                if (pti1[0] - dx) <=  ptj1[0]  and ptj1[0] <= pti1[0]:	# pxi - dx < pxj < pxi
                    if (pti1[1] - dy) <=  ptj1[1]  and ptj1[1] <= pti1[1]:	# pyi - dy < pyj < pyi
                        if pti2[0] <=  ptj2[0]  and ptj2[0] <= (pti2[0] + dx):	# pxi < pxj < pxi + dx
                            if pti2[1] <=  ptj2[1]  and ptj2[1] <= (pti2[1] + dy):	# pyi < pyj < pyi + dy
                                statJ = True	# jが親だった場合、検索終了
                                break
            if statJ == True:	# 親があった場合、削除リストにiを追加
                rectDelNo.append(i)
        rectDelNo.reverse()			# 後から順に削除
        for i in range(len(rectDelNo)):
            del signRectList[rectDelNo[i]]
    return contours, signRectList

def makeAImgColor(imgColor, pt):
    pt1, pt2 = pt
    if ((pt2[1] - pt1[1]) > 0) and ((pt2[0] - pt1[0]) > 0):
        imgCrop = imgColor[pt1[1] : pt2[1], pt1[0] : pt2[0]]		# [row1 : row2, col1 : col2]
        w = pt2[0] - pt1[0]
        h = pt2[1] - pt1[1]
        ww = round(max(w, h) * 1.1) 	# w, hの大きい方の長さwwで正方形画像、余白10%
        spc = np.full((ww, ww, 3), 255, dtype=np.uint8)		# 画像を白に
        wy = (ww-h)//2	#高さ方向の余白を計算
        wx = (ww-w)//2	#幅方向の余白を計算
        spc[wy:wy+h, wx:wx+w, :] = imgCrop	# 余白の内側に標識画像を入れる
        img50x50 = cv2.resize(spc, (50, 50)) # 50 x 50サイズに揃える
        imgAI = img50x50.astype("float32") / 255		# CNNの場合、入力画像は50x50x3(GBR)
        statOK = True
    else:
        imgAI = imgColor.copy()
        statOK = False
    return statOK, imgAI 

def recognizeSign(imgAI):
    probArray = model.predict(np.array([imgAI]))	# 入力はimgのリスト、1個だけなので[imgAI]
    result = probArray.argmax()	# probArray(np.array)の 最大値のindex(0-10)が検出標識
    prob = probArray.max()	# 検出標識の確率
    return result, prob

def getSignName(signList):
    signDict = {}
    for no, fname in enumerate(signList):
        signName = os.path.basename(fname)		# フォルダー名無しにする
        classNo = int(signName[:2])	# 先頭2文字がクラス番号
        signName = signName[2:-8]	# Sign名の取り出し(クラス番号、'-STD.png' 除去)
        signDict [classNo] = signName
    return signDict

def drawResult(allContours, signRectList, recogSignList, imgColor, imgThresh):
    rgbIm = imgThresh.copy()
    imgThreshColor = cv2.merge((rgbIm, rgbIm, rgbIm))	# 二値化画像をRGBに
    imgThreshColor = cv2.drawContours(imgThreshColor, allContours, -1, (0,255,0), 1)	# 全ての輪郭を「グリーン」で描画
    for i in range(len(recogSignList)):	# 検出したサイン
        signNo, signIndex, signName, prob = recogSignList[i]
        contNo, [pt1, pt2], cont = signRectList[signNo]
        (xb, yb) = pt1
        pttxt = (xb, yb-10)
        pttxt2 = (xb-20, yb-10)
        signNoName = str(signIndex) + ":" + signName
        cv2.putText(imgColor, signNoName, pttxt, font, 0.5, (0, 255, 255), 1, cv2.LINE_AA, False)	# クラス番号と認識標識名を表示
        cv2.rectangle(imgColor, pt1, pt2, (255, 255, 0), 2)	# 検出サイン外形矩形をブルーで囲む
        cv2.rectangle(imgThreshColor, pt1, pt2, (255, 255, 0), 2)	# 検出サイン外形矩形をブルーで囲む
    cv2.namedWindow('Result', cv2.WINDOW_AUTOSIZE)
    cv2.imshow('Result',imgColor)
    cv2.moveWindow('Result', 500,0)
    cv2.namedWindow('imgThreshColor', cv2.WINDOW_AUTOSIZE)
    cv2.imshow('imgThreshColor',imgThreshColor)
    cv2.moveWindow('imgThreshColor', 500, 540)		#Display = 1920 x 1080

def contCheck(contNo, cont):
    (xb, yb, wb, hb) = cv2.boundingRect(cont)
    conArea = cv2.contourArea(cont)  # 輪郭の面積計算(単位がピクセルではない)
    #回転を考慮した外接矩形面積は実態に近い
    rectR = cv2.minAreaRect(cont)		# rectR : ((x,y), (w,h), angle) x,y,w,h,angleはfloat 
    box = cv2.boxPoints(rectR)	# 4角のコーナーの座標
    boxNP = np.int0(box)	# 整数に。[[x1,y1],[x2,y2],[x3,y3],[x4,y4]]
    conBoxArea = cv2.contourArea(boxNP)  # 輪郭の面積計算 → 単位はピクセルではないので、w x hは使えない
    stat = False if conArea /  conBoxArea < 0.4 else True		# 輪郭面積が計算上の矩形面積より小さい場合は対象外
    return stat

"""メイン関数"""
if __name__ == '__main__':
    import os, glob
    iFlag = True
    signList = glob.glob("./pictureSTD/*.png") 
    signDict = getSignName(signList)	# 標準標識画像のファイル名から「クラス番号」と「標識名」を得る
    while(iFlag):
        ret, frame = cap.read()
        cv2.namedWindow('VIDEO', cv2.WINDOW_AUTOSIZE)
        cv2.imshow('VIDEO', frame)
        cv2.moveWindow('VIDEO', 0, 540)
        k =  cv2.waitKey(1) & 0xFF
        if k == ord('q'):
            print("QUIT")
            break
        elif  k == ord('d'):	# 文字検出
            imgGray, imgThresh = transImage(frame)		# 解析用の画像変換
            allContours, signRectList = detectSignFromThresh(imgThresh)
                    # allContours: 検出したすべての輪郭データ(原画に表示)
                    # signRectList = リスト[no, (p1x, p1y), contours]
            recogSignList = []
            if len(signRectList) > 0:		# サインを1個以上検出した場合
                for signNo in range(len(signRectList)):
                    contNo, [pt1, pt2], cont = signRectList[signNo]
                    if contCheck(contNo, cont):	# contoursデータから標識らしさをチェック
                        statOK, imgAI = makeAImgColor(frame, [pt1, pt2])	# 認識する50x50のカラー画像に加工(imgAI)
                        if statOK:
                            signIndex, prob = recognizeSign(imgAI)
                                # signIndex:認識した標識index, prob:確率
                            print("Recognized Sign Index = ", signIndex, signDict[signIndex], prob)
                            recogSignList.append([signNo, signIndex, signDict[signIndex], prob])
            else:
                print("NG! 認識ラベル無し")
            drawResult(allContours, signRectList, recogSignList, frame, imgThresh)
        else:
            continue
    k =  cv2.waitKey(0) 
    cap.release()
    cv2.destroyAllWindows()

 

<実行結果>
交通標識認識3画
二値化画像拡大
標識認識
<標識を認識のための画像処理>

まずはOpenCVを使い入力画像をグレースケール化、gaussianフィルターでノイズ抑制し、アダプティブ二値化を適用した。この二値画像では多くの輪郭が検出されるが、標識と他の画像を区別するため、50 x 50pixel以下の小さい輪郭は除去した。また標識の外側の二重線が輪郭として認識され、一つの標識に複数の輪郭が検出される(入れ子状態になる)ことがあり、この場合一番内側の輪郭だけを採用。標識の輪郭を検出するステップは次の通り。
              ①二値化後の画像から輪郭検出(階層情報付)
              ②輪郭階層で、親の輪郭を持たず、子の輪郭を持つ輪郭を選択
              ③上記①の中で、50 x 50 pixel以下の小さい輪郭(ノイズ)を除去
              ④上記②の中で、入れ子状態(同一の画像を示している輪郭)の外側輪郭を除去
CNN学習で認識した結果は黄色で「クラス番号」「標識名」を記した。今回の認識では、輪郭検出できれば、交通標識は100%の認識率となった。