image

Данная статья будет полезна новичкам, которые только начали использовать библиотеку OpenCV и еще не знают все её возможности. В частности, на основе биоинспирированного модуля библиотеки OpenCV можно сделать адаптивный к освещению детектор движения. Данный детектор движения будет работать в полумраке лучше, чем обычное вычитание двух кадров.

Немного о Retina


Библиотека OpenCV содержит класс Retina, в котором есть пространственно-временной фильтр двух информационных каналов (parvocellular pathway и magnocellular pathway) модели сетчатки глаза. Нас интересует канал magnocellular, который по сути уже и есть детектор движения: остается только получить координаты участка изображения, где есть движение, и каким-то образом не реагировать на помехи, которые возникают, если детектору движения показывают статичную картинку.

image

Помехи на выходе канала magno при отсутствии движения на изображении

Код


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

Подключение и инициализация модуля
#include "opencv2/bioinspired.hpp" // Подключаем модуль

cv::Ptr<cv::bioinspired::Retina> cvRetina; // Модуль сетчатки глаза

// Инициализация
void initRetina(cv::Mat* inputFrame) {
    cvRetina = cv::bioinspired::createRetina(
        inputFrame->size(), // Устанавливаем размер изображения 
        false, // Выбранный режим обработки: без обработки цвета
        cv::bioinspired::RETINA_COLOR_DIAGONAL, // Тип выборки цвета
        false, // отключить следующие два параметра
        1.0, // Не используется. Определяет коэффициент уменьшения выходного кадра
        10.0); // Не используется
    // сохраняем стандартные настройки
    cvRetina->write("RetinaDefaultParameters.xml");
    // загруждаем настройки
    cvRetina->setup("RetinaDefaultParameters.xml");
    // очищаем буфер
    cvRetina->clearBuffers();
}


В файле *RetinaDefaultParameters.xml* будут сохранены настройки по умолчанию. Возможно, будет смысл их подправить.

RetinaDefaultParameters
<?xml version="1.0"?>
<opencv_storage>
<OPLandIPLparvo>
  <colorMode>0</colorMode>
  <normaliseOutput>1</normaliseOutput>
  <photoreceptorsLocalAdaptationSensitivity>0.89e-001</photoreceptorsLocalAdaptationSensitivity>
  <photoreceptorsTemporalConstant>5.0000000000000000e-001</photoreceptorsTemporalConstant>
  <photoreceptorsSpatialConstant>1.2999997138977051e-001</photoreceptorsSpatialConstant>
  <horizontalCellsGain>0.3</horizontalCellsGain>
  <hcellsTemporalConstant>1.</hcellsTemporalConstant>
  <hcellsSpatialConstant>7.</hcellsSpatialConstant>
  <ganglionCellsSensitivity>0.89e-001</ganglionCellsSensitivity></OPLandIPLparvo>
<IPLmagno>
  <normaliseOutput>1</normaliseOutput>
  <parasolCells_beta>0.1</parasolCells_beta>
  <parasolCells_tau>0.1</parasolCells_tau>
  <parasolCells_k>7.</parasolCells_k>
  <amacrinCellsTemporalCutFrequency>1.2000000476837158e+000</amacrinCellsTemporalCutFrequency>
  <V0CompressionParameter>5.4999998807907104e-001</V0CompressionParameter>
  <localAdaptintegration_tau>0.</localAdaptintegration_tau>
  <localAdaptintegration_k>7.</localAdaptintegration_k></IPLmagno>
</opencv_storage>

Для себя я менял пару параметров (ColorMode и amacrinCellsTemporalCutFrequency). Ниже представлен перевод описания некоторых параметров для выхода magno.

normaliseOutput — определяет, будет ли (true) выход масштабироваться в диапазоне от 0 до 255 не (false)

ColorMode — определяет, будет ли (true) использоваться цвет для обработки, или (false) будет идти обработка серого изображения.

photoreceptorsLocalAdaptationSensitivity — чувствительность фоторецепторов (от 0 до 1).

photoreceptorsTemporalConstant — постоянная времени фильтра нижних частот первого порядка фоторецепторов, использовать его нужно, чтобы сократить высокие временные частоты (шум или быстрое движение). Используется блок кадров, типичное значение 1 кадр.

photoreceptorsSpatialConstant — пространственная константа фильтра нижних частот первого порядка фоторецепторов. Можно использовать его, чтобы сократить высокие пространственные частоты (шум или толстые контуры). Используется блок пикселей, типичное значение — 1 пиксель.

horizontalCellsGain — усиление горизонтальной сети ячеек. Если значение равно 0, то среднее значение выходного сигнала равно нулю. Если параметр находится вблизи 1, то яркость не фильтруется и по-прежнему достижима на выходе. Типичное значение равно 0.

HcellsTemporalConstant — постоянная времени фильтра нижних частот первого порядка горизонтальных клеток. Этот пункт нужен, чтобы вырезать низкие временные частоты (локальные вариации яркости). Используется блок кадров, типичное значение — 1 кадр.

HcellsSpatialConstant — пространственная константа фильтра нижних частот первого порядка горизонтальных клеток. Нужно использовать для того, чтобы вырезать низкие пространственные частоты (локальная яркость). Используется блок пикселей, типичное значение — 5 пикселей.

ganglionCellsSensitivity — сила сжатия локального выхода адаптации ганглиозных клеток, установите значение в диапазоне от 0,6 до 1 для достижения наилучших результатов. Значение возрастает соответственно тому, как падает чувствительность. И выходной сигнал насыщается быстрее. Рекомендуемое значение — 0,7.

Для ускорения вычислений есть смысл предварительно уменьшить входящее изображение с помощью функции *cv::resize*. Для определения наличия помех можно использовать значение средней яркости изображения или энтропию. Также в одном из проектов я использовал подсчет пикселей выше и ниже определенного уровня яркости. Ограничительные рамки можно получить с помощью функции для поиска контуров. Под спойлером представлен код детектора движения, который не претендует на работоспособность, а лишь показывают примерную возможную реализацию.

Код детектора движения

// размер буфера для медианного фильтра средней яркости и энтропии
#define CV_MOTION_DETECTOR_MEDIAN_FILTER_N 512

// буферы для фильтров
static float meanBuffer[CV_MOTION_DETECTOR_MEDIAN_FILTER_N];
static float entropyBuffer[CV_MOTION_DETECTOR_MEDIAN_FILTER_N];
// количество кадров
static int numFrame = 0;

// Возвращает медиану массива
float getMedianArrayf(float* data, unsigned long nData);

// детектор движения
//  inputFrame - входное изображение RGB типа CV_8UC3
// arrayBB - массив ограничительных рамок
void updateMotionDetector(cv::Mat* inputFrame,std::vector<cv::Rect2f>& arrayBB) {
        cv::Mat retinaOutputMagno; // изображение на выходе magno
        cv::Mat imgTemp; // изображение для порогового преобразования
        float medianEntropy, medianMean; // отфильтрованные значения
        cvRetina->run(*inputFrame);
        // загружаем изображение детектора движения
        cvRetina->getMagno(retinaOutputMagno);
        // отобразим на экране, если нужно для отладки
        cv::imshow("retinaOutputMagno", retinaOutputMagno);
        // подсчет количества кадров до тех пор, пока их меньше заданного числа
        if (numFrame < CV_MOTION_DETECTOR_MEDIAN_FILTER_N) {
            numFrame++;
        }
        // получаем среднее значение яркости всех пикселей
        float mean = cv::mean(retinaOutputMagno)[0];
        // получаем энтропию
        float entropy = calcEntropy(&retinaOutputMagno);
        // фильтруем данные
        if (numFrame >= 2) {
            // фильтруем значения энтропии
            // сначала сдвинем буфер значений 
            // энтропии и запишем новый элемент
            for (i = numFrame - 1; i > 0; i--) {
                entropyBuffer[i] = entropyBuffer[i - 1];
            }
            entropyBuffer[0] = entropy;
            // фильтруем значения средней яркости
            // сначала сдвинем буфер значений 
            // средней яркости и запишем новый элемент
            for (i = numFrame - 1; i > 0; i--) {
               meanBuffer[i] = meanBuffer[i - 1];
            }
            meanBuffer[0] = mean;
            // для фильтрации применим медианный фильтр
            medianEntropy = getMedianArrayf(entropyBuffer, numFrame);
            medianMean = getMedianArrayf(meanBuffer, numFrame);
        } else {
            medianEntropy = entropy;
            medianMean = mean;
        }
        // если средняя яркость не очень высокая, то на изображении движение, а не шум
        // if (medianMean >= mean) {
        // если энтропия меньше медианы, то на изображении движение, а не шум
        if ((medianEntropy * 0.85) >= entropy) {

            // делаем пороговое преобразование
            // как правило, области с движением достаточно яркие
            // поэтому можно обойтись и без медианы средней яркости
            // cv::threshold(retinaOutputMagno, imgTemp,150, 255.0, CV_THRESH_BINARY);
            // пороговое преобразование с учетом медианы средней яркости
            cv::threshold(retinaOutputMagno, imgTemp,150, 255.0, CV_THRESH_BINARY);
            // найдем контуры
            std::vector<std::vector<cv::Point>> contours;
            cv::findContours(imgTemp, contours, CV_RETR_EXTERNAL,  
                                   CV_CHAIN_APPROX_SIMPLE);
            if (contours.size() > 0) {
                // если контуры есть
                arrayBB.resize(contours.size());
                // найдем ограничительные рамки
                float xMax, yMax;
                float xMin, yMin;
                for (unsigned long i = 0; i < contours.size(); i++) {
                    xMax = yMax = 0;
                    xMin = yMin = imgTemp.cols;
                    for (unsigned long z = 0; z < contours[i].size(); z++) {
                         if (xMax < contours[i][z].x) {
                             xMax = contours[i][z].x;
                         }
                         if (yMax < contours[i][z].y) {
                             yMax = contours[i][z].y;
                         }
                         if (xMin > contours[i][z].x) {
                            xMin = contours[i][z].x;
                         }
                         if (yMin > contours[i][z].y) {
                            yMin = contours[i][z].y;
                         }
                    }
                    arrayBB[i].x = xMin;
                    arrayBB[i].y = yMin;
                    arrayBB[i].width = xMax - xMin ;
                    arrayBB[i].height = yMax - yMin;
                }
            } else {
                arrayBB.clear();
            }
        } else {
            arrayBB.clear();
        }
        // освободим память
        retinaOutputMagno.release();
        imgTemp.release();
}

// быстрая сортировка массива
template<typename aData>
void quickSort(aData* a, long l, long r) {
        long i = l, j = r;
        aData temp, p;
        p = a[ l + (r - l)/2 ];
        do {
            while ( a[i] < p ) i++;
            while ( a[j] > p ) j--;
            if (i <= j) {
                temp = a[i]; a[i] = a[j]; a[j] = temp;
                i++; j--;
            }
        } while ( i<=j );
        if ( i < r )
            quickSort(a, i, r);
        if ( l < j )
            quickSort(a, l , j);
};

// Возвращает медиану массива
float getMedianArrayf(float* data, unsigned long nData) {
        float medianData;
        float mData[nData];
        register unsigned long i;
        if (nData == 0)
            return 0;
        if (nData == 1) {
            medianData = data[0];
            return medianData;
        }
        for (i = 0; i != nData; ++i) {
            mData[i] = data[i];
        }
        quickSort(mData, 0, nData - 1);
        medianData = mData[nData >> 1];
        return medianData;
    };


image

Пример работы детектора движения.
Поделиться с друзьями
-->

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


  1. Nagdiel
    11.02.2017 10:34

    Спасибо за пост! Не знал, что bioinspired можно использовать для выделения движущихся объектов (честно сказать, руки не дошли, чтобы разобраться с этим модулем). Хотелось бы, однако, увидеть больше примеров. Например, что будет при наличии в кадре слабого движения — покачивания деревьев, атмосферной турбулентности. Было бы неплохо увидеть сравнение качества работы этого подхода с другими классическими подходами: на основе оценки фона, ViBe, оптического потока. Каковы требования алгоритма к качеству исходного видеопотока? Как он реагирует на наличие артефактов сжатия?


    1. Nagdiel
      11.02.2017 10:38

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


    1. ELEKTRO_YAR
      11.02.2017 22:36

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

      видео


      1. Nagdiel
        12.02.2017 11:36

        Тут-то как раз проблема! Покачивание деревьев или колебания яркости точек изображения в силу атмосферной турбулентности приводят к ложным срабатываниям по движению, что плохо для охранных систем, или как в Вашем примере, для турели. Сейчас много работают над тем, чтобы разделить слабые колебания вызванные фоновым движением, от движения реальных объектов. Что Вы планируете делать, чтобы минимизировать количество ложных срабатываний? Увеличение порога, здесь, думается, не панацея, так как в этом случае увеличивается вероятность пропуска цели. Фильтровать объекты по размеру — тоже не всегда вариант. Все это как раз можно видеть на Вашем видеопримере. На первой его части система хорошо срабатывает даже по малоразмерным и медленно перемещающимся объектам, в то время как на второй половине, довольно крупные и достаточно подвижные объекты (люди на заднем плане) не обнаруживаются. Автомобиль наоборот при приближении начинает выделяться как несколько объектов, что тоже обычно нежелательно. Связано ли это как-то с настройками алгоритма? Т.е. две части сюжета обрабатывались с одними настройками или разными?


        1. ELEKTRO_YAR
          12.02.2017 20:19

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


  1. Akon32
    11.02.2017 11:11

    Для чего своя реализация сортировки? Есть же стандартные std::sort(), std::stable_sort().


    1. Nagdiel
      11.02.2017 11:19

      Кроме того, в OpenCV есть своя сортировка


    1. ELEKTRO_YAR
      11.02.2017 22:32

      Да можно и стандартную сортировку использовать, код сортировки простой, оптимизировать дальше некуда.


      1. Akon32
        13.02.2017 20:43

        оптимизировать дальше некуда.

        Вообще-то (в общем случае) есть, иначе бы не придумывали бы всяких timsort. В стандартной библиотеке С++, правда, её нет.


  1. palexab
    11.02.2017 12:18
    +1

    Хотелось бы увидеть сравнение результата с тем, что дают opencv-шные методы обнаружения движения через накопление фона.
    cv::bgsegm::BackgroundSubtractorMOG
    cv::BackgroundSubtractorMOG2


  1. Alex_ME
    11.02.2017 14:04

    Можете пояснить про сдвиг буфера?


    1. ELEKTRO_YAR
      11.02.2017 22:30

      Это для медианного фильтра нужно. Потом буфер сортируется, и элемент из середины массива и есть медиана.


      1. Nagdiel
        12.02.2017 11:36

        Я думаю, тут скорее интересно, как используется медианный фильтр?


        1. ELEKTRO_YAR
          12.02.2017 15:10

          Идея такая, что резкие пики энтропии или средней яркости приходятся на моменты, когда в кадре есть движение. Тогда медиана будет соответствовать таким значениям, где движения в кадре нет. Если значения энтропии или средней яркости текущего кадра выходят за пределы медианных значений, то включается пороговое срабатывание и обнаруживаются движения. В остальных случаях алгоритм дальше не продолжается, так как он будет пытаться найти в кадре слишком слабые движения (т.е. начнет регистрировать шум).


  1. arcman
    11.02.2017 21:39

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


    1. ELEKTRO_YAR
      11.02.2017 22:28

      Очень интересно. Где можно подробнее узнать?


      1. arcman
        13.02.2017 11:12

        Я в свое время начал с этой статьи (перевода):
        https://habrahabr.ru/post/120562/
        Так же имеет смысл ознакомиться с оригиналом и комментариями к нему.
        В итоге у нас на на Allwinner A13 (1GHz ARM v7) картинка с камеры (640х480) обрабатывалась за 4мс.
        Алгоритм простой — каждую секунду делается кадр, вычисляется хеш и находится «дистанция» с предыдущим.
        Когда в кадре ничего не происходит, дистанция обычно 0-1.
        До этого использовали алгоритм основанный на подсчете количества пикселей, яркость которых изменилась более чем на N — он был медленный и ненадежный, сильно реагировал на цветовые/яркостные флуктуации.


        1. Nagdiel
          13.02.2017 11:24

          Правильно я понимаю, Вы только сам факт движения фиксируете, но сами объекты не выделяете?


          1. arcman
            13.02.2017 11:32

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


  1. Nagdiel
    12.02.2017 11:58

    Еще, но это скорее в качестве наблюдения. Выход magno канала напоминает текстуру, генерируемую по марковскому случайному полю, помеха явно пространственно корелирована. Как Вы считаете, могло бы улучшить результаты выделения объектов использование алгоритмов сегментации марковских полей?