Análisis de principio y algoritmo de Yolov5-Face

YOLOv5-Cara


imagen

En los últimos años, CNN se ha utilizado ampliamente en la detección de rostros. Sin embargo, muchos detectores de rostros requieren el uso de detectores de rostros especialmente diseñados para detectar rostros, y el autor de YOLOv5 trata la detección de rostros como una tarea general de detección de objetivos.

YOLOv5Face agrega un cabezal de regresión de referencia de 5 puntos (regresión de puntos clave) basado en YOLOv5 y utiliza la pérdida de ala para restringir el cabezal de regresión de referencia. YOLOv5Face diseña detectores con diferentes tamaños de modelos, desde modelos grandes hasta modelos ultrapequeños, para lograr una detección en tiempo real en dispositivos integrados o móviles.

imagen-20230529222235376

Los resultados experimentales en el conjunto de datos de WiderFace muestran que YOLOv5Face puede lograr un rendimiento de vanguardia en casi todos los subconjuntos Fácil, Medio y Difícil, superando a los detectores de rostros diseñados específicamente.

Dirección de Github: https://www.github.com/deepcam-cn/yolov5-face

1. ¿Por qué detección de rostros = detección general?

1.1 YOLOv5 Detección de rostros

En el método YOLOv5Face, la detección de rostros se considera una tarea general de detección de objetivos. Al igual que en TinaFace, el rostro humano se utiliza como objetivo. Como se comenta en TinaFace:

  • Desde una perspectiva de datos, las características del rostro humano como la postura, la escala, la oclusión, la iluminación y el desenfoque también aparecerán en otras tareas generales de detección;
  • Ver el género a partir de los atributos únicos del rostro, como la expresión y el maquillaje, también puede corresponder a cambios de forma y de color en problemas generales de detección.

1.2 Punto de referencia YOLOv5Face

​ Landmark es una existencia relativamente especial, pero no es la única. Son sólo puntos clave de un objeto. Por ejemplo, en la detección de matrículas, también se utiliza Landmark. Agregar la regresión de Landmark al encabezado del modelo de predicción objetivo es relativamente simple con un solo clic. Entonces, desde la perspectiva de los desafíos que enfrenta la detección de rostros, en la detección de objetivos en general existen rostros pequeños, escenas densas, etc. Por lo tanto, la detección de rostros puede considerarse como una subtarea general de detección de objetivos.

2. Objetivos de diseño y principales contribuciones de YOLOv5Face

2.1 Objetivos de diseño

YOLOv5Face ha rediseñado y modificado YOLOv5 para la detección de rostros, teniendo en cuenta las diferentes complejidades y aplicaciones de rostros grandes, rostros pequeños, supervisión de Landmark, etc. El objetivo de YOLOv5Face es proporcionar una combinación de modelos para diferentes aplicaciones, desde las muy complejas hasta las muy simples, para obtener el mejor equilibrio entre rendimiento y velocidad en dispositivos integrados o móviles.

2.2 Principales contribuciones

  1. YOLOV5 fue rediseñado como detector de rostros y se llamó YOLOv5Face. Se realizaron modificaciones clave a la red para mejorar el rendimiento en términos de precisión promedio promedio (mAP) y velocidad;
  2. Se diseña una serie de modelos de diferentes tamaños, desde modelos grandes hasta modelos medianos y modelos ultrapequeños, para satisfacer las necesidades de diferentes aplicaciones. Además del Backbone utilizado en YOLOv5, también se implementa un Backbone basado en ShuffleNetV2, que proporciona un rendimiento de última generación y una velocidad rápida para dispositivos móviles;
  3. El modelo YOLOv5Face se evalúa en el conjunto de datos WiderFace. En imágenes con resolución VGA, casi todos los modelos alcanzan rendimiento y velocidad SOTA. Esto también prueba la conclusión anterior: no es necesario rediseñar un detector facial, porque YOLO5 puede completarlo.

3. Arquitectura YOLOv5Face

3.1 Arquitectura del modelo

3.1.1 Diagrama del modelo

YOLOv5Face utiliza YOLOv5 como base para mejorar y rediseñar para adaptarse a la detección de rostros. El objetivo principal aquí es detectar modificaciones en rostros pequeños y grandes.

640

La arquitectura de red del detector facial YOLO5 se muestra en la Figura 1. Consta de Backbone, Neck y Head y describe la arquitectura general de la red. En YOLOv5, se utiliza CSPNet Backbone. SPP y PAN se utilizan en Neck para fusionar estas características. Tanto la regresión como la clasificación también se utilizan en Head.

3.1.2 Módulo CBS

Insertar descripción de la imagen aquí
En la figura se define un bloque CBS, que consta de las funciones de activación Conv, BN y SiLU. CBS Block también se utiliza en muchos otros bloques.

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 Salida del cabezal

640 (2)

Muestra las etiquetas de salida de Head, que incluyen cuadros delimitadores (bbox), confianza (conf), clasificación (cls) y puntos de referencia de 5 puntos. Estos puntos de referencia son mejoras de YOLOv5, lo que lo convierte en un detector de rostros con salida de puntos de referencia. Sin Puntos de Referencia, la longitud del último vector debería ser 6 en lugar de 16. Cuadro (4) + Confianza (1) + Puntos clave (5*2) + cls (categoría)

El tamaño de salida en P3 es 80×80×16, 40×40×16 en P4, 20×20×16 en P5 y, opcionalmente, 10×10×16 en P6 para cada anclaje. El tamaño real debe multiplicarse por la cantidad de anclajes.

3.1.4 estructura del tallo

640 (3)

La imagen muestra la estructura principal, que se utiliza para reemplazar la capa de enfoque original en YOLOv5. La introducción de bloques Stem para la detección de rostros en YOLOv5 es una de las innovaciones de YOLOv5Face.

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

Reemplazar el módulo Focus original en la red con el módulo Stem mejora la capacidad de generalización de la red y reduce la complejidad computacional sin reducir el rendimiento . Aunque CBS se utiliza en las ilustraciones del módulo Stem, al observar el código se puede ver que el segundo y el cuarto CBS son convoluciones de 1 × 1, el primer y el tercer CBS son de 3 × 3, zancada = 2 de convolución. Con el archivo yaml, puede ver que el tamaño de la imagen ha cambiado de 640 × 640 a 160 × 160 después de la raíz.

3.1.5 Estructura del PSIC

Insertar descripción de la imagen aquí

El diseño de CSP Block está inspirado en DenseNet. Sin embargo, en lugar de agregar la entrada y salida completas después de algunas capas de CNN, la entrada se divide en 2 partes. La mitad pasa a través de un bloque CBS, es decir, algunos bloques de cuello de botella, y la otra mitad se calcula a través de la capa 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))

Insertar descripción de la imagen aquí

La capa de cuello de botella se expresa como:

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 Estructura del SPP

Insertar descripción de la imagen aquí

En este bloque, YOLOv5Face ha modificado los tamaños del kernel de 13 × 13, 9 × 9 y 5 × 5 en YOLOv5 a 7 × 7, 5 × 5 y 3 × 3. Esta mejora es más adecuada para la detección de rostros y mejora la Rendimiento de la detección de rostros Precisión de la detección de rostros.

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))

Al mismo tiempo, YOLOv5Face agrega un bloque de salida P6 con zancada = 64. P6 puede mejorar el rendimiento de detección de rostros grandes. (Los modelos de detección de rostros anteriores se centraban principalmente en mejorar el rendimiento de detección de rostros pequeños. Aquí el autor se centra en el efecto de detección de rostros grandes y mejora el rendimiento de detección de rostros grandes para mejorar el rendimiento de detección general del modelo). El tamaño del mapa de características de P6 es 10x10.

Tenga en cuenta que aquí solo se consideran imágenes de entrada con resolución VGA. Para ser más precisos, los bordes más largos de la imagen de entrada se escalan a 640 y los bordes más cortos se escalan en consecuencia. Los bordes más cortos también se ajustan para que sean múltiplos de la zancada máxima del bloque SPP. Por ejemplo, cuando no se usa P6, el lado más corto debe ser un múltiplo de 32; cuando se usa P6, el lado más corto debe ser un múltiplo de 64.

3.2 Mejoras en el lado de entrada

Algunos métodos de aumento de datos para la detección de objetos no son adecuados para su uso en la detección de rostros, incluidos los volteos hacia arriba y hacia abajo y el aumento de datos en mosaico.

  • Quitar los giros hacia arriba y hacia abajo puede mejorar el rendimiento del modelo .
  • El aumento de datos de mosaico para caras pequeñas reducirá el rendimiento del modelo , pero Mosaic para caras de mediana y gran escala puede mejorar el rendimiento .
  • El recorte aleatorio ayuda a mejorar el rendimiento .

Nota: Existen diferencias de escala entre el conjunto de datos COCO y el conjunto de datos WiderFace. El conjunto de datos WiderFace tiene datos relativamente más a pequeña escala.

3.3 Devoluciones de hitos

3.3.1 Punto de referencia de salida

El punto de referencia es una característica importante del rostro humano. Se pueden utilizar para tareas como comparación de rostros, reconocimiento de rostros, análisis de expresiones faciales, análisis de edad, etc. Hito tradicional consta de 68 puntos. Cuando se simplificaron a 5 puntos, estos puntos de referencia de 5 puntos se utilizaron ampliamente en el reconocimiento facial. La calidad de la identificación facial afecta directamente la calidad de la alineación y el reconocimiento facial.

  • Los detectores de objetos generales no incluyen Landmark. Se puede agregar directamente como Head de retorno. Por eso, el autor lo agregó a YOLO5Face. La salida de Landmark se utilizará para alinear las imágenes de rostros antes de enviarlas a la red de reconocimiento de rostros.

3.3.2 Función de pérdida de referencia Ala

La función de pérdida general para la regresión Landmark es L2, L1 o L1 suave. MTCNN utiliza la función de pérdida L2. Sin embargo, los autores descubrieron que estas funciones de pérdida no son sensibles a pequeños errores. Para superar este problema, se propone la pérdida de ala:
ala ⁡ ( x ) = { w ln ⁡ ( 1 + ∣ x ∣ / ϵ ) si ∣ x ∣ < w ∣ x ∣ − C en caso contrario \operatorname{wing}(x)= \begin{cases}w \ln (1+|x| / \epsilon) & \text { if }|x|<w \\ |x|-C & \text { de lo contrario }\end{cases}ala ( x )={ wen ( 1+x ∣/ ϵ )x C si x <wde lo contrario 
w: w:w: número positivowww limita el rango de la parte no lineal a[ − w , w ] [-w, w][ -w , _w ] dentro del intervalo;
ϵ \epsilonϵ : Restringe la curvatura de la región no lineal, yC = w − w ln ⁡ ( 1 + x ϵ ) C=ww \ln \left(1+\frac{x}{\epsilon}\right)C=wwen( 1+ϵx) es una constante que se puede utilizar con suavizado para conectar las partes lineales y no lineales de la pieza. ϵ \épsilonEl valor de ϵ es un valor muy pequeño porque hará que el entrenamiento de la red sea inestable y provocará problemas de explosión de gradiente debido a pequeños errores.
De hecho, la parte no lineal de la función de pérdida de ala simplemente tomaln ⁡ ( x ) \ln (x)ln ( x )[ ϵ/w , 1 + ϵ/w ] [\epsilon/w, 1+\epsilon/w][ ϵ / w ,1+ϵ / w ] y a lo largo del eje X eYYEl eje Y lo escala a W. Además, a lo largode YYSe aplica una traslación al eje Y de modo que ala(0) = 0 (0)=0( 0 )=0 , e imponer continuidad a la función de pérdida.
Vector de punto de referencias = { si } s=\left\{s_i\right\}s={ syo}与其 verdad fundamentals ′ = { si } s^{\prime}=\left\{s_i\right\}s={ syo}的损失函数为:
pérdida ⁡ L ( s ) = ∑ i ala ⁡ ( si − si ′ ) \operatorname{pérdida}_L(s)=\sum_i \operatorname{wing}\left(s_i-s_i^{\prime }\bien)pérdidaL( s )=iala( syosi)
dondei = 1, 2, …, 10 i=1,2, \ldots, 10i=1 ,2 ,,10 .
Deje que la función de pérdida de detección de objetivos general en YOLOv5 seapérdida O los s_Olos s _O, entonces la nueva función de pérdida total es:
pérdida ⁡ ( s ) = pérdida ⁡ O + λ L ⋅ pérdida ⁡ L \operatorname{loss}(s)=\operatorname{loss}_O+\lambda_L \cdot \operatorname{loss}_Lperdida ( s )=pérdidaO+ yoLpérdidaL
donde λ L \lambda_LyoLes el factor de ponderación de la función de pérdida de regresión Landmark.
Adquisición del hito: donde i = 1, 2, …, 10 i=1,2, \ldots, 10i=1 ,2 ,,10 .
Deje que la función de pérdida de detección de objetivos general en YOLOv5 seapérdida O los s_Olos s _O, entonces la nueva función de pérdida total es:
pérdida ⁡ ( s ) = pérdida ⁡ O + λ L ⋅ pérdida ⁡ L \operatorname{loss}(s)=\operatorname{loss}_O+\lambda_L \cdot \operatorname{loss}_Lperdida ( s )=pérdidaO+ yoLpérdidaL
donde λ L \lambda_LyoLes el factor de ponderación de la función de pérdida de regresión Landmark.

Adquisición de hitos:

#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)

La pérdida de ala se calcula de la siguiente manera:

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)

Analizar y comparar las funciones de pérdida L1, L2 y Smooth 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)pérdida( s ,s )=yo = 12 litrosF( syosi)
dondesss es la verdad fundamental de los puntos clave de la cara, la funciónf ( x ) f(x)f ( x ) es equivalente a:
pérdida L1
L 1 ( x ) = ∣ x ∣ L 1(x)=|x|L 1 ( x )=x
pérdida de L2
L 2 ( x ) = 1 2 x 2 L 2(x)=\frac{1}{2} x^2L2 ( x ) _=21X2

Suave ⁡ L 1 ( x ) : suave ⁡ L 1 ( x ) = { 1 2 x 2 si ∣ x ∣ < 1 ∣ x ∣ − 1 2 en caso contrario \operatorname{Suave}_{L 1}(x): \operatorname {suave}_{L 1}(x)= \begin{cases}\frac{1}{2} x^2 & \text { if }|x|<1 \\ |x|-\frac{1} {2} & \text { de lo contrario }\end{cases}LisoL 1( x ):lisoL 1( x )={ 21X2x 21 si x <1de lo contrario 

Función de pérdida para xxLas derivadas de x son:
d L 2 ( x ) dx = x \frac{d L_2(x)}{dx}=xd xdl _2( x )=X

d L 1 ( x ) dx = { 1 si x ≥ 0 − 1 en caso contrario \frac{d L_1(x)}{dx}= \begin{cases}1 & \text { if } x \geq 0 \\ -1 & \text {de lo contrario}\end{casos}d xdl _1( x )={ 1 1 si  x0de lo contrario 

d suave ⁡ L 1 ( x ) dx = { x si ∣ x ∣ < 1 ± 1 en caso contrario \frac{d \operatorname{smooth}_{L 1}(x)}{dx}= \begin{cases}x & \text { si }|x|<1 \\ \pm 1 & \text { en caso contrario }\end{casos}d xdlisoL 1( x )={ X± 1 si x <1de lo contrario 

  • Función de pérdida L2, cuando xxPérdida de L2 a xxcuando x aumentaLa derivada de x también aumenta, lo que conduce a la etapa inicial del entrenamiento, cuando la diferencia entre el valor predicho y la verdad fundamental es demasiado grande, el gradiente de la función de pérdida al valor predicho es muy grande, lo que resulta en un entrenamiento inestable.

  • La derivada de la pérdida L1 es una constante. En la etapa posterior del entrenamiento, cuando la diferencia entre el valor predicho y la verdad fundamental es muy pequeña, el valor absoluto de la derivada de la pérdida respecto al valor predicho sigue siendo 1. En esta vez, si la tasa de aprendizaje permanece sin cambios, la función de pérdida fluctúa cerca del valor estable y es difícil continuar convergiendo para lograr una mayor precisión.

  • función de pérdida suave de L1, cuando x es pequeña, para xxEl gradiente de x también se hará más pequeño, y enxxCuando x es muy grande, paraxxEl valor absoluto del gradiente de x alcanza un límite superior de 1 y no será demasiado grande para destruir los parámetros de la red. El L1 suave evita perfectamente los defectos de las pérdidas de L1 y L2.
    Además, según fast rcnn, "... la pérdida de L1 es menos sensible a los valores atípicos que la pérdida de L2 utilizada en R-CNN y SPPnet". Es decir, la L1 suave hace que la pérdida sea más robusta a los valores atípicos, es decir, en comparación con La función de pérdida L2 no es sensible a valores atípicos ni a valores atípicos, y los cambios de gradiente son relativamente pequeños, por lo que no es fácil huir durante el entrenamiento.
    Insertar descripción de la imagen aquí

La figura anterior muestra gráficos de estas funciones de pérdida. Cabe señalar que la pérdida Smoolth L1 es un caso especial de la pérdida de Huber. La función de pérdida L2 se usa ampliamente en la detección de puntos clave faciales, pero la pérdida L2 es sensible a los valores atípicos.

3.3.3 Pérdida de ala

Todas las funciones de pérdida funcionan bien en presencia de errores grandes. Esto muestra que el entrenamiento de redes neuronales debería prestar más atención a muestras con errores pequeños o medianos. Para lograr este objetivo, se propone una nueva función de pérdida, Wing Loss basada en la localización facial de Landmark basada en CNN.

Insertar descripción de la imagen aquí

Cuando NME está en 0,04, la proporción de datos de prueba es cercana a 1, por lo que en la sección de 0,04 a 0,05, que es la llamada sección de errores grandes, no hay más datos distribuidos, lo que indica que cada función de pérdida funciona muy bien en La sección de errores grandes, bien.

El rendimiento inconsistente del modelo radica en los segmentos de errores pequeños y errores medios, por ejemplo, se dibuja una línea vertical donde el NME es 0,02, que es muy diferente. Por lo tanto, el autor propone que se debe prestar más atención a muestras de errores de rango pequeño o mediano durante el proceso de capacitación.

Puedes usar ln ⁡ x \ln xenx para mejorar la influencia de pequeños errores, su gradiente es1 x \frac{1}{x}X1, el valor cercano a 0 será mayor, el tamaño de paso óptimo es x 2 x^2X2 , de modo que el gradiente esté "dominado" por errores pequeños y el tamaño del paso esté "dominado" por errores grandes. Esto restablece el equilibrio entre errores de diferentes tamaños. Sin embargo, para evitar grandes pasos de actualización en direcciones potencialmente incorrectas, es importante no compensar en exceso los efectos de errores de posicionamiento más pequeños. Esto se puede lograr eligiendo una función logarítmica con un desplazamiento positivo.
Pero este tipo de función de pérdida es adecuada para manejar errores de posicionamiento relativamente pequeños. En la detección de puntos clave de caras salvajes, se pueden tratar posturas extremas donde los errores de posicionamiento inicial pueden ser muy grandes, en cuyo caso la función de pérdida debería facilitar una recuperación rápida de estos grandes errores. Esto sugiere que la función de pérdida debería comportarse más comoL 1 L 1L 1 oL 2 L 2L2._ _ _ DesdeL 2 L 2L2 es sensible a valores atípicos, por lo que se elige L1.
Entonces,Wing Lossdebería comportarse como una función logarítmica con compensación para errores pequeños y comoL 1 L 1L1 _

3.4 NMS de posprocesamiento

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 cara de yolov5s
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. Entrenamiento modelo

4.1 Descargar código fuente

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

4.2 Descargar un conjunto de datos más amplio

Después de la descarga, descomprima la ubicación y colóquela en la carpeta Wideface debajo de la carpeta de datos en el proyecto yolov5-face-master.

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

4.3 Ejecute train2yolo.py y val2yolo.py

Cree una nueva carpeta Widefaceyolo en la carpeta de datos y configure los subdirectorios para entrenar, probar y val.

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

Convierta el conjunto de datos al formato utilizado para el entrenamiento de yolo. Una vez completado, la carpeta aparecerá de la siguiente manera:

imagen-20230529234006920

imagen-20230529234020666

4.4 tren

4.4.1 Cambiar el archivo de configuración del entrenamiento

wideface.yaml cambia el directorio al directorio del conjunto de datos

# 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']

imagen-20230530105752825

4.4.2 Visualización del entrenamiento

tensorboard --logdir runs/train

4.4.3 Informes de errores relacionados

  1. No se puede dibujar la imagen
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)

Solución Comente esta línea de código:

imagen-20230530212314052

  1. los pesos no se guardan

Después de entrenar durante mucho tiempo, descubrí que los pesos guardados estaban vacíos. Tenga en cuenta que la época relevante es mayor que 20 antes de guardar el código de peso.

imagen-20230530212411461

imagen-20230530212522918

4.5 detectar

Cambiar código

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 Exportación ONNX y configuración del entorno 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())

imagen-20230530232400331

4.7 Razonamiento OnnXruntime

El código se muestra a continuación:

#!/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")

imagen-20230531152543624

Supongo que te gusta

Origin blog.csdn.net/weixin_42917352/article/details/131366739
Recomendado
Clasificación