Данная статья будет полезна новичкам, которые только начали использовать библиотеку 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* будут сохранены настройки по умолчанию. Возможно, будет смысл их подправить.
RetinaDefaultParameters
Для себя я менял пару параметров (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.
<?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;
};
Пример работы детектора движения.
Поделиться с друзьями
Nagdiel
Спасибо за пост! Не знал, что bioinspired можно использовать для выделения движущихся объектов (честно сказать, руки не дошли, чтобы разобраться с этим модулем). Хотелось бы, однако, увидеть больше примеров. Например, что будет при наличии в кадре слабого движения — покачивания деревьев, атмосферной турбулентности. Было бы неплохо увидеть сравнение качества работы этого подхода с другими классическими подходами: на основе оценки фона, ViBe, оптического потока. Каковы требования алгоритма к качеству исходного видеопотока? Как он реагирует на наличие артефактов сжатия?
Nagdiel
Также интересна оценка вычислительной сложности по сравнению с другими подходами.
ELEKTRO_YAR
Слабое движение будет хорошо заметно, вот использовал в программе самонаведения на цель, идущего человека издалека видит хорошо.
Nagdiel
Тут-то как раз проблема! Покачивание деревьев или колебания яркости точек изображения в силу атмосферной турбулентности приводят к ложным срабатываниям по движению, что плохо для охранных систем, или как в Вашем примере, для турели. Сейчас много работают над тем, чтобы разделить слабые колебания вызванные фоновым движением, от движения реальных объектов. Что Вы планируете делать, чтобы минимизировать количество ложных срабатываний? Увеличение порога, здесь, думается, не панацея, так как в этом случае увеличивается вероятность пропуска цели. Фильтровать объекты по размеру — тоже не всегда вариант. Все это как раз можно видеть на Вашем видеопримере. На первой его части система хорошо срабатывает даже по малоразмерным и медленно перемещающимся объектам, в то время как на второй половине, довольно крупные и достаточно подвижные объекты (люди на заднем плане) не обнаруживаются. Автомобиль наоборот при приближении начинает выделяться как несколько объектов, что тоже обычно нежелательно. Связано ли это как-то с настройками алгоритма? Т.е. две части сюжета обрабатывались с одними настройками или разными?
ELEKTRO_YAR
Сам алгоритм из opencv так работает, что если в кадре есть крупное движение, незначительные движения на выходе magno менее светлые, и после порогового преобразования их вовсе нет.
Akon32
Для чего своя реализация сортировки? Есть же стандартные std::sort(), std::stable_sort().
Nagdiel
Кроме того, в OpenCV есть своя сортировка
ELEKTRO_YAR
Да можно и стандартную сортировку использовать, код сортировки простой, оптимизировать дальше некуда.
Akon32
Вообще-то (в общем случае) есть, иначе бы не придумывали бы всяких timsort. В стандартной библиотеке С++, правда, её нет.
palexab
Хотелось бы увидеть сравнение результата с тем, что дают opencv-шные методы обнаружения движения через накопление фона.
cv::bgsegm::BackgroundSubtractorMOG
cv::BackgroundSubtractorMOG2
Alex_ME
Можете пояснить про сдвиг буфера?
ELEKTRO_YAR
Это для медианного фильтра нужно. Потом буфер сортируется, и элемент из середины массива и есть медиана.
Nagdiel
Я думаю, тут скорее интересно, как используется медианный фильтр?
ELEKTRO_YAR
Идея такая, что резкие пики энтропии или средней яркости приходятся на моменты, когда в кадре есть движение. Тогда медиана будет соответствовать таким значениям, где движения в кадре нет. Если значения энтропии или средней яркости текущего кадра выходят за пределы медианных значений, то включается пороговое срабатывание и обнаруживаются движения. В остальных случаях алгоритм дальше не продолжается, так как он будет пытаться найти в кадре слишком слабые движения (т.е. начнет регистрировать шум).
arcman
Для детектирования движения в кадре очень хорошо себя показал перцептивный хеш.
После некоторой оптимизации время обработки кадра стало сравнимо с копированием этого кадра в памяти.
ELEKTRO_YAR
Очень интересно. Где можно подробнее узнать?
arcman
Я в свое время начал с этой статьи (перевода):
https://habrahabr.ru/post/120562/
Так же имеет смысл ознакомиться с оригиналом и комментариями к нему.
В итоге у нас на на Allwinner A13 (1GHz ARM v7) картинка с камеры (640х480) обрабатывалась за 4мс.
Алгоритм простой — каждую секунду делается кадр, вычисляется хеш и находится «дистанция» с предыдущим.
Когда в кадре ничего не происходит, дистанция обычно 0-1.
До этого использовали алгоритм основанный на подсчете количества пикселей, яркость которых изменилась более чем на N — он был медленный и ненадежный, сильно реагировал на цветовые/яркостные флуктуации.
Nagdiel
Правильно я понимаю, Вы только сам факт движения фиксируете, но сами объекты не выделяете?
arcman
Да, нам нужен был только датчик движения.
Следующая задача была на определение наличия крупных посторонних объектов в кадре, но она была решена таким же образом (сравнением дистанции с накопленным фоном).
Главным критерием была скорость обработки.
Nagdiel
Еще, но это скорее в качестве наблюдения. Выход magno канала напоминает текстуру, генерируемую по марковскому случайному полю, помеха явно пространственно корелирована. Как Вы считаете, могло бы улучшить результаты выделения объектов использование алгоритмов сегментации марковских полей?