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


Ну так распишем подробнее наши задачи:


  • Фиксировать людей и автомобили — выделять их на изображении и генерировать соответствующие экземпляры классов с необходимыми полями.
  • Определять номер авто, если он попал в кадр определенной камеры
  • Сравнить текущий кадр с предыдущим на равенство объектов, чтобы мы могли узнать

Ок, подумал я, и взял в руки толстую змею, python, значится. Было решено использовать нейронную сетку Mask R-Cnn в связи с ее простотой и современными характеристиками. Также, разумеется, для манипуляция с изображениями будем использовать OpenCV.


Установка среды


Будем использовать Windows 10, из-за того, что ты вероятнее всего, используешь именно ее.
Подразумевается, что у тебя уже присутствует 64 битный Python. Если же нет, то можно скачать пакет, например, отсюда


Установка пакетов


git clone https://github.com/matterport/Mask_RCNN
cd Mask_RCNN
pip3 install -r requirements.txt
python3 setup.py install

Если по какой-то причине не удается собрать из исходников, существует версия из pip:


pip3 install mrcnn --user

К пакету, разумеется, поставятся все зависимости.


Этап 1. Создание простейшей программы-распознавателя.


Сделаем необходимые импорты


import os
import cv2

import mrcnn.config
import mrcnn
from mrcnn.model import MaskRCNN

Нейросеть требует создания конфига с переопределенными полями


class MaskRCNNConfig(mrcnn.config.Config):
    NAME = "coco_pretrained_model_config"
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1
    DETECTION_MIN_CONFIDENCE = 0.8 # минимальный процент отображения прямоугольника
    NUM_CLASSES = 81

Укажем расположение файла с весами. Пусть в данном примере он будет лежать в папке с этим файлом. Если его нет, то он скачается.


import mrcnn.utils
DATASET_FILE = "mask_rcnn_coco.h5"
if not os.path.exists(DATASET_FILE):
    mrcnn.utils.download_trained_weights(DATASET_FILE)

Создадим нашу модель с настройками выше


model = MaskRCNN(mode="inference", model_dir="logs", config=MaskRCNNConfig())
model.load_weights(DATASET_FILE, by_name=True)

И пожалуй, начнем обработку всех изображений в каталоге images в текущей директории.


IMAGE_DIR = os.path.join(os.getcwd(), "images")
for filename in os.listdir(IMAGE_DIR):
    image = cv2.imread(os.path.join(IMAGE_DIR, filename))
    rgb_image = image[:, :, ::-1]
    detections = model.detect([rgb_image], verbose=1)[0]

Что же мы увидим в detections?


    print(detections)

Например, что-то похожее:


{'rois': array([[ 303, 649, 542, 1176],[ 405, 2, 701, 319]]), 'class_ids': array([3, 3]), 
'scores': array([0.99896, 0.99770015], dtype=float32), 
'masks': array()}

В данном случае нашли 2 объекта.
rois — массивы координат левого нижнего и правого верхнего угла
class_ids — числовые идентификаторы найденных объектов, пока нам нужно знать, что 1 — человек, 3 — машина, 8 — грузовик.
scores — насколько модель уверена в решении, этот параметр можно отсеивать через DETECTION_MIN_CONFIDENCE в конфиге, отсекая все неподходящие варианты.
masks — контур объекта. Данные используются для рисования маски объекта. Т.к. они достаточно объемны, и не предназначены для понимания человеком, приводить в статье их не буду.


Ок, мы могли бы на этом остановиться, но мы же хотим посмотреть на изображение, которое обычно выдают гайды по использованию нейросеток с красиво выделенными объектами?


Проще было бы вызвать функцию mrcnn.visualize.display_instances, но мы не будем так делать, напишем свою.


Функция будет принимать изображение, и основные параметры, полученные из словаря из первых шагов.


def visualize_detections(image, masks, boxes, class_ids, scores):
    import numpy as np
    bgr_image = image[:, :, ::-1]

    CLASS_NAMES = ['BG',"person", "bicycle", "car", "motorcycle", "bus", "truck"]
    COLORS = mrcnn.visualize.random_colors(len(CLASS_NAMES))

    for i in range(boxes.shape[0]):        
        y1, x1, y2, x2 = boxes[i]

        classID = class_ids[i]            
        label = CLASS_NAMES[classID]
        font = cv2.FONT_HERSHEY_DUPLEX
        color = [int(c) for c in np.array(COLORS[classID]) * 255]
        text = "{}: {:.3f}".format(label, scores[i])
        size = 0.8
        width = 2

        cv2.rectangle(bgr_image, (x1, y1), (x2, y2), color, width)
        cv2.putText(bgr_image, text, (x1, y1-20), font, size, color, width)        


Исходное изображение


Хотя одним из основных преимуществ этой нейронной сети является решение задач Instance segmentation — получение контуров объектов, мы пока этим не воспользовались, разберем это.


Для реализации масок добавим пару строчек перед отрисовкой прямоугольника для каждого найденного объекта.


mask = masks[:, :, i]  # берем срез
image = mrcnn.visualize.apply_mask(image, mask, color, alpha=0.6) # рисование маски

Результат:


Версия с белыми масками


Этап II. Первые успехи. Распознавание номеров машин.


Для распознавания нам нужен четкий кадр машины вблизи, поэтому было решено брать только кадры с КПП, а потом сравнивать на похожесть(об этом в следующей главе). Этот способ, правда, дает слишком большую неточность, т.к. машины могут быть очень похожими визуально и мой алгоритм пока не может избегать такие ситуации.


Было решено использовать готовую либу от украинского производителя nomeroff-net (не реклама). Т.к. почти весь код можно найти в примерах к модели, то приводить полное описание не буду.


Скажу лишь, что эту функцию можно запускать с исходным изображением или распознанную машину можно вырезать из кадра и подать в эту функцию.


import sys
import matplotlib.image as mpimg
import os
sys.path.append(cfg.NOMEROFF_NET_DIR)
from NomeroffNet import  filters, RectDetector, TextDetector, OptionsDetector, Detector, textPostprocessing

nnet = Detector(cfg.MASK_RCNN_DIR, cfg.MASK_RCNN_LOG_DIR)
nnet.loadModel("latest")

rectDetector = RectDetector()
optionsDetector = OptionsDetector()
optionsDetector.load("latest")
textDetector = TextDetector.get_static_module("ru")()
textDetector.load("latest")

def detectCarNumber(imgPath: str) -> str:
    img = mpimg.imread(imgPath)
    NP = nnet.detect([img])

    cvImgMasks = filters.cv_img_mask(NP)

    arrPoints = rectDetector.detect(cvImgMasks)
    zones = rectDetector.get_cv_zonesBGR(img, arrPoints)

    regionIds, stateIds, _c = optionsDetector.predict(zones)
    regionNames = optionsDetector.getRegionLabels(regionIds)

    # find text with postprocessing by standart  
    textArr = textDetector.predict(zones)
    textArr = textPostprocessing(textArr, regionNames)
    return textArr

textArr на выходе будет представлять массив строк с номерами машин, найденных на кадре, например:
["К293РР163"], или [""], [] — если подходящие номера не были найдены.


Этап III. Опознаем объекты на схожесть.


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


Для этого нужно узнать как мы будем проводить сравнение двух объектов.


Предложу для этих целей sift алгоритм. Оговоримся, что он не входит в основную часть OpenCV, поэтому нам необходимо доставить contrib модули дополнительно. К сожалению, алгоритм запатентован и его использование в коммерческих программах ограничено. Но мы нацелены на научно-исследовательскую деятельность, не так ли?


pip3 install opencv-contrib-python --user

~~ Перегружаем оператор == ~~ Пишем функцию, принимающую 2 сравниваемых объекта в виде матриц. Например, мы их получаем после вызова функции cv2.open(path)


Напишем реализацию нашего алгоритма.


def compareImages(img1, img2) -> bool:
    sift = cv2.xfeatures2d.SIFT_create()

Находим ключевые точки и дескрипторы с помощью SIFT. Пожалуй, хелп для этих функций приводить не буду, ведь его всегда его можно вызвать в интерактивной оболочке как help(somefunc)


    kp1, des1 = sift.detectAndCompute(img1, None)
    kp2, des2 = sift.detectAndCompute(img2, None)

Настроим наш алгоритм.


    FLANN_INDEX_KDTREE = 0
    indexParams = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    searchParams = dict(checks=50)
    flann = cv2.FlannBasedMatcher(indexParams, searchParams)

Теперь запустим его.


    matches = flann.knnMatch(des1, des2, k=2)

Посчитаем сходства между изображениями


    matchesCount = 0 
    for m, n in matches:
        if m.distance < cfg.cencitivity*n.distance:
            matchesCount += 1

    return matchesCount > cfg.MIN_MATCH_COUNT

А теперь, попробуем его использовать
Для этого, после обнаружения объектов, нам нужно их вырезать с исходного изображения


Я не смог написать ничего лучше, чем сохранить на медленную память, а потом уже оттуда считывать.


    def extractObjects(objects, binaryImage, outputImageDirectory, filename=None):
        for item in objects:
            y1, x1, y2, x2 = item.coordinates
            # вырежет все объекты в отдельные изображения
            cropped = binaryImage[y1:y2, x1:x2]
            beforePoint, afterPoint = filename.split(".")
            outputDirPath = os.path.join(os.path.split(outputImageDirectory)[0], "objectsOn" + beforePoint)
                if not os.path.exists(outputDirPath):
                    os.mkdir(outputDirPath)
                coordinates = str(item).replace(" ", ",")
                pathToObjectImage = "{}{}.jpg".format(item.type, coordinates)
                cv2.imwrite(os.path.join(outputDirPath, str(pathToObjectImage)), cropped)

Теперь у нас есть объекты в каталоге <outputImageDirectory>/objectsOn<imageFilename>


Теперь, если у нас есть как минимум 2 таких каталога, то мы можем сравнивать объекты в них. Запустим функцию, написанную ранее


if compareImages(previousObjects, currentObjects):
    print(“Эти объекты одинаковы!”)

Или мы можем сделать другое действие, вроде маркировки этих объектов одинаковым айдишником.


Конечно, как и все нейронные сети, эта склонна давать иногда ошибочные результаты.


В целом, мы выполнили 3 задачи, поставленные в начале, так что будем закругляться. Я сомневаюсь, что эта статья открыла глаза людям, которые написали хотя бы одну программу, решающую задачи image recognition/image segmentation, но я надеюсь, что я помог хотя бы одному начинающему разработчику).


Полный исходный код проекта можно посмотреть тут.

Комментарии (11)


  1. DonAgosto
    06.01.2020 22:18
    +1

    Windows 10… 64 битный Python

    Для такой связки наверно лучше сразу делать на Anaconda. Ибо рано или поздно понадобятся бинарные зависимости, которые только в исходниках. А настраивать окружение для сборки под Windows для новичка — ну такое себе, проще застрелиться


    1. flipix
      07.01.2020 17:15

      Может wsl?


      1. DonAgosto
        07.01.2020 18:21

        ну это если хочется собирать грабли вместе с прочими энтузиастами-первопроходцами
        по мне проще уж тогда полноценную виртуалку поднять


        1. flipix
          07.01.2020 18:41

          Нормально работает, та же самая убунта. Если грабли и были, то под ноги не попались.


          1. aminought
            08.01.2020 11:12

            Насколько я знаю, CUDA пока не работает на WSL.


  1. AigizK
    07.01.2020 10:55

    Может кто знает, есть ли готовые сетки для распознавания движущихся машин издалека, типа таких:
    С Mask R-Cnn не получилось распознать.


    1. windscape
      07.01.2020 17:15
      +1

      У меня с YOLOv3 и MobileNetSSD хорошо получалось. Само собой результат не 100% но хороший.


    1. Nepherhotep
      07.01.2020 20:23
      +1

      Для движущихся машин издалека, лучше тупо вычислять optical flow.


    1. enclis
      08.01.2020 18:48

      Что значит не распознало? Не нашло ни одной машины? Как уже написали выше, если есть доступ к видео, то правильнее использовать какой-нибудь optical flow. Но если нет, то вот что можно получить при использовании HTC HRNetv2p-W32 20e:

      Качество изображения плохое, поэтому всех машин оно конечно не найдёт.


      1. enclis
        08.01.2020 20:13

        и HTC dconv_c3-c5_mstrain_400_1400_x101_64x4d_fpn 20e:

        результат уже получше


  1. Alex_ME
    07.01.2020 14:17
    +3

    Тут недавно была познавательная статья о задаче object detection'а с более новыми архитектурами. И особенно познавательный комментарий, в котором описаны еще несколько крутых (вроде как) архитектур.
    https://habr.com/ru/post/482794/#comment_21089834