Предлагаю читателям Хабрахабра статью о том, как школьная физика и OpenCV позволяют создать иллюзию волн на воде.
Основная сложность — подобрать красивое уравнение и перенести эффект преломления света на границе сред трехмерного мира на плоскую картинку.
Решение проблемы можно разделить на два этапа:
- Создать карту распределения кругов/волн на воде;
- Объедините полученную карту с заданным изображением.
Карта волн
Я думаю, все помнят из школьного курса физики, что круги на воде – это не что иное, как распространение колебаний по поверхности воды.Строгий подход к решению задачи предполагает решение дифференциального волнового уравнения в двумерном пространстве, однако полной физической достоверности в нашем случае не требуется – можно взять готовое решение из школьного учебника физики или придумать собственное.
уравнение.
где с – коэффициент затухания; L – длина волны; Икс 0 , да 0 — координаты центра волны; т- время.
Длину волны лучше менять в зависимости от размера изображения.
Мы помним из школьного курса, что косинус принимает значения [-1;1], но интенсивность цвета равна [0;255], поэтому необходимо немного видоизменить нашу волновую функцию.
Наконец, яркость каждого пикселя волнового слоя задается следующим образом:
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% физическую точность не претендуем ).
На рисунке показано, что происходит с лучом света, падающим на наклонную поверхность; нужно найти, насколько сместился исходящий луч относительно входящего.
Расстояние CD — это желаемое смещение, определяемое как ( решение здесь ):
где н 1 =1 и п 2 =1,33 – показатели преломления первой (воздуха) и второй (воды) сред соответственно.
Применительно к нашим волнам и пикселям уравнение можно записать так:
В данном случае альфа — это угол наклона касательной к волне в точке [i,j], который рассчитывается как:
Конечно, производную можно вычислить и аналитически, что повысит точность, но это сделает процесс изменения волнового уравнения более трудоемким.
В этом случае опять же мы можем либо рассчитать смещение для каждой точки изображения отдельно (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 #физика #Обработка изображений #Обработка изображений
-
Мониторинг Нагрузки На Веб-Сайт И Сеть
19 Oct, 24 -
Как Читать Отчеты Ит-Компаний
19 Oct, 24 -
Сервлеты И Отражение
19 Oct, 24