图像处理之Retinex算法(C++)
文章目录
前言
Retinex 理论的基本思想就是光照强度决定了原始图像中所有像素点的动态范围大小,而原始图像的固有属性则是由物体自身的反射系数决定,即假设反射图像和光照图像相乘为原始图像。所以 Retinex 的思路即是去除光照的影响,保留住物体的固有属性。
如图所示,假设观察者处成像的图像为I(x,y),可以通过下式表达:
参数解释:
I(x,y)–源图像;
L(x,y)–表示光照分量;
R(x,y)–表示物体本身固有性质的反射分量。
对公式两端取对数,公式为:
上式即为Retinex理论处理的基本过程。
Retinex算法处理的基本过程如下:
流程图解读:图像I(x,y)经过Log变换转换成i(x,y),然后对图像I(x,y)进行亮度图像估计,在对估计的亮度分量进行Log变换得到l(x,y),二者相减得到物体反射分量的Log变换r(x,y),再进行Log变换的反变换Exp,即得到R(x,y)。此处亮度图像估计即通过高斯滤波或者引导滤波对图像滤波。
参考资料:Retinex理论
一、单尺度Retinex(SSR)
1.原理
单尺度 Retinex 算法的处理过程非常拟合人眼的视觉成像过程,该算法的基本思路是:先构建高斯环绕函数,然后利用高斯环绕函数分别对图像的三个色彩通道 (R 、G 和 B) 进行滤波,则滤波后的图像就是我们所估计的光照分量,接着再在对数域中对原始图像和光照分量进行相减得到反射分量作为输出结果图像。该算法能压缩图像的动态范围、一定程度上的保持图像的颜色和细节的增强。改进:将高斯环绕函数对三个色彩通道滤波,改为使用引导滤波对三个彩色通道滤波。
参数解释:
Ii(x,y)–原始图像;
Li(x,y)–表示光照分量;
Ri(x,y)–表示物体本身固有性质的反射分量;
G(x,y)–高斯环绕函数;
Ii(x,y)G(x,y)–对原始图像使用高斯核进行卷积运算。
SSR算法中标准差σ一般取80-100。
2.代码实现
#include <iostream>
#include <opencv.hpp>
using namespace std;
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
cv::Mat single_scale_retinex(const cv::Mat& src,double sigma)
{
cv::Mat gaussImg, log_src(src.size(),CV_32F), log_gaussImg(src.size(), CV_32F);
cv::Mat difference(src.size(), CV_32F);
cv::Mat dst(src.size(), CV_32F);
// 对输入图像进行高斯模糊处理
cv::GaussianBlur(src, gaussImg, cv::Size(0,0), sigma);
// 对模糊后的图像和原图像分别进行对数运算,得到两个对数图像
// 差分操作前确保两个矩阵类型和尺寸相同
// 对差分后的图像进行指数运算
gaussImg.convertTo(gaussImg, CV_32F);
for(int i=0;i<gaussImg.rows;i++)
for (int j = 0; j < gaussImg.cols; j++)
{
log_gaussImg.at<float>(i, j) = log(gaussImg.at<float>(i, j) + 1);
log_src.at<float>(i, j) = log(src.at<uchar>(i, j) + 1);
difference.at<float>(i, j) = log_src.at<float>(i, j) - log_gaussImg.at<float>(i, j);
//dst.at<float>(i, j) = exp(difference.at<float>(i, j));
}
// 归一化到0-255范围内(疑问:此处对差分图像归一化反而图像显得正常,参考https://blog.csdn.net/TmacDu/article/details/103499795博主也是这样实现)
cv::normalize(difference, dst, 0, 255, cv::NORM_MINMAX, CV_8U);
return dst;
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
void retinex_process(const cv::Mat& src, cv::Mat& dst, double sigma)
{
std::vector<cv::Mat> channels;
std::vector<cv::Mat> channels_dst;
cv::split(src, channels);
for (int i = 0; i < channels.size(); i++)
{
channels_dst.push_back(single_scale_retinex(channels[i], sigma));
}
cv::merge(channels_dst, dst);
}
int main()
{
// 读取图片
string filepath = "F://work_study//algorithm_demo//retinex_test1.jpg";
cv::Mat src = cv::imread(filepath);
if (src.empty())
{
return -1;
}
cv::Mat dst(src.size(),CV_8UC3,cv::Scalar(0,0,0));
//执行单尺度的Retinex算法
retinex_process(src, dst,80);
cv::imshow("dst.jpg", dst);
cv::waitKey(0);
return 0;
}
3.结果展示
二、多尺度Retinex(MSR)
1.原理
SSR算法在动态范围压缩和色调恢复的两种效果中,只能以牺牲一种功能为代价来改进另一个,因此Jobson等一批研究者们针对单尺度Retinex模型中存在的不足,提出了将不同尺度下的增强结果线性地组合在一起,充分将局部信息和整体信息考虑进去的多尺度Retinex算法。这种算法的主要思想就是结合几种不同的尺度的中心围绕函数通过加权平均以后来估计光照分量。MSR算法可以产生同时拥有良好动态范围压缩、色彩稳定性以及良好色调恢复的单一输出图像。MSR算法的公式为:
参数解释:
N–表示尺度的个数,即高斯滤波的标准差的个数,一般将N的取值为3,用三个不同尺度的高斯滤波器对原始图像进行滤波,尺度的比例建议为15:80:250。
wk–经过实验验证,发现w1=w2=w3=1/3时,适用于大量的低照度图像,运算简单。
Fk(x,y)–在第k个尺度上的高斯滤波函数。
参考资料:MSR资料
2.代码实现
#include <iostream>
#include <opencv.hpp>
using namespace std;
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
cv::Mat single_scale_retinex(const cv::Mat& src,double sigma)
{
cv::Mat gaussImg, log_src(src.size(),CV_32F), log_gaussImg(src.size(), CV_32F);
cv::Mat difference(src.size(), CV_32F);
cv::Mat dst(src.size(), CV_32F);
// 对输入图像进行高斯模糊处理
cv::GaussianBlur(src, gaussImg, cv::Size(0,0), sigma);
// 对模糊后的图像和原图像分别进行对数运算,得到两个对数图像
// 差分操作前确保两个矩阵类型和尺寸相同
// 对差分后的图像进行指数运算
gaussImg.convertTo(gaussImg, CV_32F);
for(int i=0;i<gaussImg.rows;i++)
for (int j = 0; j < gaussImg.cols; j++)
{
log_gaussImg.at<float>(i, j) = log(gaussImg.at<float>(i, j) + 1);
log_src.at<float>(i, j) = log(src.at<uchar>(i, j) + 1);
difference.at<float>(i, j) = log_src.at<float>(i, j) - log_gaussImg.at<float>(i, j);
//dst.at<float>(i, j) = exp(difference.at<float>(i, j));
}
// 归一化到0-255范围内(疑问:此处对差分图像归一化反而图像显得正常,参考https://blog.csdn.net/TmacDu/article/details/103499795博主也是这样实现)
cv::normalize(difference, dst, 0, 255, cv::NORM_MINMAX, CV_8U);
return dst;
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
void retinex_process(const cv::Mat& src, cv::Mat& dst, double sigma)
{
std::vector<cv::Mat> channels;
std::vector<cv::Mat> channels_dst;
cv::split(src, channels);
for (int i = 0; i < channels.size(); i++)
{
channels_dst.push_back(single_scale_retinex(channels[i], sigma));
}
cv::merge(channels_dst, dst);
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param std::vector<double> sigma 高斯核的标准差
* @param std::vector<double> w 权重系数
* @breif 多尺度Retinex图像处理
*/
void multi_retinex_process(const cv::Mat& src, cv::Mat& dst, std::vector<double>& sigma, std::vector<double>& w)
{
std::vector<cv::Mat> temp(sigma.size());
for (int i = 0; i < sigma.size(); i++)
{
retinex_process(src,temp[i],sigma[i]);
}
for (int i = 0; i < w.size(); i++)
{
dst += w[i] * temp[i];
}
}
int main()
{
// 读取图片
string filepath = "F://work_study//algorithm_demo//retinex_test3.jpg";
cv::Mat src = cv::imread(filepath);
if (src.empty())
{
return -1;
}
cv::Mat dst(src.size(),CV_8UC3,cv::Scalar(0,0,0));
//高斯滤波器的尺度
std::vector<double> sigma={15,80,250};
//各个尺度下的SSR的权重系数
std::vector<double> w(3, 1 / 3.0);
//执行多尺度的Retinex算法
multi_retinex_process(src, dst,sigma,w);
cv::imwrite("dst.jpg", dst);
cv::waitKey(0);
return 0;
}
3.结果展示
三、带色彩恢复的多尺度Retinex(MSRCR)
1.原理
在上图中,我们可以看出无论是单尺度还是多尺度Retinex图像增强后都会发生图像色彩失真,原因是R、G、B三通道的像素值比例发生改变,针对这个现象,Jobson和Rahman等人又一次提出带颜色恢复的多尺度 Retinex(MSRCR)。该算法可以将 MSR 得到的结果按照一定的比例进行调整以求恢复原来的比例数值,具体是通过引入了颜色恢复因子 C ,其公式如下:
参数解释:
β–增益常数,经验参数为46。
α–调节因子,经验参数为125。
Ci(x,y)–第i个彩色通道的色彩恢复函数(CRF),用来调节三个通道颜色在图像中所占的比例。
MSRCR可以提供必要的颜色恢复,从而把相对较暗区域而无法观察到的信息图像细节展现出来,消除了MSR输出中明显的颜色失真和灰色区域。可以为大多数图像提供良好的效果。
参数解释:
t–偏移量系数;
G–增益系数;
2.代码实现
#include <iostream>
#include <opencv.hpp>
using namespace std;
using namespace cv;
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
cv::Mat single_scale_retinex(const cv::Mat& src, double sigma)
{
cv::Mat gaussImg, log_src(src.size(), CV_32F), log_gaussImg(src.size(), CV_32F);
cv::Mat difference(src.size(), CV_32F);
cv::Mat dst(src.size(), CV_32F);
// 对输入图像进行高斯模糊处理
cv::GaussianBlur(src, gaussImg, cv::Size(0, 0), sigma);
// 对模糊后的图像和原图像分别进行对数运算,得到两个对数图像
// 差分操作前确保两个矩阵类型和尺寸相同
// 对差分后的图像进行指数运算
gaussImg.convertTo(gaussImg, CV_32F);
for (int i = 0; i < gaussImg.rows; i++)
for (int j = 0; j < gaussImg.cols; j++)
{
log_gaussImg.at<float>(i, j) = log(gaussImg.at<float>(i, j) + 1);
log_src.at<float>(i, j) = log(src.at<uchar>(i, j) + 1);
difference.at<float>(i, j) = log_src.at<float>(i, j) - log_gaussImg.at<float>(i, j);
//dst.at<float>(i, j) = exp(difference.at<float>(i, j));
}
// 归一化到0-255范围内(疑问:此处对差分图像归一化反而图像显得正常,参考https://blog.csdn.net/TmacDu/article/details/103499795博主也是这样实现)
// 计算带颜色恢复的归一化放到最后,此处注释
//cv::normalize(difference, dst, 0, 255, cv::NORM_MINMAX, CV_8U);
return difference;
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double sigma 高斯核的标准差
* @breif 单尺度Retinex图像处理
*/
void retinex_process(const cv::Mat& src, cv::Mat& dst, double sigma)
{
std::vector<cv::Mat> channels;
std::vector<cv::Mat> channels_dst;
cv::split(src, channels);
for (int i = 0; i < channels.size(); i++)
{
channels_dst.push_back(single_scale_retinex(channels[i], sigma));
}
cv::merge(channels_dst, dst);
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param std::vector<double> sigma 高斯核的标准差
* @param std::vector<double> w 权重系数
* @breif 多尺度Retinex图像处理
*/
void multi_retinex_process(const cv::Mat& src, cv::Mat& dst, std::vector<double>& sigma, std::vector<double>& w)
{
std::vector<cv::Mat> temp(sigma.size());
for (int i = 0; i < sigma.size(); i++)
{
retinex_process(src, temp[i], sigma[i]);
}
for (int i = 0; i < w.size(); i++)
{
dst += w[i] * temp[i];
}
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param double a 调节因子
* @param double b 增益常数
* @breif 计算颜色恢复因子
*/
void color_restoration(const cv::Mat& src, cv::Mat& dst, double a, double b)
{
dst = cv::Mat(src.size(), CV_32FC3);
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
int sum = src.at<cv::Vec3b>(i, j)[0] + src.at<cv::Vec3b>(i, j)[1] + src.at<cv::Vec3b>(i, j)[2];
dst.at<cv::Vec3f>(i, j)[0] = b * (log(a * src.at<cv::Vec3b>(i, j)[0] + 1.0) - log(sum + 1.0));
dst.at<cv::Vec3f>(i, j)[1] = b * (log(a * src.at<cv::Vec3b>(i, j)[1] + 1.0) - log(sum + 1.0));
dst.at<cv::Vec3f>(i, j)[2] = b * (log(a * src.at<cv::Vec3b>(i, j)[2] + 1.0) - log(sum + 1.0));
}
}
/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param std::vector<double> sigma 高斯核的标准差
* @param std::vector<double> w 权重系数
* @param double G 增益系数
* @param double t 偏移量系数
* @param double a 调节因子
* @param double b 增益常数
* @breif 带颜色恢复的多尺度Retinex图像处理
*/
void multi_retinex_color_restoration_process(const cv::Mat& src, cv::Mat& dst, std::vector<double>& sigma, std::vector<double>& w, double G, double t, double a, double b)
{
dst.convertTo(dst, CV_32FC3);
//计算多尺度Retinex图像
cv::Mat multiRSImg(src.size(), CV_32FC3, cv::Scalar(0, 0, 0));
multi_retinex_process(src, multiRSImg, sigma, w);
//计算颜色恢复因子
cv::Mat colorResImg(src.size(), CV_32FC3, cv::Scalar(0, 0, 0));
color_restoration(src, colorResImg, a, b);
//进行带颜色恢复的多尺度Retinex计算
//关键点:saturate_cast<uchar>相当于对图像色彩做了保护
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
dst.at<cv::Vec3f>(i, j)[0] = saturate_cast<uchar>(G * ((multiRSImg.at<cv::Vec3f>(i, j)[0] * colorResImg.at<cv::Vec3f>(i, j)[0]) + t));
dst.at<cv::Vec3f>(i, j)[1] = saturate_cast<uchar>(G * ((multiRSImg.at<cv::Vec3f>(i, j)[1] * colorResImg.at<cv::Vec3f>(i, j)[1]) + t));
dst.at<cv::Vec3f>(i, j)[2] = saturate_cast<uchar>(G * ((multiRSImg.at<cv::Vec3f>(i, j)[2] * colorResImg.at<cv::Vec3f>(i, j)[2]) + t));
}
cv::normalize(dst, dst, 0, 255, cv::NORM_MINMAX, CV_8UC3);
}
int main()
{
// 读取图片
string filepath = "F://work_study//algorithm_demo//retinex_test4.jpg";
cv::Mat src = cv::imread(filepath);
if (src.empty())
{
return -1;
}
cv::Mat dst(src.size(), CV_8UC3, cv::Scalar(0, 0, 0));
//高斯滤波器的尺度
std::vector<double> sigma = { 15,80,250 };
//各个尺度下的SSR的权重系数
std::vector<double> w(3, 1 / 3.0);
multi_retinex_color_restoration_process(src, dst, sigma, w, 5, 25, 125, 46);
cv::imwrite("dst.jpg", dst);
cv::waitKey(0);
return 0;
}
3.结果展示
参数比较多,不好调,大家可以阅读一位大佬写的文章,只需要调一个参数,文章链接:MSRCR
本文调节的参数在代码中,尝试很久,效果跟大佬文章效果类似。
总结
本文介绍了单尺度、多尺度、带颜色恢复的Retinex的算法原理和实现,使用C++和Opencv一步一步进行实现,代码结构简单清晰,便于阅读,但是效率有待提高,后续对代码进行重构,欢迎大家阅读和讨论代码中的不足。
本文代码均已在本地运行正确,有问题欢迎交流。