Привет Хабр!

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

Почему контурный анализ?

Контурный анализ имеет довольно слабую устойчивость к помехам, но простота и быстродействие позволили вполне успешно применить данный подход.



Однако, на практике оказалось довольно непросто реализовать поиск нужных коэффициентов на платформе Android (Виджеты OpenCV применять не пробовал, вместо этого разделил экран на фреймы, где слева настройки, справа видеопоток с задней камеры). Конкретная реализация UI и логики проекта доступна по ссылке внизу.

Последовательность операций поиска дорожного знака представлена следующим образом:

  • Подавление шумов

    Например, через нелинейный фильтр

    cv::medianBlur(original, original, 3);
    

  • Преобразование изображения из RGBA в HSV

    При использовании цветовой модели HSV точнее выделяется базовый красный цвет дорожного знака на основании цветового тона, насыщенности и яркости.

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

    cv::Mat hsv;
    cv::cvtColor(original, hsv, cv::COLOR_RGB2HSV);
    if (layerType == LAYER_HSV) {
      *(cv::Mat*)matAddress = hsv;
    }
    

  • Выделение дорожного знака по цвету



    В процессе обработки изображение разделяется на отдельные каналы H, S и V, которые в дальнейшем бинаризуются по определенному порогу и объединяются посредством логического сложения в конечную матрицу изображения.

    std::vector<cv::Mat> channels;
    cv::split(hsv, channels);
    cv::Mat minHueThreshold = channels[0] < lowerHue;
    if (layerType == LAYER_HUE_LOWER) {
      *(cv::Mat*)matAddress = minHueThreshold;
    }
    cv::Mat maxHueThreshold = channels[0] > upperHue;
    if (layerType == LAYER_HUE_UPPER) {
      *(cv::Mat*)matAddress = maxHueThreshold;
    }
    if (layerType == LAYER_HUE) {
      *(cv::Mat*)matAddress = minHueThreshold | maxHueThreshold;
    }
    cv::Mat saturationThreshold = channels[1] > minSaturation;
    if (layerType == LAYER_SATURATION) {
      *(cv::Mat*)matAddress = saturationThreshold;
    }
    cv::Mat valueThreshold = channels[2] > minValue;
    if (layerType == LAYER_VALUE) {
      *(cv::Mat*)matAddress = valueThreshold;
    }
    cv::Mat colorFiltered =
        (minHueThreshold | maxHueThreshold) & saturationThreshold & valueThreshold;
    if (layerType == LAYER_RED_FILTERED) {
      *(cv::Mat*)matAddress = colorFiltered;
    }
    

  • Детектирование дорожного знака по внешнему контуру



    Прежде всего осуществляется операция растягивания, которая устраняет шум и способствует объединению областей изображения, которые были разделены предметами, тенями.

    cv::Mat colorDilated;
    cv::dilate(colorFiltered, colorDilated, cv::Mat());
    if (layerType == LAYER_DILATED) {
      *(cv::Mat*)matAddress = colorDilated;
    }
    

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

    cv::SimpleBlobDetector::Params params;
    params.filterByColor = false;
    params.filterByConvexity = false;
    params.filterByInertia = false;
    params.filterByArea = true;
    // A = 254.46900494077 px^2 при минимальном диаметре круга в 9 px
    params.minArea = 255;
    // A = 723822.94738709 px^2 при максимальном диаметре круга в 480 px
    params.maxArea = 723823;
    params.filterByCircularity = true;
    params.minCircularity = 0.85f;
    cv::Ptr<cv::SimpleBlobDetector> detector =
        cv::SimpleBlobDetector::create(params);
    std::vector<cv::KeyPoint> keyPoints;
    detector->detect(colorDilated, keyPoints);
    

    В векторе keyPoints записываются координаты и радиус контура дорожного знака, которые затем выделяются на экране.

Не скажу, что в условиях реальной местности работает хорошо (возможно, алгоритм детектирования не самый лучший, но он оказался самым простым в использовании). И есть трудности в поисках оптимальных коэффициентов (вдоволь «поохотился» на дорожные знаки в своем городе; встречал даже выгоревшие от солнца знаки, красного на них вовсе не было).

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

Из дополнительных плюшек по опыту использования OpenCV в Android:

Поворот изображения с камеры, если оно перевернуто
extern "C"

JNIEXPORT void JNICALL Java_ru_dksta_prohibitingsigndetector_ActivityMain_rotation(JNIEnv /* *env */,
    jclass /* activity */, jlong matAddress, jint angle) {
    CV_Assert(angle % 90 == 0 && angle <= 360 && angle >= -360);
    cv::Mat* mat = (cv::Mat*) matAddress;
    if (angle == 180 || angle == -180) {
        cv::flip(*mat, *mat, -1);
    }
}


Реализация Salt&Pepper шума
extern "C"

JNIEXPORT void JNICALL Java_ru_dksta_prohibitingsigndetector_ActivityMain_saltPepperNoise(JNIEnv /* *env */,
    jclass /* activity */, jlong matAddress) {
    cv::Mat* mat = (cv::Mat*) matAddress;
    cv::Mat noise = cv::Mat::zeros((*mat).rows, (*mat).cols, CV_8U);
    cv::randu(noise, 0, 255);
    cv::Mat black = noise < 30;
    cv::Mat white = noise > 225;
    (*mat).setTo(255, white);
    (*mat).setTo(0, black);
}


Отображения видео с камеры с обработкой в режиме 'картинка в картинке'
if (secondView) {
  cv::Mat miniView = colorDilated.clone();
  cv::cvtColor(miniView, miniView, cv::COLOR_GRAY2RGB);
  cv::resize(miniView, miniView, cv::Size(), 0.6, 0.6, cv::INTER_LINEAR);
  cv::Size miniSize = miniView.size();
  cv::Size maxSize = original.size();
  int startY = maxSize.height - miniSize.height;
  for (int y = startY; y < maxSize.height; y++) {
    for (int x = 0; x < miniSize.width; x++) {
      (*(cv::Mat*)matAddress).at<cv::Vec3b>(cv::Point(x, y)) =
          miniView.at<cv::Vec3b>(cv::Point(x, y - startY));
    }
  }
}


Написание текста в OpenCV
cv::Mat* mat = (cv::Mat*)matAddress;
int textStartY = TEXT_LINE_HEIGHT;
std::ostringstream output;
output << std::setw(2) << std::setfill('0') << fpsCount << " FPS";
cv::putText(*mat, output.str(), cv::Point(TEXT_START_X, textStartY), FONT_FACE,
            FONT_SCALE, GREEN, TEXT_THICKNESS);
output.seekp(0);
textStartY += TEXT_LINE_HEIGHT;
cv::putText(*mat, getLayerTypeDesc(layerType),
            cv::Point(TEXT_START_X, textStartY), FONT_FACE, FONT_SCALE, GREEN,
            TEXT_THICKNESS);


> Ссылка на проект

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


  1. ZaMaZaN4iK
    06.10.2017 17:33
    +1

    А где именно распознавание знаков? Я вижу только детектор окружностей


    1. androidovshchik Автор
      06.10.2017 18:15

      Спасибо, что поправили. Не совсем такой заголовок должен быть


  1. Imp5
    06.10.2017 18:37

    Однако, на практике оказалось довольно непросто реализовать поиск нужных коэффициентов на платформе Android

    Похоже, коэффициенты были подогнаны вообще под конкретную картинку.

    Например, через линейный фильтр

    medianBlur — это не линейный фильтр.


    1. androidovshchik Автор
      06.10.2017 18:46

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


  1. IronHead
    06.10.2017 23:05
    +1

    1) Знаки могут быть не только в красном кружочке, но и в синем квадрате, треугольнике и пр
    2) Где распознавание скорости со знака?
    3) Что за приложение разрабатывается, какая скорость распознавания планируется?
    Пока от статьи складывается впечатление, как будто вы сумели прикрутить OpenCV на андроид и решили похвастаться.


    1. androidovshchik Автор
      07.10.2017 04:42

      Я профессионально работаю с андроид, для меня это не составило труда (но я не фанатик тем не менее алгоритмов) Я не хочу вдаваться в такие подробности. Я представил краткий рабочий код, потому что когда это разрабатывал по всему интернету не находил ничего стоящего в плане кода (либо это были полноценные проекты со 100500 строками кода и попробуй в них разобраться). Мое желание — поделится, думаю кому-то может пригодиться.


      Приложение ищет (не распознает) только запрещающие знаки. Скорость работы такая, что на Nexus 9 с Nvidia Tegra K1 в среднем 20 fps (до 30 доходит), а на Nexus 5x со Snapdragon 808 где-то 10 fps