PythonとPySimpleGUIとSSDを合体させて物体検出プログラムを作ってみた話::物体数カウントや検出物体の個別確認も可能

2021年6月19日

Object detection

久しぶりの投稿です。

最近、転職したこともあり中々時間が取れず更新がままならないですが、今持てる知識を全投入して物体検出プログラムを作成してみました。

本プログラムの特徴

  1. 物体検出はSSDを使用
  2. GUIはPysimpleGUIを使用
  3. 円グラフはMatplotlibで日本語表示させて表示
  4. USBカメラは3台切替可能
  5. 物体の分類数をカウントして表示
  6. 物体の総数をカウントして表示
  7. 物体の内訳数をカウントして表示
  8. 物体の単価と内訳数から小計を計算して表示
  9. バインディングボックスの形を変えることが可能(四角枠と線で示す)
  10. 検出物体の名前と検出精度の表示/非表示が可能
  11. 検出物体を別画面で個別に確認可能(動画/静止画の切替や拡大も可能)
  12. Main monitor と Total Price 画面をキャプチャして保存することが可能

動作映像

本プログラムは、これまでブログに掲載してきた知識をベースに作成しています。

興味がある方はリンクを貼っておきましたので、参考にしてもらえればと思います。

プログラムのポイント

物体検出に使用するSSDについて

SSDを使った物体検出については過去何度も記事にしています。詳しくは以下の記事を参考にしてください。

  1. Pythonを学んで1年ちょっとの初心者が、物体検出プログラム(SSD)を作成し、実務を想定して改良してみた話
  2. SSDでひなまつりバージョンのチロルチョコが検知できるか試してみた(Pythonで作るプログラム#2)
  3. 物体検出プログラム(SSD)で小さい物体を検出する方法を考えたので、ひなまつりチロルチョコで検証してみた話
  4. 物体検出プログラム(SSD)で小さい物体を検出するプログラムを作ったのでソースコードを解説します。

情報表示画面の作成について

映像の左上、左下、右上の画面は、情報表示用の画面です。

背景を黒にしているのは計算負荷をなるべく小さくしたいからです。

計算負荷が小さいと表示速度が速くなります。

リアルタイム性が必要なら高速化は必須です。

以下のコードをみてください。

import cv2
import numpy as np

h = 500
w = 500

img_1 = np.zeros((h, w, 3), np.uint8)
img_2 = np.empty((h, w, 3), np.float32)

cv2.imshow('test_1', img_1)
cv2.imshow('test_2', img_2)

cv2.waitKey(0)
cv2.destroyAllWindows()

実行すると以下のような黒い画面が表示されます。

どちらの画面も背景は黒ですが、表示時間(処理速度)が全く違います。

パソコンの性能にもよりますが、表示速度の傾向は同じだと思います。

test_1 = 0.1582 sec
test_2 = 0.0339 sec

test_2の方が約5倍も速く表示できます。

次のグラフをみてください。

表示させる画面のサイズと表示時間を比較したものです。

グラフから、表示させる画面サイズによらず test_2の方がtest_1よりも圧倒的に表示速度が速いことが分かります。

test_2の方が早い理由は、np.empty( )を使っているためです。

np.empty( ) はNumPyの初期化時に使う関数の一つで、配列を作成する際に高速化が必要ならおすすめの関数です。

np.empty( ) を使うと配列作成時に各要素の値が 0 になることは保証されません。

そのかわり、画面表示が高速化できます。

本プログラムでは左下の画面作成時に np.empty( ) を用いています。この画面は、検出した物体を一つずつ表示させることができ、拡大して確認することができます。

一方、左上、右上の文字表示用の画面には np.zeros( ) を用いています。

理由は日本語を表示させるためです。画面に日本語を表示させる場合と英語を表示させる場合ではコードが違います。 

日本語と英語の表示方法についてはこちらの記事を参照してください。

np.zeros( ) は np.empty( ) より表示速度が遅くなります。

ですが、日本語を表示させる場合は np.zeros( ) で配列作成しておく必要があるので、目的に応じてどちらを使うか選択すれば良いと思います。

日本語の表示にこだわらなければ、もう少しプログラムを高速化できると思います。

Matplotlibで日本語表示させる方法について

Matplotlibで日本語を表示させるには、ちょっと工夫が必要です。

通常、日本語を表示させようとすると文字化けしてしまいます。

そんなときは、以下のコードを参考にしてください。

import matplotlib.pyplot as plt
import japanize_matplotlib

# 円グラフを作成する関数を定義
def plot(y, labels):
    fig, ax = plt.subplots(figsize=(7, 4))
    ax.pie(y, labels=labels, startangle=90, counterclock=False, autopct='%1.1f%%')
    ax.axis('equal')
    fig.savefig('./cnn_act/fig_test/fig2.png') # グラフの保存場所

y = [10, 20, 30, 40, 50] # グラフの値
labels_jp = ['いち', 'に', 'さん', 'よん', 'ご'] # 日本語ラベル

plot(y, labels_jp) # 関数の第一引数と第二引数にグラフの値と日本語ラベルを代入

plt.show() # グラフの表示

上記プログラムを実行すると以下の円グラフが作成できます。

ラベルが日本語で表示されているのが分かると思います。

ポイントは、japanize_matplotlib を import していることです。

もし import できない場合はAnacondaプロンプトで以下のコードを実行してください。

Anaconda についてはこちらの記事を参照ください。

> pip install Japanize-matplotlib

うまくインストールできれば、matplotlib で日本語ラベルが表示できます。

GUIの作成方法について

作成方法についてはこちらの記事を参照してください。

ここでは、過去の記事に載せていなかったポイントを書きます。

PysimpleGUIを使ってイベント処理をする際、二通りのやり方があります。

以下のコードをみてください。

event, values = window.read(timeout=20)

上記コードは、イベントループの中で実行されるwindowの読み込みコードです。

ここで言うwindowとは画像をupdateするコードのことです。

具体的には下記のwindow[ ].update( )の部分です。

# 映像をpng画像に変換してwindow画面を更新
    imgbytes = cv2.imencode('.png', frame_1)[1].tobytes()
    window['-IMAGE-'].update(data=imgbytes)

このコードは、frame_1に代入されているndarray型の映像データをpng画像に変換して、window画面を更新するという動作をします。

windowの読み込みコードで、左側に記載された event の部分と value の部分では、動作の役割が異なります。

条件文で判定する際、if event == ' Capture ' : 以下のコードについて、event は一回限りの実行、values は連続した実行が可能です。

具体的には以下のコードをみてください。

if event == 'Capture':

        # 日付の取得
        d_today = datetime.date.today()
        dt_now = datetime.datetime.now()
            
        cv2.imwrite('./capture' + str("/") + str(no) + str("_") +
                    str(d_today) + str("_") +
                    str(dt_now.hour) + str("_") +
                    str(dt_now.minute) + str("_") +
                    str(dt_now.second) + '.jpg', frame_1)

上記コードは、もし Capture ボタンが押されたら、frame_1の画像データを日付をファイル名として、captureという名前のフォルダに jpg 画像として保存するという動作をします。

この場合は、’ Capture ' ボタンが押されたときのみコードが実行されれば良いので、event の方を使います。

一方で、以下のコードをみてください。

if values['-Off-']:
   img = np.zeros((500, 500, 3), np.float32)

上記コードは、もし ' – Off – ' というボタンまたはキーが押されたらその間ずっと500×500pxの 0 配列(黒色)が img に代入され続ける動作をします。

ボタンを押した際にどのような動作をさせたいかを考えて、event で条件設定するか、values で条件設定するか決める必要があります。

または、両方を組み合わせて条件設定する場合もあります。

なお、本プログラムでは、組み合わせて条件設定している箇所が多いです。

以上、断片的に色々書きましたが、また何か新しいものが出来たら紹介したいと思います。