概述
本篇博客主要记录如何使用OpenCV对图片进行常规的处理,包括如下内容
- 对比度
- 亮度
- 色调
- 饱和度
- 色温
- 高光
- 阴影
- 锐化
- 暗角
代码示例
例子的地址
例子为CMake项目,目前只在MacOS上运行。运行项目中的Target OpenCVHelloWorld
,可查看效果
例子使用的测试原图
对比度和亮度
将图片转换到HSL空间,然后使用对比度和亮度对L分量进行重新计算,OpenCV中L分量的取值从0到255,只越大,像素的明度越高
L_new = L * 对比度 + 亮度
对应代码如下
auto hslVal = hslCopy.at<cv::Vec3b>(i,j); // 取出hsl空间的一个像素
int lval = _contractScale * hslVal[2] + _lightnessOffset; // 按照公式计算新的L,这里_contractScale可以取0以上的浮点数,_lightnessOffset取0到255
if (lval > 255) {
lval = 255;
}
if (lval < 0) {
lval = 0;
}
色调
色调则是依赖HSL空间的H分量,OpenCV中H分量的取值从0到180,通过改变H值可以改变像素的颜色
auto hslVal = hslCopy.at<cv::Vec3b>(i,j);
int hVal = hslVal[0] + _hueOffset;
while (hVal > 180) {
// 如果超过180,循环到小于180为止
hVal -= 180;
}
饱和度
饱和度则是依赖HSL空间的S分量,OpenCV中S分量的取值从0到255,通过改变S值可以改变像素的饱和度
auto hslVal = hslCopy.at<cv::Vec3b>(i,j);
int sval = hslVal[1] + _saturationOffset;
if (sval > 255) {
sval = 255;
}
if (sval < 0) {
sval = 0;
}
色温
色温通过修改RGB中分量的占比实现,温度高,则提高R,G分量,温度低,则提高B分量
auto bgrVal = finalImg.at<cv::Vec3b>(i,j);
int bVal = bgrVal[0];
int gVal = bgrVal[1];
int rVal = bgrVal[2];
// 使用乘法,保证各个分量不会同步增长,导致全屏泛红(蓝色)
if (_temperatureScale > 0) {
rVal *= 1.0 + _temperatureScale;
gVal *= (1.0 + _temperatureScale * 0.4f);
} else {
bVal *= 1.0f -_temperatureScale;
}
rVal = std::min(255, rVal);
gVal = std::min(255, gVal);
bVal = std::min(255, bVal);
finalImg.at<cv::Vec3b>(i,j) = cv::Vec3b{(unsigned char)bVal, (unsigned char)gVal, (unsigned char)rVal};
这里我让g分量的增长小于r,从而呈现出偏橙色的暖色效果
高光和阴影
为了单独改变高光和阴影,首先要确定高光和阴影的选区
cv::cvtColor(originImg, highlightMask, cv::COLOR_BGR2GRAY);
for(int i = 0; i < highlightMask.rows; i++) {
for (int j = 0; j < highlightMask.cols; j++) {
auto grayVal = highlightMask.at<unsigned char>(i, j);
if (grayVal > 150) {
highlightMask.at<unsigned char>(i, j) = 255;
} else {
float falloffFactor = highlightMask.at<unsigned char>(i, j) / 150.0f;
falloffFactor = powf(falloffFactor, 2);
highlightMask.at<unsigned char>(i, j) = (int)(falloffFactor * 255);
}
}
}
cv::cvtColor(originImg, shadowMask, cv::COLOR_BGR2GRAY);
for(int i = 0; i < shadowMask.rows; i++) {
for (int j = 0; j < shadowMask.cols; j++) {
auto grayVal = shadowMask.at<unsigned char>(i, j);
if (grayVal < 50) {
shadowMask.at<unsigned char>(i, j) = 255;
} else {
float falloffFactor = (255 - shadowMask.at<unsigned char>(i, j)) / (255.0 - 50.0);
falloffFactor = powf(falloffFactor, 2);
shadowMask.at<unsigned char>(i, j) = (int)(falloffFactor * 255);
}
}
}
通过上面的代码得到高光和阴影的2个mask,也就是2张表示高光和阴影区域的灰度图。为了边缘过渡平滑,不在区间内的按照梯度衰减,而不是都设置为0 接下来使用mask调整L值即可,配合对比度和亮度调整,整体如下
unsigned char highlightFactor = highlightMask.at<unsigned char>(i, j);
unsigned char shadowFactor = shadowMask.at<unsigned char>(i, j);
int lval = _contractScale * hslVal[2] + _lightnessOffset + highlightFactor / 255.0 * _highlightOffset + shadowFactor / 255.0 * _shadowOffset;
highlightFactor
和shadowFactor
控制是否使用对应的增益,_highlightOffset
和_shadowOffset
控制增益的强度,可以取正值和负值
锐化
将原图和高斯模糊后的图进行相减,即可得到锐化的效果
// 得到高斯模糊后的图
cv::GaussianBlur(originImg, blurMask, {0, 0}, 5);
// 使用addWeighted进行相减
cv::addWeighted(finalImg, 1.0 + _sharpenOffset, blurMask, -_sharpenOffset, 0, finalImg);
_sharpenOffset
可以取0到0.9的值,值越大,效果与明显。
暗角
暗角的原理是,根据像素点离中心的距离,减弱像素的明度,从而达到四个角变暗的效果
float distanceToCenter = sqrt(pow(i - middleRow, 2) + pow(j - middleCol, 2));
// 进行重映射,保证像素从距离50%之后才开始变暗,并且通过_cornerOffset来控制暗角的比例,为0则无暗角
float cornerFactor = 1.0 - std::max(distanceToCenter / radius * 2.0 - 1.0, 0.0) * _cornerOffset;
// lval就是明度L值
lval *= cornerFactor;
自然饱和度
自然饱和度相对于饱和度,会优先提升饱和度低的像素,具体算法参考了开源软件github.com/tannerhella…,代码如下,adjustment
取值从-1到1
cv::Vec3b CVPixelOpUtil::calcNatureSaturation(cv::Vec3b rgbColor, float adjustment) {
cv::Vec3b finalColor = rgbColor;
float realAdj = -adjustment; // -1 ~ 1
int avg = (rgbColor[0] + rgbColor[1] * 2 + rgbColor[2]) >> 2;
int max = std::max({(int)rgbColor[0], (int)rgbColor[1] ,(int)rgbColor[2]});
float finalAdj = (max - avg) / 127.0 * realAdj;
if (finalColor[0] != max) finalColor[0] = cv::saturate_cast<unsigned char>(finalColor[0] + (max - finalColor[0]) * finalAdj);
if (finalColor[1] != max) finalColor[1] = cv::saturate_cast<unsigned char>(finalColor[1] + (max - finalColor[1]) * finalAdj);
if (finalColor[2] != max) finalColor[2] = cv::saturate_cast<unsigned char>(finalColor[2] + (max - finalColor[2]) * finalAdj);
return finalColor;
}
对于人像,可以明显看出自然饱和度的优势