Pull to refresh

OpenCV. Поиск дорожных знаков методом контурного анализа в Android

Reading time 4 min
Views 11K
Привет Хабр!

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

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

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



Однако, на практике оказалось довольно непросто реализовать поиск нужных коэффициентов на платформе 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);


Ссылка на проект
Tags:
Hubs:
+2
Comments 6
Comments Comments 6

Articles