作者:王先荣
前言
颜色直方图直观的显示了图像在色彩空间的分布状况,本文将讨论在EmguCv及OpenCv中跟直方图相关的一些基本操作,包括:计算、显示、处理、对比及反向投影,并谈谈在实践过程中得到的一些经验。如无特别说明,下文所提的直方图均指颜色直方图。
直方图的计算
EmguCv将OpenCv的一系列直方图函数封装到了类DenseHistogram里面,可以用方法Calculate方便的计算图像的直方图。不过值得注意的是,该方法接受的第一个参数是“单通道”图像数组;而一般情况下的图像都是3通道的,在计算之前我们需要用Image<TColor,TDepth>.Split方法将其分解成单通道图像,然后选择需要参与直方图计算的通道。下面有几段计算直方图的代码,分别计算单通道(红色)直方图、色调和饱和度直方图。
/// <summary>
/// 计算直方图(红色)
/// </summary>
private void CalcHistRed()
{
//计算
int rBins = 256;
RangeF rRange = new RangeF(0f, 255f);
Image<Bgr, Byte> imageSource = new Image<Bgr, Byte>((Bitmap)pbSource.Image);
Image<Gray, Byte> imageRed = imageSource.Split()[2];
DenseHistogram hist = new DenseHistogram(rBins, rRange);
hist.Calculate(new IImage[] { imageRed }, false, null);
//显示
pbHistogram.Image = GenerateHistImage(hist).Bitmap;
//释放资源
imageSource.Dispose();
imageRed.Dispose();
hist.Dispose();
}
/// <summary>
/// 计算直方图(色调和饱和度)
/// </summary>
private void CalcHistHs()
{
//计算
int hBins = 180;
RangeF hRange = new RangeF(0f, 179f); //色调的范围在0~180之间
int sBins = 256;
RangeF sRange = new RangeF(0f, 255f);
Image<Bgr, Byte> imageSource = new Image<Bgr, Byte>((Bitmap)pbSource.Image);
Image<Hsv, Byte> imageHsv = imageSource.Convert<Hsv, Byte>(); //将色彩空间从BGR转换到HSV
Image<Gray, Byte>[] imagesHsv = imageSource.Split(); //分解成H、S、V三部分
DenseHistogram hist = new DenseHistogram(new int[] { hBins, sBins }, new RangeF[] { hRange, sRange });
hist.Calculate(new IImage[] { imagesHsv[0], imagesHsv[1] }, false, null);
//显示
pbHistogram.Image = GenerateHistImage(hist).Bitmap;
//释放资源
imageSource.Dispose();
imageHsv.Dispose();
foreach (Image<Gray, Byte> image in imagesHsv)
image.Dispose();
hist.Dispose();
}
直方图的显示
我们可以用以下方式来查看直方图:(1)使用HistogramViewer窗体显示直方图;(2)使用HistogramBox控件显示直方图;(3)用自己写的方法将直方图转换成图像,然后显示出来。这3个方式依次从易到难,不过对直方图的显示控制程度却依次提高。
1.HistogramViewer窗体
HistogramViewer窗体的3个静态方法可以很方便的显示直方图,它们的定义如下:
Show(IImage) 显示指定图像的直方图:它会自动为图像的每个通道生成直方图,然后显示一个或者多个一维直方图;
Show(IImage, Int32) 显示指定图像的直方图:除了可以自己指定直方图的区间数目之外,跟前一个方法一样;
Show(DenseHistogram, String) 显示指定的一维直方图:可以在图上显示一个标题,注意这个方法只能显示一维直方图。
HistogramViewer窗体还有一个名为HistogramCtrl的属性,它用来获取窗体内的HistogramBox控件。
下面的代码演示了HistogramViewer的用法:
/// <summary>
/// 计算直方图(RGB)
/// </summary>
private void CalcHistRgb()
{
//在HistogramViewer中显示直方图
Image<Bgr, Byte> imageSource = new Image<Bgr, byte>((Bitmap)pbSource.Image);
//直接用HistogranViewer的静态函数Show来查看直方图,不过不能控制行为
HistogramViewer.Show(imageSource, 256);
//可以用HistogramViewer对象来显示直方图,有较多的控制
HistogramViewer hv = new HistogramViewer();
hv.Text = "RGB直方图";
hv.ShowInTaskbar = false;
hv.HistogramCtrl.GenerateHistograms(imageSource, 256);
hv.WindowState = FormWindowState.Maximized;
hv.Show(this);
//释放资源
imageSource.Dispose();
}
2.HistogramBox控件
HistogramBox控件有4个方法跟直方图有关:
AddHistogram(String, Color, DenseHistogram) 向控件添加一块一维直方图:可以指定标题及绘制的颜色;
ClearHistogram() 移除控件中的所有直方图;
GenerateHistograms(IImage, Int32) 为指定图像的每个通道生成直方图:可以指定直方图的区间数目;
Refresh() 绘制直方图:对控件的直方图进行任何改动之后,都要调用Refresh来重新绘制才行。
/// <summary>
/// 计算直方图(色调)
/// </summary>
private void CalcHistHue()
{
//计算
int hBins = 180;
RangeF hRange = new RangeF(0f, 179f); //色调的范围在0~180之间
Image<Bgr, Byte> imageSource = new Image<Bgr, Byte>((Bitmap)pbSource.Image);
Image<Hsv, Byte> imageHsv = imageSource.Convert<Hsv, Byte>(); //将色彩空间从BGR转换到HSV
Image<Gray, Byte> imageHue = imageSource.Split()[0]; //分解Hue部分
DenseHistogram hist = new DenseHistogram(hBins, hRange);
hist.Calculate(new IImage[] { imageHue }, false, null);
//显示(注意:这里的变量histBox是一个HistogramBox控件的对象)
histBox.AddHistogram("色调直方图", Color.FromArgb(255, 0, 0), hist);
histBox.Refresh();
//释放资源
imageSource.Dispose();
imageHsv.Dispose();
imageHue.Dispose();
hist.Dispose();
}
3.自己写方法将直方图转换成图像
HistogramViewer和HistogramBox很方便,但是它们只能显示一维的直方图,如果要显示二维甚至三维的直方图,那么只有自己写方法了。
//生成直方图图示
private Image<Bgr,Byte> GenerateHistImage(DenseHistogram hist)
{
Image<Bgr, Byte> imageHist = null;
float minValue, maxValue;
int[] minLocations, maxLocations;
hist.MinMax(out minValue, out maxValue, out minLocations, out maxLocations);
if (hist.Dimension == 1)
{
int bins = hist.BinDimension[0].Size;
int width = bins;
int height = 300;
imageHist = new Image<Bgr, Byte>(width, height, new Bgr(255d, 255d, 255d));
double heightPerTick = 1d * height / maxValue;
Bgr color=new Bgr(0d,0d,255d);
//遍历每个bin对应的值,并画一条线
for (int i = 0; i < bins; i++)
{
LineSegment2D line = new LineSegment2D(new Point(i, height), new Point(i, (int)(height - heightPerTick * hist[i])));
imageHist.Draw(line, color, 1);
}
}
else if (hist.Dimension == 2)
{
int scale = 2;
int width = hist.BinDimension[0].Size * scale;
int height = hist.BinDimension[1].Size * scale;
imageHist = new Image<Bgr, Byte>(width, height, new Bgr(255d, 255d, 255d));
//遍历每个bin对应的值,并画一个矩形
for (int i = 0; i < width / scale; i++)
{
for (int j = 0; j < height / scale; j++)
{
double binValue = hist[i, j];
double intensity = 1d * binValue * 255 / maxValue;
Rectangle rect = new Rectangle(i * scale, j * scale, 1, 1);
Bgr color = new Bgr(intensity, intensity, intensity);
imageHist.Draw(rect, color, 1);
}
}
}
return imageHist;
}
注意上面的方法只生成一维和二维直方图的图像,因为我很笨 -_- 。我相信聪明的你,一定能搞定三维的显示~~。
直方图的处理
在计算出直方图之后,我们经常需要对其进行一些处理才能使用,下面介绍最常用的几个:
MinMax(Single, Single, array<Int32>[], array<Int32>[]) 查找最小及最大的直方图区间及其位置;
Normalize(Double) 归一化直方图;
Threshold(Double) 为直方图设置一个阀值,小于阀值的区间将被置零。
下面的代码演示了直方图的处理:
//计算直方图
DenseHistogram hist = new DenseHistogram(bins[i], ranges[i]);
hist.Calculate(new IImage[] { images[i] }, false, null);
//阀值
if (cbThreshold.Checked && thresholdValue > 0)
hist.Threshold(thresholdValue);
//归一化
if (cbNormalize.Checked)
hist.Normalize(normalizeFactor);
//显示
histBox1.AddHistogram(captions[i], colors[i], hist);
//释放
hist.Dispose();
images[i].Dispose();
直方图的对比
OpenCv提供了5种对比直方图的方式:CORREL(相关)、CHISQR(卡方)、INTERSECT(相交)、BHATTACHARYYA、EMD(最小工作距离),其中CHISQR速度最快,EMD速度最慢且有诸多限制,但是EMD的效果最好。世界总是充满了矛盾,而我们的工作就是化解矛盾,把每种方式都测试一下,找到正好能解决问题并且速度最优的方法。
下面是直方图对比的示例代码:
//对比直方图
private void btnCompare_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
//计算直方图
DenseHistogram hist1 = CalcHist(pbSource1.Image);
DenseHistogram hist2 = CalcHist(pbSource2.Image);
//对比并显示对比结果
HISTOGRAM_COMP_METHOD compareMethod = HISTOGRAM_COMP_METHOD.CV_COMP_CORREL;
double compareResult = 0d;
if (rbCompareEmd.Checked)
{
//EMD(陆地移动距离)需要用特殊的方法计算
//将直方图转换成矩阵
Matrix<Single> matrix1 = ConvertDenseHistogramToMatrix(hist1);
Matrix<Single> matrix2 = ConvertDenseHistogramToMatrix(hist2);
compareResult = CvInvoke.cvCalcEMD2(matrix1.Ptr, matrix2.Ptr, DIST_TYPE.CV_DIST_L2, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
matrix1.Dispose();
matrix2.Dispose();
}
else
{
if (rbCompareCorrel.Checked)
compareMethod = HISTOGRAM_COMP_METHOD.CV_COMP_CORREL;
else if (rbCompareChisqr.Checked)
compareMethod = HISTOGRAM_COMP_METHOD.CV_COMP_CHISQR;
else if (rbCompareIntersect.Checked)
compareMethod = HISTOGRAM_COMP_METHOD.CV_COMP_INTERSECT;
else if (rbCompareBhattacharyya.Checked)
compareMethod = HISTOGRAM_COMP_METHOD.CV_COMP_BHATTACHARYYA;
else
throw new Exception("不存在的直方图对比方式。");
compareResult = CvInvoke.cvCompareHist(hist1.Ptr, hist2.Ptr, compareMethod);
}
sw.Stop();
txtResult.Text += string.Format("对比直方图用时:{0:F05}毫秒,对比方式:{1},对比结果:{2:F05},条件({3})\r\n",
sw.Elapsed.TotalMilliseconds, rbCompareEmd.Checked ? "EMD" : compareMethod.ToString("G"), compareResult, GetOptions());
//释放资源
hist1.Dispose();
hist2.Dispose();
}
/// <summary>
/// 计算指定图像的直方图,并进行相应的处理
/// </summary>
/// <param name="image">源图像</param>
/// <returns>返回直方图</returns>
private DenseHistogram CalcHist(Image image)
{
//定义变量
Image<Bgr, Byte> imageSource = new Image<Bgr, byte>((Bitmap)image);
string colorSpace = (string)cmbColorSpace.SelectedItem;
double thresholdValue = 0d;
double.TryParse(txtThreshold.Text, out thresholdValue);
double normalizeFactor = 1d;
Image<Gray, Byte>[] images;
int[] bins;
RangeF[] ranges;
string[] captions;
Color[] colors;
//根据色彩空间的选择,得到需要计算的色彩单通道图像、bins及范围
GetImagesBinsAndRanges(imageSource, colorSpace, out images, out bins, out ranges, out captions, out colors);
//计算各色彩通道的直方图,并进行需要的处理
DenseHistogram hist = new DenseHistogram(bins, ranges);
hist.Calculate<Byte>(images, false, null);
//阀值
if (cbThreshold.Checked && thresholdValue > 0)
hist.Threshold(thresholdValue);
//归一化
if (cbNormalize.Checked)
hist.Normalize(normalizeFactor);
return hist;
}
需要注意的是:EMD方式要求先将直方图转换成矩阵:
/// <summary>
/// 将直方图转换成矩阵;
/// 注意:只支持1、2、3维直方图
/// </summary>
/// <param name="hist">直方图</param>
/// <returns>返回矩阵</returns>
private Matrix<Single> ConvertDenseHistogramToMatrix(DenseHistogram hist)
{
Matrix<Single> matrix = null;
if (hist != null)
{
int cols = hist.Dimension + 1; //矩阵的列数为直方图维数加1
int rows = 1; //矩阵的行数为直方图所有bin的乘积
foreach (MCvMatND.Dimension bin in hist.BinDimension)
rows *= bin.Size;
//初始化矩阵
matrix = new Matrix<Single>(rows, cols);
//填充矩阵
if (hist.Dimension == 1)
{
// 1维直方图
for (int idx0 = 0; idx0 < hist.BinDimension[0].Size; idx0++)
{
float binValue = (float)hist[idx0];
matrix[idx0, 0] = binValue;
matrix[idx0, 1] = idx0;
}
}
else if (hist.Dimension == 2)
{
// 2维直方图
int bins0 = hist.BinDimension[0].Size;
int bins1 = hist.BinDimension[1].Size;
for (int idx0 = 0; idx0 < bins0; idx0++)
{
for (int idx1 = 0; idx1 < bins1; idx1++)
{
float binValue = (float)hist[idx0, idx1];
int row = idx0 * bins1 + idx1;
matrix[row, 0] = binValue;
matrix[row, 1] = idx0;
matrix[row, 2] = idx1;
}
}
}
else if (hist.Dimension == 3)
{
// 3维直方图
int bins0 = hist.BinDimension[0].Size;
int bins1 = hist.BinDimension[1].Size;
int bins2 = hist.BinDimension[2].Size;
for (int idx0 = 0; idx0 < bins0; idx0++)
{
for (int idx1 = 0; idx1 < bins1; idx1++)
{
for (int idx2 = 0; idx2 < bins2; idx2++)
{
float binValue = (float)hist[idx0, idx1, idx2];
int row = idx0 * bins1 * bins2 + idx1 * bins2 + idx2;
matrix[row, 0] = binValue;
matrix[row, 1] = idx0;
matrix[row, 2] = idx1;
matrix[row, 3] = idx2;
}
}
}
}
else
{
throw new ArgumentException("直方图维数超出范围,只支持1、2、3维直方图。", "DenseHistogram hist");
}
}
return matrix;
}
EMD方法会占用很很很大量的内存,在使用前请注意直方图的维数及区间数目,不然会出现内存不足的异常。关于这点,请参看我的另一篇文章《关于使用cvCalcEMD2计算两个直方图间最小工作距离的限制(Why cvCalcEMD2 Throw Insufficient Memory Exception)》。还有一点值得注意的是,不同的对比方式对待结果的方式很不一样,结果越大不一定说明匹配度更高,具体请参看《学习OpenCv》这本书的相关章节。
对于直方图的相关和相交对比,结果值越大(即亮度较高)的地方表示匹配程度越高;
对于直方图的卡方、Bhattacharyya、EMD对比,结果值越小(即越黑暗)的地方表示匹配程度越高。
右图是左图用ACDSEE的去除杂点功能处理之后的结果
直方图的反向投影
现在说说直方图的反向投影,我觉得这是直方图中最难的部分,我看了跟cvCalcBackProjectPatch相关的读书章节、中文参考及英文帮助,还是不明白......而网上居然没有使用cvCalcBackProjectPatch的例程(当然本文写成之后就有例程了)。前天晚上在半梦半醒之间进入了冥想状态,突然开窍了,然而在试验的时候还是遇到了不少问题。
1.反向投影的作用是什么?
反向投影用于在输入图像(通常较大)中查找特定图像(通常较小或者仅1个像素,以下将其称为模板图像)最匹配的点或者区域,也就是定位模板图像出现在输入图像的位置。
2.反向投影如何查找(工作)?
查找的方式就是不断的在输入图像中切割跟模板图像大小一致的图像块,并用直方图对比的方式与模板图像进行比较。
假设我们有一张100x100的输入图像,有一张10x10的模板图像,查找的过程是这样的:
(1)从输入图像的左上角(0,0)开始,切割一块(0,0)至(10,10)的临时图像;
(2)生成临时图像的直方图;
(3)用临时图像的直方图和模板图像的直方图对比,对比结果记为c;
(4)直方图对比结果c,就是结果图像(0,0)处的像素值;
(5)切割输入图像从(0,1)至(10,11)的临时图像,对比直方图,并记录到结果图像;
(6)重复(1)~(5)步直到输入图像的右下角。
(本图片引用自http://www.opencv.org.cn)
3.反向投影的结果是什么?
反向投影的结果包含了:以每个输入图像像素点为起点的直方图对比结果。可以把它看成是一个二维的浮点型数组,二维矩阵,或者单通道的浮点型图像。
4.特殊情况怎么样?
如果输入图像和模板图像一样大,那么反向投影相当于直方图对比。如果输入图像比模板图像还小,直接罢工~~。
5.使用时有什么要注意的地方?
需要注意的地方比较多,我们对照反向投影函数来说:
void cvCalcBackProjectPatch(
IplImage** image, /*输入图像:是一个单通道图像数组,而非实际图像*/
CvArr* dst, /*输出结果:是一个单通道32位浮点图像,它的宽度为W-w+1,高度为H-h+1,这里的W和H是输入图像的宽度和高度,w和h是模板图像的宽度和高度*/
CvSize patch_size, /*模板图像的大小:宽度和高度*/
CvHistogram* hist, /*模板图像的直方图:直方图的维数和输入图像的个数相同,并且次序要一致;例如:输入图像包含色调和饱和度,那么直方图的第0维是色调,第1维是饱和度*/
int method, /*对比方式:跟直方图对比中的方式类似,可以是:CORREL(相关)、CHISQR(卡方)、INTERSECT(相交)、BHATTACHARYYA*/
float factor /*归一化因子,一般都设置成1,否则很可能会出错;中文、英文以及各路转载的文档都错了,这个参数的实际类型是double,而非float,我看了源代码才搞定这个地方*/
);
还有最需要注意的地方:这个函数的执行效率非常的低,在使用之前尤其需要注意图像的大小,直方图的维数,对比方式。如果说对比单个直方图对现在的电脑来说是清风拂面,那么反向投影是狂风海啸。对于1010x1010的RGB输入图像,10x10的模板图像,需要生成1百万次3维直方图,对比1百万次3维直方图。
直方图反向投影的示例如下:
//计算反向投影
private void btnCalcBackProjectPatch_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
//定义变量
Image<Bgr, Byte> imageSource = new Image<Bgr, byte>((Bitmap)pbSource.Image);
double scale = GetScale(); //用一个缩放因子减小源图像的大小,可以极大的提高处理速度
Image<Bgr, Byte> imageSource2 = imageSource.Resize(scale, INTER.CV_INTER_LINEAR);
Image<Bgr, Byte> imageTarget = new Image<Bgr, byte>((Bitmap)pbTarget.Image);
Image<Bgr, Byte> imageTarget2 = imageTarget.Resize(scale, INTER.CV_INTER_LINEAR);
Image<Gray, Single> imageDest = null;
Size patchSize = imageTarget2.Size;
string colorSpace = (string)cmbColorSpace.SelectedItem; //色彩空间的选择对处理速度也有很大的影响,更少的色彩空间数目能极大的提高处理速度
double normalizeFactor = 1d; //归一化因子,一般设置为1,且必须为double类型
Image<Gray, Byte>[] imagesSource;
int[] bins;
RangeF[] ranges;
string[] captions;
Color[] colors;
//得到源图像需要参与反向投影的各色彩图像集合
FormProcessHist.GetImagesBinsAndRanges(imageSource2, colorSpace, out imagesSource, out bins, out ranges, out captions, out colors);
//计算目标图像的直方图
DenseHistogram histTarget = CalcHist(imageTarget2.Bitmap);
//反向投影
HISTOGRAM_COMP_METHOD compareMethod = GetHistogramCompareMethod();
try
{
imageDest = BackProjectPatch<Byte>(imagesSource, patchSize, histTarget, compareMethod, (float)normalizeFactor); //DenseHistogram类的方法BackProjectPath不能用,这里自己实现了一个类似的方法
}
catch(Exception ex)
{
txtResult.Text += string.Format("在执行反向投影时发生错误,错误描述:{0},错误源:{1}\r\n", ex.Message, ex.Source);
}
//显示结果
if (imageDest != null)
pbResult.Image = imageDest.Bitmap;
sw.Stop();
txtResult.Text += string.Format("直方图反向投影用时:{0:F05}毫秒,对比方式:{1:G},({2})\r\n", sw.Elapsed.TotalMilliseconds, compareMethod, GetOptions());
//释放资源
imageSource.Dispose();
imageSource2.Dispose();
imageTarget.Dispose();
imageTarget2.Dispose();
if (imageDest != null)
imageDest.Dispose();
foreach (Image<Gray, Byte> image in imagesSource)
image.Dispose();
histTarget.Dispose();
}
在左图(输入图像)中查找特定人脸(模板图像),结果如右图,中间亮白色的区域为最匹配区域
如果您跟我一样在.net下使用EmguCv,那么恭喜你,还有需要解决的问题。EmguCv的直方图反向投影部分有几处错误,不能直接使用。错误列表如下:
(1)CvInvoke.cvCalcBackProjectPatch方法不能使用,会出现“在dll中找不到入口”的异常;
(2)DenseHistogram.BackProjectPatch方法不能使用,也会出现“在dll中找不到入口”的异常;
(3)分明将归一化因子设置成1,但是依然提示我“归一化因子错误,如果不确定归一化因子,请将其设置为1”;
(4)返回的结果图像类型错误。
通过查看OpenCv和EmguCv的源代码及输出dll文件,确定两者的问题所在:
(1)在OpenCv中只有一个名为cvCalcBackProjectPatch的宏,而不是内联函数,宏在编译成dll之后当然不会变成输出API了;
(2)OpenCv的文档中将归一化因子的类型错写成float类型,如果在.net中用P/INVOKE调用,不会有编译错误,但是会有无法解决的运行时错误;
(3)既然OpenCv错了,EmguCv也将错就错,没有调用正确的函数cvCalcArrBackProjectPatch。
在.net中使用直方图的反向投影可以用下面的正确方式:
/* P/INVOKE调用正确的API函数cvCalcArrBackProjectPatch */
[DllImport("cv200.dll", EntryPoint = "cvCalcArrBackProjectPatch")]
public static extern void cvCalcBackProjectPatch(
IntPtr[] images,
IntPtr dst,
Size patchSize,
IntPtr hist,
HISTOGRAM_COMP_METHOD method,
double factor);
/// <summary>
/// 计算直方图的反向投影
/// </summary>
/// <param name="srcs">Source images, all are of the same size and type</param>
/// <param name="factor">Normalization factor for histograms, will affect normalization scale of destination image, pass 1. if unsure. </param>
/// <param name="patchSize">Size of patch slid though the source images.</param>
/// <param name="method">Comparison method, passed to cvCompareHist.</param>
/// <typeparam name="TDepth">The type of depth of the image</typeparam>
/// <returns>Destination back projection image of the same type as the source images</returns>
public Image<Gray, Single> BackProjectPatch<TDepth>(Image<Gray, TDepth>[] srcs, Size patchSize, DenseHistogram hist, HISTOGRAM_COMP_METHOD method, double factor) where TDepth : new()
{
Debug.Assert(srcs.Length == hist.Dimension, "直方图的维数和源图像的数目必须相同");
IntPtr[] imgPtrs =
Array.ConvertAll<Image<Gray, TDepth>, IntPtr>(
srcs,
delegate(Image<Gray, TDepth> img) { return img.Ptr; });
Size size = srcs[0].Size;
size.Width = size.Width - patchSize.Width + 1;
size.Height = size.Height - patchSize.Height + 1;
Image<Gray, Single> res = new Image<Gray, float>(size);
cvCalcBackProjectPatch(imgPtrs, res.Ptr, patchSize, hist.Ptr, method, factor);
return res;
}
已经将发现的上述错误提交给OpenCv及EmguCv的BUG LIST,希望能尽快解决。
本文相关代码:点击下载。
感谢您耐心看完本文,希望对您有所帮助。