pythonでSoft-NMSを実装する(2)Soft-NMS適用例

Soft-NMS(Soft Non-Maximum Suppression)は、SSDやYOLOといった物体検出AIの後処理として使用されるNMSの改良型アルゴリズムです。

この記事では、次の記事で紹介したSoft-NMSサンプルコードの適用例を紹介します。

この記事でできること

ニューラルネットワーク(ディープラーニング)等を用いた機械学習による物体検出AIでは、たくさんの検出枠(この記事では矩形と呼んでいる長方形のこと)が出力されます。

次の画像は、いらすとやの「離れて給食を食べる子供のイラスト」の二人の子供の顔付近にいくつかの矩形を付与した画像です。

「離れて給食を食べる子供のイラスト」に矩形を付与した画像
「離れて給食を食べる子供のイラスト」に矩形を付与した画像

この画像にSoft-NMSを適用した例が次の画像です。

Soft-NMSを適用し、信頼度が閾値未満の矩形を削除した画像
Soft-NMSを適用し、信頼度が閾値未満の矩形を削除した画像

ただし、Soft-NMSは矩形を直接削除する処理ではなく、信頼度の(スコア)高い矩形と重複する矩形の信頼度(スコア)を下げるアルゴリズムとなっています。この部分がNMSと大きく違います。

この画像では、Soft-NMSを適用した後、信頼度が閾値未満の矩形を削除し、残った矩形のみを描画しました。

この記事では、過去の記事でコードを紹介したSoft-NMSを行う関数soft_nms()の適用例を紹介します。

本題前の準備

本題に入る前にいくつか準備が必要です。必要な部分だけ参考にして頂ければと。

準備: 関数soft_nms()で扱う矩形を表す配列リスト例

このエントリでは、いらすとやの「離れて給食を食べる子供のイラスト」の画像を例に説明します。

このイラストでは二人の子供がおり、二人の顔を囲む矩形の座標を手入力で指定しました。また、適当に矩形を6個追加しました。

# 人の顔を囲む矩形(長方形)
bboxes = [[64,91,406,440],  [883,99,1228,442]]
classes = ["man", "woman"]
# 信頼度の値は適当
scores = [0.9, 0.9]

# 適当に顔からずらした矩形を6個追加
# 数値は適用
bboxes += [[25,50,360,440], [100,131,456,495], 
           [930,140,1280,500], [833,59,1158,402],
           [300, 70, 500, 340], [850, 300, 930, 460]]
classes += ["man", "man", "woman", "woman", "man", "woman"]
scores += [0.8, 0.8, 0.8, 0.8, 0.8, 0.8]

重要なのは、矩形は2次元配列 (2次元のリスト) “bboxes”で表し、各矩形を4つの座標で表現していることです。ここでは矩形の座標を[xmin, ymin, xmax, ymax]で表現しています。この4座標を1セットとして矩形の数分だけ格納したのが2次元配列”bboxes”ということになります。

[xmin, ymin, xmax, ymax]という座標の考え方は次の記事で以前紹介しました。

また、各矩形に対して、検出クラスを配列”classes”に、検出信頼度(スコア)を配列”scores”に格納しています。

これらの3つの配列(リスト)の要素の順番は任意ではありません。3つの配列で同じインデックスの要素が、同一の矩形に対応している必要があります。配列(リスト)の要素の順番は、3つ配列”bboxes”、”classes”、”scores”で一緒にしましょうということです。

準備: 画像への矩形の描画

画像の読み込みと矩形の描画は次のように行いました。pythonのpillowパッケージを使って画像を読み込み、描画しています。

    from PIL import Image
    # 画像を読み込み
    input_img_name = "img/school_kyusyoku_hanareru.png"
    # 離れて給食を食べる子供のイラスト
    # https://www.irasutoya.com/2020/06/blog-post_29.html
    img = Image.open(input_img_name)

    # soft-NMS適用前の画像を保存
    img_1 = draw_rect(img, bboxes, scores, classes)
    out_img_name="img/school_kyusyoku_hanareru_before_soft-nms.png"
    img_1.save(out_img_name)

ここで使用している関数”draw_rect()”は、矩形を描画して、さらに矩形に付随するテキスト(ここでは、クラスとスコア)を描画する関数です。次の記事にコードを記載しました。

上述の”bboxes”、”classes”、”scores”を使って、このコードを実行すると次のような画像が出力されます。この画像をSoft-NMS適用前の画像とします。

Soft-NMS適用前の画像
Soft-NMS適用前の画像

準備: スコアが閾値未満の矩形を削除する関数

ここでは、一応、スコアが閾値未満の矩形を削除する関数の例を紹介しておきます。

def th_bboxes(bboxes, scores, classes, score_threshold=0.5):
    # 関数の返り値となるリストを生成
    th_bboxes = []
    th_scores = []
    th_classes = []
    for i, score in enumerate(scores):
        if score >= score_threshold:
            # スコアがscore_threshold以上の要素のみ返り値リストに保存
            th_bboxes.append(bboxes[i])
            th_scores.append(scores[i])
            th_classes.append(classes[i])

    return th_bboxes, th_scores, th_classes

引数”score_threshold”以上のスコアの矩形のみを返し、”score_threshold”未満のスコアの矩形を削除する関数です。

リストbboxes, scores, classesから、scoresの要素が”score_threshold”未満となるインデックスをscoresから削除し、さらにbboxes, classesからも削除しています。

Soft-NMSの適用例(関数soft_nms()の使用例)

さて、本題です。

上に記載した画像や矩形の例に対し、Soft-NMSの関数soft_nms()を適用した例を紹介します。

以前の記事で説明した通り、Soft-NMSのアルゴリズムで使用されるスコア更新関数は2通りで、
線形的なスコア更新関数、指数関数的なスコア更新関数の2つがあります。それぞれについて適用例を示します。

線形的なスコア更新関数を使用した適用例

矩形のスコアを次の関数で更新する場合です。

\[
\begin{align}
f(score, iou) =
\begin{cases}
score * (1-iou) \quad (IoU \geq IoU_ threshold) \cr
score \qquad\qquad\qquad (otherwise)
\end{cases}
\end{align}
\]

このケースでは、関数”soft_nms()”を次のように使用します。引数のbboxes, scores, classesは上で説明した通りの形式です。

    # soft-NMSを適用(線形的なscore更新関数)
    bboxes, scores, classes = \
        soft_nms(bboxes, scores, classes, iou_threshold=0.4, linear=True)

スコア更新関数として線形的な関数を使用する場合には、引数”linear”をTrueにします。また、soft-NMSでスコアを更新する閾値”iou_threshold”は、今回”iou_threshold=0.4″としました。

関数”soft_nms()”適用後の矩形を画像に出力したのが次の画像です。

Soft-NMS適用後の画像(線形的なスコア更新関数)
Soft-NMS適用後の画像(線形的なスコア更新関数)

Soft-NMS適用前に比べて矩形の信頼度(スコア)が低下しているのがわかると思いますが(画像内の文字が小さく、見えなかったらすみません)、他の矩形との重複度が高い(IoUがiou_threshold以上の)矩形だけスコアが下がり、重複が小さい矩形のスコアはそのままとなっています。

さらに、スコアの小さい矩形を削除してみます。次のようにして信頼度(スコア)が閾値”score_threshold”0.8未満の矩形を削除した画像が次に図示する画像です。

    # スコア0.8未満の矩形を削除
    bboxes, scores, classes = \
        th_bboxes(bboxes, scores, classes, score_threshold=0.8)
Soft-NMS適用後、スコアが閾値未満の矩形を削除後の画像(線形的なスコア更新関数)
Soft-NMS適用後、スコアが閾値未満の矩形を削除後の画像(線形的なスコア更新関数)

繰り返しになりますが、ここまでの一連の処理の内容を記載しておきます。Soft-NMSを適用し、信頼度(スコア)が閾値未満の矩形を削除して、画像として描画するまでの一連の処理のコード例となっています。

    from PIL import Image
    import copy
    # 人の顔を囲む矩形(長方形)
    bboxes = [[64,91,406,440], 
              [883,99,1228,442]]
    classes = ["man", "woman"]
    # 信頼度の値は適当
    scores = [0.9, 0.9]

    # 適当に顔からずらした矩形を追加
    bboxes += [[25,50,360,440], [100,131,456,495], 
               [930,140,1280,500], [833,59,1158,402],
               [300, 70, 500, 340], [850, 300, 930, 460]]
    classes += ["man", "man", "woman", "woman", "man", "woman"]
    scores += [0.8, 0.8, 0.8, 0.8, 0.8, 0.8]

    # 画像を読み込み
    input_img_name = "img/school_kyusyoku_hanareru.png"
    # 離れて給食を食べる子供のイラスト https://www.irasutoya.com/2020/06/blog-post_29.html
    img = Image.open(input_img_name)

    # soft-NMS適用前の画像を保存
    img_1 = copy.copy(img)
    img_1 = draw_rect(img_1, bboxes, scores, classes)
    out_img_name="img/school_kyusyoku_hanareru_before_soft-nms.png"
    img_1.save(out_img_name)
    del img_1

    # soft-NMSを適用(線形的なscore更新関数)
    bboxes, scores, classes = \
        soft_nms(bboxes, scores, classes, iou_threshold=0.4, linear=True)

    # スコア0.8未満の矩形を削除
    bboxes, scores, classes = \
        th_bboxes(bboxes, scores, classes, score_threshold=0.8)

    # soft-NMS適用後の画像を保存
    img_1 = copy.copy(img)
    img_1 = draw_rect(img_1, bboxes, scores, classes)
    out_img_name="img/school_kyusyoku_hanareru_after_soft-nms_l_th.png"
    img_1.save(out_img_name)

細かい点ですが、このコードでは読み込んだ画像の”クラスオブジェクト”imgを、draw_rect()での上書きを避けるため、copy.copy(img)として別の変数に退避しました。

指数関数的なスコア更新関数を使用した適用例

次に、矩形のスコアを次のような指数関数で更新する場合を説明します。

\[
f(score, iou) = score * \exp (-\frac{iou^{2}}{\sigma})
\]

関数”soft_nms()”の引数”linear”をFalseにすればOKです。

    # soft-NMSを適用(指数関数的なscore更新関数)
    bboxes, scores, classes = \
        soft_nms(bboxes, scores, classes, sigma=0.5, linear=False)

今回は”sigma=0.5″としました。

このスコア更新関数が、上で紹介した線形関数的なものと何が違うかというと、IoU(Intersection over Union)に対して閾値”IoU_threshold”で分岐することなく、全ての矩形に対して適用し、スコアを更新するところです。

他の矩形とはあまり重複のない矩形のスコアも更新され、低下することになります。例えば、この場合のSoft-NMS適用後の画像は次の通りで、全ての矩形のスコアが低下しているのがわかると思います(画像内の文字が小さくわからなかったらすみません)。

Soft-NMS適用後の画像(指数関数的なスコア更新関数)
Soft-NMS適用後の画像(指数関数的なスコア更新関数)

上の方に掲載した画像の、線形関数的なスコア更新関数と比較すると違いがわかって頂けるかと。

さらに、次のようにして閾値”score_threshold”が0.8未満の矩形を削除してみると、よりわかりやすいです。

    # スコア0.8未満の矩形を削除
    bboxes, scores, classes = \
        th_bboxes(bboxes, scores, classes, score_threshold=0.8)
Soft-NMS適用後、スコアが閾値未満の矩形を削除後の画像(指数関数的なスコア更新関数)
Soft-NMS適用後、スコアが閾値未満の矩形を削除後の画像(指数関数的なスコア更新関数)

このように、指数関数で更新するスコア更新関数を用いることで、他の矩形とはあまり重複のない矩形も削除することができました。

ただ、これは今回たまたまそのような例であったというだけかもしれないです。必ずしも、指数関数的なスコア更新関数が優れているとは限らないと思います。

同じ例に従来NMSを使用した例

参考までに、soft-NMS適用前の画像と上の方で説明した矩形に対して従来のNMSを適用した例を載せておきます。

# 従来NMSを適用
bboxes, scores, classes = \
    nms(bboxes, scores, classes, iou_threshold=0.5)

ここで使用した関数”nms()”は次の記事で以前サンプルコードを紹介した関数です。重複した矩形をNMSにより削除して返す関数となっています。

NMS適用後の画像は次のようになりました。

NMS適用後の画像
NMS適用後の画像

NMSはIoUが閾値以上の重複した矩形のうち、スコアが大きいものを残すアルゴリズムとなっているので、他の矩形と重複の領域が小さい矩形は削除しきれず、そのまま出力されます。

今回使用した例では顕著ですが、soft-NMSがNMSの改良手法であることが、この例から少しわかって頂けると思っています。

おわりに

ニューラルネットワーク(ディープラーニング)を用いた機械学習による物体検出アルゴリズムの後段処理に使用されるSoft-NMSの適用例を紹介しました。

NMSの代わりに、その改良型であるSoft-NMSを使用しても、直ちに検出精度が向上するかはわかりませんが、パラメータ調整等を行って適切に適用することで、有効な手法であると言えそうです。

物体検出AIの検出精度をあと少し上げたいという場合にsoft-NMSの適用を検討してみてはいかがでしょうか。

なお、Soft-NMSを高速化する実装例をこちらで紹介していますので、参考にしてみてください。

コメント

タイトルとURLをコピーしました