Yolov5-Faceの原理解析とアルゴリズム解析

YOLOv5-フェイス


写真

近年、CNNは顔検出に広く使用されています。ただし、多くの顔検出器では顔を検出するために特別に設計された顔検出器を使用する必要があり、YOLOv5 の作成者は顔検出を一般的なターゲット検出タスクとして扱います。

YOLOv5Face は、YOLOv5 に基づいて 5 ポイント ランドマーク回帰ヘッド (キー ポイント回帰) を追加し、ウィング損失を使用してランドマーク回帰ヘッドを制約します。YOLOv5Face は、大型モデルから超小型モデルまで、さまざまなモデル サイズの検出器を設計し、組み込みデバイスやモバイル デバイスでのリアルタイム検出を可能にします。

画像-20230529222235376

WiderFace データセットの実験結果では、YOLOv5Face がほぼすべての Easy、Medium、Hard サブセットで最先端のパフォーマンスを達成でき、特別に設計された顔検出器を上回っていることが示されています。

Github アドレス: https://www.github.com/deepcam-cn/yolov5-face

1. なぜ顔検出 = 一般的な検出なのか?

1.1 YOLOv5Face 顔検出

YOLOv5Face 手法では、顔検出は一般的なターゲット検出タスクとして扱われます。TinaFace と同様に、人間の顔がターゲットとして使用されます。TinaFace で説明されているように:

  • データの観点から見ると、姿勢、スケール、オクルージョン、照明、ぼかしなどの人間の顔の特徴は、他の一般的な検出タスクにも表示されます。
  • 表情やメイクといった顔の固有の属性から性別を捉えることは、一般的な検出問題における形状の変化や色の変化にも対応できます。

1.2 YOLOv5Face ランドマーク

ランドマークは比較的特殊な存在ですが、それだけではありません。それらはオブジェクトのキーポイントにすぎません。たとえば、ナンバープレート検出では、Landmark も使用されます。ターゲット予測モデルの先頭にランドマーク回帰を追加するのは、ワンクリックで比較的簡単です。したがって、顔検出が直面する課題の観点から見ると、一般的なターゲット検出には、マルチスケール、小さな顔、密集したシーンなどが存在します。したがって、顔検出は一般的なオブジェクト検出サブタスクとみなすことができます。

2. YOLOv5Face の設計目標と主な貢献

2.1 設計目標

YOLOv5Face は、大きな顔、小さな顔、ランドマークの監視などのさまざまな複雑さとアプリケーションを考慮して、顔検出用に YOLOv5 を再設計および修正しました。YOLOv5Face の目標は、非常に複雑なものから非常に単純なものまで、さまざまなアプリケーションにモデルの組み合わせを提供し、組み込みデバイスまたはモバイル デバイスでパフォーマンスと速度の最適なトレードオフを実現することです。

2.2 主な貢献

  1. YOLOV5 は顔検出器として再設計され、YOLOv5Face と呼ばれました。平均平均精度 (mAP) と速度の点でパフォーマンスを向上させるために、ネットワークに主要な変更が加えられました。
  2. さまざまな用途のニーズを満たすために、大型モデルから中型モデル、超小型モデルまで、さまざまなサイズのモデルをシリーズ設計しています。YOLOv5 で使用されるバックボーンに加えて、ShuffleNetV2 ベースのバックボーンも実装されており、モバイル デバイスに最先端のパフォーマンスと高速速度を提供します。
  3. YOLOv5Face モデルは WiderFace データセットで評価されます。VGA 解像度の画像では、ほぼすべてのモデルが SOTA のパフォーマンスと速度を実現します。これも先ほどの結論を裏付けるもので、YOLO5 で顔検出器を完成させることができるため、顔検出器を再設計する必要はありません。

3. YOLOv5Face アーキテクチャ

3.1 モデルのアーキテクチャ

3.1.1 モデル図

YOLOv5Face は YOLOv5 をベースラインとして使用し、顔検出に適応するように改善および再設計します。ここでの主な目的は、小さな顔と大きな顔に対する変更を検出することです。

640

YOLO5 顔検出器のネットワーク アーキテクチャを図 1 に示します。これはバックボーン、ネック、ヘッドで構成され、全体的なネットワーク アーキテクチャを記述します。YOLOv5 では、CSPNet バックボーンが使用されます。これらの機能を融合するためにネックにはSPPとPANが使用されています。Head では回帰と分類の両方が使用されます。

3.1.2 CBS モジュール

ここに画像の説明を挿入
この図では、Conv、BN、および SiLU アクティベーション関数で構成される CBS ブロックが定義されています。CBS ブロックは他の多くのブロックでも使用されます。

class Conv(nn.Module):
    # Standard convolution
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
        super(Conv, self).__init__()
        # 卷积层
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
        # BN层
        self.bn = nn.BatchNorm2d(c2)
        # SiLU激活层
        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())

    def forward(self, x):
        return self.act(self.bn(self.conv(x)))

3.1.3 ヘッド出力

640 (2)

境界ボックス (bbox)、信頼度 (conf)、分類 (cls)、および 5 ポイント ランドマークを含む Head の出力ラベルを表示します。これらのランドマークは YOLOv5 の改良版であり、ランドマーク出力を備えた顔検出器になります。ランドマークがない場合、最後のベクトルの長さは 16 ではなく 6 になるはずです。ボックス (4) + 信頼度 (1) + キーポイント (5*2) + cls (カテゴリー)

各アンカーの出力サイズは、P3 で 80×80×16、P4 で 40×40×16、P5 で 20×20×16、およびオプションで P6 で 10×10×16 です。実際のサイズにはアンカーの数を掛ける必要があります。

3.1.4 ステム構造

640 (3)

この図は、YOLOv5 の元のフォーカス レイヤーを置き換えるために使用されるステム構造を示しています。YOLOv5 での顔検出のための Stem ブロックの導入は、YOLOv5Face の革新の 1 つです。

class StemBlock(nn.Module):
    def __init__(self, c1, c2, k=3, s=2, p=None, g=1, act=True):
        super(StemBlock, self).__init__()
        # 3×3卷积
        self.stem_1 = Conv(c1, c2, k, s, p, g, act)
        # 1×1卷积
        self.stem_2a = Conv(c2, c2 // 2, 1, 1, 0)
        # 3×3卷积
        self.stem_2b = Conv(c2 // 2, c2, 3, 2, 1)
        # 最大池化层
        self.stem_2p = nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)
        # 1×1卷积
        self.stem_3 = Conv(c2 * 2, c2, 1, 1, 0)

    def forward(self, x):
        stem_1_out = self.stem_1(x)
        stem_2a_out = self.stem_2a(stem_1_out)
        stem_2b_out = self.stem_2b(stem_2a_out)
        stem_2p_out = self.stem_2p(stem_1_out)
        out = self.stem_3(torch.cat((stem_2b_out, stem_2p_out), 1))
        return out

Stem モジュールを使用してネットワーク内の元の Focus モジュールを置き換えると、ネットワークの汎化能力が向上し、計算の複雑さが軽減され、同時にパフォーマンスは低下しませんStem モジュールの図では CBS が使用されていますが、コードを見ると、2 番目と 4 番目の CBS は 1×1 の畳み込み、1 番目と 3 番目の CBS は 3×3、stride=2 の畳み込みであることがわかります。yaml ファイルを見ると、ステムの画像サイズが 640×640 から 160×160 に変更されていることがわかります。

3.1.5 CSP の構造

ここに画像の説明を挿入

CSP ブロックの設計は DenseNet からインスピレーションを得ています。ただし、いくつかの CNN 層の後に完全な入力と出力を追加する代わりに、入力は 2 つの部分に分割されます。半分は CBS ブロック、つまりいくつかのボトルネック ブロックを通過し、残りの半分は Conv 層を通じて計算されます。

class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super(C3, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # act=FReLU(c2)
        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])

    def forward(self, x):
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))

ここに画像の説明を挿入

ボトルネック層は次のように表されます。

class Bottleneck(nn.Module):
    # Standard bottleneck
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, shortcut, groups, expansion
        super(Bottleneck, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        #第1个CBS模块
        self.cv1 = Conv(c1, c_, 1, 1)
        #第2个CBS模块
        self.cv2 = Conv(c_, c2, 3, 1, g=g)
        #元素add操作
        self.add = shortcut and c1 == c2

    def forward(self, x):
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

3.1.9 SPP の構造

ここに画像の説明を挿入

このブロックでは、YOLOv5Face は、YOLOv5 の 13×13、9×9、および 5×5 のカーネル サイズを 7×7、5×5、および 3×3 に変更しました。この改善は、顔検出により適しており、顔検出のパフォーマンス 顔検出の精度

class SPP(nn.Module):
    # 这里主要是讲YOLOv5中的kernel=(5,7,13)修改为(3, 5, 7)
    def __init__(self, c1, c2, k=(3, 5, 7)):
        super(SPP, self).__init__()
        c_ = c1 // 2  # hidden channels
        # 对应第1个CBS Block
        self.conv1 = Conv(c1, c_, 1, 1)
        # 对应第2个 cat后的 CBS Block
        self.conv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
        # ModuleList=[3×3 MaxPool2d,5×5 MaxPool2d,7×7 MaxPool2d]
        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])

    def forward(self, x):
        x = self.conv1(x)
        return self.conv2(torch.cat([x] + [m(x) for m in self.m], 1))

同時に、YOLOv5Face は stride=64 の P6 出力ブロックを追加し、P6 により大きな顔の検出パフォーマンスを向上させることができます。(これまでの顔検出モデルは、主に小さな顔の検出性能を向上させることに焦点を当てていました。ここでは、著者は大きな顔の検出効果に焦点を当て、大きな顔の検出性能を改善して、モデル全体の検出性能を向上させます)。P6 の特徴マップのサイズは 10x10 です。

ここでは、VGA 解像度の入力画像のみを考慮していることに注意してください。より正確には、入力イメージの長辺は 640 にスケーリングされ、短辺はそれに応じてスケーリングされます。短いエッジも SPP ブロックの最大ストライドの倍数になるように調整されます。たとえば、P6 を使用しない場合は短辺が 32 の倍数、P6 を使用する場合は短辺が 64 の倍数である必要があります。

3.2 入力側の改善

オブジェクト検出のための一部のデータ拡張方法 (上下反転やモザイク データ拡張など) は、顔検出での使用には適していません。

  • フリッピングを削除すると、モデルのパフォーマンスが向上します
  • 小さな顔のモザイク データを拡張するとモデルのパフォーマンスが低下しますが、中規模および大規模な顔のモザイクを使用するとパフォーマンスを向上させることができます
  • ランダムなトリミングによりパフォーマンスが向上します

注: COCO データ セットと WiderFace データ セットの間にはスケールの違いがあり、 WiderFace データ セットには比較的小規模なデータが含まれています。

3.3 画期的なリターン

3.3.1 出力ランドマーク

ランドマークは人間の顔の重要な特徴です。顔の比較、顔認識、表情分析、年齢分析などのタスクに使用できます。伝統的なランドマークは 68 のポイントで構成されています。5 点に簡略化すると、5 点ランドマークは顔認識で広く使用されます。顔識別の品質は、顔の位置合わせと顔認識の品質に直接影響します。

  • 一般物体検出器には Landmark は含まれません。リターンヘッドとして直接追加できます。そこで作者はYOLO5Faceに追加しました。Landmark 出力は、顔画像を顔認識ネットワークに送信する前に位置合わせするために使用されます。

3.3.2 ランド​​マーク損失関数ウィング

ランドマーク回帰の一般的な損失関数は、L2、L1、またはスムーズ L1 です。MTCNN は L2 損失関数を使用します。しかし、著者らは、これらの損失関数が小さな誤差に敏感ではないことを発見しました。この問題を克服するために、翼損失が提案されています。
wing ⁡ ( x ) = { w ln ⁡ ( 1 + ∣ x ∣ / ϵ ) if ∣ x ∣ < w ∣ x ∣ − C それ以外の場合、 \operatorname{wing}(x)= \begin{cases}w \ln (1+|x| / \epsilon) & \text { if }|x|<w \\ |x|-C & \text {そうでない場合 }\end{cases}× ={ wln ( 1+x ∣/ ϵ )× Cx  の場合 <wそれ以外の場合 
w:w:w正数www は、非線形部分の範囲を[ − w , w ] [-w, w][ w w ]間隔;
ϵ \epsilonϵ : 非線形領域の曲率を制約し、C = w − w ln ⁡ ( 1 + x ϵ ) C=ww \ln \left(1+\frac{x}{\epsilon}\right)C=wwln( 1+ϵ×)は、セグメントの線形部分と非線形部分を接続するためにスムーズで使用できる定数です。ϵ \イプシロンϵの値は非常に小さい値です。これは、ネットワーク トレーニングが不安定になり、小さな誤差によって勾配爆発の問題が発生するためです。
実際、翼損失関数の非線形部分は単にln ⁡ ( x ) \ln (x)ln ( x )[ ϵ/w , 1 + ϵ/w ] [\epsilon/w, 1+\epsilon/w][ ϵ / w ,1+ϵ / w ]、X 軸およびYYY軸はそれを W にスケールします。また、YYY軸は翼(0) = 0 (0)=0となるように平行移動を適用します。( 0 )=0、損失関数に連続性を課します。
ランドマーク点ベクトルs = { si } s=\left\{s_i\right\}s={ s私は}およびそのグランドトゥルースs ' = { si } s^{\prime}=\left\{s_i\right\}s={ s私は}の損失関数:
loss ⁡ L ( s ) = ∑ i wing ⁡ ( si − si ′ ) \operatorname{loss}_L(s)=\sum_i \operatorname{wing}\left(s_i-s_i^{\prime }\右)損失L( s )=( s私はs)
ここで、i = 1 , 2 , … , 10 i=1,2, \ldots, 10=1 2 10
YOLOv5 の一般的なターゲット検出損失関数をloss O los s_Oロス_ _ああとすると、新しい合計損失関数は次のようになります:
loss ⁡ ( s ) = loss ⁡ O + λ L ⋅ loss ⁡ L \operatorname{loss}(s)=\operatorname{loss}_O+\lambda_L \cdot \operatorname{loss}_L損失()=損失ああ+ lL損失L
ここで、λ L \lambda_LLランドマーク回帰損失関数の重み係数です。
ランドマークの取得: i = 1, 2, …, 10 i=1,2, \ldots, 10=1 2 10
YOLOv5 の一般的なターゲット検出損失関数をloss O los s_Oロス_ _ああとすると、新しい合計損失関数は次のようになります:
loss ⁡ ( s ) = loss ⁡ O + λ L ⋅ loss ⁡ L \operatorname{loss}(s)=\operatorname{loss}_O+\lambda_L \cdot \operatorname{loss}_L損失()=損失ああ+ lL損失L
ここで、λ L \lambda_LLこれは、ランドマーク回帰損失関数の重み係数です。

ランドマークの取得:

#landmarks
lks = t[:,6:14]
lks_mask = torch.where(lks < 0, torch.full_like(lks, 0.), torch.full_like(lks, 1.0))
#应该是关键点的坐标除以anch的宽高才对,便于模型学习。使用gwh会导致不同关键点的编码不同,没有统一的参考标准
lks[:, [0, 1]] = (lks[:, [0, 1]] - gij)
lks[:, [2, 3]] = (lks[:, [2, 3]] - gij)
lks[:, [4, 5]] = (lks[:, [4, 5]] - gij)
lks[:, [6, 7]] = (lks[:, [6, 7]] - gij)

翼の損失は次のように計算されます。

class WingLoss(nn.Module):
    def __init__(self, w=10, e=2):
        super(WingLoss, self).__init__()
        # https://arxiv.org/pdf/1711.06753v4.pdf   Figure 5
        self.w = w
        self.e = e
        self.C = self.w - self.w * np.log(1 + self.w / self.e)
 
    def forward(self, x, t, sigma=1):  #这里的x,t分别对应之后的pret,truel
        weight = torch.ones_like(t) #返回一个大小为1的张量,大小与t相同
        weight[torch.where(t==-1)] = 0
        diff = weight * (x - t)
        abs_diff = diff.abs()
        flag = (abs_diff.data < self.w).float()
        y = flag * self.w * torch.log(1 + abs_diff / self.e) + (1 - flag) * (abs_diff - self.C) #全是0,1
        return y.sum()
 
class LandmarksLoss(nn.Module):
    # BCEwithLogitLoss() with reduced missing label effects.
    def __init__(self, alpha=1.0):
        super(LandmarksLoss, self).__init__()
        self.loss_fcn = WingLoss()#nn.SmoothL1Loss(reduction='sum')
        self.alpha = alpha
 
    def forward(self, pred, truel, mask): #预测的,真实的 600(原来为62*10)(推测是去掉了那些没有标注的值)
        loss = self.loss_fcn(pred*mask, truel*mask)  #一个值(tensor)
        return loss / (torch.sum(mask) + 10e-14)

L1、L2、および平滑 L1 損失関数の分析と比較
loss ⁡ ( s , s ′ ) = ∑ i = 1 2 L f ( si − si ′ ) \operatorname{loss}\left(\mathbf{s}, \mathbf{ s }^{\prime}\right)=\sum_{i=1}^{2 L} f\left(s_i-s_i^{\prime}\right)損失( s s _=i = 12L _f( s私はs)
ここで、sssは顔のキーポイントのグラウンドトゥルース、関数f ( x ) f(x) です。f ( x )は次と同等です。
L1 損失
L 1 ( x ) = ∣ x ∣ L 1(x)=|x|L1 ( × ) _=x
L2 損失
L 2 ( x ) = 1 2 x 2 L 2(x)=\frac{1}{2} x^2L2 ( × ) _=21バツ2

スムーズ ⁡ L 1 ( x ) : スムーズ ⁡ L 1 ( x ) = { 1 2 x 2 if ∣ x ∣ < 1 ∣ x ∣ − 1 2 それ以外の場合 \operatorname{Smooth}_{L 1}(x): \operatorname {smooth}_{L 1}(x)= \begin{cases}\frac{1}{2} x^2 & \text { if }|x|<1 \\ |x|-\frac{1} {2} & \text { それ以外の場合 }\end{cases}スムーズL1 _( × ):スムーズL1 _( × )={ 21バツ2× 21x  の場合 <1それ以外の場合 

xxの損失関数xの導関数は次のとおりです。
d L 2 ( x ) dx = x \frac{d L_2(x)}{dx}=xdx _dL _2( x )=バツ

d L 1 ( x ) dx = { 1 if x ≥ 0 − 1 それ以外の場合 \frac{d L_1(x)}{dx}= \begin{cases}1 & \text { if } x \geq 0 \\ -1 & \text { それ以外の場合 }\end{cases}dx _dL _1( x )={ 1 1× の場合 0それ以外の場合 

d スムーズ ⁡ L 1 ( x ) dx = { x if ∣ x ∣ < 1 ± 1 それ以外の場合 \frac{d \operatorname{smooth}_{L 1}(x)}{dx}= \begin{cases}x & \text { if }|x|<1 \\ \pm 1 & \text { それ以外の場合 }\end{cases}dx _dスムーズL1 _( x )={ バツ± 1x  の場合 <1それ以外の場合 

  • L2 損失関数、xxの場合xが増加した場合のL2 損失対xxxの導関数

  • L1 損失の導関数は定数であり、トレーニングの後半段階で、予測値とグラウンドトゥルースの差が非常に小さい場合、予測値に対する損失の導関数の絶対値は 1 のままです。今度は、学習率が変わらない場合、損失関数は安定値付近で変動し、より高い精度を達成するために収束し続けることが困難になります。

  • 滑らかな L1 損失関数 (x が小さい場合、xxの場合)xの傾きも小さくなり、xxxが非常に大きい場合、xxxの勾配の絶対値は上限の 1 に達しますが、ネットワーク パラメーターを破壊するほど大きくはなりません。スムーズな L1 は、L1 および L2 損失の欠陥を完全に回避します。
    さらに、fast rcnn によると、「...L1 損失は、R-CNN や SPPnet で使用される L2 損失よりも外れ値の影響を受けにくい。」つまり、スムーズな L1 により、損失が外れ値に対してより堅牢になります。 L2 損失関数は外れ値や外れ値の影響を受けにくく、勾配の変化は比較的小さく、トレーニング中に逃げるのは簡単ではありません。
    ここに画像の説明を挿入

上の図は、これらの損失関数のグラフを示しています。Smoolth L1 損失は Huber 損失の特殊なケースであることに注意してください。L2 損失関数は顔のキー ポイント検出に広く使用されていますが、L2 損失は外れ値の影響を受けやすいです。

3.3.3 翼の損失

すべての損失関数は、大きな誤差が存在する場合でも良好に機能します。これは、ニューラル ネットワークのトレーニングでは、小規模または中程度のエラーを持つサンプルに重点を置く必要があることを示しています。この目標を達成するために、新しい損失関数、つまり顔のランドマーク位置特定のための CNN に基づくウィング損失が提案されています。

ここに画像の説明を挿入

NME が 0.04 の場合、テスト データの比率は 1 に近いため、いわゆる大きな誤差セクションである 0.04 から 0.05 のセクションでは、それ以上データが分散されておらず、各損失関数が非常によく機能していることがわかります。大きなエラーのセクション。良好です。

モデルの一貫性のないパフォーマンスは、小さなエラーと中程度のエラーのセグメントにあります。たとえば、NME が 0.02 であるところに垂直線が引かれていますが、これは大きく異なります。したがって、著者は、トレーニング プロセス中に小規模または中程度の範囲のエラー サンプルにさらに注意を払う必要があると提案しています。

ln ⁡ x \ln xを使用できますlnx は小さな誤差の影響を強調するため、その勾配は1 x \frac{1}{x}バツ1、値が 0 に近いほど大きくなり、最適なステップ サイズはx 2 x^2です。バツしたがって、勾配は小さな誤差によって「支配」され、ステップサイズは大きな誤差によって「支配」される。これにより、さまざまなサイズのエラー間のバランスが復元されます。ただし、潜在的に誤った方向への大きな更新ステップを防ぐために、小さな位置決め誤差の影響を過剰に補正しないことが重要です。これは、正のオフセットを持つ対数関数を選択することで実現できます。
ただし、このタイプの損失関数は、比較的小さな位置決め誤差を処理す​​るのに適しています。野生の顔のキーポイント検出では、初期位置特定エラーが非常に大きい可能性がある極端な姿勢に対処することができ、その場合、損失関数はこれらの大きなエラーからの迅速な回復を促進する必要があります。これは、損失関数がL 1 L 1L1またはL2L2L2._ _ _ L 2 L 2以降L2は外れ値の影響を受けやすいため、L1 が選択されました。
したがって、ウィング損失は、小さなエラーの場合はオフセットを伴う対数関数として動作し、L 1 L 1L1。_ _ _

3.4 NMS の後処理

3.4.1 yolov5

def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()):
    """Performs Non-Maximum Suppression (NMS) on inference results
    Returns:
         detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
    """
 
    nc = prediction.shape[2] -5  # number of classes
3.4.2 yolov5s-face
def non_max_suppression_face(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()):
    """Performs Non-Maximum Suppression (NMS) on inference results
    Returns:
         detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
    """
    # 不同之处
    nc = prediction.shape[2] - 15  # number of classes

4. モデルのトレーニング

4.1 ソースコードのダウンロード

git clone https://github.com/deepcam-cn/yolov5-face

4.2 より広範なデータセットをダウンロードする

ダウンロード後、その場所を解凍し、 yolov5-face-master プロジェクトの data フォルダーの下のWidefaceフォルダーに配置します。

https://drive.google.com/file/d/1tU_IjyOwGQfGNUvZGwWWM4SwxKp2PUQ8/view?usp=sharing

4.3 train2yolo.py と val2yolo.py を実行する

データ フォルダーの下に新しい Widefaceyolo フォルダーを作成し、サブディレクトリを train、test、および val に設定します。

python train2yolo.py ./widerface/train ../data/widerfaceyolo/train 
 python val2yolo.py ./widerface  ../data/widerfaceyolo/val

データセットを yolo トレーニングに使用される形式に変換します。完了すると、フォルダーは次のように表示されます。

画像-20230529234006920

画像-20230529234020666

4.4 電車

4.4.1 トレーニング設定ファイルの変更

Wideface.yaml はディレクトリをデータセット ディレクトリに変更します

# train and val data as 1) directory: path/images/, 2) file: path/images.txt, or 3) list: [path1/images/, path2/images/]
train: ./data/widerfaceyolo/train  # 16551 images
val: ./data/widerfaceyolo/val  # 16551 images
#val: /ssd_1t/derron/yolov5-face/data/widerface/train/  # 4952 images

# number of classes
nc: 1

# class names
names: [ 'face']

画像-20230530105752825

4.4.2 トレーニングの視覚化

tensorboard --logdir runs/train

4.4.3 関連するエラー

  1. 画像を描画できません
Traceback (most recent call last):
  File "D:\yolov5-face\train.py", line 523, in <module>
    train(hyp, opt, device, tb_writer, wandb)
  File "D:\yolov5-face\train.py", line 410, in train
    plot_results(save_dir=save_dir)  # save as results.png
  File "D:\yolov5-face\utils\plots.py", line 393, in plot_results
    assert len(files), 'No results.txt files found in %s, nothing to plot.' % os.path.abspath(save_dir)

解決策 このコード行をコメントアウトします。

画像-20230530212314052

  1. 重みは保存されません

長時間トレーニングした後、保存された重みが空であることがわかりました。重みコードを保存する前に、関連するエポックが 20 より大きいことに注意してください。

画像-20230530212411461

画像-20230530212522918

4.5 検出

コードを変更する

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    # 更改权重,指定权重类型
    parser.add_argument('--weights', nargs='+', type=str, default='yolov5s-face.pt', help='model.pt path(s)')
    parser.add_argument('--source', type=str, default='0', help='source')  # file/folder, 0 for webcam
    parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)')
    parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
    parser.add_argument('--name', default='exp', help='save results to project/name')
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
    parser.add_argument('--save-img', action='store_true', help='save results')
    parser.add_argument('--view-img', default=True,action='store_true', help='show results')
    opt = parser.parse_args()
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = load_model(opt.weights, device)
    detect(model, opt.source, device, opt.project, opt.name, opt.exist_ok, opt.save_img, opt.view_img)

4.6 ONNX エクスポートと TensorRT 環境の構成

"""Exports a YOLOv5 *.pt model to ONNX and TorchScript formats

Usage:
    $ export PYTHONPATH="$PWD" && python models/export.py --weights ./weights/yolov5s.pt --img 640 --batch 1
"""

import argparse
import sys
import time

sys.path.append('./')  # to run '$ python *.py' files in subdirectories

import torch
import torch.nn as nn

import models
from models.experimental import attempt_load
from utils.activations import Hardswish, SiLU
from utils.general import set_logging, check_img_size
import onnx

if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', type=str, default='./yolov5s-face.pt', help='weights path')  # from yolov5/models/
    parser.add_argument('--img_size', nargs='+', type=int, default=[640, 640], help='image size')  # height, width
    parser.add_argument('--batch_size', type=int, default=1, help='batch size')
    parser.add_argument('--dynamic', action='store_true', default=False, help='enable dynamic axis in onnx model')
    parser.add_argument('--onnx2pb', action='store_true', default=False, help='export onnx to pb')
    parser.add_argument('--onnx_infer', action='store_true', default=True, help='onnx infer test')
    #=======================TensorRT=================================
    parser.add_argument('--onnx2trt', action='store_true', default=True, help='export onnx to tensorrt')
    parser.add_argument('--fp16_trt', action='store_true', default=True, help='fp16 infer')
    #================================================================
    opt = parser.parse_args()
    opt.img_size *= 2 if len(opt.img_size) == 1 else 1  # expand

    print(opt)

    set_logging()
    t = time.time()

    # Load PyTorch model
    model = attempt_load(opt.weights, map_location=torch.device('cpu'))  # load FP32 model
    delattr(model.model[-1], 'anchor_grid')
    model.model[-1].anchor_grid=[torch.zeros(1)] * 3 # nl=3 number of detection layers
    model.model[-1].export_cat = True
    model.eval()
    labels = model.names

    print(labels)
    # exit()

    # Checks
    gs = int(max(model.stride))  # grid size (max stride)
    opt.img_size = [check_img_size(x, gs) for x in opt.img_size]  # verify img_size are gs-multiples

    # Input 给定一个输入
    img = torch.zeros(opt.batch_size, 3, *opt.img_size)  # image size(1,3,320,192) iDetection

    # Update model
    for k, m in model.named_modules():
        m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility
        if isinstance(m, models.common.Conv):  # assign export-friendly activations
            if isinstance(m.act, nn.Hardswish):
                m.act = Hardswish()
            elif isinstance(m.act, nn.SiLU):
                m.act = SiLU()

        # elif isinstance(m, models.yolo.Detect):
        #     m.forward = m.forward_export  # assign forward (optional)
        if isinstance(m, models.common.ShuffleV2Block):#shufflenet block nn.SiLU
            for i in range(len(m.branch1)):
                if isinstance(m.branch1[i], nn.SiLU):
                    m.branch1[i] = SiLU()
            for i in range(len(m.branch2)):
                if isinstance(m.branch2[i], nn.SiLU):
                    m.branch2[i] = SiLU()
    y = model(img)  # dry run

    # ONNX export
    print('\nStarting ONNX export with onnx %s...' % onnx.__version__)
    f = opt.weights.replace('.pt', '.onnx')  # filename
    model.fuse()  # only for ONNX
    input_names=['input']
    output_names=['output']
    torch.onnx.export(model, img, f, verbose=False, opset_version=12, 
        input_names=input_names,
        output_names=output_names,
        dynamic_axes = {
    
    'input': {
    
    0: 'batch'},
                        'output': {
    
    0: 'batch'}
                        } if opt.dynamic else None)

    # Checks
    onnx_model = onnx.load(f)  # load onnx model
    onnx.checker.check_model(onnx_model)  # check onnx model
    print('ONNX export success, saved as %s' % f)
    # Finish
    print('\nExport complete (%.2fs). Visualize with https://github.com/lutzroeder/netron.' % (time.time() - t))

    # exit()
    # onnx infer
    if opt.onnx_infer:
        import onnxruntime
        import numpy as np
        providers =  ['CPUExecutionProvider']
        session = onnxruntime.InferenceSession(f, providers=providers)
        im = img.cpu().numpy().astype(np.float32) # torch to numpy
        y_onnx = session.run([session.get_outputs()[0].name], {
    
    session.get_inputs()[0].name: im})[0]
        print("pred's shape is ",y_onnx.shape)
        print("max(|torch_pred - onnx_pred|) =",abs(y.cpu().numpy()-y_onnx).max())

画像-20230530232400331

4.7 OnnXruntime 推論

コードは以下のように表示されます。

#!/usr/bin/env python 
# -*- coding: utf-8 -*-
# @Time    : 2023/5/30 23:24
# @Author  : 陈伟峰
# @Site    : 
# @File    : onnxruntime_infer.py
# @Software: PyCharm
import time
import numpy as np
import argparse
import onnxruntime
import os, torch
import cv2, copy

from detect_face import scale_coords_landmarks, show_results
from utils.general import non_max_suppression_face, scale_coords


def allFilePath(rootPath, allFIleList):  # 遍历文件
    fileList = os.listdir(rootPath)
    for temp in fileList:
        if os.path.isfile(os.path.join(rootPath, temp)):
            allFIleList.append(os.path.join(rootPath, temp))
        else:
            allFilePath(os.path.join(rootPath, temp), allFIleList)


def my_letter_box(img, size=(640, 640)):  #
    '''
    将输入的图像img按照指定的大小size进行缩放和填充,
    使其适应指定的大小。
    具体来说,
    它首先获取输入图像的高度h、宽度w和通道数c,然后计算出缩放比例r,并根据缩放比例计算出新的高度new_h和宽度new_w。
    接着,它计算出在新图像中上、下、左、右需要填充的像素数,并使用cv2.resize函数将输入图像缩放到新的大小。最后,
    它使用cv2.copyMakeBorder函数在新图像的上、下、左、右四个方向进行填充,
    并返回填充后的图像img、缩放比例r、左侧填充像素数left和上方填充像素数top

    Args:
        img:
        size:

    Returns:

    '''
    h, w, c = img.shape

    # cv2.imshow("res",img)
    # cv2.waitKey(0)
    r = min(size[0] / h, size[1] / w)

    new_h, new_w = int(h * r), int(w * r)

    top = int((size[0] - new_h) / 2)
    left = int((size[1] - new_w) / 2)
    bottom = size[0] - new_h - top
    right = size[1] - new_w - left
    img_resize = cv2.resize(img, (new_w, new_h))
    # print(top,bottom,left,right)
    # exit()
    img = cv2.copyMakeBorder(img_resize, top, bottom, left, right, borderType=cv2.BORDER_CONSTANT,
                             value=(114, 114, 114))
    # cv2.imshow("res",img)
    # cv2.waitKey(0)
    return img, r, left, top


def xywh2xyxy(boxes):  # xywh坐标变为 左上 ,右下坐标 x1,y1  x2,y2
    xywh = copy.deepcopy(boxes)
    xywh[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
    xywh[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
    xywh[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
    xywh[:, 3] = boxes[:, 1] + boxes[:, 3] / 2
    return xywh


def detect_pre_precessing(img, img_size):  # 检测前处理
    img, r, left, top = my_letter_box(img, img_size)
    # cv2.imwrite("1.jpg",img)
    img = img[:, :, ::-1].transpose(2, 0, 1).copy().astype(np.float32)
    img = img / 255
    img = img.reshape(1, *img.shape)
    return img, r, left, top


def restore_box(boxes, r, left, top):  # 返回原图上面的坐标
    boxes[:, [0, 2, 5, 7, 9, 11]] -= left
    boxes[:, [1, 3, 6, 8, 10, 12]] -= top

    boxes[:, [0, 2, 5, 7, 9, 11]] /= r
    boxes[:, [1, 3, 6, 8, 10, 12]] /= r
    return boxes


def post_precessing(dets, r, left, top, conf_thresh=0.3, iou_thresh=0.5):  # 检测后处理
    """
    这段代码是一个用于检测后处理的函数。它的输入包括检测结果(dets)、
    图像的缩放比例(r)、左上角坐标(left和top)、置
    信度阈值(conf_thresh)和IoU阈值(iou_thresh)。
    函数的主要功能是对检测结果进行筛选和处理,包括去除置信度低于阈值的检测框、将检测框的坐标从中心点和宽高格式转换为左上角和右下角格式、
    计算每个检测框的得分并选取最高得分的类别作为输出、对输出进行非极大值抑制(NMS)处理、最后将输出的检测框坐标还原到原始图像中

    Args:
        dets:
        r:
        left:
        top:
        conf_thresh:
        iou_thresh:

    Returns:
    """
    # 置信度
    choice = dets[:, :, 4] > conf_thresh
    dets = dets[choice]
    dets[:, 13:15] *= dets[:, 4:5]
    # 前四个值为框
    box = dets[:, :4]

    boxes = xywh2xyxy(box)

    score = np.max(dets[:, 13:15], axis=-1, keepdims=True)
    index = np.argmax(dets[:, 13:15], axis=-1).reshape(-1, 1)

    output = np.concatenate((boxes, score, dets[:, 5:13], index), axis=1)
    reserve_ = nms(output, iou_thresh)
    output = output[reserve_]
    output = restore_box(output, r, left, top)
    return output


def nms(boxes, iou_thresh):  # nms
    index = np.argsort(boxes[:, 4])[::-1]
    keep = []
    while index.size > 0:
        i = index[0]
        keep.append(i)
        x1 = np.maximum(boxes[i, 0], boxes[index[1:], 0])
        y1 = np.maximum(boxes[i, 1], boxes[index[1:], 1])
        x2 = np.minimum(boxes[i, 2], boxes[index[1:], 2])
        y2 = np.minimum(boxes[i, 3], boxes[index[1:], 3])

        w = np.maximum(0, x2 - x1)
        h = np.maximum(0, y2 - y1)

        inter_area = w * h
        union_area = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) + (
                    boxes[index[1:], 2] - boxes[index[1:], 0]) * (boxes[index[1:], 3] - boxes[index[1:], 1])
        iou = inter_area / (union_area - inter_area)
        idx = np.where(iou <= iou_thresh)[0]
        index = index[idx + 1]
    return keep


if __name__ == "__main__":
    begin = time.time()
    parser = argparse.ArgumentParser()
    parser.add_argument('--detect_model', type=str, default=r'yolov5s-face.onnx', help='model.pt path(s)')  # 检测模型
    # parser.add_argument('--rec_model', type=str, default='weights/plate_rec.onnx', help='model.pt path(s)')#识别模型
    parser.add_argument('--image_path', type=str, default='imgs', help='source')
    parser.add_argument('--img_size', type=int, default=640, help='inference size (pixels)')
    parser.add_argument('--output', type=str, default='result1', help='source')
    parser.add_argument('--device', type=str, default='cpu', help='device ')
    # parser.add_argument('--device', type=str, default='cpu', help='device ')
    # parser.add_argument('--device', type=str, default='cpu', help='device ')
    opt = parser.parse_args()

    device = opt.device
    file_list = []
    allFilePath(opt.image_path, file_list)
    providers = ['CPUExecutionProvider']
    clors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), (0, 255, 255)]
    img_size = (opt.img_size, opt.img_size)
    sess_options = onnxruntime.SessionOptions()
    # sess_options.optimized_model_filepath = os.path.join(output_dir, "optimized_model_{}.onnx".format(device_name))
    session_detect = onnxruntime.InferenceSession(opt.detect_model, providers=providers)
    # session_rec = onnxruntime.InferenceSession(opt.rec_model, providers=providers )
    if not os.path.exists(opt.output):
        os.mkdir(opt.output)
    save_path = opt.output
    count = 0
    for pic_ in file_list:
        count += 1
        print(count, pic_, end=" ")
        img = cv2.imread(pic_)
        img0 = copy.deepcopy(img)
        img, r, left, top = my_letter_box(img0, size=img_size)

        img = img.transpose(2, 0, 1).copy()
        img = torch.from_numpy(img).to(device)
        img = img.float()  # uint8 to fp16/32
        img /= 255.0  # 0 - 255 to 0.0 - 1.0
        if img.ndimension() == 3:
            img = img.unsqueeze(0)
        im = img.cpu().numpy().astype(np.float32)  # torch to numpy
        pred = session_detect.run([session_detect.get_outputs()[0].name], {
    
    session_detect.get_inputs()[0].name: im})[0]
        pred = non_max_suppression_face(torch.tensor(pred, dtype=torch.float), 0.3, 0.5)
        for i, det in enumerate(pred):  # detections per image
            if len(det):
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape).round()

                # Print results
                for c in det[:, -1].unique():
                    n = (det[:, -1] == c).sum()  # detections per class

                det[:, 5:15] = scale_coords_landmarks(img.shape[2:], det[:, 5:15], img0.shape).round()

                for j in range(det.size()[0]):
                    xyxy = det[j, :4].view(-1).tolist()
                    conf = det[j, 4].cpu().numpy()
                    landmarks = det[j, 5:15].view(-1).tolist()
                    class_num = det[j, 15].cpu().numpy()

                    img0 = show_results(img0, xyxy, conf, landmarks, class_num)
            cv2.imshow('result', img0)
            k = cv2.waitKey(0)
    # print(len(pred[0]), 'face' if len(pred[0]) == 1 else 'faces')
    # outputs = post_precessing(y_onnx,r,left,top) #检测后处理

# print(f"总共耗时{time.time() - begin} s")

画像-20230531152543624

おすすめ

転載: blog.csdn.net/weixin_42917352/article/details/131366739