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

в 5:00, , рубрики: c++, opencv, детектор движения, обработка изображений, техническое зрение

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

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

Автор: ELEKTRO_YAR

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js