Как стать автором
Обновить

Простой графический редактор с использованием OpenCV

Время на прочтение5 мин
Количество просмотров31K
В этой статье я расскажу, как достаточно быстро и просто написать редактор изображений на C++ с использованием библиотеки компьютерного зрения opencv. Реализованы такие эффекты, как насыщенность, экспозиция, резкость, контрастность и другие. Никакой магии!

image

Внимание! Под катом много графики и кода.

Итак, начнем…

Насыщенность


Ингредиенты:
— система цветности HSV,
— функция разбиения на слои «split»,
— функция объединения слоев «merge».

Для изменения насыщенности изображение преобразуется в систему цветности HSV и разбивается на слои. К значениям слоя «Sature» прибавляется шаг. Слои объединяются. Все просто:

Насыщенность
void CImageEditor::Sature(int step)
{
	try
	{
		std::vector<Mat> hsv;
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_RGB2HSV_FULL);
		cv::split(*m_imgEdit, hsv);
		hsv[1] += step * 5;
		cv::merge(hsv, *m_imgEdit);
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_HSV2RGB_FULL);
	}
	catch (Exception ex)
	{
	}
}


ДоПосле

Экспозиция


Ингредиенты:
— система цветности HSV,
— функция «split», «merge», а также функция преобразования гистограммой «LUT»,
— гистограмма преобразованная функцией x + sin(x * 0.01255) * step * 10,
— защита от переполнения байтовых значений гистограммы.
Как и в случае с насыщенностью, изображение преобразуется в HSV и разбивается на слои. Для слоя «Value» выполняем преобразование с помощью гистограммы, заданной функцией i + sin(i * 0.01255) * step * 10. При этом не забываем защититься от переполнения байтового числа.
Экспозиция
void CImageEditor::Expo(int step)
{
	try
	{
		std::vector<Mat> hsv;
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_RGB2HSV_FULL);
		Mat lut = GetGammaExpo(step);
		cv::split(*m_imgEdit, hsv);
		cv::LUT(hsv[2], lut, hsv[2]);
		cv::merge(hsv, *m_imgEdit);
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_HSV2RGB_FULL);
	}
	catch (Exception ex)
	{
	}
}
cv::Mat CImageEditor::GetGammaExpo(int step)
{
	Mat result(1, 256, CV_8UC1);

	uchar* p = result.data;
	for (int i = 0; i < 256; i++)
	{
		p[i] = AddDoubleToByte(i, std::sin(i * 0.01255) * step * 10);
	}

	return result;
}
byte CImageEditor::AddDoubleToByte(byte bt, double d)
{
	byte result = bt;
	if (double(result) + d > 255)
		result = 255;
	else if (double(result) + d < 0)
		result = 0;
	else
	{
		result += d;
	}
	return result;
}


ДоПосле
График функции x + sin(x * 0.01255) * step * 10
image
Функция в основном затрагивает середину диапазона.

Оттенок


Ингредиенты:
— система цветности RGB,
— функция «split», «merge» и «LUT»,
— гистограммы, преобразованные функцией экспозиции, для красного, синего и зеленого каналов,
— защита от переполнения значений гистограммы.

Параметр оттенка характеризует наличие в изображении зеленого и пурпурного цвета. В системе цветности RGB можно управлять зеленым слоем, но при этом нужно не забывать компенсировать падение яркости другим двумя слоями. Для преобразования красного и синего слоев используется положительная гамма-функция экспозиции, для зеленого – отрицательная.

Оттенок
void CImageEditor::Hue(int step)
{
	try
	{
		std::vector<Mat> rgb;
		Mat lut0 = GetGammaExpo(step), lut1 = GetGammaExpo(-step), lut2 = GetGammaExpo(step);
		cv::split(*m_imgEdit, rgb);
		LUT(rgb[0], lut0, rgb[0]);
		LUT(rgb[1], lut1, rgb[1]);
		LUT(rgb[2], lut2, rgb[2]);
		cv::merge(rgb, *m_imgEdit);
	}
	catch (Exception ex)
	{
	}
}


ДоПосле

Цветовая температура


Ингредиенты: те же, что и в оттенке, но гистограммы для красного и зеленого положительные, а для синего слоя двойная отрицательная.

Цветовая температура характеризует наличие в изображении желтого и синего цветов. Значит будем «крутить» синий.

Цветовая температура
void CImageEditor::Temperature(int step)
{
	try
	{
		std::vector<Mat> rgb;
		Mat lut0 = GetGammaExpo(-step*2), lut1 = GetGammaExpo(step), lut2 = GetGammaExpo(step);
		cv::split(*m_imgEdit, rgb);
		LUT(rgb[0], lut0, rgb[0]);
		LUT(rgb[1], lut1, rgb[1]);
		LUT(rgb[2], lut2, rgb[2]);
		cv::merge(rgb, *m_imgEdit);
	}
	catch (Exception ex)
	{
	}
}


ДоПосле

Свет и тени


Ингредиенты:
— система цветности HSV,
— функция «split», «merge», «LUT»,
— гистограмма теней, преобразованная функцией (0.36811145*e)^(-(x^1.7))*0.2x*step,
— гистограмма светов, преобразованная функцией (0.36811145*e)^(-(256 — x)^1.7)*0.2(256-x)*step,
— защита от переполнения значений гистограммы.

Параметр «свет» характеризует яркость светлых областей изображения, а параметр «тени» — яркость темных областей. Преобразовывать будем канал яркостей.

<img src="" alt=«image»/>

На графике функция преобразования теней обозначается красной линией, функция света – зеленой.

Свет и тени
void CImageEditor::White(int step)
{
	try
	{
		std::vector<Mat> hsv;
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_RGB2HSV_FULL);
		cv::split(*m_imgEdit, hsv);

		Mat lut = GetGammaLightShadow(step, true);
		LUT(hsv[2], lut, hsv[2]);
		cv::merge(hsv, *m_imgEdit);
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_HSV2RGB_FULL);
	}
	catch (Exception ex)
	{
		AfxMessageBox(CString(CStringA(ex.msg.begin())));
		throw;
	}
}
void CImageEditor::Shadow(int step)
{
	try
	{
		std::vector<Mat> hsv;
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_RGB2HSV_FULL);
		cv::split(*m_imgEdit, hsv);

		Mat lut = GetGammaLightShadow(step, false);
		LUT(hsv[2], lut, hsv[2]);
		cv::merge(hsv, *m_imgEdit);
		cv::cvtColor(*m_imgEdit, *m_imgEdit, cv::ColorConversionCodes::COLOR_HSV2RGB_FULL);
	}
	catch (Exception ex)
	{
		AfxMessageBox(CString(CStringA(ex.msg.begin())));
		throw;
	}
}
Mat CImageEditor::GetGammaLightShadow(int step, bool reverse)
{
	Mat result(1, 256, CV_8UC1);
	for (int i = 0; i < 256; i++)
	{
		*(result.data + i) = AddDoubleToByte(i, std::pow(0.36811145*M_E, 
			-std::pow(abs((reverse ? 256 : 0) - i), 1.7))*0.2*step*abs((reverse ? 256 : 0) - i));
	}
	return result;
}


СветТени

Контраст


Ингредиенты:
— система цветности RGB,
— функция «split», «merge», «LUT»,
— уровень контраста «(100+step)/100»,
— гистограмма контрастности, полученная из формулы ((x/255 – 0.5)*constrastLevel + 0.5)*255.

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

Контрастность
void CImageEditor::Contrast(int step)
{
	try
	{
		std::vector<Mat> rgb;
		cv::split(*m_imgEdit, rgb);
		Mat lut(1, 256, CV_8UC1);
		double contrastLevel = double(100 + step) / 100;
		uchar* p = lut.data;
		double d;
		for (int i = 0; i < 256; i++)
		{
			d = ((double(i) / 255 - 0.5)*contrastLevel + 0.5) * 255;
			if (d > 255)
				d = 255;
			if (d < 0)
				d = 0;
			p[i] = d;
		}
		LUT(rgb[0], lut, rgb[0]);
		LUT(rgb[1], lut, rgb[1]);
		LUT(rgb[2], lut, rgb[2]);
		cv::merge(rgb, *m_imgEdit);
	}
	catch (Exception ex)
	{
		AfxMessageBox(CString(CStringA(ex.msg.begin())));
		throw;
	}
}


image

Красная линия – повышенный контраст, зеленая – пониженный.

ДоПосле

Резкость


Ингредиенты:
— функция размытия «blur»,
— матрица свертки, с рассчитанными коэффициентами,
— функция преобразования матрицей свертки «filter2D»,
— копия изображения.

Резкость (четкость) определяется выделением отдельных элементов, их контуров. Величина, обратная резкости – размытость.
В opencv для размытия изображения используем функцию blur, принимающую в качестве параметров исходное изображение, выходное изображение, и размер матрицы размытия. От размера матрицы размытия и зависит сила размытия. Этот размер должен быть четным, чтобы не указывать вручную центр матрицы.

Четкость в opencv проще всего повысить с помощью матрицы свертки, используя специальную для этого матрицу. Функция «filter2D», которая принимает исходное изображение, результирующее изображение, количество бит на значение матрицы свертки, матрицу свертки, выполняет непосредственно преобразование. Итак, как будет выглядеть метод повышения/понижения четкости.

Резкость
void CImageEditor::Clarity(int step)
{
	try
	{
		if (step < 0)
		{
			cv::blur(*m_imgEdit, *m_imgEdit, cv::Size(-step * 2 + 1, -step * 2 + 1));
		}
		else
		{
			Mat dst = m_imgEdit->clone();
			float matr[9] {
				-0.0375 - 0.05*step, -0.0375 - 0.05*step, -0.0375 - 0.05*step,
				-0.0375 - 0.05*step, 1.3 + 0.4*step, -0.0375 - 0.05*step,
				-0.0375 - 0.05*step, -0.0375 - 0.05*step, -0.0375 - 0.05*step
			};
			Mat kernel_matrix = Mat(3, 3, CV_32FC1, &matr);
			cv::filter2D(*m_imgEdit, dst, 32, kernel_matrix);
			m_imgEdit = make_shared<Mat>(dst);
		}
	}
	catch (Exception ex)
	{
		AfxMessageBox(CString(CStringA(ex.msg.begin())));
		throw;
	}
}


ДоПосле

Итог


Почти никакой магии. Ну а магические числа найдены эмпирически, поэтому вместо них можно использовать свои, наиболее подходящие.
Ссылка на демонстрационное приложение.
Теги:
Хабы:
Всего голосов 25: ↑22 и ↓3+19
Комментарии24

Публикации

Истории

Работа

Программист C++
131 вакансия
QT разработчик
6 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн