In this article I will explain how to quickly and simply write an image editor in C ++ using the opencv computer vision library. Implemented effects such as saturation, exposure, sharpness, contrast, and others. No magic!

Attention! Under the cut a lot of graphics and code.
So, let's begin…
')
Saturation
Ingredients:
- HSV color system,
- the function of splitting into layers «split»,
- merge layer merging function.
To change the saturation, the image is converted to an HSV color system and split into layers. A step is added to the “Sature” layer values. Layers are combined. It's simple:
Saturationvoid 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) { } }


Exposition
Ingredients:
- HSV color system,
- the function “split”, “merge”, and also the transformation function by the histogram “LUT”,
- histogram transformed by the function x + sin (x * 0.01255) * step * 10,
- protection against overflow byte values ​​of the histogram.
As with saturation, the image is converted to HSV and split into layers. For the “Value” layer, we perform the transformation using the histogram specified by the function i + sin (i * 0.01255) * step * 10. At the same time, we do not forget to protect ourselves from overflowing the byte number.
Exposition 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; }


Graph of the function x + sin (x * 0.01255) * step * 10

The function mainly affects the middle of the range.
Tint
Ingredients:
- RGB color system
- function "split", "merge" and "LUT",
- histograms transformed by the exposure function for the red, blue and green channels,
- protection against histogram overflow.
The hue parameter indicates the presence of green and purple in the image. In the RGB color system, you can control the green layer, but you must not forget to compensate for the drop in brightness of the other two layers. A positive gamma exposure function is used to transform the red and blue layers, negative for the green.
Tint 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) { } }


Colour temperature
Ingredients: the same as in shade, but histograms for red and green are positive, and for the blue layer, double negative.
The color temperature indicates the presence in the image of yellow and blue colors. So we will "turn" blue.
Colour temperature 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) { } }


Light and shade
Ingredients:
- HSV color system,
- function "split", "merge", "LUT",
- the histogram of shadows transformed by the function (0.36811145 * e) ^ (- (x ^ 1.7)) * 0.2x * step,
- histogram of lights, transformed by the function (0.36811145 * e) ^ (- (256 - x) ^ 1.7) * 0.2 (256-x) * step,
- protection against histogram overflow.
The “light” parameter characterizes the brightness of the bright areas of the image, and the “shadow” parameter defines the brightness of the dark areas. We will convert the channel brightness.
<img src = "

"alt =" image "/>
In the graph, the shadow conversion function is indicated by a red line, the light function by a green line.
Light and shade 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; }


Contrast
Ingredients:
- RGB color system
- function "split", "merge", "LUT",
- contrast level "(100 + step) / 100",
- contrast histogram obtained from the formula ((x / 255 - 0.5) * constrastLevel + 0.5) * 255.
The contrast is determined by the difference in brightness. Those. to increase the contrast we need to push the range of brightness from the center to the edges. Conversion is performed for all layers.
Contrast 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; } }

Red line - increased contrast, green - reduced.


Sharpness
Ingredients:
- blur function "blur",
- convolution matrix, with calculated coefficients,
- the transformation function by the convolution matrix “filter2D”,
- a copy of the image.
Sharpness (clarity) is determined by the selection of individual elements, their contours. The inverse of the sharpness is blurring.
In opencv, for blurring an image, we use the blur function, which takes as parameters the original image, the output image, and the size of the blur matrix. The size of the blur matrix and the strength of the blur. This size must be even so as not to manually specify the center of the matrix.
The clarity in opencv is easiest to enhance with a convolution matrix, using a special matrix for this. The “filter2D” function, which receives the original image, the resulting image, the number of bits per convolution matrix value, the convolution matrix, performs the conversion directly. So, how the method of increase / decrease of clearness will look.
Sharpness 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; } }


Total
Almost no magic. Well, the magic numbers are found empirically, so you can use your own, the most appropriate ones instead.
Link to the demo application.