Implementazione di YOLO v3 in Tensorflow (TF-Slim)

Articolo originale in lingua inglese di Paweł Kapica

Riguardo all’autore:

Sono il co-fondatore e CEO di Impeccable.AI. La mia esperienza precedente include lavorare per Microsoft e trattabile su AI/ ML problemi come ML Engineer.

A Impeccable.AI stiamo costruendo una piattaforma di sviluppo AI (ha uno spazio anche per i notebook) per aiutare i team che sviluppano prodotti di IA a lavorare più velocemente e collaborare meglio. Stiamo adottando un approccio orizzontale che offre strumenti che sono “assistenti intelligenti” e offrono la massima flessibilità. Sappiamo che i nostri clienti – gli esperti nei loro domini – sanno esattamente cosa fare e come farlo.

Immagine demo dell’autore con oggetti rilevati

Vorrei condividere il mio codice con le soluzioni ad alcuni problemi con cui ho lottato durante l’attuazione.

Non mi concentrerò molto su aspetti non legati all’attuazione. Presumo che abbiate familiarità con CNN, rilevamento degli oggetti, architettura YOLO v3 ecc. così come Tensorflow e TF-Slim framework. In caso contrario, potrebbe essere meglio iniziare con corrispondenti documenti/ tutorial. Non spiegherò cosa fa ogni singola riga, piuttosto presente il codice di lavoro con spiegazioni su alcuni problemi su cui mi sono imbattuto.

Tutto il codice necessario per eseguire questo rilevatore e alcuni demo sono disponibili nel mio repo GitHub. Ho provato su Ubuntu 16.04, Tensorflow 1.8.0 e CUDA 9.0.

Questo post è organizzato come segue:

  1. Setup
  2. Implementazione degli strati Darknet-53
  3. Implementazione dei livelli di rilevamento YOLO v3
  4. Conversione dei pesi COCO pre-formati
  5. Implementazione di algoritmi di post-elaborazione
  6. Sommario

1. Setup

Voglio organizzare il codice in modo simile a come è organizzato nel repository dei repository models di Tensorflow. Io uso TF-Slim, perché definiamo argomenti comuni come la funzione di attivazione, parametri di normalizzazione batch ecc. come globali, quindi rende la definizione di reti neurali molto più veloce.

Iniziamo con il file yolo_v3.py, dove metteremo le funzioni che inizializzano la rete così come le funzioni per caricare pesi pre-addestrati.

Aggiungere le costanti necessarie (sintonizzate dagli autori di YOLO) da qualche parte sopra il file.

_BATCH_NORM_DECAY = 0.9
_BATCH_NORM_EPSILON = 1e-05
_LEAKY_RELU = 0.1

YOLO v3 normalizza l’ingresso per essere nel range 0.. 1. La maggior parte dei livelli nel rivelatore fanno la normalizzazione batch subito dopo la convoluzione, non hanno pregiudizi e utilizzare Leaky Relu attivazione. È conveniente definire l’ambito Arg sottile per gestire questi casi per l’uso. Nei layer che non usano BN e LReLU dovremo implicitamente definirlo.

# transpose the inputs to NCHW
if data_format == 'NCHW':
    inputs = tf.transpose(inputs, [0, 3, 1, 2])

# normalize values to range [0..1]
inputs = inputs / 255

# set batch norm params
batch_norm_params = {
    'decay': _BATCH_NORM_DECAY,
    'epsilon': _BATCH_NORM_EPSILON,
    'scale': True,
    'is_training': is_training,
    'fused': None,  # Use fused batch norm if possible.
}

# Set activation_fn and parameters for conv2d, batch_norm.
with slim.arg_scope([slim.conv2d, slim.batch_norm, _fixed_padding], data_format=data_format, reuse=reuse):
    with slim.arg_scope([slim.conv2d], normalizer_fn=slim.batch_norm, normalizer_params=batch_norm_params,
                        biases_initializer=None, activation_fn=lambda x: tf.nn.leaky_relu(x, alpha=_LEAKY_RELU)):
        with tf.variable_scope('darknet-53'):
            inputs = darknet53(inputs)

Ora siamo pronti a definire gli strati Darknet-53.

 

 

2. Implementazione di Darknet-53 strati.

Nel documento YOLO v3, gli autori presentano una nuova architettura più profonda di estrattore di funzionalità chiamato Darknet-53. Come suggerisce il nome, contiene 53 strati convoluzionali, ciascuno seguito da livello di normalizzazione batch e attivazione Leaky Relu. Il downsampling è fatto dai livelli Conv con stride=2.

Fonte: carta YOLO v3

Prima di definire livelli convoluzionali, dobbiamo renderci conto che l’implementazione degli autori utilizza padding fisso indipendentemente dalla dimensione dell’input. Per ottenere lo stesso comportamento, possiamo usare la funzione qui sotto (ho leggermente modificato il codice trovato qui).

@tf.contrib.framework.add_arg_scope
def _fixed_padding(inputs, kernel_size, *args, mode='CONSTANT', **kwargs):
    """
    Pads the input along the spatial dimensions independently of input size.

    Args:
      inputs: A tensor of size [batch, channels, height_in, width_in] or
        [batch, height_in, width_in, channels] depending on data_format.
      kernel_size: The kernel to be used in the conv2d or max_pool2d operation.
                   Should be a positive integer.
      data_format: The input format ('NHWC' or 'NCHW').
      mode: The mode for tf.pad.

    Returns:
      A tensor with the same format as the input with the data either intact
      (if kernel_size == 1) or padded (if kernel_size > 1).
    """
    pad_total = kernel_size - 1
    pad_beg = pad_total // 2
    pad_end = pad_total - pad_beg

    if kwargs['data_format'] == 'NCHW':
        padded_inputs = tf.pad(inputs, [[0, 0], [0, 0],
                                        [pad_beg, pad_end], [pad_beg, pad_end]], mode=mode)
    else:
        padded_inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end],
                                        [pad_beg, pad_end], [0, 0]], mode=mode)
    return padded_inputs

_fixed_padding input pad lungo la dimensione altezza e larghezza con un numero appropriato di 0 (quando mode=’CONSTANT’). Useremo anche mode=’SYMMETRIC’ più avanti.

Adesso possiamo definire la funzione _conv2d_fixed_padding:

def _conv2d_fixed_padding(inputs, filters, kernel_size, strides=1):
    if strides > 1:
        inputs = _fixed_padding(inputs, kernel_size)
    inputs = slim.conv2d(inputs, filters, kernel_size, stride=strides, padding=('SAME' if strides == 1 else 'VALID'))
    return inputs
Il modello Darknet-53 è costruito da un certo numero di blocchi con 2 livelli Conv e connessione di collegamento seguita dal livello di downsampling. Per evitare il codice della caldaia, definiamo la funzione _darknet_block:

def _darknet53_block(inputs, filters):
    shortcut = inputs
    inputs = _conv2d_fixed_padding(inputs, filters, 1)
    inputs = _conv2d_fixed_padding(inputs, filters * 2, 3)

    inputs = inputs + shortcut
    return inputs

Infine, abbiamo tutti i mattoni necessari per il modello Darknet-53:

def darknet53(inputs):
    """
    Builds Darknet-53 model.
    """
    inputs = _conv2d_fixed_padding(inputs, 32, 3)
    inputs = _conv2d_fixed_padding(inputs, 64, 3, strides=2)
    inputs = _darknet53_block(inputs, 32)
    inputs = _conv2d_fixed_padding(inputs, 128, 3, strides=2)

    for i in range(2):
        inputs = _darknet53_block(inputs, 64)

    inputs = _conv2d_fixed_padding(inputs, 256, 3, strides=2)

    for i in range(8):
        inputs = _darknet53_block(inputs, 128)

    inputs = _conv2d_fixed_padding(inputs, 512, 3, strides=2)

    for i in range(8):
        inputs = _darknet53_block(inputs, 256)

    inputs = _conv2d_fixed_padding(inputs, 1024, 3, strides=2)

    for i in range(4):
        inputs = _darknet53_block(inputs, 512)

    return inputs

Originariamente, c’è global avg pool layer e Softmax dopo l’ultimo blocco, ma non sono utilizzati da YOLO v3 (così in realtà, abbiamo 52 strati invece di 53 😉

3. Implementazione degli strati di rilevamento YOLO v3.

Le caratteristiche estratte da Darknet-53 sono dirette agli strati di rilevamento. Il modulo di rilevamento è costruito da un certo numero di livelli Conv raggruppati in blocchi, strati di upsampling e 3 livelli Conv con funzione di attivazione lineare, facendo rilevamenti a 3 diverse scale. Iniziamo con la funzione helper di scrittura _yolo_block:

def _yolo_block(inputs, filters):
    inputs = _conv2d_fixed_padding(inputs, filters, 1)
    inputs = _conv2d_fixed_padding(inputs, filters * 2, 3)
    inputs = _conv2d_fixed_padding(inputs, filters, 1)
    inputs = _conv2d_fixed_padding(inputs, filters * 2, 3)
    inputs = _conv2d_fixed_padding(inputs, filters, 1)
    route = inputs
    inputs = _conv2d_fixed_padding(inputs, filters * 2, 3)
    return route, inputs

Le attivazioni dal quinto livello del blocco vengono poi instradate ad un altro livello Conv e sovracampionate, mentre le attivazioni dal sesto livello vanno al livello _detection_layerche andremo a definire ora:

def _detection_layer(inputs, num_classes, anchors, img_size, data_format):
    num_anchors = len(anchors)
    predictions = slim.conv2d(inputs, num_anchors * (5 + num_classes), 1, stride=1, normalizer_fn=None,
                              activation_fn=None, biases_initializer=tf.zeros_initializer())

    shape = predictions.get_shape().as_list()
    grid_size = _get_size(shape, data_format)
    dim = grid_size[0] * grid_size[1]
    bbox_attrs = 5 + num_classes

    if data_format == 'NCHW':
        predictions = tf.reshape(predictions, [-1, num_anchors * bbox_attrs, dim])
        predictions = tf.transpose(predictions, [0, 2, 1])

    predictions = tf.reshape(predictions, [-1, num_anchors * dim, bbox_attrs])

    stride = (img_size[0] // grid_size[0], img_size[1] // grid_size[1])

    anchors = [(a[0] / stride[0], a[1] / stride[1]) for a in anchors]

    box_centers, box_sizes, confidence, classes = tf.split(predictions, [2, 2, 1, num_classes], axis=-1)

    box_centers = tf.nn.sigmoid(box_centers)
    confidence = tf.nn.sigmoid(confidence)

    grid_x = tf.range(grid_size[0], dtype=tf.float32)
    grid_y = tf.range(grid_size[1], dtype=tf.float32)
    a, b = tf.meshgrid(grid_x, grid_y)

    x_offset = tf.reshape(a, (-1, 1))
    y_offset = tf.reshape(b, (-1, 1))

    x_y_offset = tf.concat([x_offset, y_offset], axis=-1)
    x_y_offset = tf.reshape(tf.tile(x_y_offset, [1, num_anchors]), [1, -1, 2])

    box_centers = box_centers + x_y_offset
    box_centers = box_centers * stride

    anchors = tf.tile(anchors, [dim, 1])
    box_sizes = tf.exp(box_sizes) * anchors
    box_sizes = box_sizes * stride

    detections = tf.concat([box_centers, box_sizes, confidence], axis=-1)

    classes = tf.nn.sigmoid(classes)
    predictions = tf.concat([detections, classes], axis=-1)
    return predictions

Questo livello trasforma le previsioni grezze secondo le seguenti equazioni. Poiché YOLO v3 su ogni scala rileva oggetti di dimensioni e proporzioni diverse, viene passato l’argomento ancore, che è un elenco di 3 tuple (altezza, larghezza) per ogni scala. Gli ancoraggi devono essere personalizzati per il set di dati (in questo tutorial useremo gli ancoraggi per il set di dati COCO). Basta aggiungere questa costante da qualche parte sopra yolo_v3.py file.

_ANCHORS = [(10, 13), (16, 30), (33, 23), (30, 61), (62, 45), (59, 119), (116, 90), (156, 198), (373, 326)]
Fonte: YOLO v3 paper

Abbiamo bisogno di una piccola funzione helper _get_size che restituisce altezza e larghezza dell’input:

def _get_size(shape, data_format):
    if len(shape) == 4:
        shape = shape[1:]
    return shape[1:3] if data_format == 'NCHW' else shape[0:2]

Come accennato in precedenza, l’ultimo elemento che dobbiamo implementare YOLO v3 è lo strato di sovracampionamento. Il rivelatore YOLO utilizza il metodo di upsampling bilineare. Perché non possiamo semplicemente usare il metodo standard tf.image.resize_bilinear da Tensorflow API? Il motivo è che, come per oggi (TF versione 1.8.0), tutti i metodi di upsampling utilizzano la modalità pad costante. Il metodo standard pad in YOLO authors repo e in PyTorch is edge (un buon confronto delle modalità padding può essere trovato qui). Questa piccola differenza ha un impatto significativo sui rilevamenti (e mi è costato un paio d’ore di debug).

Per aggirare questo problema, inseriremo manualmente gli ingressi con 1 pixel e mode=’SYMMETRIC’, che è l’equivalente della modalità edge.

# we just need to pad with one pixel, so we set kernel_size = 3
inputs = _fixed_padding(inputs, 3, 'NHWC', mode='SYMMETRIC')

La funzione Whole _upsample figura come di seguito:

def _upsample(inputs, out_shape, data_format='NCHW'):
    # we need to pad with one pixel, so we set kernel_size = 3
    inputs = _fixed_padding(inputs, 3, mode='SYMMETRIC')

    # tf.image.resize_bilinear accepts input in format NHWC
    if data_format == 'NCHW':
        inputs = tf.transpose(inputs, [0, 2, 3, 1])

    if data_format == 'NCHW':
        height = out_shape[3]
        width = out_shape[2]
    else:
        height = out_shape[2]
        width = out_shape[1]

    # we padded with 1 pixel from each side and upsample by factor of 2, so new dimensions will be
    # greater by 4 pixels after interpolation
    new_height = height + 4
    new_width = width + 4

    inputs = tf.image.resize_bilinear(inputs, (new_height, new_width))

    # trim back to desired size
    inputs = inputs[:, 2:-2, 2:-2, :]

    # back to NCHW if needed
    if data_format == 'NCHW':
        inputs = tf.transpose(inputs, [0, 3, 1, 2])

    inputs = tf.identity(inputs, name='upsampled')
    return inputs

AGGIORNAMENTO: Grazie a Srikanth Vidapanakal, ho controllato il codice sorgente di darknet e ho scoperto che il metodo di upsampling è il più vicino, non bilineare. Non abbiamo più bisogno di inserire immagini. Il codice aggiornato è già disponibile nel mio repo.

La funzione Fixed _upsample figura come di seguito:

def _upsample(inputs, out_shape, data_format='NCHW'):
    # tf.image.resize_nearest_neighbor accepts input in format NHWC
    if data_format == 'NCHW':
        inputs = tf.transpose(inputs, [0, 2, 3, 1])

    if data_format == 'NCHW':
        new_height = out_shape[3]
        new_width = out_shape[2]
    else:
        new_height = out_shape[2]
        new_width = out_shape[1]

    inputs = tf.image.resize_nearest_neighbor(inputs, (new_height, new_width))

    # back to NCHW if needed
    if data_format == 'NCHW':
        inputs = tf.transpose(inputs, [0, 3, 1, 2])

    inputs = tf.identity(inputs, name='upsampled')
    return inputs

Le attivazioni sovracampionate sono concatenate lungo l’asse dei canali con attivazioni da strati Darknet-53. Questo è il motivo per cui dobbiamo tornare alla funzione darknet53function e restituire le attivazioni dai livelli Conv prima dei livelli di downsampling 4th e 5th.

def darknet53(inputs):
    """
    Builds Darknet-53 model.
    """
    inputs = _conv2d_fixed_padding(inputs, 32, 3)
    inputs = _conv2d_fixed_padding(inputs, 64, 3, strides=2)
    inputs = _darknet53_block(inputs, 32)
    inputs = _conv2d_fixed_padding(inputs, 128, 3, strides=2)

    for i in range(2):
        inputs = _darknet53_block(inputs, 64)

    inputs = _conv2d_fixed_padding(inputs, 256, 3, strides=2)

    for i in range(8):
        inputs = _darknet53_block(inputs, 128)

    route1 = inputs
    inputs = _conv2d_fixed_padding(inputs, 512, 3, strides=2)

    for i in range(8):
        inputs = _darknet53_block(inputs, 256)

    route2 = inputs
    inputs = _conv2d_fixed_padding(inputs, 1024, 3, strides=2)

    for i in range(4):
        inputs = _darknet53_block(inputs, 512)

    return route1, route2, inputs

Ora siamo pronti a definire il modulo del rilevatore. Torniamo alla funzione yolo_v3function e aggiungiamo le seguenti righe sotto l’ambito dell’Arg sottile:

with tf.variable_scope('darknet-53'):
    route_1, route_2, inputs = darknet53(inputs)

with tf.variable_scope('yolo-v3'):
    route, inputs = _yolo_block(inputs, 512)
    detect_1 = _detection_layer(inputs, num_classes, _ANCHORS[6:9], img_size, data_format)
    detect_1 = tf.identity(detect_1, name='detect_1')

    inputs = _conv2d_fixed_padding(route, 256, 1)
    upsample_size = route_2.get_shape().as_list()
    inputs = _upsample(inputs, upsample_size, data_format)
    inputs = tf.concat([inputs, route_2], axis=1 if data_format == 'NCHW' else 3)

    route, inputs = _yolo_block(inputs, 256)

    detect_2 = _detection_layer(inputs, num_classes, _ANCHORS[3:6], img_size, data_format)
    detect_2 = tf.identity(detect_2, name='detect_2')

    inputs = _conv2d_fixed_padding(route, 128, 1)
    upsample_size = route_1.get_shape().as_list()
    inputs = _upsample(inputs, upsample_size, data_format)
    inputs = tf.concat([inputs, route_1], axis=1 if data_format == 'NCHW' else 3)

    _, inputs = _yolo_block(inputs, 128)

    detect_3 = _detection_layer(inputs, num_classes, _ANCHORS[0:3], img_size, data_format)
    detect_3 = tf.identity(detect_3, name='detect_3')

    detections = tf.concat([detect_1, detect_2, detect_3], axis=1)
    return detections

4. Converting pre-trained COCO weights

Abbiamo definito l’architettura del rivelatore. Per usarlo, dobbiamo addestrarlo sul nostro set di dati o usare pesi pre-piovuti. I pesi pretrainati sul set di dati COCO sono disponibili per uso pubblico. Possiamo scaricarli usando questo comando:

wget https://pjreddie.com/media/files/yolov3.weights

La struttura di questo file binario è la seguente:
I primi 3 valori int32 sono informazioni di intestazione: numero di versione principale, numero di versione minore, numero di sovversione, seguito dal valore Int64: numero di immagini viste dalla rete durante l’allenamento. Dopo di loro, ci sono 62 001 757 float32 valori che sono pesi di ogni Conv e batch norma strato. È importante ricordare che vengono salvati nel formato maggiore della riga, che è opposto al formato utilizzato da Tensorflow (colonna-maggiore).

Quindi, come dovremmo leggere i pesi da questo file?

Iniziamo dal primo livello Conv. La maggior parte degli strati di convoluzione sono immediatamente seguiti dal livello di normalizzazione batch. In questo caso, è necessario leggere prima lettura 4* nume_filtri pesi di livello norma batch: gamma, beta, media mobile e varianza in movimento, thenkernel_size[0] * kernel_size[1] * num_filters *input_channels pesi di livello Conv.

Nel caso opposto, quando il livello Conv non è seguito dal livello di norma batch, invece di leggere i parametri di norma batch, abbiamo bisogno di readnum_filters bias pesi.

Iniziamo a scrivere il codice della funzione load_weights. Richiede 2 argomenti: un elenco di variabili nel nostro grafico e un nome del file binario.

Iniziamo con l’apertura del file, saltando i primi 5 int32 valori e leggendo tutto il resto come un elenco:

def load_weights(var_list, weights_file):
    with open(weights_file, "rb") as fp:
        _ = np.fromfile(fp, dtype=np.int32, count=5)

        weights = np.fromfile(fp, dtype=np.float32)

Quindi useremo due puntatori, prima per iterare sulla lista delle variabili var_list e seconda per iterare sulla lista con le variabili caricate. Dobbiamo controllare il tipo di strato che segue quello attualmente elaborato e leggere il numero appriopriate di valori. Nel codice i verrà iterato su var_list e ptr itererà su pesi. Restituiremo una lista di ops tf.assign. Controllo il tipo di strato semplicemente confrontando il suo nome. (Sono d’accordo che è un po’ brutto, ma non conosco un modo migliore di farlo. Questo approccio sembra funzionare per me.)

ptr = 0
i = 0
assign_ops = []
while i < len(var_list) - 1:
    var1 = var_list[i]
    var2 = var_list[i + 1]
    # do something only if we process conv layer
    if 'Conv' in var1.name.split('/')[-2]:
        # check type of next layer
        if 'BatchNorm' in var2.name.split('/')[-2]:
            # load batch norm params
            gamma, beta, mean, var = var_list[i + 1:i + 5]
            batch_norm_vars = [beta, gamma, mean, var]
            for var in batch_norm_vars:
                shape = var.shape.as_list()
                num_params = np.prod(shape)
                var_weights = weights[ptr:ptr + num_params].reshape(shape)
                ptr += num_params
                assign_ops.append(tf.assign(var, var_weights, validate_shape=True))

            # we move the pointer by 4, because we loaded 4 variables
            i += 4
        elif 'Conv' in var2.name.split('/')[-2]:
            # load biases
            bias = var2
            bias_shape = bias.shape.as_list()
            bias_params = np.prod(bias_shape)
            bias_weights = weights[ptr:ptr + bias_params].reshape(bias_shape)
            ptr += bias_params
            assign_ops.append(tf.assign(bias, bias_weights, validate_shape=True))

            # we loaded 2 variables
            i += 1
        # we can load weights of conv layer
        shape = var1.shape.as_list()
        num_params = np.prod(shape)

        var_weights = weights[ptr:ptr + num_params].reshape((shape[3], shape[2], shape[0], shape[1]))
        # remember to transpose to column-major
        var_weights = np.transpose(var_weights, (2, 3, 1, 0))
        ptr += num_params
        assign_ops.append(tf.assign(var1, var_weights, validate_shape=True))
        i += 1return assign_ops

E questo è tutto! Ora possiamo ripristinare i pesi del modello eseguendo linee di codice simili a queste:

with tf.variable_scope('model'):
    model = yolo_v3(inputs, 80)model_vars = tf.global_variables(scope='model')
assign_ops = load_variables(model_vars, 'yolov3.weights')sess = tf.Session()
sess.run(assign_ops)

Per l’uso futuro, probabilmente sarà molto più facile esportare i pesi utilizzando tf.train.Saver e caricare da un punto di controllo.

5. Implementation of post-processing algorithms

Il nostro modello restituisce un tensore di forma:
x 10647 x (classi numeriche + 5 caselle di delimitazione)
Il numero 10647 è uguale alla somma 507 +2028 + 8112, che sono i numeri di possibili oggetti rilevati su ogni scala. I 5 valori che descrivono gli attributi del riquadro di delimitazione indicano center_x, center_y, width, height. Nella maggior parte dei casi, è più facile lavorare sulle coordinate di due punti: in alto a sinistra e in basso a destra. Convertiamo l’output del rivelatore in questo formato.
La funzione che lo fa è abbastanza semplice:

def detections_boxes(detections):
    center_x, center_y, width, height, attrs = tf.split(detections, [1, 1, 1, 1, -1], axis=-1)
    w2 = width / 2
    h2 = height / 2
    x0 = center_x - w2
    y0 = center_y - h2
    x1 = center_x + w2
    y1 = center_y + h2

    boxes = tf.concat([x0, y0, x1, y1], axis=-1)
    detections = tf.concat([boxes, attrs], axis=-1)
    return detections

È normale che il nostro rilevatore rilevi lo stesso oggetto più volte (con centri e dimensioni leggermente diverse). Nella maggior parte dei casi non vogliamo mantenere tutti questi rilevamenti che differiscono solo da un piccolo numero di pixel. La soluzione standard a questo problema è la soppressione Non-max. Una buona descrizione di questo metodo è disponibile qui.

Perché non usiamo la funzione tf.image.non_max_suppression da Tensorflow API? Ci sono 2 ragioni principali. In primo luogo, a mio parere è molto meglio eseguire NMS per classe, perché potremmo avere una situazione in cui gli oggetti da 2 classi diverse altamente si sovrappongono e NMS globale sopprimerà una delle caselle. In secondo luogo, alcune persone si lamentano che questa funzione è lenta, perché non è stato ancora ottimizzato.

Implementiamo l’algoritmo NMS. Per prima cosa abbiamo bisogno di una funzione per calcolare iou (Intersezione su Unione) di due caselle di delimitazione:

def _iou(box1, box2):
    b1_x0, b1_y0, b1_x1, b1_y1 = box1
    b2_x0, b2_y0, b2_x1, b2_y1 = box2

    int_x0 = max(b1_x0, b2_x0)
    int_y0 = max(b1_y0, b2_y0)
    int_x1 = min(b1_x1, b2_x1)
    int_y1 = min(b1_y1, b2_y1)

    int_area = (int_x1 - int_x0) * (int_y1 - int_y0)

    b1_area = (b1_x1 - b1_x0) * (b1_y1 - b1_y0)
    b2_area = (b2_x1 - b2_x0) * (b2_y1 - b2_y0)

    iou = int_area / (b1_area + b2_area - int_area + 1e-05)
    return iou

Ora possiamo scrivere il codice della funzione di soppressionenon_max_. Io uso la libreria NumPy per operazioni vettoriali veloci.

def non_max_suppression(predictions_with_boxes, confidence_threshold, iou_threshold=0.4):
    """
    Applies Non-max suppression to prediction boxes.

    :param predictions_with_boxes: 3D numpy array, first 4 values in 3rd dimension are bbox attrs, 5th is confidence
    :param confidence_threshold: the threshold for deciding if prediction is valid
    :param iou_threshold: the threshold for deciding if two boxes overlap
    :return: dict: class -> [(box, score)]
    """

Prende 3 argomenti: uscite dal nostro rivelatore YOLO v3, soglia di confidenza e soglia iou. Il corpo di questa funzione è il seguente:

conf_mask = np.expand_dims((predictions_with_boxes[:, :, 4] > confidence_threshold), -1)
predictions = predictions_with_boxes * conf_mask

result = {}
for i, image_pred in enumerate(predictions):
    shape = image_pred.shape
    non_zero_idxs = np.nonzero(image_pred)
    image_pred = image_pred[non_zero_idxs]
    image_pred = image_pred.reshape(-1, shape[-1])

    bbox_attrs = image_pred[:, :5]
    classes = image_pred[:, 5:]
    classes = np.argmax(classes, axis=-1)

    unique_classes = list(set(classes.reshape(-1)))

    for cls in unique_classes:
        cls_mask = classes == cls
        cls_boxes = bbox_attrs[np.nonzero(cls_mask)]
        cls_boxes = cls_boxes[cls_boxes[:, -1].argsort()[::-1]]
        cls_scores = cls_boxes[:, -1]
        cls_boxes = cls_boxes[:, :-1]

        while len(cls_boxes) > 0:
            box = cls_boxes[0]
            score = cls_scores[0]
            if not cls in result:
                result[cls] = []
            result[cls].append((box, score))
            cls_boxes = cls_boxes[1:]
            ious = np.array([_iou(box, x) for x in cls_boxes])
            iou_mask = ious < iou_threshold
            cls_boxes = cls_boxes[np.nonzero(iou_mask)]
            cls_scores = cls_scores[np.nonzero(iou_mask)]

return result

Questo è praticamente tutto. Abbiamo implementato tutte le funzioni necessarie affinché YOLO v3 funzioni.

6. Summary

Nel tutorial repo è possibile trovare il codice e alcuni script demo per l’esecuzione di rilevamenti. Il rilevatore funziona sia nei formati di dati NHWC che NCHW, quindi puoi scegliere facilmente quale formato funziona più velocemente sulla tua macchina.

Se avete domande, non esitate a contattarmi.

Ho intenzione di scrivere la prossima parte di questo tutorial in cui mostrerò come allenare (sintonizzare) YOLO v3 su set di dati personalizzati.

Grazie per la lettura. Fatemi sapere se vi è piaciuto applaudendo e/ o la condivisione! : )

 

Articoli su Towards Data Science: https://medium.com/@pawelkapica

Profilo Linkedin: https://www.linkedin.com/in/pakapica/

 

Share:

Contenuti
Torna in alto