Детектор руху на основі биоинспирированного модуля 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.89 e-001</photoreceptorsLocalAdaptationSensitivity>
<photoreceptorsTemporalConstant>5.0000000000000000 e-001</photoreceptorsTemporalConstant>
<photoreceptorsSpatialConstant>1.2999997138977051 e-001</photoreceptorsSpatialConstant>
<horizontalCellsGain>0.3</horizontalCellsGain>
<hcellsTemporalConstant>1.</hcellsTemporalConstant>
<hcellsSpatialConstant>7.</hcellsSpatialConstant>
<ganglionCellsSensitivity>0.89 e-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.2000000476837158 e+000</amacrinCellsTemporalCutFrequency>
<V0CompressionParameter>5.4999998807907104 e-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
Приклад роботи детектора руху.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.