序文
なぜこの記事を書こうと思ったかというと、最近、ターゲット検出モデルとそれに関連する後処理を組み込みデバイスに移行したい場合、C++ の opencv ライブラリを使用することができず、 cv2 は使用できません。dnn.nms 関数は nms の後処理に使用されます。c で実装する必要があるため、nms の処理を理解し、c nms を手動で記述する必要があります。そこで、インターネットで検索しました。 Softnms の Python ソースコードを解釈してみる 実際のところ、softnms と nms の違いは nms における各ボックスのスコア乗算の重みに過ぎないので、この記事は nms のソースコード解釈とも言えます。
考え
初心者の中には nms を理解できない人もいるかもしれないので、ここではまず全体的なプロセスと考え方について説明します。
たとえば、yolov5s の coco 事前トレーニング重みから派生した onnx モデルの出力は float32[1,20160,85] ベクトルであることがわかります。
1 はバッチを意味します。この基本概念については説明しません。
85 は、coco80 カテゴリ + 4 つの座標 (x1、x2、y1、y2) + 1 の信頼度 (信頼度) を意味します。
20160 は、出力に 20160 行があること、つまり非常に多くのボックスがあることを意味します。したがって、もちろん、すべてのボックスを目標として数えることはできません。また、画像が乱雑なボックスでいっぱいであることがわかるでしょう。冗長なフレームをフィルタリングするのに役立つメソッドが必要です。nmsの考え方を簡単に理解すると、各フレームと残りのフレームの iou を計算し、設定された iou しきい値を超えるフレームを削除することになります。iou については、wiki iou
から転送された以下の図を参照してください。つまり、2 つのボックスの重なり合う領域をそれらが合計でカバーする領域で割ったものと、交差部分を和集合で割ったものです。
コードとキーの問題
コードはSoftnmsから選択され、いくつかのわかりやすい print 関数が追加され、tensorflow の依存関係が削除されています。
import numpy as np
import time
def py_cpu_softnms(dets, sc, Nt=0.3, sigma=0.5, thresh=0.001, method=2):
"""
py_cpu_softnms
:param dets: boexs 坐标矩阵 format [y1, x1, y2, x2]
:param sc: 每个 boxes 对应的分数
:param Nt: iou 交叠门限
:param sigma: 使用 gaussian 函数的方差
:param thresh: 最后的分数门限
:param method: 使用的方法
:return: 留下的 boxes 的 index
"""
# 就是在框矩阵的最后一列加上从0开始的序号
N = dets.shape[0]
indexes = np.array([np.arange(N)])
dets = np.concatenate((dets, indexes.T), axis=1)
print(f'dets is {
dets}\n')
# 顺序是y1,x1,y2,x2
y1 = dets[:, 0]
x1 = dets[:, 1]
y2 = dets[:, 2]
x2 = dets[:, 3]
print(y1)
print(x1)
print(y2)
print(x2)
scores = sc
print(f'scores is {
scores}\n')
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
print(f'areas is {
areas}\n')
for i in range(N):
# 临时存储方便后面参数交换
tBD = dets[i, :].copy()
print(f'tBD before is{
tBD}')
tscore = scores[i].copy()
print(f'tscore is {
tscore}')
tarea = areas[i].copy()
pos = i + 1
# 选取最大分数
if i != N-1:
maxscore = np.max(scores[pos:], axis=0)
print(f'maxscore is : {
maxscore}')
maxpos = np.argmax(scores[pos:], axis=0)
print(f'maxpos is : {
maxpos}')
# 这里如果是最后一位了就直接选取它自己,节省时间
else:
maxscore = scores[-1]
maxpos = 0
if tscore < maxscore:
print(1)
dets[i, :] = dets[maxpos + i + 1, :]
dets[maxpos + i + 1, :] = tBD
tBD = dets[i, :]
print(f'dets After is{
dets}')
print(f'tBD After is{
tBD}')
scores[i] = scores[maxpos + i + 1]
scores[maxpos + i + 1] = tscore
tscore = scores[i]
areas[i] = areas[maxpos + i + 1]
areas[maxpos + i + 1] = tarea
tarea = areas[i]
# IoU 计算
xx1 = np.maximum(dets[i, 1], dets[pos:, 1])
yy1 = np.maximum(dets[i, 0], dets[pos:, 0])
xx2 = np.minimum(dets[i, 3], dets[pos:, 3])
yy2 = np.minimum(dets[i, 2], dets[pos:, 2])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[pos:] - inter)
print(ovr)
# 三种方法: 1.linear 2.gaussian 3.原始的NMS
if method == 1: # linear
weight = np.ones(ovr.shape)
weight[ovr > Nt] = weight[ovr > Nt] - ovr[ovr > Nt]
elif method == 2: # gaussian
weight = np.exp(-(ovr * ovr) / sigma)
else: # 原始的NMS
weight = np.ones(ovr.shape)
weight[ovr > Nt] = 0
scores[pos:] = weight * scores[pos:]
print(f'scores [pos:] is {
scores[pos:]}')
# 选择正确的box序号
inds = dets[:, 4][scores > thresh]
keep = inds.astype(int)
return keep
def test():
# 模拟数据
boxes = np.array([[200, 200, 400, 400], [220, 220, 420, 420], [200, 240, 400, 440], [240, 200, 440, 400], [1, 1, 2, 2]], dtype=np.float32)
print(boxes.shape)
boxscores = np.array([0.9, 0.8, 0.7, 0.6, 0.5], dtype=np.float32)
index = py_cpu_softnms(boxes, boxscores, method=3)
print(index)
if __name__ == '__main__':
test()
なぜこの for ループがこれほど複雑なのか疑問に思っている人も多いと思われますが、他のすべてのボックスの iou を計算するだけで十分ではないでしょうか。
実際、これがこのコードの微妙な点で、上記のコードのように 5 つのボックスをシミュレートする場合、他のすべてのボックスの iou を計算すると、 5x4=20 回計算する必要があります。
ただし、毎回後ろの最も大きいスコアを選択し、現在のスコアがそれより小さい場合は、位置を交換すると、iou の計算回数は4+3+2+1=5x4/2=になります。 10 回行うと時間の半分が短縮されます。理解できない場合は、もう一度実行することをお勧めします。これを言葉で伝えるのはまだ難しいため、詳しく説明する価値があります。
オリジナルの nms とソフト nms の違いはコードのどこに反映されていますか?
コード内の重み行列に注意してください。ここで重みを選択するには 3 つの方法があります。重みの各計算後、現在のスコア マトリックスが乗算されます。元の nms の場合、すべての値がそれと重なっていて、しきい値より大きいものは 0 に設定されます。これは、それらのボックスが次の計算プロセスで無視されることを意味しますが、softnms の場合、それが線形であってもガウスであっても、一定のスコアを予約します。実際、この方法は多くのアルゴリズムで使用されており、モデルに反映されていますが、実際にモデルのパフォーマンスを向上させることができるかどうかは不明であり、実際に試してみる必要があります。 。