Данная статья будет полезна новичкам, которые только начали использовать библиотеку OpenCV и еще не знают все её возможности. В частности, на основе биоинспирированного модуля библиотеки OpenCV можно сделать адаптивный к освещению детектор движения. Данный детектор движения будет работать в полумраке лучше, чем обычное вычитание двух кадров.
Немного о Retina
Библиотека OpenCV содержит класс Retina, в котором есть пространственно-временной фильтр двух информационных каналов (parvocellular pathway и magnocellular pathway) модели сетчатки глаза. Нас интересует канал magnocellular, который по сути уже и есть детектор движения: остается только получить координаты участка изображения, где есть движение, и каким-то образом не реагировать на помехи, которые возникают, если детектору движения показывают статичную картинку.
Помехи на выходе канала 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 будут сохранены настройки по умолчанию. Возможно, будет смысл их подправить.
<?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;
};
Пример работы детектора движения.
Автор: ELEKTRO_YAR