Opencv И Иллюзия Кругов На Воде

Предлагаю читателям Хабрахабра статью о том, как школьная физика и OpenCV позволяют создать иллюзию волн на воде.

Основная сложность — подобрать красивое уравнение и перенести эффект преломления света на границе сред трехмерного мира на плоскую картинку.

Решение проблемы можно разделить на два этапа:

  • Создать карту распределения кругов/волн на воде;
  • Объедините полученную карту с заданным изображением.



Карта волн

Я думаю, все помнят из школьного курса физики, что круги на воде – это не что иное, как распространение колебаний по поверхности воды.

Строгий подход к решению задачи предполагает решение дифференциального волнового уравнения в двумерном пространстве, однако полной физической достоверности в нашем случае не требуется – можно взять готовое решение из школьного учебника физики или придумать собственное.

уравнение.



OpenCV и иллюзия кругов на воде

где с – коэффициент затухания; L – длина волны; Икс 0 , да 0 — координаты центра волны; т- время.

Длину волны лучше менять в зависимости от размера изображения.

Мы помним из школьного курса, что косинус принимает значения [-1;1], но интенсивность цвета равна [0;255], поэтому необходимо немного видоизменить нашу волновую функцию.

Наконец, яркость каждого пикселя волнового слоя задается следующим образом:

OpenCV и иллюзия кругов на воде

PoolDepth – толщина слоя воды, в данном случае еще и амплитуда волны в начальный момент времени.

Слишком большое значение приведет к значительному искажению изображения и будет некрасивым, а слишком маленькое значение практически не повлияет на изображение.

Для картинки размером 100х100 подошло значение PoolDepth=20, для картинки размером 1600х1600 оказалось возможным установить предельное значение PoolDepth=127. Карта волн без справочной таблицы

  
  
  
   

void makeWaveMap(Mat& image) { float simulPeriod = 10.0; // Period of simulation static float time = 0.0; const float dt = 0.05; // Time step float poolDepth = 20.0; int maxImageSize = image.cols > image.rows ? image.cols : image.rows; for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { float radius = sqrt((i - image.rows/2)*(i - image.rows/2) + \ (j - image.cols/2)*(j - image.cols/2)); float z = (1.0 + waveFunction(radius, time, maxImageSize))*poolDepth; image.at<uchar>(i, j) = saturate_cast<uchar>(z); } } time+= dt; time*= (time < simulPeriod); }

Обратите внимание, что размер карты волн соответствует размеру исходного изображения.

С учетом того, что волновая функция вычисляется в каждой точке, мы получаем гигантский объем вычислений уже для сравнительно небольшого изображения (в каждой точке мы вычисляем один квадратный корень, одну показательную функцию и два косинуса - редкий ноутбук нарисует 2-мегапиксельное изображение в этой ситуации).

На самом деле нет необходимости явно вычислять в каждой точке — уравнение одномерное! На каждом временном шаге мы создаем таблицу значений волновой функции, а затем линейную интерполяцию.

Карта волн со справочной таблицей

void makeWaveMapLUT(Mat& image) { float simulPeriod = 10.0; // Period of simulation static float time = 0.0; const float dt = 0.05; // Time step float poolDepth = 20.0; int nLUT = image.cols > image.rows ? image.cols : image.rows; int maxImageSize = nLUT; float waveFuncLUT[nLUT]; for (int i = 0; i < nLUT; i++) { float radius = saturate_cast<float>(i); waveFuncLUT[i] = waveFunction(radius, time, maxImageSize); } for (int i = 0; i < image.rows; i++) { for (int j = 0; j < image.cols; j++) { float radius = sqrt((i - image.rows/2)*(i - image.rows/2) + \ (j - image.cols/2)*(j - image.cols/2)); int iRad = cvRound(radius); float dR = radius - saturate_cast<float>(iRad); float wF = waveFuncLUT[iRad] + (waveFuncLUT[iRad+1] - waveFuncLUT[iRad])*dR; float z = (1.0 + wF)*poolDepth; image.at<uchar>(i, j) = saturate_cast<uchar>(z); } } time+= dt; time*= (time < simulPeriod); }

Теперь у нас есть более-менее приличная реализация алгоритма составления карты волнового движения и мы можем приступить к наложению ее на исходное изображение.



Наложение карты волн на исходное изображение

Искажение изображения под водой происходит из-за преломления света на границе раздела.

По сути, вам нужно решить школьную задачу о преломлении света на клине.

При нормальном падении света на плоскую горизонтальную границу никаких искажений не происходит (изменение цвета картинки в результате интерференции здесь мы не рассматриваем - мы считаем, что лужа не глубокая и на 100% физическую точность не претендуем ).



OpenCV и иллюзия кругов на воде

На рисунке показано, что происходит с лучом света, падающим на наклонную поверхность; нужно найти, насколько сместился исходящий луч относительно входящего.

Расстояние CD — это желаемое смещение, определяемое как ( решение здесь ):

OpenCV и иллюзия кругов на воде

где н 1 =1 и п 2 =1,33 – показатели преломления первой (воздуха) и второй (воды) сред соответственно.

Применительно к нашим волнам и пикселям уравнение можно записать так:

OpenCV и иллюзия кругов на воде

В данном случае альфа — это угол наклона касательной к волне в точке [i,j], который рассчитывается как:

OpenCV и иллюзия кругов на воде

Конечно, производную можно вычислить и аналитически, что повысит точность, но это сделает процесс изменения волнового уравнения более трудоемким.

В этом случае опять же мы можем либо рассчитать смещение для каждой точки изображения отдельно (void makeWaveMap(Mat&)), но лучше сделать справочную таблицу при первом вызове подпрограммы и использовать ее позже (void makeWaveMapLUT(Mat&) ).

Наложение без поиска по таблице

void blendWaveAndImage(Mat& sourceImage, Mat& targetImage, Mat& waveMap) { static float rFactor = 1.33; // refraction factor of water for (int i = 1; i < sourceImage.rows-1; i++) { for (int j = 1; j < sourceImage.cols-1; j++) { float alpha, beta; float xDiff = waveMap.at<uchar>(i+1, j) - waveMap.at<uchar>(i, j); float yDiff = waveMap.at<uchar>(i, j+1) - waveMap.at<uchar>(i, j); alpha = atan(xDiff); beta = asin(sin(alpha)/rFactor); int xDisplace = cvRound(tan(alpha - beta)*waveMap.at<uchar>(i, j)); alpha = atan(yDiff); beta = asin(sin(alpha)/rFactor); int yDisplace = cvRound(tan(alpha - beta)*waveMap.at<uchar>(i, j)); Vec3b Intensity = sourceImage.at<Vec3b>(i,j); /* Check whether displacement fits the image size */ int dispNi = i + xDisplace; int dispNj = j + yDisplace; dispNi = (dispNi > sourceImage.rows || dispNi < 0 ? i : dispNi); dispNj = (dispNj > sourceImage.cols || dispNj < 0 ? j : dispNj); Intensity = sourceImage.at<Vec3b>(dispNi, dispNj); targetImage.at<Vec3b>(i,j) = Intensity; } } }

Наложение со справочной таблицей

void blendWaveAndImageLUT(Mat& sourceImage, Mat& targetImage, Mat& waveMap) { static float rFactor = 1.33; // refraction factor of water static float dispLUT[512]; //Lookup table for displacement static int nDispPoint = 512; for (int i = 0; i < nDispPoint; i++) { float diff = saturate_cast<float>(i - 255); float alpha = atan(diff); float beta = asin(sin(alpha)/rFactor); dispLUT[i] = tan(alpha - beta); } nDispPoint = 0; for (int i = 1; i < sourceImage.rows-1; i++) { for (int j = 1; j < sourceImage.cols-1; j++) { int xDiff = waveMap.at<uchar>(i+1, j) - waveMap.at<uchar>(i, j); int yDiff = waveMap.at<uchar>(i, j+1) - waveMap.at<uchar>(i, j); int xDisplace = cvRound(dispLUT[xDiff+255]*waveMap.at<uchar>(i, j)); int yDisplace = cvRound(dispLUT[yDiff+255]*waveMap.at<uchar>(i, j)); Vec3b Intensity = sourceImage.at<Vec3b>(i,j); /* Check whether displacement fits the image size */ int dispNi = i + xDisplace; int dispNj = j + yDisplace; dispNi = (dispNi > sourceImage.rows || dispNi < 0 ? i : dispNi); dispNj = (dispNj > sourceImage.cols || dispNj < 0 ? j : dispNj); Intensity = sourceImage.at<Vec3b>(dispNi, dispNj); targetImage.at<Vec3b>(i,j) = Intensity; } } }

Полный текст программы, а также тестовые изображения Здесь .



Результаты программы в картинках и цифрах

Среднее время обработки (создание карты волн и наложение ее на исходное изображение) одного кадра для картинки размером 1600х1600 составило 0,137 сек и 0,415 сек с справочными таблицами и без них соответственно (на видео картинка 100х100 пикселей ).

Теги: #OpenCV #физика #Обработка изображений #Обработка изображений

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.