一、直方图概述(Histogram Overview)
在统计学中,直方图是一种对数据分布情况的图形表示,是一种二维统计图表,他的两个坐标分别是统计样本(图像、视频帧)和样本的某种属性(亮度,像素值,梯度,方向,色彩等等任何特征)。
也可以这么理解,直方图是对数据的统计,并把统计值显示到事先设定好的bin(矩形条)中,bin中的数值是从数据中计算出的特征的统计量。总之,直方图获取的是数据分布的统计图,通常直方图的维数要低于原始数据。
图像直方图是用一表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素数。可以借助观察该直方图了解需要如何调整亮度分布的直方图。这种直方图中,横坐标的左侧为纯黑、较暗的区域,而右侧为较亮、纯白的区域。因此,一张较暗图片的图像直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。计算机视觉邻域常借助图像直方图来实现图像的二值化。
灰度直方图是一幅图像中个像素灰度值出现次数或频数的统计结果,它只反映该图像中灰度值出现的频率,而未反映某一灰度值像素所在的位置。也就是说,它只包含了该图像中某个灰度值的像素出现的概率,而丢失了其所在的位置的信息。
任一幅图像,都能唯一地算出一幅与它对应的直方图。但不同的图像,可能有相同的直方图。即图像与直方图之间是多对一的映射关系。
直方图意义:
1. 直方图是图像中像素强度分布的图形表达方式。
2. 直方图统计了每一个强度值所具有的像素个数。
直方图广泛应用于许多计算机视觉应用中。通过标记帧和帧之间显著的边缘和颜色的统计变化,来检测视频中场景的变换。通过在每个兴趣点设置一个有相近特征的直方图所构成的标签,用以确定图像中的兴趣点。边缘、色彩、角度等直方图构成了可以被传递给目标识别分类器的一个通用特征类型。色彩和边缘的直方图还可以用来识别网络视频是否被复制等。直方图是计算机视觉中最经典的工具之一,也是一个很好的图像特征表示手段。
直方图术语: dims
:需要统计的特征的数目。例如:dims=1
,表示我们仅统计灰度值。 bins
:每个特征空间子区段的数目。 range
:每个特征空间的取值范围。
二、直方图均衡化
- 直方图在图像增强中的小应用
直方图均衡化是通过拉伸像素强度的分布范围,使得在0~255灰阶上的分布更加均衡,提高了图像的对比度,达到改善图像主观视觉效果的目的。对比度较低的图像适合使用直方图均衡化方法来增强图像细节。
OpenCV里面的API介绍:
C++ void equalizeHist(InputArray src, OutputArray dst)
//第一个参数,源图像,需为8位单通道图像
//第二个参数,输出图像,尺寸、类型和源图像一致
该函数采用如下步骤对输入图像进行直方图均衡化:
- 计算输入图像的直方图H;
- 进行直方图归一化,直方图的组距的和为255;
- 计算直方图积分:
- 以H'作为查询表进行图像变换:
也就是把直方图的每个灰度级进行归一化处理,求每种灰度的累积分布,得到一个映射的灰度映射表,然后根据相应的灰度值来修正原图中的每个像素。
示例程序:
//equalizeHist(直方图均衡化);提高图像对比度
#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace cv;
using namespace std;
const char*output = "histogram iamge";
int main(int argc, char*argv)
{
Mat src, dst, dst1;
src = imread("C:\\Users\\59235\\Desktop\\imag\\antiquity_girl2.jpeg");
if (!src.data)
{
printf("could not load image...\n");
return -1;
}
char input[] = "input image";
namedWindow(input, CV_WINDOW_AUTOSIZE);
namedWindow(output, CV_WINDOW_AUTOSIZE);
//转换成灰度图像,使原图成为单通道图像
cvtColor(src, dst, CV_BGR2GRAY);
//查看图像通道数
int c = dst.channels();
cout << "=" << endl << c << endl;
//直方图均衡化
equalizeHist(dst, dst1);
imshow(input, dst);
imshow(output, dst1);
waitKey(0);
return 0;
}
效果图: (原图) (直方图均衡化后效果图)
当然前面是针对对彩色图像进行均衡化也是可以实现的
彩色图像直方均衡化示例程序:
//彩色图像直方图均衡化
#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<cmath>
using namespace cv;
using namespace std;
const char*output = "histogram iamge";
int main(int argc, char*argv)
{
Mat src, dst, dst1;
src = imread("C:\\Users\\zhj\\Desktop\\image\\雾天.png");
if (!src.data)
{
printf("could not load image...\n");
return -1;
}
char input[] = "input image";
namedWindow(input, CV_WINDOW_AUTOSIZE);
namedWindow(output, CV_WINDOW_AUTOSIZE);
imshow(input, src);
//分割通道
vector<Mat>channels;
split(src, channels);
Mat blue, green, red;
blue = channels.at(0);
green = channels.at(1);
red = channels.at(2);
//分别对BGR通道做直方图均衡化
equalizeHist(blue, blue);
equalizeHist(green, green);
equalizeHist(red, red);
//合并通道
merge(channels, dst);
imshow(output, dst);
waitKey(0);
return 0;
}
效果图: (原图) (均衡化效果图)
三、直方图的计算与绘制
OpenCV里面的直方图计算的API介绍:
C++ Void calcHist(
const Mat* images,//输入图像指针
int images,// 图像数目
const int* channels,// 通道数
InputArray mask,// 输入mask,可选,不用
OutputArray hist,//输出的直方图数据
int dims,// 维数
const int* histsize,// 直方图级数
const float* ranges,// 值域范围
bool uniform,// true by default
bool accumulate)// false by defaut
//寻找最值函数
C++ void minMaxLoc(InputArray src, double* minVal, double* maxVal=0,
Point* minLoc=0,Point* maxLoc=0,InputArray mask=noArray())
//第一个参数:输入单通道阵列
//第二个参数:返回最小值的指针,若无需返回,此值置为NULL
//第三个参数:返回最大值的指针,若无需返回,此值置为NULL
//第四个参数:返回最小位置的指针(二维情况下),若无需返回,此值置为NULL
//第五个参数:返回最大位置的指针(二维情况下),若无需返回,此值置为NULL
//第六个参数:用于选择子阵列的可选掩膜
示例程序:
//直方图的计算与绘制
#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace cv;
using namespace std;
const char*output = "histogram iamge";
int main(int argc, char*argv)
{
Mat src, dst, dst1;
src = imread("C:\\Users\\zhj\\Desktop\\image\\heart.jpg");
if (!src.data)
{
printf("could not load image...\n");
return -1;
}
char input[] = "input image";
namedWindow(input, CV_WINDOW_AUTOSIZE);
namedWindow(output, CV_WINDOW_AUTOSIZE);
imshow(input, src);
//步骤一:分通道显示
vector<Mat>bgr_planes;
split(src, bgr_planes);
//split(// 把多通道图像分为多个单通道图像 const Mat &src, //输入图像 Mat* mvbegin)// 输出的通道图像数组
//步骤二:计算直方图
int histsize = 256;
float range[] = { 0,256 };
const float*histRanges = { range };
Mat b_hist, g_hist, r_hist;
calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histsize, &histRanges, true, false);
calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histsize, &histRanges, true, false);
calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histsize, &histRanges, true, false);
//归一化
int hist_h = 400;//直方图的图像的高
int hist_w = 512;直方图的图像的宽
int bin_w = hist_w / histsize;//直方图的等级
Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));//绘制直方图显示的图像
normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat());//归一化
normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
//步骤三:绘制直方图(render histogram chart)
for (int i = 1; i < histsize; i++)
{
//绘制蓝色分量直方图
line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(b_hist.at<float>(i - 1))),
Point((i)*bin_w, hist_h - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2, CV_AA);
//绘制绿色分量直方图
line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(g_hist.at<float>(i - 1))),
Point((i)*bin_w, hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, CV_AA);
//绘制红色分量直方图
line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(r_hist.at<float>(i - 1))),
Point((i)*bin_w, hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, CV_AA);
}
imshow(output, histImage);
waitKey(0);
return 0;
}
效果图: (原图) (直方图)
四、直方图比较
直方图比较,是用一定的标准来判断两个直方图的相似度方法;
OpenCV中提供的API是:
//对输入的两张图像计算得到直方图H1与H2,归一化到相同的尺度空间然后可以通过计算H1与H2的之间的距离得到两个直方图的相似程度进而比较图像本身的相似程度。
//Opencv提供的比较方法有四种:Correlation 相关性比较 Chi - Square 卡方比较 Intersection 十字交叉性 Bhattacharyya distance 巴氏距离
//步骤:首先把图像从RGB色彩空间转换到HSV色彩空间cvtColor
//计算图像的直方图,然后归一化到[0~1]之间calcHist和normalize;
//使用上述四种比较方法之一进行比较compareHist
四种比较方法:
注意:方法1和方法3的是当值越大时表示相似度越高
示例程序:
//直方图比较
#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace cv;
using namespace std;
string convertToString(double d);
int main(int argc, char*argv)
{
Mat base, test1, test2;
Mat hsvbase, hsvtest1, hsvtest2;
base = imread("C:\\Users\\zhj\\Desktop\\image\\test.jpg");
test1 = imread("C:\\Users\\zhj\\Desktop\\image\\SaltNoise.jpg");
test2 = imread("C:\\Users\\zhj\\Desktop\\image\\GaussianNoise.jpg");
if (!base.data)
{
printf("could not load image...\n");
return -1;
}
//步骤一:从RGB空间转换到HSV空间
cvtColor(base, hsvbase, CV_BGR2HSV);
cvtColor(test1, hsvtest1, CV_BGR2HSV);
cvtColor(test2, hsvtest2, CV_BGR2HSV);
//步骤二:计算直方图与归一化
int h_bins = 50;
int s_bins = 60;
int histsize[] = { h_bins,s_bins };
//hue varies from 0 to 179,saturation from 0 to 255
float h_ranges[] = { 0,180 };
float s_ranges[] = { 0,256 };
const float*histRanges[] = { h_ranges,s_ranges };
//use the 0-th and 1-st channels
int channels[] = { 0,1 };
MatND hist_base;
MatND hist_test1;
MatND hist_test2;
//计算直方图
calcHist(&hsvbase, 1, channels, Mat(), hist_base, 2, histsize, histRanges, true, false);
calcHist(&hsvtest1, 1, channels, Mat(), hist_test1, 2, histsize, histRanges, true, false);
calcHist(&hsvtest2, 1, channels, Mat(), hist_test2, 2, histsize, histRanges, true, false);
//归一化
normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat());//归一化
normalize(hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat());
normalize(hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat());
//步骤三:比较直方图,并返回值
double basebase = compareHist(hist_base, hist_base, CV_COMP_BHATTACHARYYA);//比较直方图
double basetest1 = compareHist(hist_base, hist_test1, CV_COMP_BHATTACHARYYA);
double basetest2 = compareHist(hist_base, hist_test2, CV_COMP_BHATTACHARYYA);
double test1test2 = compareHist(hist_test1, hist_test2, CV_COMP_BHATTACHARYYA);
printf("test1 with test2 correlation value :%f", test1test2);
//在原图中显示相关性参数
putText(base, convertToString(basebase), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, CV_AA);
putText(test1, convertToString(basetest1), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, CV_AA);
putText(test2, convertToString(basetest2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, CV_AA);
putText(test2, convertToString(test1test2), Point(100, 100), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, CV_AA);
namedWindow("base", CV_WINDOW_AUTOSIZE);
namedWindow("test1", CV_WINDOW_AUTOSIZE);
namedWindow("test2", CV_WINDOW_AUTOSIZE);
imshow("base", base);
imshow("test1", test1);
imshow("test2", test2);
waitKey(0);
return 0;
}
//由于comparehist计算出来的相关性的值是一个double型,这个函数就是把double转变为string
string convertToString(double d)
{
ostringstream os;
if (os << d)
return os.str();
return "invalid conversion";
}
结果图: (原图) (添加椒盐噪声) (添加高斯噪声)
分析:图一和图一直方图相似度最大是1,原图与图二,图三的相似度分别为0.552549、0.462446;图二和图三的相似度是0.905106;显然结果是正确的。
五、直方图的反向投影
反向投影是反映直方图模型在目标图像中的分布情况;简单点说就是用直方图模型去目标图像中寻找是否有相似的对象。通常用HSV色彩空间的HS两个通道直方图模型
步骤:
- 建立直方图模型
- 计算待测图像直方图并映射到模型中
- 从模型反向计算生成图像
需要用到的API介绍:
C++ void calcBackProject(const Mat* arrays, int narrays, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale=1, bool uniform=true )
参数解释:
const Mat* images:输入图像,图像深度必须位CV_8U,CV_16U或CV_32F中的一种,尺寸相同,每一幅图像都可以有任意的通道数
int nimages:输入图像的数量
const int* channels:用于计算反向投影的通道列表,通道数必须与直方图维度相匹配,第一个数组的通道是从0到image[0].channels()-1,第二个数组通道从图像image[0].channels()到image[0].channels()+image[1].channels()-1计数
InputArray hist:输入的直方图,直方图的bin可以是密集(dense)或稀疏(sparse)
OutputArray backProject:目标反向投影输出图像,是一个单通道图像,与原图像有相同的尺寸和深度
const float ranges**:直方图中每个维度bin的取值范围
double scale=1:可选输出反向投影的比例因子
bool uniform=true:直方图是否均匀分布(uniform)的标识符,有默认值true
C++ void mixChannels(const Mat*src, size_t nsrcs, Mat* dst, size_t ndsts, const int* fromTo, size_t npairs)
//src– 输入矩阵,可以为一个也可以为多个,但是矩阵必须有相同的大小和深度.
//nsrcs– 输入矩阵的个数。
//dst– 输出矩阵,可以为一个也可以为多个,但是所有的矩阵必须事先分配空间(如用create),大小和深度须与输入矩阵等同.
//ndsts– Number of matrices in dst.输出矩阵的个数。
//fromTo –设置输入矩阵的通道对应输出矩阵的通道,规则如下:首先用数字标记输入矩阵的各个通道。输入矩阵个数可能多于一个并且每个矩阵的通道可能不一样,
//第一个输入矩阵的通道标记范围为:0 ~src[0].channels() - 1,第二个输入矩阵的通道标记范围为:src[0].channels() ~src[0].channels() + src[1].channels() - 1,
//以此类推;其次输出矩阵也用同样的规则标记,第一个输出矩阵的通道标记范围为:0 ~dst[0].channels() - 1,第二个输入矩阵的通道标记范围为:dst[0].channels()
//~dst[0].channels() + dst[1].channels() - 1, 以此类推;最后,数组fromTo的第一个元素即fromTo[0]应该填入输入矩阵的某个通道标记,而fromTo的第二个元素即
//fromTo[1]应该填入输出矩阵的某个通道标记,这样函数就会把输入矩阵的fromTo[0]通道里面的数据复制给输出矩阵的fromTo[1]通道。fromTo后面的元素也是这个
//道理,总之就是一个输入矩阵的通道标记后面必须跟着个输出矩阵的通道标记。
//npairs– Number of index pairs in fromTo.即参数fromTo中的有几组输入输出通道关系,其实就是参数fromTo的数组元素个数除以2.
示例程序:
//直方图的反向投影
//加载图片imread
//将图像从RGB色彩空间转换到HSV色彩空间cvtColor
//计算直方图和归一化calcHist与normalize
//Mat与MatND其中Mat表示二维数组,MatND表示三维或者多维数据,此处均可以用Mat表示。
//计算反向投影图像 - calcBackProject
#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace std;
using namespace cv;
Mat src, hsv_src;
Mat hue;
int bins = 12;
void Hist_And_Backprojection(int, void*);
int main(int argc, char*argv)
{
src = imread("C:\\Users\\59235\\Desktop\\imag\\test.jpg");
if (!src.data)
{
printf("could not load image...\n");
return -1;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
//将图像从RGB色彩空间转换到HSV色彩空间
cvtColor(src, hsv_src, CV_BGR2HSV);
hue.create(hsv_src.size(), hsv_src.depth());
int nchannels[] = { 0,0 };
//mixChannels主要就是把输入的矩阵(或矩阵数组)的某些通道拆分复制给对应的输出矩阵(或矩阵数组)的某些通道中,其中的对应关系就由fromTo参数制定.
mixChannels(&hsv_src, 1, &hue, 1, nchannels, 1);
createTrackbar("Histogram Bins:", "input", &bins, 180, Hist_And_Backprojection);
Hist_And_Backprojection(0, 0);
imshow("input", src);
waitKey(0);
return 0;
}
void Hist_And_Backprojection(int, void*)
{
//计算直方图
float range[] = { 0,180 };
const float *histRanges = { range };
Mat h_hist;
calcHist(&hue, 1, 0, Mat(), h_hist, 1, &bins, &histRanges, true, false);
//归一化
normalize(h_hist, h_hist, 0, 255, NORM_MINMAX, -1, Mat());
//计算反向投影图像 - calcBackProject
Mat backProjectIamge;
calcBackProject(&hue, 1, 0, h_hist, backProjectIamge, &histRanges, 1, true);
namedWindow("BackProjectIamge", CV_WINDOW_AUTOSIZE);
imshow("BackProjectIamge", backProjectIamge);
//画直方图
int hist_h = 400;
int hist_w = 400;
int bin_w = (hist_w / bins);
Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));
for (size_t i = 1; i < bins; i++)
{
rectangle(histImage,
Point((i - 1)*bin_w, (hist_h - cvRound(h_hist.at<float>(i - 1)*(400 / 255)))),
Point(i*bin_w, (hist_h - cvRound(h_hist.at<float>(i)*(400 / 255)))),
Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("Histogram", histImage);
return;
}
效果图: (原图) (反向投影图) (直方图模型)
六、模板匹配
模板匹配(TemplateMatching)就是在一幅图像中寻找和模板图像(template)最相似的区域,该方法原理简单计算速度快,能够应用于目标识别,目标跟踪等多个领域。
C++: void matchTemplate(InputArray image, InputArray templ, OutputArray result, int method);
C: void cvMatchTemplate(const CvArr* image, const CvArr* templ, CvArr* result, int method);
//参数介绍在下面代码里面
匹配方法(method):
示例程序:
//模板匹配(template match)
//模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
//工作方法,在带检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
//OpenCV 提供了 6 种计算两幅图像相似度的方法。
//差值平方和匹配 CV_TM_SQDIFF
//标准化差值平方和匹配 CV_TM_SQDIFF_NORMED
//相关匹配 CV_TM_CCORR
//标准相关匹配 CV_TM_CCORR_NORMED
//相关匹配 CV_TM_CCOEFF
//标准相关匹配 CV_TM_CCOEFF_NORMED
#include"stdafx.h"
#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace std;
using namespace cv;
Mat test, temp;
const char*input = "deteected image";
int match_method = CV_TM_SQDIFF;
int max_track = 5;
void template_match_demo(int, void*);
int main(int argc, char*argv)
{
test = imread("C:/Users/zhj/Desktop/image/mixed_02.png");//待检测图像
temp = imread("C:/Users/zhj/Desktop/image/template1.png");//模板图像
if (test.empty())
{
cout << "could not load image...\n" << endl;
return -1;
}
namedWindow(input, CV_WINDOW_AUTOSIZE);
namedWindow("template image", CV_WINDOW_AUTOSIZE);
namedWindow("template_match_demo", CV_WINDOW_AUTOSIZE);
namedWindow("result", CV_WINDOW_AUTOSIZE);
imshow(input, test);
imshow("template image", temp);
createTrackbar("template match means", input, &match_method, max_track, template_match_demo);//上面六种方法对应数字依次是0到5;
template_match_demo(0, 0);
waitKey(0);
return 0;
}
void template_match_demo(int, void*)
{
int result_rows = test.rows - temp.rows + 1;
int result_cols = test.cols - test.cols + 1;
Mat result(result_rows, result_cols, CV_32FC1);
//模板匹配
matchTemplate(test, temp, result, match_method);
//matchTemplate(
//InputArray image,// 源图像,必须是8-bit或者32-bit浮点数图像
//InputArray templ,// 模板图像,类型与输入图像一致
//OutputArray result,// 输出结果,必须是单通道32位浮点数,假设源图像WxH,模板图像wxh,则结果必须为W - w + 1, H - h + 1的大小。
//int method,//使用的匹配方法
//InputArray mask = noArray()//(optional) )
//归一化
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
//寻找模板匹配的最大最小匹配值
Point minLoc;
Point maxLoc;
Point match_loc;
double min_value, max_value;
minMaxLoc(result, &min_value, &max_value, &minLoc, &maxLoc, Mat());
//void minMaxLoc(const MatND& src, double* minVal, double* maxVal, int* minIdx=0, int* maxIdx=0, const MatND& mask=MatND() );
//说明:1 minMaxLoc寻找矩阵(一维数组当作向量, 用Mat定义) 中最小值和最大值的位置.
// 2 参数若不需要, 则置为NULL或者0, 即可.
//3 minMaxLoc针对Mat和MatND的重载中, 第5个参数是可选的(optional), 不使用不传递即可
//如果选用平方(平方差和标准平方差)不同的方法应该取最小值,其他取最大值,这与算法的原理有关,其计算匹配值,当匹配值越小时,匹配程度越高
if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED)
{
match_loc = minLoc;
}
else
{
match_loc = maxLoc;
}
//将待检测图中与模板匹配出框起来
rectangle(test, Rect(match_loc.x, match_loc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2,CV_AA);
rectangle(result, Rect(match_loc.x, match_loc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2, CV_AA);
imshow("template_match_demo", result);
imshow("result", test);
return;
}
效果图:
转载自原文链接, 如需删除请联系管理员。
原文链接:【OpenCV学习笔记】之直方图(Histogram),转载请注明来源!