Ejemplo de módulo opencv dnn (17) detección de objetivos object_detection de yolo v5

Dos meses después del yolo v4 presentado en el artículo anterior [ ejemplo de módulo opencv dnn (16) detección de objetivos object_detection - yolov4] , Ultralytics lanzó la primera versión oficial de YOLOV5, cuyo rendimiento es comparable al de YOLO V4.

Insertar descripción de la imagen aquí

Yolo v5 en realidad no tiene relación de herencia con Yolo v4. Ambos están mejorados en base a yolo v3. Sin embargo, debido a que no ha publicado artículos correspondientes, protocolos de código abierto y otros problemas, se ha cuestionado que no pueda considerarse como una nueva generación. de YOLO. Sin embargo, para nuestro aprendizaje y uso, siempre que pueda cazar ratones, un gato blanco o un gato negro es un buen gato.

1. Explicación de las diferencias entre Yolo v5 y Yolo v4

Compare YOLO V5 y V4 desde los siguientes aspectos, describa brevemente las características de sus respectivas nuevas tecnologías y compare las diferencias y similitudes entre los dos.

1.1 Aumento de datos: mejora de datos

YOLO V4 utiliza una combinación de múltiples tecnologías de mejora de datos para una sola imagen. Además de la distorsión geométrica clásica y la distorsión de iluminación, también utiliza de manera innovadora la tecnología de oclusión de imagen (borrado aleatorio, recorte, escondite, máscara de cuadrícula, mezcla). -Combinación de imágenes, el autor utiliza una combinación de tecnologías CutMix y Mosaic. Además, el autor también utilizó el entrenamiento de autoadversidad (SAT) para mejorar los datos.

El autor de YOLO V5 aún no ha publicado un artículo, por lo que su canal de aumento de datos solo puede entenderse desde la perspectiva del código.
YOLOV5 pasará cada lote de datos de entrenamiento a través del cargador de datos y al mismo tiempo mejorará los datos de entrenamiento.
El cargador de datos realiza tres tipos de mejora de datos: escalado, ajuste del espacio de color y mejora del mosaico.
Curiosamente, hay informes en los medios de que Glen Jocher, el autor de YOLO V5, es el creador de Mosaic Augmentation. Él cree que la enorme mejora en el rendimiento de YOLO V4 se debe en gran medida a la mejora de los datos en mosaico. Tal vez no esté convencido. lanzamiento de YOLO V4
, YOLO V5 se lanzó en solo dos meses. Por supuesto, continuar usando el nombre YOLO V5 o adoptar otros nombres en el futuro depende primero de si los resultados finales de la investigación de YOLO V5 realmente pueden liderar a YOLO V4.
Pero es innegable que la mejora de los datos de mosaico puede resolver eficazmente el "problema de objetos pequeños" más problemático en el entrenamiento de modelos, es decir, los objetos pequeños no se detectan con tanta precisión como los objetos grandes.

1.2 Anclajes de cuadro delimitador de aprendizaje automático: cuadro de anclaje adaptativo

En el YOLO V3 anterior, se utilizaron k-means y algoritmos de aprendizaje genético para analizar el conjunto de datos personalizados para obtener un cuadro de anclaje preestablecido adecuado para la predicción del cuadro de límites de objetos en el conjunto de datos personalizado.
Insertar descripción de la imagen aquí
En YOLO V5, el cuadro de anclaje se aprende automáticamente en función de los datos de entrenamiento. YOLO V4 no tiene un cuadro de anclaje adaptativo .

Para el conjunto de datos COCO, el tamaño del cuadro de anclaje bajo el tamaño de imagen de 640 × 640 se preestableció en el archivo de configuración *.yaml de YOLO V5:

anchors:
  - [10,13, 16,30, 33,23]  		# P3/8
  - [30,61, 62,45, 59,119]  	# P4/16
  - [116,90, 156,198, 373,326]  # P5/32

Para conjuntos de datos personalizados, dado que el marco de reconocimiento de objetivos a menudo necesita escalar el tamaño de la imagen original y el tamaño del objeto de destino en el conjunto de datos puede ser diferente del conjunto de datos COCO, YOLO V5 aprenderá automáticamente el tamaño del cuadro de anclaje. de nuevo.
Insertar descripción de la imagen aquí
En la imagen de arriba, YOLO V5 está aprendiendo el tamaño del cuadro de anclaje automático. Para el conjunto de datos BDD100K, después de escalar la imagen en el modelo a 512, el cuadro de anclaje óptimo es:
Insertar descripción de la imagen aquí

1.3 Red parcial de etapas cruzadas (CSP)

Tanto YOLO V5 como V4 utilizan CSPDarknet como columna vertebral. El nombre completo de CSPNet es Cross Stage Partial Networks, que es una red parcial entre etapas. CSPNet resuelve el problema de duplicación de información de gradiente en la optimización de la red en otros grandes marcos de redes neuronales convolucionales Backbone e integra los cambios de gradiente en el mapa de características de principio a fin, reduciendo así la cantidad de parámetros del modelo y valores FLOPS, lo que no solo garantiza la velocidad de razonamiento. y precisión, y tamaño reducido del modelo.

1.4 Red de agregación Neck-Path (PANET)

Neck se utiliza principalmente para generar pirámides de características. La pirámide de características mejorará la detección de objetos en diferentes escalas por parte del modelo, permitiéndole reconocer el mismo objeto en diferentes tamaños y escalas.

Antes de que apareciera PANET, FPN había sido lo último en la capa de agregación de características del marco de detección de objetos hasta la aparición de PANET.

En la investigación de YOLO V4, se considera que PANET es la red de fusión de características más adecuada para YOLO, por lo que tanto YOLO V5 como V4 usan PANET como Neck para agregar características.

1.5 Capa de detección universal Head-YOLO

El modelo Head se utiliza principalmente para la parte de detección final. Aplica cuadros de anclaje en el mapa de características y produce un vector de salida final con probabilidades de clase, puntuaciones de objetos y cuadros delimitadores.

En el modelo YOLO V5, el modelo Head es el mismo que el de las versiones anteriores de YOLO V3 y V4.
Insertar descripción de la imagen aquí
Estos cabezales con diferentes escalas se utilizan para detectar objetos de diferentes tamaños (entrada 608, salida final con reducción de resolución 5 veces), cada cabezal tiene un total de (80 clases + 1 probabilidad + 4 coordenadas) * 3 cuadros de anclaje, un total de 255 canales.

1.5, Función de activación - función de activación

La elección de la función de activación es crucial para las redes de aprendizaje profundo. El autor de YOLO V5 utilizó las funciones de activación Leaky ReLU y Sigmoid.

En YOLO V5, la capa intermedia/oculta usa la función de activación Leaky ReLU y la capa de detección final usa la función de activación Sigmoide. YOLO V4 utiliza la función de activación Mish.

Mish supera a Swish en 39 puntos de referencia y a ReLU en 40 puntos de referencia, y algunos resultados muestran mejoras del 3 al 5 % en la precisión de los puntos de referencia. Pero tenga en cuenta que la activación de Mish es computacionalmente más costosa en comparación con ReLU y Swish.
Insertar descripción de la imagen aquí

1.6 Función de optimización - función de optimización

El autor de YOLO V5 nos proporciona dos funciones de optimización, Adam y SGD, y ambas hiperparámetros de entrenamiento coincidentes preestablecidos. El valor predeterminado es SGD.

YOLO V4 utiliza SGD.

El autor de YOLO V5 recomienda que si necesita entrenar conjuntos de datos personalizados más pequeños, Adam es una opción más adecuada, aunque la tasa de aprendizaje de Adam es generalmente menor que la de SGD.

Pero si entrenas un gran conjunto de datos, SGD funciona mejor que Adam para YOLOV5.

De hecho, no existe una conclusión unificada en la comunidad académica sobre cuál es mejor, SGD o Adam, y depende de la situación real del proyecto.

El cálculo de pérdidas de la serie YOLO de función de costo
se basa en la puntuación de objetividad, la puntuación de probabilidad de clase y la puntuación de regresión del cuadro delimitador.

YOLO V5 utiliza GIOU Loss como pérdida del cuadro delimitador y utiliza entropía cruzada binaria y función de pérdida Logits para calcular la pérdida de probabilidad de clase y la puntuación objetivo. Al mismo tiempo, también podemos usar el parámetro fl_gamma para activar la pérdida focal para calcular la función de pérdida.

YOLO V4 utiliza CIOU Loss como pérdida del cuadro delimitador. En comparación con otros métodos mencionados, CIOU brinda una convergencia más rápida y un mejor rendimiento.
Insertar descripción de la imagen aquí

Los resultados de la figura anterior se basan en Faster R-CNN y se puede ver que CIoU en realidad funciona mejor que GIoU.

1.7, Puntos de referencia: YOLO V5 VS YOLO V4

Antes de una discusión detallada en el artículo, solo podemos comparar el desempeño de los dos observando los indicadores COCO publicados por el autor y combinados con las evaluaciones de ejemplo posteriores realizadas por los grandes.

1.7.1 Evaluación oficial del desempeño

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
En las dos figuras anteriores, la relación entre FPS y ms/img está invertida. Después de la conversión de unidades, podemos encontrar que YOLO V5 puede alcanzar 250 FPS en la V100GPU y tiene un mAP alto.

Dado que el entrenamiento original de YOLO V4 está en 1080TI, que es mucho más bajo que el rendimiento de V100, y los puntos de referencia de AP_50 y AP_val son diferentes, es imposible obtener los puntos de referencia de los dos basándose únicamente en la tabla anterior.

Afortunadamente, WongKinYiu, el segundo autor de YOLO V4, utilizó la GPU V100 para proporcionar puntos de referencia comparables.
Insertar descripción de la imagen aquí

Como se puede ver en el gráfico, el rendimiento de los dos es realmente muy similar, pero según los datos, YOLO V4 sigue siendo el mejor marco de detección de objetos. YOLO V4 es altamente personalizable. Si no le temen a más configuraciones personalizadas, YOLO V4 basado en Darknet sigue siendo el más preciso.

Vale la pena señalar que YOLO V4 en realidad utiliza una gran cantidad de tecnologías de mejora de datos en la base de código Ultralytics YOLOv3. Estas tecnologías también se ejecutan en YOLO V5. El impacto de la tecnología de mejora de datos en los resultados debe esperar hasta el artículo del autor. análisis.

1.7.2 Tiempo de entrenamiento

Según la investigación de Roboflow, YOLO V5 entrena muy rápido, superando con creces a YOLO V4 en velocidad de entrenamiento. Para el conjunto de datos personalizado de Roboflow, YOLO V4 tardó 14 horas en alcanzar la evaluación de validación máxima, mientras que YOLO V5 solo tardó 3,5 horas.

Insertar descripción de la imagen aquí

1.7.3 Tamaño del modelo

Los tamaños de los diferentes modelos en la figura son: V5x: 367 MB, V5l: 192 MB, V5m: 84 MB, V5s: 27 MB, YOLOV4: 245 MB. El tamaño del modelo YOLO V5s es muy pequeño, lo que reduce los costos de implementación y favorece una implementación rápida
. del modelo.
Insertar descripción de la imagen aquí

1.7.4 Tiempo de razonamiento

Insertar descripción de la imagen aquí

En una sola imagen (tamaño de lote 1), YOLOV4 infiere en 22 ms y YOLOV5s infiere en 20 ms.

La implementación de YOLOV5 utiliza de forma predeterminada la inferencia por lotes (tamaño de lote 36) y divide el tiempo de procesamiento por lotes por la cantidad de imágenes en el lote. El tiempo de inferencia de una sola imagen puede alcanzar 7 ms, que es 140 FPS. Este es el estado actual de -la técnica en el campo de la detección de objetos.

Utilicé el modelo que entrené para realizar inferencia en tiempo real en 10,000 imágenes de prueba. La velocidad de inferencia de YOLOV5 es muy sorprendente. Cada imagen requiere solo 7 ms de tiempo de inferencia. Junto con el tamaño del modelo de más de 20 megabytes, no tiene rival en términos de flexibilidad.

Pero, de hecho, esto no es justo para YOLO V4. Dado que YOLO V4 no implementa el razonamiento por lotes predeterminado, está en desventaja en comparación. Deberían realizarse muchas pruebas en los dos marcos de detección de objetos bajo el mismo punto de referencia.

En segundo lugar, YOLO V4 lanzó recientemente una versión pequeña. La comparación de rendimiento y velocidad entre YOLO V5 y V4 pequeña requiere un análisis más práctico.

1.8 Comparación y resumen

En general, YOLO V4 es mejor que YOLO V5 en rendimiento, pero más débil que YOLO V5 en flexibilidad y velocidad.

Dado que YOLO V5 todavía se está actualizando rápidamente, los resultados finales de la investigación de YOLO V5 aún no se han analizado.

Personalmente, creo que para estos marcos de detección de objetos, el rendimiento de la capa de fusión de características es muy importante. Actualmente, ambos usan PANET, pero según la investigación de Google Brain, BiFPN es la mejor opción para la capa de fusión de características. Quien pueda integrar esta tecnología probablemente logrará mejoras significativas en el rendimiento.

Insertar descripción de la imagen aquí

Aunque YOLO V5 sigue siendo inferior, YOLO V5 todavía tiene las siguientes ventajas significativas:

  • El uso del marco Pytorch es muy fácil de usar y puede entrenar fácilmente sus propios conjuntos de datos. En comparación con el marco Darknet adoptado por YOLO V4, el marco Pytorch es más fácil de poner en producción.

  • El código es fácil de leer e integra una gran cantidad de tecnologías de visión por computadora, lo que favorece el aprendizaje y la referencia.

  • No solo es fácil configurar el entorno, sino que el entrenamiento del modelo también es muy rápido y la inferencia por lotes produce resultados en tiempo real.

  • Capacidad para realizar inferencias eficientes directamente en imágenes individuales, imágenes en lotes, vídeos e incluso entradas de puertos de cámara web

  • Puede convertir fácilmente el archivo de peso de Pytorch al formato ONXX utilizado por Android y luego convertirlo al formato utilizado por OPENCV, o convertirlo al formato IOS a través de CoreML e implementarlo directamente en la aplicación móvil.

  • Finalmente, la velocidad de reconocimiento de objetos de YOLO V5 hasta 140 FPS es muy impresionante y la experiencia del usuario es excelente.

2. prueba yolo v5

La dirección actual del proyecto yolo v5 es https://github.com/ultralytics/yolov y la versión se actualizó a v7.0.

2.1 prueba de Python

2.1.1 Instalación

git clone https://github.com/ultralytics/yolov5  # clone
cd yolov5
pip install -r requirements.txt  # install

2.1.2 Razonamiento

  • Utilizando la inferencia del concentrador yolov5, el último modelo se descargará automáticamente desde la versión YOLOv5.

    	import torch
    	# Model
    	model = torch.hub.load("ultralytics/yolov5", "yolov5s")  # or yolov5n - yolov5x6, custom
    	# Images
    	img = "https://ultralytics.com/images/zidane.jpg"  # or file, Path, PIL, OpenCV, numpy, list
    	# Inference
    	results = model(img)
    	# Results
    	results.print()  # or .show(), .save(), .crop(), .pandas(), etc.
    	```
    
    
  • La inferencia usando detect.py
    detect.py ejecuta la inferencia en varias fuentes. El modelo se descarga automáticamente desde la última versión de YOLOv5 y los resultados se guardan en ejecuciones/detección.

    python detect.py --weights yolov5s.pt --source 0                               # webcam
                                                   img.jpg                         # image
                                                   vid.mp4                         # video
                                                   screen                          # screenshot
                                                   path/                           # directory
                                                   list.txt                        # list of images
                                                   list.streams                    # list of streams
                                                   'path/*.jpg'                    # glob
                                                   'https://youtu.be/LNwODJXcvt4'  # YouTube
                                                   'rtsp://example.com/media.mp4'  # RTSP, RTMP, HTTP stream
    

2.1.3 Salida de prueba

Preste atención a la comparación de uso y eficiencia operativa de los parámetros --dnn y --half, centrándose en los datos de tiempo de tres indicadores de preproceso, inferencia y nms.

(yolo_pytorch) E:\DeepLearning\yolov5>python detect.py --weights yolov5n.pt --source data/images/bus.jpg
detect: weights=['yolov5n.pt'], source=data/images/bus.jpg, data=data\coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs\detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=False, vid_stride=1
YOLOv5  v7.0-167-g5deff14 Python-3.9.16 torch-1.13.1+cu117 CUDA:0 (NVIDIA GeForce GTX 1080 Ti, 11264MiB)

Fusing layers...
YOLOv5n summary: 213 layers, 1867405 parameters, 0 gradients
image 1/1 E:\DeepLearning\yolov5\data\images\bus.jpg: 640x480 4 persons, 1 bus, 121.0ms
Speed: 1.0ms pre-process, 121.0ms inference, 38.0ms NMS per image at shape (1, 3, 640, 640)
Results saved to runs\detect\exp2


(yolo_pytorch) E:\DeepLearning\yolov5>python detect.py --weights yolov5n.pt --source data/images/bus.jpg --device 0
detect: weights=['yolov5n.pt'], source=data/images/bus.jpg, data=data\coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=0, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs\detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=True, vid_stride=1
YOLOv5  v7.0-167-g5deff14 Python-3.9.16 torch-1.13.1+cu117 CUDA:0 (NVIDIA GeForce GTX 1080 Ti, 11264MiB)

Fusing layers...
YOLOv5n summary: 213 layers, 1867405 parameters, 0 gradients
image 1/1 E:\DeepLearning\yolov5\data\images\bus.jpg: 640x480 4 persons, 1 bus, 11.0ms
Speed: 0.0ms pre-process, 11.0ms inference, 7.0ms NMS per image at shape (1, 3, 640, 640)
Results saved to runs\detect\exp2


(yolo_pytorch) E:\DeepLearning\yolov5>python detect.py --weights yolov5n.pt --source data/images/bus.jpg --dnn
detect: weights=['yolov5n.pt'], source=data/images/bus.jpg, data=data\coco128.yaml, imgsz=[640, 640], conf_thres=0.25, iou_thres=0.45, max_det=1000, device=, view_img=False, save_txt=False, save_conf=False, save_crop=False, nosave=False, classes=None, agnostic_nms=False, augment=False, visualize=False, update=False, project=runs\detect, name=exp, exist_ok=False, line_thickness=3, hide_labels=False, hide_conf=False, half=False, dnn=True, vid_stride=1
YOLOv5  v7.0-167-g5deff14 Python-3.9.16 torch-1.13.1+cu117 CUDA:0 (NVIDIA GeForce GTX 1080 Ti, 11264MiB)

Fusing layers...
YOLOv5n summary: 213 layers, 1867405 parameters, 0 gradients
image 1/1 E:\DeepLearning\yolov5\data\images\bus.jpg: 640x480 4 persons, 1 bus, 10.0ms
Speed: 0.0ms pre-process, 10.0ms inference, 4.0ms NMS per image at shape (1, 3, 640, 640)
Results saved to runs\detect\exp3

Otras comparaciones de pruebas

        pre-process、 inference、   nms
cpu:        1           121       38
gpu:        0           11         7
dnn:        0		     10         4
gpu-half:    0           10         4
dnn-half:    1           11         4 

2.2 prueba de c++

Aquí, el módulo opencv dnn se utiliza para cargar el modelo de formato onnx exportado por yolov5 para realizar pruebas.

2.2.1 Exportación de modelo

El sitio web oficial en realidad proporciona archivos de exportación en formato onnx para cada versión del modelo, pero todos son modelos de media precisión y no se pueden usar directamente en opencv dnn.

Aquí tomamos yolov5x como ejemplo para exportar el modelo onnx. La primera vez que lo use, puede ver los parámetros del archivo py o verlo a través de la línea de comando, de la siguiente manera. Tenga en cuenta que al exportar, seleccione la versión adecuada de onnx opset para adaptarse a la versión opencv dnn .

(yolo_pytorch) E:\DeepLearning\yolov5>python export.py --weights yolov5x.pt --include onnx --opset 12
export: data=E:\DeepLearning\yolov5\data\coco128.yaml, weights=['yolov5x.pt'], imgsz=[640, 640], batch_size=1, device=cpu, half=False, inplace=False, keras=False, optimize=False, int8=False, dynamic=False, simplify=False, opset=12, verbose=False, workspace=4, nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45, conf_thres=0.25, include=['onnx']
YOLOv5  v7.0-167-g5deff14 Python-3.9.16 torch-1.13.1+cu117 CPU

Fusing layers...
YOLOv5x summary: 444 layers, 86705005 parameters, 0 gradients

PyTorch: starting from yolov5x.pt with output shape (1, 25200, 85) (166.0 MB)

ONNX: starting export with onnx 1.14.0...
ONNX: export success  10.0s, saved as yolov5x.onnx (331.2 MB)

Export complete (15.0s)
Results saved to E:\DeepLearning\yolov5
Detect:          python detect.py --weights yolov5x.onnx
Validate:        python val.py --weights yolov5x.onnx
PyTorch Hub:     model = torch.hub.load('ultralytics/yolov5', 'custom', 'yolov5x.onnx')
Visualize:       https://netron.app

2.2.2, prueba de código opencv dnn c ++

El código del tema es el mismo que en yolov4, las principales diferencias son:

  • El preprocesamiento se puede realizar de acuerdo con la situación, ya sea para escalar y llenar, asegurando que el tamaño sea consistente con la entrada de la red, consulte formatToSquare()la función.
  • Hay algunos pequeños ajustes en el procesamiento de datos de la salida de la red en el código de posprocesamiento.

El código completo es el siguiente.

#pragma once

#include "opencv2/opencv.hpp"

#include <fstream>
#include <sstream>
#include <random>

using namespace cv;
using namespace dnn;

 float inpWidth;
 float inpHeight;
 float confThreshold, scoreThreshold, nmsThreshold;
 std::vector<std::string> classes;
 std::vector<cv::Scalar> colors;

 bool letterBoxForSquare = true;

 cv::Mat formatToSquare(const cv::Mat &source);

 void postprocess(Mat& frame, cv::Size inputSz, const std::vector<Mat>& out, Net& net);

 void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame);

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dis(100, 255);

int main()
{
    
    
    // 根据选择的检测模型文件进行配置 
    confThreshold = 0.25;
    scoreThreshold = 0.45;
    nmsThreshold = 0.5;
    float scale = 1/255.0;  //0.00392
    Scalar mean = {
    
    0,0,0};
    bool swapRB = true;
    inpWidth = 640;
    inpHeight = 640;

    String model_dir = R"(E:\DeepLearning\yolov5)";
    String modelPath = model_dir + R"(\yolov5n.onnx)";
    String configPath;

    String framework = "";
    int backendId = cv::dnn::DNN_BACKEND_CUDA;
    int targetId = cv::dnn::DNN_TARGET_CUDA;

    String classesFile = R"(model\object_detection_classes_yolov3.txt)";

    // Open file with classes names.
    if(!classesFile.empty()) {
    
    
        const std::string& file = classesFile;
        std::ifstream ifs(file.c_str());
        if(!ifs.is_open())
            CV_Error(Error::StsError, "File " + file + " not found");
        std::string line;
        while(std::getline(ifs, line)) {
    
    
            classes.push_back(line);
            colors.push_back(cv::Scalar(dis(gen), dis(gen), dis(gen)));
        }
    } 
    // Load a model.
    Net net = readNet(modelPath, configPath, framework);
    net.setPreferableBackend(backendId);
    net.setPreferableTarget(targetId);

    std::vector<String> outNames = net.getUnconnectedOutLayersNames();
    {
    
    
        int dims[] = {
    
    1,3,inpHeight,inpWidth};
        cv::Mat tmp = cv::Mat::zeros(4, dims, CV_32F);
        std::vector<cv::Mat> outs;

        net.setInput(tmp);
        for(int i = 0; i<10; i++)
            net.forward(outs, outNames); // warmup
    }

    // Create a window
    static const std::string kWinName = "Deep learning object detection in OpenCV";

    cv::namedWindow(kWinName, 0);

    // Open a video file or an image file or a camera stream.
    VideoCapture cap;
    //cap.open(0);
    cap.open(R"(E:\DeepLearning\yolov5\data\images\bus.jpg)");

    cv::TickMeter tk;
    // Process frames.
    Mat frame, blob;

    while(waitKey(1) < 0) {
    
    
        //tk.reset();
        //tk.start();

        cap >> frame;
        if(frame.empty()) {
    
    
            waitKey();
            break;
        }

        // Create a 4D blob from a frame.
        cv::Mat modelInput = frame;
        if(letterBoxForSquare && inpWidth == inpHeight)
            modelInput = formatToSquare(modelInput);
            
        blobFromImage(modelInput, blob, scale, cv::Size2f(inpWidth, inpHeight), mean, swapRB, false);

        // Run a model.
        net.setInput(blob);

        std::vector<Mat> outs;
        //tk.reset();
        //tk.start();

        auto tt1 = cv::getTickCount();
        net.forward(outs, outNames);
        auto tt2 = cv::getTickCount();

        tk.stop();
        postprocess(frame, modelInput.size(), outs, net);
        //tk.stop();

        // Put efficiency information.
        std::vector<double> layersTimes;
        double freq = getTickFrequency() / 1000;
        double t = net.getPerfProfile(layersTimes) / freq;
        std::string label = format("Inference time: %.2f ms  (%.2f ms)", t, /*tk.getTimeMilli()*/ (tt2 - tt1) / cv::getTickFrequency() * 1000);
        cv::putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0));

        cv::imshow(kWinName, frame);
    }
    return 0;
}

cv::Mat formatToSquare(const cv::Mat &source)
{
    
    
    int col = source.cols;
    int row = source.rows;
    int _max = MAX(col, row);
    cv::Mat result = cv::Mat::zeros(_max, _max, CV_8UC3);
    source.copyTo(result(cv::Rect(0, 0, col, row)));
    return result;
}

void postprocess(Mat& frame, cv::Size inputSz, const std::vector<Mat>& outs, Net& net)
{
    
    
    // yolov5 has an output of shape (batchSize, 25200, 85) (Num classes + box[x,y,w,h] + confidence[c])

    auto tt1 = cv::getTickCount();

    //float x_factor = frame.cols / inpWidth;
    //float y_factor = frame.rows / inpHeight;
    float x_factor = inputSz.width / inpWidth;
    float y_factor = inputSz.height / inpHeight;

    std::vector<int> class_ids;
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;

    int rows = outs[0].size[1];
    int dimensions = outs[0].size[2];

    float *data = (float *)outs[0].data;

    for(int i = 0; i < rows; ++i) {
    
    
        float confidence = data[4];

        if(confidence >= confThreshold) {
    
    
            float *classes_scores = data + 5;

            cv::Mat scores(1, classes.size(), CV_32FC1, classes_scores);
            cv::Point class_id;
            double max_class_score;

            minMaxLoc(scores, 0, &max_class_score, 0, &class_id);

            if(max_class_score > scoreThreshold) {
    
    
                confidences.push_back(confidence);
                class_ids.push_back(class_id.x);

                float x = data[0];
                float y = data[1];
                float w = data[2];
                float h = data[3];

                int left = int((x - 0.5 * w) * x_factor);
                int top = int((y - 0.5 * h) * y_factor);
                int width = int(w * x_factor);
                int height = int(h * y_factor);
               
                boxes.push_back(cv::Rect(left, top, width, height));
            }
        }

        data += dimensions;
    }

    std::vector<int> indices;
    NMSBoxes(boxes, confidences, scoreThreshold, nmsThreshold, indices);
     
    auto tt2 = cv::getTickCount();
    std::string label = format("NMS time: %.2f ms",  (tt2 - tt1) / cv::getTickFrequency() * 1000);
    cv::putText(frame, label, Point(0, 30), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0));

    for(size_t i = 0; i < indices.size(); ++i) {
    
    
        int idx = indices[i];
        Rect box = boxes[idx];
        drawPred(class_ids[idx], confidences[idx], box.x, box.y,
                 box.x + box.width, box.y + box.height, frame);
    }
}

void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame)
{
    
    
    rectangle(frame, Point(left, top), Point(right, bottom), Scalar(0, 255, 0));

    std::string label = format("%.2f", conf);
    Scalar color = Scalar::all(255);
    if(!classes.empty()) {
    
    
        CV_Assert(classId < (int)classes.size());
        label = classes[classId] + ": " + label;
        color = colors[classId];
    }

    int baseLine;
    Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);

    top = max(top, labelSize.height);
    rectangle(frame, Point(left, top - labelSize.height),
              Point(left + labelSize.width, top + baseLine), color, FILLED);
    cv::putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.5, Scalar());
}

2.2.3 Resultados de la prueba

Cuando la prueba anterior de Python usó GPU, la inferencia directa tomó 10 ms y NMS tomó 4 ms. Cuando se usa opencv dnn para abrir dnn aquí, la inferencia directa tomó ~7 ms y NMS tomó ~0,3 ms.
Insertar descripción de la imagen aquí

3. Entrenamiento de conjuntos de datos personalizados

Aquí, yolov5s se utiliza como modelo de preentrenamiento para entrenar un modelo de detección de objetivos que contiene 4 tipos de vehículos.

3.1 Preparación del conjunto de datos

Primero etiquete la imagen usted mismo, por ejemplo, tomando el formato voc como ejemplo, use la herramienta labelImg para etiquetar. El formato de archivo de etiquetado predeterminado de coco es xml, que debe convertirse a txt mediante un script (además, puede use directamente la herramienta labelme para guardarlo directamente en el formato txt requerido por yolo).

Aquí sólo nos centramos en las carpetas JPEGImagesy labels. Una vez completada la anotación, coloque la imagen y el archivo de anotación generado en cualquier directorio. Por ejemplo E:\DeepLearning\yolov5\custom-data\vehicle, luego coloque la imagen y el archivo de anotación en las carpetas de imágenes y etiquetas respectivamente (ruta predeterminada de yolov5; de lo contrario, deberá modificar la función img2label_paths en yolov5 /utils/dataloaders.py dos parámetros).

vehicle
├── images
│   ├── 20151127_114556.jpg
│   ├── 20151127_114946.jpg
│   └── 20151127_115133.jpg
├── labels
│   ├── 20151127_114556.txt
│   ├── 20151127_114946.txt
│   └── 20151127_115133.txt

Después de eso, prepare los archivos de lista train.txt, val.txt y test.txt para el conjunto de entrenamiento, el conjunto de verificación y el conjunto de prueba (opcional). Las rutas absolutas de las imágenes se almacenan en los tres archivos y la proporción se selecciona aleatoriamente, como 7:2:1.

3.2 Archivo de configuración

Copie los archivos data/coco.yaml y model/yolov5s.yaml al directorio del conjunto de datos y realice modificaciones.

Por ejemplo, el archivo de descripción del conjunto de datosmyvoc.yaml

train: E:/DeepLearning/yolov5/custom-data/vehicle/train.txt
val: E:/DeepLearning/yolov5/custom-data/vehicle/val.txt
 
# number of classes
nc: 4
 
# class names
names: ["car", "huoche", "guache", "keche"]

El archivo de configuración del modelo de red yolov5s.yamlsolo modifica el parámetro nc al número real de categorías de detección de objetivos

# Parameters
nc: 4  # number of classes
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50  # layer channel multiple
anchors:
  - [10,13, 16,30, 33,23]  # P3/8
  - [30,61, 62,45, 59,119]  # P4/16
  - [116,90, 156,198, 373,326]  # P5/32

3.3 Formación

Como se mencionó anteriormente, una vez completado el trabajo de preparación, la estructura del directorio es la siguiente:
Insertar descripción de la imagen aquíluego entrenamos 20 epocs y el script para el entrenamiento de una sola GPU es el siguiente:

python train.py
	 --weights yolov5s.pt 
	 --cfg custom-data\vehicle\yolov5s.yaml 
	 --data custom-data\vehicle\myvoc.yaml 
	 --epoch 20 
	 --batch-size=32 
	 --img 640 
	 --device 0

El contenido del resultado de la formación es

E:\DeepLearning\yolov5>python train.py --weights yolov5s.pt --cfg custom-data\vehicle\yolov5s.yaml --data custom-data\vehicle\myvoc.yaml --epoch 20 --batch-size=32 --img 640 --device 0
train: weights=yolov5s.pt, cfg=custom-data\vehicle\yolov5s.yaml, data=custom-data\vehicle\myvoc.yaml, hyp=data\hyps\hyp.scratch-low.yaml, epochs=20, batch_size=32, imgsz=640, rect=False, resume=False, nosave=False, noval=False, noautoanchor=False, noplots=False, evolve=None, bucket=, cache=None, image_weights=False, device=0, multi_scale=False, single_cls=False, optimizer=SGD, sync_bn=False, workers=8, project=runs\train, name=exp, exist_ok=False, quad=False, cos_lr=False, label_smoothing=0.0, patience=100, freeze=[0], save_period=-1, seed=0, local_rank=-1, entity=None, upload_dataset=False, bbox_interval=-1, artifact_alias=latest
fatal: unable to access 'http://github.com/ultralytics/yolov5.git/': Recv failure: Connection was reset
Command 'git fetch origin' timed out after 5 seconds
YOLOv5  v7.0-167-g5deff14 Python-3.9.16 torch-1.13.1+cu117 CUDA:0 (NVIDIA GeForce GTX 1080 Ti, 11264MiB)

hyperparameters: lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0
Comet: run 'pip install comet_ml' to automatically track and visualize YOLOv5  runs in Comet
TensorBoard: Start with 'tensorboard --logdir runs\train', view at http://localhost:6006/

                 from  n    params  module                                  arguments
  0                -1  1      3520  models.common.Conv                      [3, 32, 6, 2, 2]
  1                -1  1     18560  models.common.Conv                      [32, 64, 3, 2]
  2                -1  1     18816  models.common.C3                        [64, 64, 1]
  3                -1  1     73984  models.common.Conv                      [64, 128, 3, 2]
  4                -1  2    115712  models.common.C3                        [128, 128, 2]
  5                -1  1    295424  models.common.Conv                      [128, 256, 3, 2]
  6                -1  3    625152  models.common.C3                        [256, 256, 3]
  7                -1  1   1180672  models.common.Conv                      [256, 512, 3, 2]
  8                -1  1   1182720  models.common.C3                        [512, 512, 1]
  9                -1  1    656896  models.common.SPPF                      [512, 512, 5]
 10                -1  1    131584  models.common.Conv                      [512, 256, 1, 1]
 11                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']
 12           [-1, 6]  1         0  models.common.Concat                    [1]
 13                -1  1    361984  models.common.C3                        [512, 256, 1, False]
 14                -1  1     33024  models.common.Conv                      [256, 128, 1, 1]
 15                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']
 16           [-1, 4]  1         0  models.common.Concat                    [1]
 17                -1  1     90880  models.common.C3                        [256, 128, 1, False]
 18                -1  1    147712  models.common.Conv                      [128, 128, 3, 2]
 19          [-1, 14]  1         0  models.common.Concat                    [1]
 20                -1  1    296448  models.common.C3                        [256, 256, 1, False]
 21                -1  1    590336  models.common.Conv                      [256, 256, 3, 2]
 22          [-1, 10]  1         0  models.common.Concat                    [1]
 23                -1  1   1182720  models.common.C3                        [512, 512, 1, False]
 24      [17, 20, 23]  1     24273  models.yolo.Detect                      [4, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]
YOLOv5s summary: 214 layers, 7030417 parameters, 7030417 gradients, 16.0 GFLOPs

Transferred 342/349 items from yolov5s.pt
AMP: checks passed
optimizer: SGD(lr=0.01) with parameter groups 57 weight(decay=0.0), 60 weight(decay=0.0005), 60 bias
train: Scanning E:\DeepLearning\yolov5\custom-data\vehicle\train... 998 images, 0 backgrounds, 0 corrupt: 100%|██████████| 998/998 [00:07<00:00, 141.97it/s]
train: New cache created: E:\DeepLearning\yolov5\custom-data\vehicle\train.cache
val: Scanning E:\DeepLearning\yolov5\custom-data\vehicle\val... 998 images, 0 backgrounds, 0 corrupt: 100%|██████████| 998/998 [00:13<00:00, 72.66it/s]
val: New cache created: E:\DeepLearning\yolov5\custom-data\vehicle\val.cache

AutoAnchor: 4.36 anchors/target, 1.000 Best Possible Recall (BPR). Current anchors are a good fit to dataset
Plotting labels to runs\train\exp13\labels.jpg...
Image sizes 640 train, 640 val
Using 8 dataloader workers
Logging results to runs\train\exp13
Starting training for 20 epochs...

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       0/19      6.36G    0.09633      0.038    0.03865         34        640: 100%|██████████| 32/32 [00:19<00:00,  1.66it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:11<00:00,  1.45it/s]
                   all        998       2353      0.884      0.174      0.248     0.0749

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       1/19       9.9G    0.06125    0.03181    0.02363         26        640: 100%|██████████| 32/32 [00:14<00:00,  2.18it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.50it/s]
                   all        998       2353      0.462      0.374       0.33      0.105

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       2/19       9.9G    0.06124    0.02353    0.02014         18        640: 100%|██████████| 32/32 [00:14<00:00,  2.22it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.58it/s]
                   all        998       2353      0.469      0.472      0.277      0.129

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       3/19       9.9G    0.05214    0.02038     0.0175         27        640: 100%|██████████| 32/32 [00:14<00:00,  2.22it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.56it/s]
                   all        998       2353       0.62       0.64      0.605      0.279

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       4/19       9.9G    0.04481    0.01777    0.01598         23        640: 100%|██████████| 32/32 [00:14<00:00,  2.17it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.60it/s]
                   all        998       2353      0.803      0.706      0.848      0.403

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       5/19       9.9G     0.0381    0.01624    0.01335         19        640: 100%|██████████| 32/32 [00:14<00:00,  2.16it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.55it/s]
                   all        998       2353      0.651      0.872        0.8      0.414

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       6/19       9.9G    0.03379    0.01534    0.01134         28        640: 100%|██████████| 32/32 [00:14<00:00,  2.18it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.58it/s]
                   all        998       2353       0.94      0.932      0.978      0.608

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       7/19       9.9G    0.03228    0.01523    0.00837         10        640: 100%|██████████| 32/32 [00:14<00:00,  2.21it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:09<00:00,  1.67it/s]
                   all        998       2353      0.862      0.932      0.956      0.591

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       8/19       9.9G     0.0292    0.01458   0.007451         20        640: 100%|██████████| 32/32 [00:14<00:00,  2.21it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.56it/s]
                   all        998       2353       0.97      0.954      0.986      0.658

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
       9/19       9.9G    0.02739    0.01407   0.006553         29        640: 100%|██████████| 32/32 [00:15<00:00,  2.12it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.58it/s]
                   all        998       2353      0.982      0.975      0.993       0.74

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      10/19       9.9G     0.0248    0.01362   0.005524         30        640: 100%|██████████| 32/32 [00:14<00:00,  2.14it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.55it/s]
                   all        998       2353      0.985      0.973      0.993      0.757

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      11/19       9.9G    0.02377    0.01271   0.005606         27        640: 100%|██████████| 32/32 [00:15<00:00,  2.13it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.52it/s]
                   all        998       2353      0.964      0.975      0.989      0.725

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      12/19       9.9G    0.02201    0.01247   0.005372         33        640: 100%|██████████| 32/32 [00:14<00:00,  2.19it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.57it/s]
                   all        998       2353      0.988      0.988      0.994       0.83

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      13/19       9.9G    0.02103    0.01193   0.004843         22        640: 100%|██████████| 32/32 [00:14<00:00,  2.14it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.57it/s]
                   all        998       2353      0.981      0.987      0.994      0.817

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      14/19       9.9G    0.02017    0.01167    0.00431         22        640: 100%|██████████| 32/32 [00:14<00:00,  2.20it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:09<00:00,  1.60it/s]
                   all        998       2353       0.96      0.952      0.987      0.782

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      15/19       9.9G    0.01847    0.01158   0.004043         32        640: 100%|██████████| 32/32 [00:14<00:00,  2.20it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.56it/s]
                   all        998       2353      0.988      0.992      0.994      0.819

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      16/19       9.9G    0.01771     0.0114   0.003859         24        640: 100%|██████████| 32/32 [00:14<00:00,  2.20it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.55it/s]
                   all        998       2353      0.967       0.96       0.99      0.832

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      17/19       9.9G    0.01665    0.01077   0.003739         32        640: 100%|██████████| 32/32 [00:14<00:00,  2.22it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.59it/s]
                   all        998       2353      0.992      0.995      0.994       0.87

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      18/19       9.9G    0.01559    0.01067   0.003549         45        640: 100%|██████████| 32/32 [00:14<00:00,  2.21it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:10<00:00,  1.53it/s]
                   all        998       2353      0.991      0.995      0.995      0.867

      Epoch    GPU_mem   box_loss   obj_loss   cls_loss  Instances       Size
      19/19       9.9G    0.01459    0.01009   0.003031         31        640: 100%|██████████| 32/32 [00:14<00:00,  2.18it/s]
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:11<00:00,  1.42it/s]
                   all        998       2353      0.994      0.995      0.994      0.885

20 epochs completed in 0.143 hours.
Optimizer stripped from runs\train\exp13\weights\last.pt, 14.4MB
Optimizer stripped from runs\train\exp13\weights\best.pt, 14.4MB

Validating runs\train\exp13\weights\best.pt...
Fusing layers...
YOLOv5s summary: 157 layers, 7020913 parameters, 0 gradients, 15.8 GFLOPs
                 Class     Images  Instances          P          R      mAP50   mAP50-95: 100%|██████████| 16/16 [00:11<00:00,  1.37it/s]
                   all        998       2353      0.994      0.995      0.994      0.885
                   car        998       1309      0.995      0.999      0.995      0.902
                huoche        998        507      0.993      0.988      0.994      0.895
                guache        998        340      0.988      0.993      0.994      0.877
                 keche        998        197      0.999          1      0.995      0.866
Results saved to runs\train\exp13

Durante el proceso de entrenamiento, puedes utilizar el tensorboard para ver visualmente la curva de entrenamiento. Inícielo en el directorio yolov5 tensorboard --logdir runs\trainy luego http://localhost:6006/acceda a él para verlo:
Insertar descripción de la imagen aquí
la velocidad de entrenamiento es muy rápida, con 998 imágenes, y solo toma unos 8 minutos entrenar 20epoc. El modelo de entrenamiento guardado se almacena en runs\train\exp13el directorio.

Otras capturas de pantalla relacionadas

resultados.png

tren_batch1.jpg
Utilice el script python detect.py --weights runs\train\exp13\weights\best.pt --source custom-data\vehicle\images\11.jpgpara probar el siguiente
Insertar descripción de la imagen aquí
cuadro de resultados
Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/wanggao_1990/article/details/132758180
Recomendado
Clasificación