Детектор движения на основе биоинспирированного модуля OpenCV


image

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



Немного о Retina


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


image

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


Код


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


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

cv::Ptr 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



  0
  1
  0.89e-001
  5.0000000000000000e-001
  1.2999997138977051e-001
  0.3
  1.
  7.
  0.89e-001

  1
  0.1
  0.1
  7.
  1.2000000476837158e+000
  5.4999998807907104e-001
  0.
  7.


Для себя я менял пару параметров (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& 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> 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
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

Пример работы детектора движения.

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

  • 11 февраля 2017 в 10:34

    0

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

      0

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

    0

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

    • 11 февраля 2017 в 11:19

      0

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

© Habrahabr.ru