图像去雾之何凯明暗通道先验去雾算法原理及c++代码实现

    在图像去雾这个领域,几乎没有人不知道《Single Image Haze Removal Using Dark Channel Prior》这篇文章,该文是2009年CVPR最佳论文。作者

何凯明博士,2007年清华大学毕业,2011年香港中文大学博士毕业,可谓是功力深厚,感叹于国内一些所谓博士的水平,何这样的博士才可以真正叫做

Doctor。

     关于何博士的一些资料和论文,大家可以访问这里:http://research.microsoft.com/en-us/um/people/kahe/

    

     本文主要上是对《Single Image Haze Removal Using Dark Channel Prior》的翻译、整理、及部分解释、代码实现。如果您的英文水平好,建议看原

文可能来的更爽些。

    一、论文思想的简单描述 

     首先看看暗通道先验是什么:

       在绝大多数非天空的局部区域里,某一些像素总会有至少一个颜色通道具有很低的值。换言之,该区域光强度的最小值是个很小的数。

  我们给暗通道一个数学定义,对于任意的输入图像J,其暗通道可以用下式表达:

                                    

      式中Jc表示彩色图像的每个通道 ,Ω(x)表示以像素X为中心的一个窗口。 

    式(5)的意义用代码表达也很简单,首先求出每个像素RGB分量中的最小值,存入一副和原始图像大小相同的灰度图中,然后再对这幅灰度图进行最小值

滤波,滤波的半径由窗口大小决定,一般有WindowSize = 2 * Radius + 1;          

      暗通道先验的理论指出:

                                                                       

     实际生活中造成暗原色中低通道值主要有三个因素:a)汽车、建筑物和城市中玻璃窗户的阴影,或者是树叶、树与岩石等自然景观的投影;b)色彩鲜艳的物

体或表面,在RGB的三个通道中有些通道的值很低(比如绿色的草地/树/植物,红色或黄色的花朵/叶子,或者蓝色的水面);c)颜色较暗的物体或者表面,

例如灰暗色的树干和石头。总之,自然景物中到处都是阴影或者彩色,这些景物的图像的暗原色总是很灰暗的。

     在作者的论文中,统计了5000多副图像的特征,也都基本符合这个先验,因此,我们可以认为其实一条定理。

      有了这个先验,接着就需要进行一些数学方面的推导来最终解决问题。

  首先,在计算机视觉和计算机图形中,下述方程所描述的雾图形成模型被广泛使用:

                                                   

   其中,I(X)就是我们现在已经有的图像(待去雾的图像),J(x)是我们要恢复的无雾的图像,A是全球大气光成分, t(x)为透射率。现在的已知条件就是

I(X),要求目标值J(x),显然,这是个有无数解的方程,因此,就需要一些先验了。

  将式(1)稍作处理,变形为下式:

                                                    

    如上所述,上标C表示R/G/B三个通道的意思。

    首先假设在每一个窗口内透射率t(x)为常数,定义他为,并且A值已经给定,然后对式(7)两边求两次最小值运算,得到下式:

                                  

    上式中,J是待求的无雾的图像,根据前述的暗原色先验理论有:

                                               

     因此,可推导出:

                                                         

    把式(10)带入式(8)中,得到:

                                                 

    这就是透射率的预估值。

    在现实生活中,即使是晴天白云,空气中也存在着一些颗粒,因此,看远处的物体还是能感觉到雾的影响,另外,雾的存在让人类感到景深的存在,因此,有

必要在去雾的时候保留一定程度的雾,这可以通过在式(11)中引入一个在[0,1] 之间的因子,则式(11)修正为:

                                               

     本文中所有的测试结果依赖于:  ω=0.95。

     上述推论中都是假设全球达气光A值时已知的,在实际中,我们可以借助于暗通道图来从有雾图像中获取该值。具体步骤如下:

      1) 从暗通道图中按照亮度的大小取前0.1%的像素。

          2) 在这些位置中,在原始有雾图像I中寻找对应的具有最高亮度的点的值,作为A值。

     到这一步,我们就可以进行无雾图像的恢复了。由式(1)可知:  J = ( I - A)/t + A  

     现在I,A,t都已经求得了,因此,完全可以进行J的计算。

     当投射图t 的值很小时,会导致J的值偏大,从而使淂图像整体向白场过度,因此一般可设置一阈值T0,当t值小于T0时,令t=T0,本文中所有效果图均以

T0=0.1为标准计算。

     因此,最终的恢复公式如下:

                                

     当直接用上述理论进行恢复时,去雾的效果其实也是很明显的,比如下面一些例子:

   

       

                 有雾图                                             去雾图

      注意到第一幅图的原图两个字的周围明显有一块不协调的地方,而第二图顶部水平方向似乎有一块没有进行去雾处理,这些都是由于我们的透射率图过于粗

糙了。

      要获得更为精细的透射率图,何博士在文章中提出了了soft matting方法,能得到非常细腻的结果。但是他的一个致命的弱点就是速度特慢,不使用于实际

使用。在2011年,何博士又除了一片论文,其中提到了导向滤波的方式来获得较好的透射率图。该方法的主要过程集中于简单的方框模糊,而方框模糊有多重和

半径无关的快速算法。因此,算法的实用性特强,关于这个导向滤波算法大家在何博士的网站可以自己去研习下,除了在去雾方面外,还有着其他多方面的应

用,这部分本文不多述。

     使用了导向滤波后的去雾效果:

     

  


  二、各参数对去雾结果的影响

  第一:窗口的大小。这个对结果来说是个关键的参数,窗口越大,其包含暗通道的概率越大,暗通道也就越黑。我们不去从理论角度分析,从实践的效果来

看,似乎窗口越大,去雾的效果越不明显,如下图所示:

        

                              (a) 原始图像                            (b) 窗口大小=11

      

              (c) 窗口大小=21                         (d) 窗口大小=101 

  我的建议是窗口大小在11-51之间,即半径在5-25之间。

     式(12)中的ω具有着明显的意义,其值越小,去雾效果越不明显,举例如下:

      

                                    (a) 原始图像                                   (b)    ω=0.5         

          

                                       (c)    ω=0.8                                                                       (d)    ω=1 

    三:编码的步骤

  如果你仔细的分析了原文的细路,加上适当的参考,编码其实并不是很困难。

  1)根据原始图像求暗通道。

      2)按文中所描述的算法自动获得全球大气光的值。

     这里说明一点,原始论文中的A最终是取原始像素中的某一个点的像素,我实际上是取的符合条件的所有点的平均值作为A的值,我这样做是因为,如果是取

一个点,则各通道的A值很有可能全部很接近255,这样的话会造成处理后的图像偏色和出现大量色斑。原文作者说这个算法对天空部分不需特备处理,我实际发

现该算法对有天空的图像的效果一般都不好。天空会出现明显的过渡区域。作为解决方案,我增加了一个参数,最大全球大气光值,当计算的值大于该值时,则

就取该值。  

           

                        原图                                                         未对A值做限定                  最大A值限定为220

       3) 按式(12)计算预估的透射率图。

  在式(12)中,每个通道的数据都需要除以对应的A值,即归一化,这样做,还存在一个问题,由于A的选取过程,并不能保证每个像素分量值除以A值后都

小于1,从而导致t的值可能小于0,而这是不容许的,原文作者并没有交代这一点是如何处理的。我在实际的编码中发现,如果真的这样做了,其效果也并不是很

理想 ,因此,我最后的办法是在式(12)中,不考虑A的计算。

        4)计算导向滤波图。

   这里可以直接用原始的图像做导向图,当然也可以用其灰度图,但是用RGB导向图在下一步的计算中会占用比较大的时间。

        5)按照《Guided Image Filtering》论文中的公式(5)、(6)、(8)编码计算获得精细的透射率图。c++代码见上一篇文章。

还有一点就是,上述计算需要在[0,1]范围内进行,也就是说导向图和预估的透射率图都必须从[0,255]先映射到[0,1]在进行计算。

      关于guidedfilter中的半径r值,因为在前面进行最小值后暗通道的图像成一块一块的,为了使透射率图更加精细,建议这个r的取值不小于进行最小值滤波的

半径的4倍,如下图所示:

    

          (a)  r=最小值滤波半径的2倍                        (b) r=最小值滤波半径的8倍

      可以看到,当r比较小的时候,在透射率图中基本看不到什么细节信息,因此恢复处的图像边缘处不明显。

      参数eps的取值也有所讲究,他主要是为了防止计算中除以0的错误以及为了使得某些计算结果不至于过大,一般建议取值0.001或者更小。

      如果使用的彩色RGB图做导向图,计算时间上会增加不少,所的到的透射率图的边缘会比灰度图所处理的保留了更多的细节,效果上略微比灰度图好。

       以RGB图为导向图的计算中,涉及到3*3部分矩阵求逆的过程,如果用非matlab语言写,可以先借助于matlab的符号计算功能,以及其中的符号计算命令

simple,把计算结果算出来,然后再再其他高级语言中实现。

       (6) 按式(22)进行无雾图像的恢复。

mfc头文件:

#pragma once
#include "afxwin.h"
#include <cv.h>
#include "cxcore.h"  
#include "math.h" 
#include <highgui.h>
#include<vector>
#include <iostream>  
#include "opencv2/core/core.hpp"    
#include "opencv2/highgui/highgui.hpp"    
#include "opencv2/imgproc/imgproc.hpp"    


using namespace std;
using namespace cv;

class Ctry :
	public CCmdTarget
{
public:
	Ctry();
	virtual ~Ctry();
	DECLARE_MESSAGE_MAP()
	afx_msg void OnTryTyr1();
	afx_msg void OnTryPath();

public:
	Mat getimage(Mat &a);
	Mat guidedFilter2(cv::Mat I, cv::Mat p, int r, double eps);
	IplImage* getDarkChannel(IplImage* &src);
	IplImage* getMinIcy(IplImage* dark,int w);
	double getA(IplImage* dark, IplImage*hazeImage);
	IplImage* getTransmission(IplImage* Icy, double Ac);

	IplImage* getDehazedImage(IplImage* hazeImage, IplImage* guidedt,double Ac);

public:




};

mfc源文件:

#include "stdafx.h"
#include "Ctry.h"
#include "Resource.h"
#include<cv.h>

#define PI 3.14159

Ctry::Ctry()
{
}

Ctry::~Ctry()
{
}

BEGIN_MESSAGE_MAP(Ctry, CCmdTarget)
	ON_COMMAND(ID_TRY_TYR1, &Ctry::OnTryTyr1)
	ON_COMMAND(ID_TRY_PATH, &Ctry::OnTryPath)
END_MESSAGE_MAP()


void Ctry::OnTryTyr1()
{
	// TODO:  在此添加命令处理程序代码

	
}

void Ctry::OnTryPath()
{
	// TODO:  在此添加命令处理程序代码    
	IplImage* img = cvLoadImage("C:\\Users\\徐图之\\Desktop\\1.jpg");


	IplImage* g = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
	g=getDarkChannel(img);
	double A = getA(g, img);   //大气光强A

	IplImage* Icy = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
	Icy = getMinIcy(g, 5);

	//投射图t
	IplImage* t = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
	t = getTransmission(Icy, A);

	//获得guide image
	Mat mt = cvarrToMat(t, true);
	Mat image_src = cvarrToMat(img, true);
	Mat image_gray(image_src.size(), CV_8UC1);
	cvtColor(image_src, image_gray, CV_BGR2GRAY);
	Mat guide = getimage(image_gray);
	int r = 8;
	double eps = 0.04;
	Mat q = guidedFilter2(guide, mt, r, eps);
	IplImage* guidedt = cvCloneImage(&(IplImage)q);


	IplImage* dehazedImage = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 3);
	dehazedImage = getDehazedImage(img, guidedt, A);


	cvSaveImage("C:\\Users\\徐图之\\Desktop\\dark .jpg", g);
	cvSaveImage("C:\\Users\\徐图之\\Desktop\\guidedt .jpg", guidedt);
	cvSaveImage("C:\\Users\\徐图之\\Desktop\\t.jpg", t);
	cvSaveImage("C:\\Users\\徐图之\\Desktop\\dehazedImage84.jpg", dehazedImage);
	
}


//convert image depth to CV_64F  
Mat  Ctry::getimage(Mat &a)
{
	int hei = a.rows;
	int wid = a.cols;
	Mat I(hei, wid, CV_64FC1);
	//convert image depth to CV_64F  
	a.convertTo(I, CV_64FC1, 1.0 / 255.0);
	return I;
}



Mat  Ctry::guidedFilter2(cv::Mat I, cv::Mat p, int r, double eps)
{
	/*
	% GUIDEDFILTER   O(1) time implementation of guided filter.
	%
	%   - guidance image: I (should be a gray-scale/single channel image)
	%   - filtering input image: p (should be a gray-scale/single channel image)
	%   - local window radius: r
	%   - regularization parameter: eps
	*/

	cv::Mat _I;
	I.convertTo(_I, CV_64FC1);
	I = _I;

	cv::Mat _p;
	p.convertTo(_p, CV_64FC1);
	p = _p;

	//[hei, wid] = size(I);  
	int hei = I.rows;
	int wid = I.cols;

	//N = boxfilter(ones(hei, wid), r); % the size of each local patch; N=(2r+1)^2 except for boundary pixels.  
	cv::Mat N;
	cv::boxFilter(cv::Mat::ones(hei, wid, I.type()), N, CV_64FC1, cv::Size(r, r));

	//mean_I = boxfilter(I, r) ./ N;  
	cv::Mat mean_I;
	cv::boxFilter(I, mean_I, CV_64FC1, cv::Size(r, r));

	//mean_p = boxfilter(p, r) ./ N;  
	cv::Mat mean_p;
	cv::boxFilter(p, mean_p, CV_64FC1, cv::Size(r, r));

	//mean_Ip = boxfilter(I.*p, r) ./ N;  
	cv::Mat mean_Ip;
	cv::boxFilter(I.mul(p), mean_Ip, CV_64FC1, cv::Size(r, r));

	//cov_Ip = mean_Ip - mean_I .* mean_p; % this is the covariance of (I, p) in each local patch.  
	cv::Mat cov_Ip = mean_Ip - mean_I.mul(mean_p);

	//mean_II = boxfilter(I.*I, r) ./ N;  
	cv::Mat mean_II;
	cv::boxFilter(I.mul(I), mean_II, CV_64FC1, cv::Size(r, r));

	//var_I = mean_II - mean_I .* mean_I;  
	cv::Mat var_I = mean_II - mean_I.mul(mean_I);

	//a = cov_Ip ./ (var_I + eps); % Eqn. (5) in the paper;     
	cv::Mat a = cov_Ip / (var_I + eps);

	//b = mean_p - a .* mean_I; % Eqn. (6) in the paper;  
	cv::Mat b = mean_p - a.mul(mean_I);

	//mean_a = boxfilter(a, r) ./ N;  
	cv::Mat mean_a;
	cv::boxFilter(a, mean_a, CV_64FC1, cv::Size(r, r));
	mean_a = mean_a / N;

	//mean_b = boxfilter(b, r) ./ N;  
	cv::Mat mean_b;
	cv::boxFilter(b, mean_b, CV_64FC1, cv::Size(r, r));
	mean_b = mean_b / N;

	//q = mean_a .* I + mean_b; % Eqn. (8) in the paper;  
	cv::Mat q = mean_a.mul(I) + mean_b;

	return q;
}

IplImage* Ctry::getDarkChannel(IplImage* &src)
{
	IplImage* temp = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
	CvScalar pixel;
	double  px;
	for (int i = 0; i < src->height; i++)
	{
		for (int j = 0; j < src->width; j++)
		{
			pixel = cvGet2D(src, i, j);
			if (pixel.val[0]<pixel.val[1])
			{
				px = pixel.val[0];
			}
			else
			{
				px = pixel.val[1];
			}

			if (px >pixel.val[2])
			{
				px = pixel.val[2];
			}
			cvSetReal2D(temp, i, j, px);
		}
	}

	return  temp;
}

double Ctry::getA(IplImage* dark, IplImage* hazeImage)
{  
	double sum=0;   //像素点符合条件A的和
	int pointNum = 0;   //满足要求的像素点数
	double A;        //大气光强A
	double pix;    //暗通道图中照亮度的前0.1%范围的像素值
	CvScalar pixel;   //按图中符合A的点,在雾图中对应的像素

	float stretch_p[256], stretch_p1[256], stretch_num[256];
	//清空三个数组,初始化填充数组元素为0    
	memset(stretch_p, 0, sizeof(stretch_p));
	memset(stretch_p1, 0, sizeof(stretch_p1));
	memset(stretch_num, 0, sizeof(stretch_num));

	int nHeight = dark->height;
	int nWidth = dark->width;
	int i, j;
	for (i = 0; i<nHeight; i++)
	{
		for (j = 0; j<nWidth; j++)
		{
			double  pixel0 = cvGetReal2D(dark, i, j);
			int   pixel = (int)pixel0;
			stretch_num[pixel]++;
		}
	}
	//统计各个灰度级出现的概率  
	for (i = 0; i<256; i++)
	{
		stretch_p[i] = stretch_num[i] / (nHeight*nWidth);
	}

	//统计各个灰度级的概率,从暗通道图中按照亮度的大小取前0.1%的像素,pix为分界点
	for (i = 0; i<256; i++)
	{
		for (j = 0; j <= i; j++)
		{
			stretch_p1[i] += stretch_p[j];
			if (stretch_p1[i]>0.999)
			{
				pix = (double)i;
				i = 256;
				break;
			}

		}
	}

	for (i = 0; i< hazeImage->height; i++)
	{
		for (j = 0; j < hazeImage->width; j++)
		{
			double temp = cvGetReal2D(dark, i, j);
			if (temp > pix)
			{
				pixel = cvGet2D(hazeImage, i, j);
				pointNum++;
				sum += pixel.val[0];
				sum += pixel.val[1];
				sum += pixel.val[2];

			}
		}
	}
	A = sum / (3 * pointNum);
	if (A > 220.0)
	{
		A = 220.0;
	}
	return A;
}

//获取暗通道图像窗口中的最小值,用于后续计算透射率t,参数w为窗口的大小
IplImage* Ctry::getMinIcy(IplImage* dark, int w)
{
	IplImage* Icy = cvCreateImage(cvGetSize(dark), IPL_DEPTH_8U, 1);
	int hei = dark->height;
	int wid = dark->width;
	int hw = hei / w;
	int ww = wid / w;
	for (int i = w; i < (hw - 1)*w; i += w)
	{
		for (int j = w; j < (ww - 1)*w; j += w)
		{
			double p = cvGetReal2D(dark, i-1, j-1);  //得到窗口最右下角的一个像素点
			//得到窗口最小的像素值
			for (int ii = i - w; ii < i; ii++)
			{
				for (int jj = j - w; jj < j; jj++)
				{
					double newp = cvGetReal2D(dark, ii, jj);
					if (newp < p)
					{
						p = newp;
					}
				}
			}
			//设置Icy的值
			for (int ii = i - w; ii < i; ii++)
			{
				for (int jj = j - w; jj < j; jj++)
				{
					cvSetReal2D(Icy, ii, jj, p);
				}
			}

		}
	}

	//处理最右边一列  不包含最下一个子块
	for (int i = w; i < (hw - 1)*w; i += w)
	{
		double p = cvGetReal2D(dark, i-1, wid-1);  //得到窗口最右下角的一个像素点
		for (int ii = i - w; ii < i; ii++)
		{

			for (int j = (ww - 1)*w; j < wid; j++)
			{
				//得到窗口最小的像素值
				double newp = cvGetReal2D(dark, ii, j);
				if (newp < p)
				{
					p = newp;
				}
			}
		}	

		//设置Icy的值
		for (int ii = i - w; ii < i; ii++)
		{

			for (int j = (ww - 1)*w; j < wid; j++)
			{
				cvSetReal2D(Icy, ii, j, p);
			}
		}
	}


	//处理最下一行 不包含最后一个子块
	for (int j = w; j < (ww - 1)*w; j += w)
	{
		double p = cvGetReal2D(dark, hei-1, j);  //得到窗口最右下角的一个像素点
		for (int i = (hw - 1)*w; i < hei; i++)
		{
			for (int jj = j - w; jj < j; jj++)
			{
				//得到窗口最小的像素值
				double newp = cvGetReal2D(dark, i, jj);
				if (newp < p)
				{
					p = newp;
				}
			}
		}

		//设置Icy的值
		for (int i = (hw - 1)*w; i < hei; i++)
		{

			for (int jj = j - w; jj < j; jj++)
			{
				cvSetReal2D(Icy, i, jj, p);
			}
		}

	}

	//处理最右下角的一个子块
	double p = cvGetReal2D(dark, hei-1, wid-1);  //得到窗口最右下角的一个像素点
	for (int i = (hw - 1)*w; i < hei; i++)
	{
		for (int j = (ww - 1)*w; j < wid; j++)
		{
			//得到窗口最小的像素值
			double newp = cvGetReal2D(dark, i, j);
			if (newp < p)
			{
				p = newp;
			}

		}
	}
	for (int i = (hw - 1)*w; i < hei; i++)
	{
		for (int j = (ww - 1)*w; j < wid; j++)
		{
			cvSetReal2D(Icy, i, j, p);

		}
	}

	return Icy;
}

IplImage* Ctry::getTransmission(IplImage* Icy, double Ac)
{
	IplImage* t = cvCreateImage(cvGetSize(Icy), IPL_DEPTH_8U, 1);
	for (int i = 0; i < t->height; i++)
	{
		for (int j = 0; j < t->width; j++)
		{
			double temp = cvGetReal2D(Icy, i, j);
			double tempt = 1 - 0.95*temp / Ac;
			cvSetReal2D(t, i, j, tempt*255);
		}
	}

	return t;

}

IplImage* Ctry::getDehazedImage(IplImage* hazeImage, IplImage* guidedt, double Ac)
{
	IplImage* dehazedImage = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 3);
	IplImage* r = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);
	IplImage* g = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);
	IplImage* b = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);

	cvSplit(hazeImage, b, g, r, NULL);
	
	IplImage* dehaze_r = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);
	IplImage* dehaze_g = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);
	IplImage* dehaze_b = cvCreateImage(cvGetSize(hazeImage), IPL_DEPTH_8U, 1);

	for (int i = 0; i < r->height; i++)
	{
		for (int j = 0; j < r->width; j++)
		{
			double tempt = cvGetReal2D(guidedt, i, j);
			if (tempt/255 < 0.1)
			{
				tempt = 25.5;
			}

			double I_r=cvGetReal2D(r, i, j);
			double de_r = 255 * (I_r - Ac) / tempt + Ac;
			cvSetReal2D(dehaze_r, i, j, de_r);

			double I_g = cvGetReal2D(g, i, j);
			double de_g = 255 * (I_g - Ac) / tempt + Ac;
			cvSetReal2D(dehaze_g, i, j, de_g);

			double I_b = cvGetReal2D(b, i, j);
			double de_b = 255 * (I_b - Ac) / tempt + Ac;
			cvSetReal2D(dehaze_b, i, j, de_b);

		}
	}

	cvMerge(dehaze_b, dehaze_g, dehaze_r, 0, dehazedImage);

	return dehazedImage;

}

    代码调试后,得到的图片有点色差,边缘有2条黑色的线,我也懒得修改,欢迎各位批评指正。   

      在原文中,有这样一段话:

  Since the scene radiance is usually not as bright as the atmospheric light, the image after haze removal looks dim. So we increase the exposure of J(x) for
display.

     意思就是说直接去雾后的图像会比原始的暗,因此在处理完后需要进行一定的曝光增强,但作者没有说明其是如何增强的, 因此,这里的图和他论文的效果

有所不同时正常的。一般在去雾处理后再用自动色剂之类的算法增强下会获得比较满意的结果。

      去雾算法目前也有着众多其他的方式,不过我所接触的,很多都是以这个为基础,因此,先弄会这个为研究其他的去雾算法能奠定坚实的基础。

    总结:我对这种去雾算法的效果还是很满意的, 效果和速度都还比较合适。


 

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值