Обнаружение объектов — это задача, которая включает определение наличия, местоположения и типа одного или нескольких объектов на изображении. Yolo, что означает «вы смотрите только один раз», представляет собой модель обнаружения объектов, в которой используется глубокая сверточная нейронная сеть. В этом блоге мы обсудим YOLOv3, вариант оригинальной модели YOLO, который обеспечивает почти самые современные (SOTA) результаты. YOLOv3 может выполнять обнаружение объектов в режиме реального времени.

Обзор модели:

В основном модель YOLOv3 состоит из двух разных частей:
1. Извлечение признаков.

2. Предсказание ограничивающей рамки.

Извлечение признаков: Yolov3 использует более крупную сеть для извлечения признаков, чем yolov2. Эта модель известна как DARKNET-53, которая имеет 53 слоя свертки с остаточными соединениями. Мы проигнорируем последние три слоя даркнета-53 (уровень avg-pool, слой fc, слой softmax), так как эти слои в основном используются для классификации изображений. В этой задаче обнаружения объектов мы используем darknet-53 только для извлечения признаков изображения, поэтому эти три слоя не понадобятся.

Darknet-53 использует новый тип блока, который называется Residual Block. Глубокие нейронные сети трудно обучать. С увеличением глубины иногда точность сети достигает предела, что приводит к увеличению ошибок обучения. Для решения этой проблемы был введен остаточный блок. Архитектурная разница между обычным блоком свертки и остаточным блоком заключается в добавлении соединения с пропуском. Сквозное соединение переносит входные данные в более глубокие слои.

Обозначим ввод через x, а желаемое отображение, которое мы хотим получить путем обучения, будет F(x). Слева на приведенном выше рисунке пунктирная рамка напрямую изучает отображение F(x). В правой части рисунка выше показано, как выглядит остаточный блок. Часть с пунктирной рамкой изучает немного другое сопоставление, F(x) - x. Это сопоставление легче изучить. Чтобы получить фактическое отображение F(x), к входным данным x добавляется отображение F(x) — x. Сплошная линия, которая добавляет ввод x к сопоставлению, называется остаточным соединением или соединением быстрого доступа. Добавление x действует как остаток, отсюда и название «остаточный блок».

Вот реализация даркнета-53 на Python.

def residual_block(input_layer, input_channel, filter_num1, filter_num2):
    short_cut = input_layer
    conv = convolutional(input_layer, filters_shape=(1, 1, input_channel, filter_num1))
    conv = convolutional(conv       , filters_shape=(3, 3, filter_num1,   filter_num2))
    residual_output = short_cut + conv
    return residual_output

def darknet53(input_data, training=False): #python implementation of darknet-53
    input_data = convolutional(input_data, (3, 3,  3,  32))
    input_data = convolutional(input_data, (3, 3, 32,  64), downsample=True)

    for i in range(1):
        input_data = residual_block(input_data,  64,  32, 64)

    input_data = convolutional(input_data, (3, 3,  64, 128), downsample=True)

    for i in range(2):
        input_data = residual_block(input_data, 128,  64, 128)

    input_data = convolutional(input_data, (3, 3, 128, 256), downsample=True)

    for i in range(8):
        input_data = residual_block(input_data, 256, 128, 256)

    route_1 = input_data
    input_data = convolutional(input_data, (3, 3, 256, 512), downsample=True)

    for i in range(8):
        input_data = residual_block(input_data, 512, 256, 512)

    route_2 = input_data
    input_data = convolutional(input_data, (3, 3, 512, 1024), downsample=True)

    for i in range(4):
        input_data = residual_block(input_data, 1024, 512, 1024)

    return route_1, route_2, input_data

Предсказание ограничительной рамки. В части предсказания сеть использует слой свертки, слой повышающей дискретизации, пропущенное соединение и слой свертки с шагом 2 в качестве слоя понижающей дискретизации. Мы не используем какой-либо слой объединения для понижения разрешения входного изображения, поскольку слой объединения теряет больше низкоуровневых функций входного изображения.

Эта сеть прогнозирует ограничивающие рамки на изображениях с пониженной дискретизацией для быстрого прогнозирования в реальном времени. Мы понижаем разрешение изображений с помощью фактора, который называется шаг. Таким образом, шаг сети равен коэффициенту, на который выходной слой меньше входного изображения. YOLOv3 использует три разных шага (8, 16, 32). Таким образом, если размер входного изображения 416×416 (без цветового канала), то выходные тензоры будут 52×52, 26×26, 13×13.

См. архитектуру yolov3 ниже для лучшего понимания.

Мы видим, что сеть использует три разные ветви модели даркнет-53. Мы добавляем слой свертки после последнего слоя модели даркнет-53 и получаем первый выходной слой y1. Этот слой в 32 раза меньше фактического входного изображения. Позже мы обсудим размеры выходных слоев (y1, y2, y3). Далее мы повышаем дискретизацию слоя в 2 раза и объединяем его со вторым выходным слоем даркнета-53. После добавления нескольких слоев свертки мы получаем выходной слой y2. Аналогичным образом мы также получаем выходной слой y3.

Читатели могут не понять, почему мы проводим обнаружение на трех разных уровнях. Ответ на этот вопрос заключается в том, что трехуровневое обнаружение помогает YOLOv3 более точно обнаруживать объекты разного размера. Слой y1 делает вывод в 32 раза меньше фактического изображения, y2 – в 16 раз, а y3 – в 8 раз. Таким образом, крупные объекты чаще обнаруживаются на слое y1, объекты среднего размера — на слое y2, а мелкие объекты — на слое y3. Этот трехуровневый метод обнаружения помогает yolov3 лучше обнаруживать, чем предыдущие варианты.

Вот код модели YOLOv3.

def YOLOv3(input_layer, NUM_CLASS, training=False): #This function returns output layers of YOLOv3 model
    # After the input layer enters the Darknet-53 network, we get three branches
    route_1, route_2, conv = darknet53(input_layer, training)
    
    conv = convolutional(conv, (1, 1, 1024,  512))
    conv = convolutional(conv, (3, 3,  512, 1024))
    conv = convolutional(conv, (1, 1, 1024,  512))
    conv = convolutional(conv, (3, 3,  512, 1024))
    conv = convolutional(conv, (1, 1, 1024,  512))
    conv_lobj_branch = convolutional(conv, (3, 3, 512, 1024))
    
    # conv_lbbox is used to predict large-sized objects , Shape = [None, 13, 13, 255] 
    conv_lbbox = convolutional(conv_lobj_branch, (1, 1, 1024, 3*(NUM_CLASS + 5)), activate=False, bn=False)

    conv = convolutional(conv, (1, 1,  512,  256))
    # upsample here uses the nearest neighbor interpolation method, which has the advantage that the
    # upsampling process does not need to learn, thereby reducing the network parameter  
    conv = upsample(conv)

    conv = tf.concat([conv, route_2], axis=-1)
    conv = convolutional(conv, (1, 1, 768, 256))
    conv = convolutional(conv, (3, 3, 256, 512))
    conv = convolutional(conv, (1, 1, 512, 256))
    conv = convolutional(conv, (3, 3, 256, 512))
    conv = convolutional(conv, (1, 1, 512, 256))
    conv_mobj_branch = convolutional(conv, (3, 3, 256, 512))

    # conv_mbbox is used to predict medium-sized objects, shape = [None, 26, 26, 255]
    conv_mbbox = convolutional(conv_mobj_branch, (1, 1, 512, 3*(NUM_CLASS + 5)), activate=False, bn=False)

    conv = convolutional(conv, (1, 1, 256, 128))
    conv = upsample(conv)

    conv = tf.concat([conv, route_1], axis=-1)
    conv = convolutional(conv, (1, 1, 384, 128))
    conv = convolutional(conv, (3, 3, 128, 256))
    conv = convolutional(conv, (1, 1, 256, 128))
    conv = convolutional(conv, (3, 3, 128, 256))
    conv = convolutional(conv, (1, 1, 256, 128))
    conv_sobj_branch = convolutional(conv, (3, 3, 128, 256))
    
    # conv_sbbox is used to predict small size objects, shape = [None, 52, 52, 255]
    conv_sbbox = convolutional(conv_sobj_branch, (1, 1, 256, 3*(NUM_CLASS +5)), activate=False, bn=False)
        
    return [conv_sbbox, conv_mbbox, conv_lbbox]

Объяснение выходного слоя: Как вы все знаете, YOLOv3 предсказывает ограничивающие рамки в трех разных масштабах. В этом разделе мы поймем выходной слой этой сети. Ниже я прикрепил изображение выходного слоя y1 (шаг 32).

Мы понизили разрешение изображения 416 × 416 до выходного тензора 13 × 13. Изображение было разделено на блоки 13×13, и для каждого блока мы прогнозируем 3 ограничивающих прямоугольника. Итак, на самом деле для каждого блока мы хотим предсказать три объекта, центры которых лежат в этом конкретном блоке. Для определенного блока, скажем, для красного блока на изображении выше, мы хотим обнаружить объекты, центры которых лежат в этом блоке. Мы видим, что этот красный блок предсказывает 255 значений или 3∗85 (три объекта на блок). Вы можете задаться вопросом, почему 85 значений на ограничивающую рамку! Эти значения представляют собой 4 смещения ограничивающей рамки, 1 показатель объектности и 80 вероятностей классов.

x, y: эти два значения являются смещением x и y ограничивающей рамки, предсказываемой этой ячейкой.

w,h: эти два значения представляют собой ширину и высоту ограничивающего прямоугольника, предсказываемого этой ячейкой.

Оценка объектности. Эта оценка является показателем достоверности того, содержит ли этот блок центр какого-либо объекта на реальном изображении. Эта оценка достоверности не зависит от какого-либо конкретного объекта.

Вероятности класса. Вероятности класса — это вероятности того, что обнаруженный объект принадлежит к определенному классу. На изображении выше мы предполагаем, что наш набор данных содержит 80 различных классов. Таким образом, вероятности класса содержат 80 значений.

Таким образом, с точки зрения глубины наша модель предсказывает значения B∗(4+1+C). Каждый блок предсказывает B ограничивающих рамок, и каждая ограничивающая рамка имеет атрибуты (4+1+C), которые описывают координаты центра, размеры, показатель объектности и C.

Якорная рамка. Возможно, имеет смысл предсказать ширину и высоту ограничивающей рамки. Но на практике это приводит к нестабильным градиентам во время тренировки. Таким образом, YOLOv3 предсказывает смещения для предопределенных ограничивающих рамок по умолчанию, называемых якорными рамками. YOLOv3 использует разные якоря в разных масштабах. Модель YOLOv3 предсказывает ограничивающие рамки в трех масштабах, и в каждом масштабе назначаются три привязки. Таким образом, всего в этой сети девять блоков привязки. Эти привязки получаются при выполнении кластеризации K-средних в наборе данных.

Обработка декодирования. Мы уже обсуждали выходные слои YOLOv3. Выходные слои предсказывают координаты ограничивающей рамки, размеры, оценку объектности и вероятности классов. Поскольку эти атрибуты являются просто результатом некоторых слоев свертки сети, значения этих атрибутов могут быть любыми. Таким образом, эти значения преобразуются некоторыми функциями.

Здесь tx, ty, tw, th — выходные данные сети. bx, by, bw, bh — преобразованные значения tx, ty, tw, th соответственно. cx, cy — координаты верхнего левого края сетки. pw, ph — размеры привязки для этой сетки.

Координаты центра: мы передаем значения tx, ty в функцию сигмоид. Сигмовидная функция преобразует значения от 0 до 1. Затем мы добавляем верхние левые координаты cx, cy, чтобы предсказать фактические координаты нашей ограничивающей рамки.

Размер ограничивающей рамки. Размеры ограничивающей рамки прогнозируются путем применения к выходным данным преобразования логарифмического пространства, а затем умножения на размер опорной рамки. Ранее мы обсуждали, зачем нам нужна якорная коробка.

Вот код обработки декодирования:

def decode(conv_output, NUM_CLASS, i=0):
    # where i = 0, 1 or 2 to correspond to the three grid scales  
    conv_shape       = tf.shape(conv_output)
    batch_size       = conv_shape[0]
    output_size      = conv_shape[1]

    conv_output = tf.reshape(conv_output, (batch_size, output_size, output_size, 3, 5 + NUM_CLASS))

    conv_raw_dxdy = conv_output[:, :, :, :, 0:2] # offset of center position     
    conv_raw_dwdh = conv_output[:, :, :, :, 2:4] # Prediction box length and width offset
    conv_raw_conf = conv_output[:, :, :, :, 4:5] # confidence of the prediction box
    conv_raw_prob = conv_output[:, :, :, :, 5: ] # category probability of the prediction box 

    # next need Draw the grid. Where output_size is equal to 13, 26 or 52  
    y = tf.range(output_size, dtype=tf.int32)
    y = tf.expand_dims(y, -1)
    y = tf.tile(y, [1, output_size])
    x = tf.range(output_size,dtype=tf.int32)
    x = tf.expand_dims(x, 0)
    x = tf.tile(x, [output_size, 1])

    xy_grid = tf.concat([x[:, :, tf.newaxis], y[:, :, tf.newaxis]], axis=-1)
    xy_grid = tf.tile(xy_grid[tf.newaxis, :, :, tf.newaxis, :], [batch_size, 1, 1, 3, 1])
    xy_grid = tf.cast(xy_grid, tf.float32)

    # Calculate the center position of the prediction box:
    pred_xy = (tf.sigmoid(conv_raw_dxdy) + xy_grid) * STRIDES[i]
    # Calculate the length and width of the prediction box:
    pred_wh = (tf.exp(conv_raw_dwdh) * ANCHORS[i]) * STRIDES[i]

    pred_xywh = tf.concat([pred_xy, pred_wh], axis=-1)
    pred_conf = tf.sigmoid(conv_raw_conf) # object box calculates the predicted confidence
    pred_prob = tf.sigmoid(conv_raw_prob) # calculating the predicted probability category box object
    return tf.concat([pred_xywh, pred_conf, pred_prob], axis=-1)

Предварительно обученный вес. Запуск глубокой нейронной сети с нуля требует много времени. Поэтому люди часто занимаются тонкой настройкой. Мы будем использовать предварительно обученные веса модели YOLOv3, обученные на наборе данных COCO. Вот ссылка на предварительно обученный файл веса. Теперь нам нужна функция, которая может извлечь предварительно обученный вес для использования в модели YOLOv3.

def load_weights(model, weights_file):
    wf = open(weights_file, 'rb')
    major, minor, revision, seen, _ = np.fromfile(wf, dtype=np.int32, count=5)

    j = 0
    for i in range(75):
        conv_layer_name = 'conv2d_%d' %i if i > 0 else 'conv2d'
        bn_layer_name = 'batch_normalization_%d' %j if j > 0 else 'batch_normalization'

        conv_layer = model.get_layer(conv_layer_name)
        filters = conv_layer.filters
        k_size = conv_layer.kernel_size[0]
        in_dim = conv_layer.input_shape[-1]

        if i not in [58, 66, 74]:
            # darknet weights: [beta, gamma, mean, variance]
            bn_weights = np.fromfile(wf, dtype=np.float32, count=4 * filters)
            # tf weights: [gamma, beta, mean, variance]
            bn_weights = bn_weights.reshape((4, filters))[[1, 0, 2, 3]]
            bn_layer = model.get_layer(bn_layer_name)
            j += 1
        else:
            conv_bias = np.fromfile(wf, dtype=np.float32, count=filters)

        # darknet shape (out_dim, in_dim, height, width)
        conv_shape = (filters, in_dim, k_size, k_size)
        conv_weights = np.fromfile(wf, dtype=np.float32, count=np.product(conv_shape))
        # tf shape (height, width, in_dim, out_dim)
        conv_weights = conv_weights.reshape(conv_shape).transpose([2, 3, 1, 0])

        if i not in [58, 66, 74]:
            conv_layer.set_weights([conv_weights])
            bn_layer.set_weights(bn_weights)
        else:
            conv_layer.set_weights([conv_weights, conv_bias])

    assert len(wf.read()) == 0, 'failed to read all data'
    wf.close()

Линия передачи данных. В этом блоге мы будем использовать набор данных Pascal VOC. Этот набор данных содержит около 17 тыс. изображений 20 классов. Размер файла около 2 Гб. Этот набор данных может содержать ненужные папки.

Папка JPEGImages содержит тренировочные изображения, а папка Annotations содержит поля истинности. Другие папки не нужны для обнаружения объектов.

Аннотации представляют собой XML-файлы и содержат некоторую ненужную информацию. Мы определим функцию parse_annotation, которая объединит все файлы аннотаций в один список Python, отбрасывая ненужные фрагменты информации.

def parse_annotation():
    train_data=[]
    for annot_name in sorted(os.listdir(annot_dir)):
        split=annot_name.split('.')
        img_name=split[0]
        img_path=image_dir+img_name
        new_data={ 'object' : [] }
        if os.path.exists(img_path+'.jpg'):
            img_path=img_path+'.jpg'
        elif os.path.exists(img_path+'.JPG'):
            img_path=img_path+'.JPG'
        elif os.path.exists(img_path+'.jpeg'):
            img_path=img_path+'.jpeg'
        elif os.path.exists(img_path+'.png'):
            img_path=img_path+'.png'
        elif os.path.exists(img_path+'.PNG'):
            img_path=img_path+'.PNG'
        else:
            print('image path not exis')

        new_data['image_path']=img_path
        annot=xTree.parse(annot_dir+annot_name)
        for elem in annot.iter():
            if elem.tag == 'width':
                new_data['width']=int(elem.text)
            if elem.tag=='height':
                new_data['height']=int(elem.text)
            if elem.tag=='object':
                obj={}
                for attr in list(elem):
                    if attr.tag=='name':
                        obj['name']=attr.text
                    if attr.tag=='bndbox':
                        for dim in list(attr):
                            obj[dim.tag]=int(round(float(dim.text)))
                new_data['object'].append(obj)
        train_data.append(new_data)
    return train_data

TrainBatchGenerator: эта функция генерирует обучающие пакетные данные из набора данных VOC2012. Эта функция возвращает две вещи: тренировочное изображение и наземную истинную ограничительную рамку для трех разных масштабов. Мы рассчитываем долговую расписку между наземной коробкой правды и якорями. Долговая расписка известна как Intersection Over Union между двумя объектами. Мы предполагаем, что все якоря и поле истинности имеют один и тот же центр тяжести.

Если IOU больше 0,3 для любого якоря, мы говорим, что эта калибровка является положительной выборкой. Используя это правило, мы можем сгенерировать достаточное количество положительных образцов. Но для любого gt-бокса, если долговая расписка не превышает 0,3, только якорь с наибольшей долговой распиской может быть помечен как положительный образец. Таким образом, каждому блоку gt назначается блок привязки. В соответствии с вышеизложенным принципом, блок gt может быть сопоставлен с несколькими блоками привязки.

def preprocess_true_boxes(bboxes):
    label = [np.zeros((output_size[i], output_size[i], anchor_per_scale,
                    5 + num_classes)) for i in range(3)]
    bboxes_xywh = [np.zeros((max_bbox_per_scale, 4)) for _ in range(3)]
    bbox_count = np.zeros((3,))
    for bbox in bboxes:
        bbox_coor = bbox[:4]
        bbox_class_ind = bbox[4]
        onehot = np.zeros(num_classes, dtype=np.float)
        onehot[bbox_class_ind] = 1.0
        uniform_distribution = np.full(num_classes, 1.0 / num_classes)
        deta = 0.01
        smooth_onehot = onehot * (1 - deta) + deta * uniform_distribution
        bbox_xywh = np.concatenate([(bbox_coor[2:] + bbox_coor[:2]) * 0.5, bbox_coor[2:] - bbox_coor[:2]], axis=-1)
        bbox_xywh_scaled = 1.0 * bbox_xywh[np.newaxis, :] / strides[:, np.newaxis]
        iou = []
        exist_positive = False #True means we have found any anchors for gt box
        for i in range(3):
            anchors_xywh = np.zeros((anchor_per_scale, 4))
            anchors_xywh[:, 0:2] = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32) + 0.5
            anchors_xywh[:, 2:4] = anchors[i]
            iou_scale = bbox_iou(bbox_xywh_scaled[i][np.newaxis, :], anchors_xywh) 
                                                    #calculates IOU between gt box and anchors
            iou.append(iou_scale)
            iou_mask = iou_scale > 0.3 #IOU_threshold
            if np.any(iou_mask):
                xind, yind = np.floor(bbox_xywh_scaled[i, 0:2]).astype(np.int32)
                label[i][yind, xind, iou_mask, :] = 0
                label[i][yind, xind, iou_mask, 0:4] = bbox_xywh
                label[i][yind, xind, iou_mask, 4:5] = 1.0
                label[i][yind, xind, iou_mask, 5:] = smooth_onehot
                bbox_ind = int(bbox_count[i] % max_bbox_per_scale)
                bboxes_xywh[i][bbox_ind, :4] = bbox_xywh
                bbox_count[i] += 1
                exist_positive = True

        if not exist_positive: # if no anchor having IOU>0.3
            best_anchor_ind = np.argmax(np.array(iou).reshape(-1), axis=-1)
            best_detect = int(best_anchor_ind / anchor_per_scale)
            best_anchor = int(best_anchor_ind % anchor_per_scale)
            xind, yind = np.floor(bbox_xywh_scaled[best_detect, 0:2]).astype(np.int32)
            label[best_detect][yind, xind, best_anchor, :] = 0
            label[best_detect][yind, xind, best_anchor, 0:4] = bbox_xywh
            label[best_detect][yind, xind, best_anchor, 4:5] = 1.0
            label[best_detect][yind, xind, best_anchor, 5:] = smooth_onehot
            bbox_ind = int(bbox_count[best_detect] % max_bbox_per_scale)
            bboxes_xywh[best_detect][bbox_ind, :4] = bbox_xywh
            bbox_count[best_detect] += 1
    label_sbbox, label_mbbox, label_lbbox = label
    sbboxes, mbboxes, lbboxes = bboxes_xywh
    return label_sbbox, label_mbbox, label_lbbox, sbboxes, mbboxes, lbboxes

На этапе предварительной обработки данных мы можем использовать метод увеличения данных, чтобы виртуально увеличить разнообразие обучающей выборки. Например, дополнение включает в себя случайное искажение цвета, random_horizontal_flip, random_crop и random_translate.

Рассчитать убытки:

Автор рассматривает задачу обнаружения цели как регрессионную задачу предсказания области цели и предсказания категории. Функция потерь YOLOv3 разделена на три части:

  • Потеря достоверности: чтобы определить, есть ли объекты в прогнозируемой ограничивающей рамке. Эта функция потерь помогает модели различать фон и области переднего плана.
  • Потеря регрессии блока: применяется только в том случае, если блок прогноза содержит объект.
  • Потеря классификации. Чтобы определить, к какой категории относится объект в поле прогноза.
def compute_loss(pred, conv, label, bboxes, i=0, classes=CLASSES):
    NUM_CLASS = len(classes)
    conv_shape  = tf.shape(conv)
    batch_size  = conv_shape[0]
    output_size = conv_shape[1]
    input_size  = STRIDES[i] * output_size
    conv = tf.reshape(conv, (batch_size, output_size, output_size, 3, 5 + NUM_CLASS))

    conv_raw_conf = conv[:, :, :, :, 4:5]
    conv_raw_prob = conv[:, :, :, :, 5:]

    pred_xywh     = pred[:, :, :, :, 0:4]
    pred_conf     = pred[:, :, :, :, 4:5]

    label_xywh    = label[:, :, :, :, 0:4]
    respond_bbox  = label[:, :, :, :, 4:5]
    label_prob    = label[:, :, :, :, 5:]

    giou = tf.expand_dims(bbox_giou(pred_xywh, label_xywh), axis=-1)
    input_size = tf.cast(input_size, tf.float32)

    bbox_loss_scale = 2.0 - 1.0 * label_xywh[:, :, :, :, 2:3] * label_xywh[:, :, :, :, 3:4] / (input_size ** 2)
    giou_loss = respond_bbox * bbox_loss_scale * (1 - giou)

    iou = bbox_iou(pred_xywh[:, :, :, :, np.newaxis, :], bboxes[:, np.newaxis, np.newaxis, np.newaxis, :, :])
    # Find the value of IoU with the real box The largest prediction box
    max_iou = tf.expand_dims(tf.reduce_max(iou, axis=-1), axis=-1)

    # If the largest iou is less than the threshold, it is considered that the prediction box contains no objects, then the background box
    respond_bgd = (1.0 - respond_bbox) * tf.cast( max_iou < IOU_LOSS_THRESH, tf.float32 ) #.5

    conf_focal = tf.pow(respond_bbox - pred_conf, 2)

    # Calculate the loss of confidence
    # we hope that if the grid contains objects, then the network output prediction box has a confidence of 1 and 0 when there is no object.
    conf_loss = conf_focal * (
            respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
            +
            respond_bgd * tf.nn.sigmoid_cross_entropy_with_logits(labels=respond_bbox, logits=conv_raw_conf)
    )

    prob_loss = respond_bbox * tf.nn.sigmoid_cross_entropy_with_logits(labels=label_prob, logits=conv_raw_prob)

    giou_loss = tf.reduce_mean(tf.reduce_sum(giou_loss, axis=[1,2,3,4]))
    conf_loss = tf.reduce_mean(tf.reduce_sum(conf_loss, axis=[1,2,3,4]))
    prob_loss = tf.reduce_mean(tf.reduce_sum(prob_loss, axis=[1,2,3,4]))

    return giou_loss, conf_loss, prob_loss

Немаксимальное подавление:

В YOLOv3 очень часто несколько ограничивающих рамок предсказывают один и тот же объект. Таким образом, нам нужно сохранить тот, у которого наивысшая оценка достоверности, и исключить другие ограничивающие рамки с высокой степенью перекрытия и низкой оценкой. Этот метод называется NMS.

Эта техника работает в три этапа:

Шаг 1: Определите, больше ли количество ограничивающих рамок 0 или нет. Если нет, завершите процесс.

Шаг 2: Выберите ограничивающую рамку с наибольшей достоверностью и уберите ее.

Шаг 3: Рассчитайте долговую расписку между выбранной ограничивающей рамкой и оставшимися ограничивающими рамками. Удалите все ограничивающие рамки, значение IOU которых выше порогового значения. Перейти к шагу 1.

Мы берем только все ограничивающие рамки, выбранные на шаге 2.

def nms(bboxes, iou_threshold, method='nms'):
    #param bboxes: (xmin, ymin, xmax, ymax, score, class)
    classes_in_img = list(set(bboxes[:, 5]))
    best_bboxes = []

    for cls in classes_in_img: #nms is applied class-wise
        cls_mask = (bboxes[:, 5] == cls)
        cls_bboxes = bboxes[cls_mask]
        # Process 1: Determine whether the number of bounding boxes is greater than 0 
        while len(cls_bboxes) > 0:
            # Process 2: Select the bounding box with the highest score according to socre order A
            max_ind = np.argmax(cls_bboxes[:, 4])
            best_bbox = cls_bboxes[max_ind]
            best_bboxes.append(best_bbox)
            cls_bboxes = np.concatenate([cls_bboxes[: max_ind], cls_bboxes[max_ind + 1:]])
            # Process 3: Calculate this bounding box A and
            # Remain all iou of the bounding box and remove those bounding boxes whose iou value is higher than the threshold 
            iou = bboxes_iou(best_bbox[np.newaxis, :4], cls_bboxes[:, :4])
            weight = np.ones((len(iou),), dtype=np.float32)
            iou_mask = iou > iou_threshold
            weight[iou_mask] = 0.0

            cls_bboxes[:, 4] = cls_bboxes[:, 4] * weight
            score_mask = cls_bboxes[:, 4] > 0.
            cls_bboxes = cls_bboxes[score_mask]

    return best_bboxes