一、概述

        在前面我们讲述了最基本的图像增强方法-直方图均衡化,今天我们来讲解一下新的图像增强的方法,Retinex算法,这一篇为简单的单尺度的Retinex算法,首先Retinex算法认为一副图像是由反射光与照射光强度构成的,其中发射光代表了图像的纹理、材质等主要的,而照射光强度则认为是像素所能到达的动态范围。下面即为Retinex算法的原理图,其中R(x, y)为反射分量,而S(x, y)为照射分量,我们可以简单的认为一副图像其实本质是由R(x, y)构成的,我们举一个简单的例子,为什么人能同时在不同环境下都能分辨出物体颜色(例如红色的外套),但相机却不行呢?在较暗的环境下,相机拍摄出来的图像颜色会有很大差异,这是因为相机拍摄的图像收到了关照强度的影响,而人的视觉符合颜色恒常性,所以Retinex算法也是利用颜色恒常性而创造出来的,Retinex算法的本质就是尽可能的保持仿射分量的存在,尽可能的减少关照强度对图像的影响。后面我们会具体讲解各个公式的来由。

二、手动高斯核生成

        在Retinex算法中,我们要假设出S(x, y)的像素值,所以我们需要通过高斯模糊去估计它,为什么选择高斯模糊呢?首先我们先了解一下高斯函数,首先我们可以知道高斯函数它的特点就是连续,并且无限可导,所以相对来说比较平滑,另外高斯函数是严格单调递减的函数,不会出现断点,即不产生伪影,其次它关于旋转对称,不偏向某方向。最后也是最重要的是关照图是低频信号,因为关照图的相邻像素不会突变,举一个简单的例子,我们周围的关照不会因为太阳或者灯光而图像变亮或者变暗,所以关照图是低频型号,而高斯模糊也是低频信号。所以高斯是相对来说比较优的方法。

手动首先高斯滤波代码如下:

std::vector<std::vector<double>> GerenateGaussianKernel(int kSize, double sigma)
{
	std::vector<std::vector<double>>kernel(kSize, std::vector<double>(kSize));
	int k = kSize / 2;
	double sum = 0.0;
	const double PI = 3.14159265358979323846;

	for (int i = -k; i <= k; ++i)
	{
		for (int j = -k; j <= k; ++j)
		{
			double value = static_cast<double>((1 / (2 * PI * sigma * sigma)) * exp(-(i * i + j * j) / (2 * sigma * sigma)));
			kernel[i + k][j + k] = value;
			sum += value;
		}
	}

	// 归一化
	for (int i = 0; i < kSize; ++i)
	{
		for (int j = 0; j < kSize; ++j)
		{
			kernel[i][j] /= sum;
		}
	}

	return kernel;
}

三、手动卷积生成

        高斯模糊需要用 高斯函数生成的卷积核 对图像做卷积,也就是利用周围像素按高斯权重加权平均。卷积公式如下,其中I(x, y)为图像像素,K(i, j)对应高斯卷积核的权重。

手动实现卷积代码如下:

cv::Mat ConvolutionFilter2DFunc(const cv::Mat& image, std::vector<std::vector<double>>& kernel)
{
	int kSize = kernel.size();
	int k = kSize / 2;
	cv::Mat dst = cv::Mat::zeros(image.size(), CV_64F);

	for (int y = 0; y < image.rows; ++y)
	{
		for (int x = 0; x < image.cols; ++x)
		{
			double sum = 0.0;  // 每次更新像素时重置sum
			for (int i = -k; i <= k; ++i)
			{
				for (int j = -k; j <= k; ++j)
				{
					// 选取有效像素进行加权求和
					int yy = std::min(std::max((y + i), 0), (image.rows - 1));
					int xx = std::min(std::max((x + j), 0), (image.cols - 1));
					sum += image.at<uchar>(yy, xx) * kernel[i + k][j + k];
				}
			}
			dst.at<double>(y, x) = sum;
		}
	}

	return dst;
}

四、手动Retinex算法实现

        Retinex算法原理认为图像像素由反射分量与照明分量共同组成,即I(x, y) = R(x, y) * L(x, y),我们需要求解出R(x, y),这个时候我们将源图像像素与关照图的像素都取对数化,即为log^{I(x, y)} = log^{R(x, y) * L(x, y)},由对数函数的性质我们可以知道R(x, y) = log^{I(x, y)} - log^{L(x, y)},通过以上步骤,就可以正确实现单通道的Retinex算法了,代码如下:

// 单尺度Retinex算法
cv::Mat singleScaleRetinex(const cv::Mat& image, double sigma)
{
	CV_Assert(image.channels() == 1);

	// 求高斯卷积核
	std::vector<std::vector<double>> kernel = GerenateGaussianKernel(7, sigma);

	// 对关照图做高斯模糊(卷积)
	cv::Mat L = ConvolutionFilter2DFunc(image, kernel);

	// 定义仿射图
	cv::Mat R = cv::Mat::zeros(image.size(), CV_64F);

	// 进行Retinex算法求解
	for (int i = 0; i < image.rows; ++i)
	{
		for (int j = 0; j < image.cols; ++j)
		{
			// 求的光照图的各点像素值
			double Il = L.at<double>(i, j) + 1;  // 避免出现0像素导致对数求解失败
			double I = image.at<uchar>(i, j) + 1;

			// 对这两个像素进行对数化相减
			double rPixel = static_cast<double>(std::log(I) - std::log(Il));
			R.at<double>(i, j) = rPixel;
		}
	}

	// 将仿射图归一化到0-255像素范围内
	cv::normalize(R, R, 0, 255, cv::NORM_MINMAX);

	// 转换为uchar整形输出图像
	R.convertTo(R, CV_8UC1);
	
	return R;
}

五、测试结果图

        我分别采用了灰度图进行Retinex算法均衡和转换为Lab图像,对L亮度通道进行Retinex算法均衡,效果如下:

完整代码提供如下:

#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
#include <cmath>


// 定义高斯核生成函数
std::vector<std::vector<double>> GerenateGaussianKernel(int kSize, double sigma)
{
	std::vector<std::vector<double>>kernel(kSize, std::vector<double>(kSize));
	int k = kSize / 2;
	double sum = 0.0;
	const double PI = 3.14159265358979323846;

	for (int i = -k; i <= k; ++i)
	{
		for (int j = -k; j <= k; ++j)
		{
			double value = static_cast<double>((1 / (2 * PI * sigma * sigma)) * exp(-(i * i + j * j) / (2 * sigma * sigma)));
			kernel[i + k][j + k] = value;
			sum += value;
		}
	}

	// 归一化
	for (int i = 0; i < kSize; ++i)
	{
		for (int j = 0; j < kSize; ++j)
		{
			kernel[i][j] /= sum;
		}
	}

	return kernel;
}


// 卷积操作
cv::Mat ConvolutionFilter2DFunc(const cv::Mat& image, std::vector<std::vector<double>>& kernel)
{
	int kSize = kernel.size();
	int k = kSize / 2;
	cv::Mat dst = cv::Mat::zeros(image.size(), CV_64F);

	for (int y = 0; y < image.rows; ++y)
	{
		for (int x = 0; x < image.cols; ++x)
		{
			double sum = 0.0;  // 每次更新像素时重置sum
			for (int i = -k; i <= k; ++i)
			{
				for (int j = -k; j <= k; ++j)
				{
					// 选取有效像素进行加权求和
					int yy = std::min(std::max((y + i), 0), (image.rows - 1));
					int xx = std::min(std::max((x + j), 0), (image.cols - 1));
					sum += image.at<uchar>(yy, xx) * kernel[i + k][j + k];
				}
			}
			dst.at<double>(y, x) = sum;
		}
	}

	return dst;
}

// 单尺度Retinex算法
cv::Mat singleScaleRetinex(const cv::Mat& image, double sigma)
{
	CV_Assert(image.channels() == 1);

	// 求高斯卷积核
	std::vector<std::vector<double>> kernel = GerenateGaussianKernel(7, sigma);

	// 对关照图做高斯模糊(卷积)
	cv::Mat L = ConvolutionFilter2DFunc(image, kernel);

	// 定义仿射图
	cv::Mat R = cv::Mat::zeros(image.size(), CV_64F);

	// 进行Retinex算法求解
	for (int i = 0; i < image.rows; ++i)
	{
		for (int j = 0; j < image.cols; ++j)
		{
			// 求的光照图的各点像素值
			double Il = L.at<double>(i, j) + 1;  // 避免出现0像素导致对数求解失败
			double I = image.at<uchar>(i, j) + 1;

			// 对这两个像素进行对数化相减
			double rPixel = static_cast<double>(std::log(I) - std::log(Il));
			R.at<double>(i, j) = rPixel;
		}
	}

	// 将仿射图归一化到0-255像素范围内
	cv::normalize(R, R, 0, 255, cv::NORM_MINMAX);

	// 转换为uchar整形输出图像
	R.convertTo(R, CV_8UC1);
	
	return R;
}



int main()
{
	cv::Mat srcImage = cv::imread("C:/Users/jiang/Desktop/data/0250.jpg");
	cv::Mat grayImage;
	cv::cvtColor(srcImage, grayImage, cv::COLOR_BGR2GRAY);
	cv::Mat ResultImage = singleScaleRetinex(grayImage, 11);

	// 转换为亮度通道图像进行测试
	cv::Mat LabImage;
	cv::cvtColor(srcImage, LabImage, cv::COLOR_BGR2Lab);

	// 分离通道只处理亮度通道
	std::vector<cv::Mat> LabImageSplit;
	cv::split(LabImage, LabImageSplit);
	cv::Mat LabResultImage = singleScaleRetinex(LabImageSplit[0], 11);

	// 将处理后的亮度通道进行合并
	cv::Mat dstLabRetinexImage;
	LabImageSplit[0] = LabResultImage;
	cv::merge(LabImageSplit, dstLabRetinexImage);
	cv::cvtColor(dstLabRetinexImage, dstLabRetinexImage, cv::COLOR_Lab2BGR);

	cv::imshow("GrayImage", grayImage);
	cv::imshow("ResultImage", ResultImage);
	cv::imshow("LabRetinexImage", dstLabRetinexImage);
	cv::waitKey(0);
	cv::destroyAllWindows();
	return 0;
}

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐