В этой статье я расскажу, как достаточно быстро и просто написать редактор изображений на 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;
	}
}


ДоПосле

Итог


Почти никакой магии. Ну а магические числа найдены эмпирически, поэтому вместо них можно использовать свои, наиболее подходящие.
Ссылка на демонстрационное приложение.

Комментарии (24)


  1. Marsikus
    02.10.2015 14:49
    +12

    Можно поинтересоваться, а зачем вы на гитхаб выкладываете запакованное в RAR архив? Первый раз такое вижу :)


    1. Obramko
      02.10.2015 15:03
      -1

      Так ведь Reposit1.


    1. alex-v-93
      02.10.2015 15:11

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


    1. slaykovsky
      02.10.2015 15:25
      +6

      За rar вообще должно быть стыдно в 2015 году.


      1. alex-v-93
        02.10.2015 15:32
        +1

        Перевыложил в нормальном виде.


        1. KvanTTT
          02.10.2015 17:04

          А зачем бинари в исходниках? Для релизов есть соответствующая функциональность.


      1. pfemidi
        02.10.2015 16:35

        Не в свете github+rar, на гитхабе действительно rar не к месту, а просто поинтересоваться: если за rar вообще должно быть стыдно в 2015 году, то за что в 2015 году и в уже наступающем 2016 должно или может быть не стыдно?


        1. slaykovsky
          02.10.2015 16:45

          Over9K открытых форматов: ZIP, Tar, etc.
          Ну и да, в git класть архив, это совсем не то пальто.


        1. KvanTTT
          02.10.2015 17:08
          +3

          Есть множество других может и не очень распространенных (как и WinRar тоже), но зато бесплатных форматов (7zip). И, как выше уже отметились, открытых. Таким образом, смысл использования проприетарного архиватора WinRar и соответствующего формата теряется. А так вообще zip является стандартом де-факто.


          1. alex-v-93
            02.10.2015 17:17
            +1

            rar архив можно открыть с помощью бесплатного 7-zip.


            1. KvanTTT
              02.10.2015 18:49

              Зато нельзя создать с помощью него.


              1. alex-v-93
                02.10.2015 19:00

                В данном контексте главное его прочитать.


          1. pfemidi
            02.10.2015 17:26
            -2

            Извините, но rar (не именно WinRar, а просто rar) распространён ничуть не меньше, чем zip. Чего не скажешь про тот же 7zip, который пусть и бесплатен, но используется хорошо если одним человеком из ста. Впрочем ответ я получил, теперь знаю почему rar'ом, который жмёт лучше чем zip (но хуже, чем bzip2, правда быстрее) пользоваться стыдно. Стыжусь, но на винде продолжаю пользоваться именно им.


            1. calx
              02.10.2015 18:05
              +2

              Сам вопрос, кто жмёт лучше или быстрее, уже давно неактуален в подавляющем большинстве случаев, поэтому нет никакого смысла использовать что-то кроме стандартного zip (или tar+gzip/bzip2 если не предполагается разворачивать архив на винде).


          1. Darthman
            02.10.2015 17:39

            Вот у меня дома нечем открыть 7zip, я им не пользуюсь. Зато WinRar есть. rar и zip для архивов в сети — стандарт де факто. Вот что мне из-за одного проекта чьего-то себе 7zip ставить? Право, не смешите. А популярные все архиваторы и разархиваторы умеют rar распаковывать. Если не ошибаюсь это даже в total comander встроено.


            1. KvanTTT
              02.10.2015 18:51
              +3

              Разве его не открывает WinRar, это даже странно. А я не пользуюсь раром уже лет 5. Зачем нужен рар, когда есть зип?


  1. sborisov
    02.10.2015 15:39
    +1

    исключения срезаются.


  1. Nagdiel
    02.10.2015 16:04
    +1

    А зачем cv::Mat в умный указатель заворачивать? Там по-моему свой счетчик ссылок имеется.


    1. alex-v-93
      02.10.2015 16:20
      -1

      Для отрисовки лучше использовать указатель, т.к. передача передача объекта требует небольшого дополнительного расхода ресурсов.


  1. Shablonarium
    02.10.2015 17:58
    +1

    Какова производительность?


    1. alex-v-93
      02.10.2015 18:27

      Хороший вопрос. Изображение ~3500x3500 по всем фильтрам на одном ядре 2.7GHz обрабатывается около одной секунды. При этом я запускал 32-битный процесс на win-64, а значит тратится время на преобразование команд в 64-bit.
      Также для 32-битного процесса есть ограничение на разрешение изображения — не больше 8000x8000. Это связано в большим объемом данных, занимаемым изображением при повышении точности, которое происходит в функции filter2D.


      1. Shablonarium
        02.10.2015 18:53
        +1

        А можно примеры сравнительно с другими продуктами или программами если есть? Хочется примерно понять какой оверхэд c OpenCV, 15% или 200%.


        1. alex-v-93
          02.10.2015 19:13

          С фотошопом, гимпом и прочими редакторами нет смысла сравнивать, т.к. любой уважающий себя редактор мгновенно обрабатывает preview изображения, выводя на экран, а потом уже фоном «допиливают» оригинал.
          Когда выбирал библиотеку для редактора, думал взять GDI+, но в сравнении с openCV GDI+ проигрывала больше, чем в 10 раз, хотя там алгоритмы яркости, контрастности и пр. уже реализованы — остается только вызвать.
          При грамотном использовании скорость работы фильтров с openCV будет не меньше, чем у фотошопа.


          1. Shablonarium
            03.10.2015 16:28
            +1

            Спасибо, понятно.