得知OpenCV有一段时间。除了研究的各种算法的内容。除了从备用,据导游书籍和资料,尝试结合链接的图像处理算法和日常生活,第一桌面上(随着摄像头)完成了一系列的视频流处理功能。开发平台Qt5.3.2+OpenCV2.4.9。
本次试验实现的功能主要有:
- 调用摄像头捕获视频流;
- 将帧图像转换为素描效果图片;
- 将帧图像卡通化处理;
- 简单地生成“怪物”形象;
- 人脸肤色变换。
本节全部的算法均由类cartoon中的函数cartoonTransform()来实现:
// Frame:输入每一帧图像 output:输出图像
cartoonTransform(cv::Mat &Frame, cv::Mat &output)
兴许将使用很多其它的OpenCV技巧实现很多其它功能,并将该应用移植到Android系统上。
一、使用OpenCV訪问摄像头
OpenCV提供了一个简便易用的框架以提取视频文件和USB摄像头中的图像帧。假设你仅仅是想读取某个视频,你仅仅须要创建一个cv::VideoCapture实例,然后在循环中提取每一帧。这里须要訪问摄像头,因此须要创建一个cv::VideoCapture对象,简单调用对象的open()方法。这里訪问摄像头的函数例如以下,首先在Qt中创建控制台项目。在main函数中加入:
int cameraNumber = 0; // 设定摄像头编号为0
if(argc > 1)
cameraNumber = atoi(argv[1]);
// 开启摄像头
cv::VideoCapture camera;
camera.open(cameraNumber);
if(!camera.isOpened())
{
qDebug() << "Error: Could not open the camera.";
exit(1);
}
// 调整摄像头的输出分辨率
camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
在摄像头被初始化后,能够使用C++流运算符将cv::VideoCapture对象转换成cv::Mat对象,这样能够获取视频的每一帧图像。
关于视频流读取可參考: http://blog.csdn.net/liyuefeilong/article/details/44066097
二、将帧图像转换为素描效果图片
要将一幅图像转换为素描效果图,能够使用不同的边缘检測算法实现。如经常使用的基于Sobel、Canny、Robert、Prewitt、Laplacian等算子的滤波器均能够实现这一操作,但处理效果各异。
1.Sobel算子:边缘检測中最经常使用的一种方法,在技术上它是以离散型的差分算子,用来运算图像亮度函数的梯度的近似值,缺点是Sobel算子并没有将图像的主题与背景严格地区分开来,换言之就是Sobel算子并没有基于图像灰度进行处理,因为Sobel算子并没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人惬意。
2.Robert算子:依据任一相互垂直方向上的差分都用来预计梯度。Robert算子採用对角方向相邻像素之差。
3.Prewitt算子:该算子与Sobel算子相似。仅仅是权值有所变化,但两者实现起来功能还是有差距的,据经验得知Sobel要比Prewitt更能准确检測图像边缘。
4.Laplacian算子:该算子是一种二阶微分算子,若仅仅考虑边缘点的位置而不考虑周围的灰度差时可用该算子进行检測。
对于阶跃状边缘。其二阶导数在边缘点出现零交叉,并且边缘点两旁的像素的二阶导数异号。
5.Canny算子:该算子的基本性能比前面几种要好。可是相对来说算法复杂。
Canny算子是一个具有滤波。增强,检測的多阶段的优化算子。在进行处理前。Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny切割算法採用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中。Canny算子还将经过一个非极大值抑制的过程。最后Canny算子还採用两个阈值来连接边缘。
相比Sobel等其它算子。Canny和Laplacian算子能得到更清晰的素描效果,而Laplacian的噪声抑制要优于Canny边缘检測,而其实素描边缘在不同帧之间经常有剧烈的变化,因此我们选择Laplacian边缘滤波器进行图像处理。
一般在进行Laplacian检測之前,须要对图像进行的预操作有:
- Laplacian算法仅仅能作用于灰度图像,因此须要将彩色帧图像进行转换;
- 平滑处理。这是因为图像的平滑处理降低了噪声的影响并且一定成都市抵消了由Laplacian算子的二阶导数引起的噪声影响。因此可使用中值滤波器来去噪。
void cartoon::cartoonTransform(cv::Mat &Frame, cv::Mat &output)
{
cv::Mat grayImage;
cv::cvtColor(Frame, grayImage, CV_BGR2GRAY);
// 设置中值滤波器參数
cv::medianBlur(grayImage, grayImage, 7);
// Laplacian边缘检測
cv::Mat edge; // 用于存放边缘检測输出结果
cv::Laplacian(grayImage, edge, CV_8U, 5);
// 对边缘检測结果进行二值化
cv::Mat Binaryzation; // 用于存放二值化输出结果
cv::threshold(edge, Binaryzation, 80, 255, cv::THRESH_BINARY_INV);
}
生成的素描效果:
三、将图像卡通化
在项目中调用一些运算量大的算法时,通常须要考虑到效率问题,比方这里将要用到的双边滤波器。
这里我们利用双边滤波器的平滑区域及保持边缘锐化的特性,将其运用到卡通图片效果生成中。
而考虑到双边滤波器执行效率较低,因此考虑在更低的分辨率中使用,这对效果影响不大,可是执行速度大大加快。
这里使用的策略是将要处理的图像的宽度和高度缩小为原来的1/2。经过双边滤波器处理后,再将其恢复为原来的尺寸。在函数cartoonTransform()中加入下面代码:
// 採用双边滤波器
// 因为算法复杂,因此需降低图像尺寸
cv::Size size = Frame.size();
cv::Size reduceSize;
reduceSize.width = size.width / 2;
reduceSize.height = size.height / 2;
cv::Mat reduceImage = cv::Mat(reduceSize, CV_8UC3);
cv::resize(Frame, reduceImage, reduceSize);
// 双边滤波器实现过程
cv::Mat tmp = cv::Mat(reduceSize, CV_8UC3);
int repetitions = 7;
for (int i=0 ; i < repetitions; i++)
{
int kernelSize = 9;
double sigmaColor = 9;
double sigmaSpace = 7;
cv::bilateralFilter(reduceImage, tmp, kernelSize, sigmaColor, sigmaSpace);
cv::bilateralFilter(tmp, reduceImage, kernelSize, sigmaColor, sigmaSpace);
}
// 因为图像是缩小后的图像。须要恢复
cv::Mat magnifyImage;
cv::resize(reduceImage, magnifyImage, size);
为了得到更好的效果。在以上代码中加入下面函数。将恢复尺寸后的图像与上一部分的素描结果相叠加。得到卡通版的图像~~
cv::Mat dst;
dst.setTo(0);
magnifyImage.copyTo(dst, Binaryzation);
//output = dst; //输出
卡通效果,阈值各方面有待优化:
四、简单地生成“怪物”形象
这里是结合了边缘滤波器和中值滤波器的还有一个小应用。即通过小的边缘滤波器找到图像中的各处边缘。之后使用中值滤波器来合并这些边缘。具体实现过程例如以下:
- 这里相同须要原图像的灰度图。因此格式转换依旧是必要的。
- 分别沿着x和y方向採用3*3的Scharr梯度滤波器(效果图);
- 使用截断值非常低的阈值进行二值化处理。
- 最后使用3*3的中值平滑滤波得到“怪物”掩码。
具体代码例如以下,相同在函数cartoonTransform()中加入:
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
五、人脸肤色变换
皮肤检測算法有非常多种,比方基于RGB color space、Ycrcb之cr分量+otsu阈值化、基于混合模型的复杂机器学习算法等。
因为这里仅仅是一个轻量级的应用,因此不考虑使用太复杂的算法。
考虑到未来要将这些图像处理算法移植到安卓上,而移动设备上的微型摄像头传感器对颜色的反应往往差异非常大,并且要在没有标定的情况下对不同肤色的人进行皮肤检測,因此对算法的鲁棒性要求较高。
这里使用了一个技巧,即在图像中规定一个区域,用户须要将脸部放到指定区域中来确定人脸在图像中的位置(其实有些手机应用也会採取这样的方法),对于移动设备来说这不是一件难事。
因此,我们须要规定人脸的区域,相同在函数cartoonTransform()中加入下面代码:
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
// 换肤模式
// 绘制脸部区域
cv::Mat faceFrame = cv::Mat::zeros(size, CV_8UC3);
cv::Scalar color = CV_RGB(128, 0, 128); // 颜色
int thickness = 4;
// 使之占整个图像高度的70%
int width = size.width;
int height = size.height;
int faceHeight = height/2 * 70/100;
int faceWidth = faceHeight * 72/100;
cv::ellipse(faceFrame, cv::Point(width/2, height/2), cv::Size(faceWidth, faceHeight),
0, 0, 360, color, thickness, CV_AA);
// imshow("test3", faceFrame);
// 绘制眼睛区域
int eyeHeight = faceHeight * 11/100;
int eyeWidth = faceWidth * 23/100;
int eyeY = faceHeight * 13/100;
int eyeX = faceWidth * 48/100;
cv::Size eyeSize = cv::Size(eyeWidth, eyeHeight);
int eyeAngle = 15; //角度
int eyeYShift = 11;
// 画右眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画右眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
// 画左眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画左眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
char *Message = "Put your face here";
cv::putText(faceFrame, Message, cv::Point(width * 13/100, height * 10/100),
cv::FONT_HERSHEY_COMPLEX,
1.0f,
color,
2,
CV_AA);
cv::addWeighted(dst, 1.0, faceFrame, 0.7, 0, dst, CV_8UC3);
//output = dst;
效果:
皮肤变色器的实现基于OpenCV的floodFill()函数,该函数相似于一些画图软件中的颜料桶(颜色填充)工具。 因为规定屏幕中间椭圆区域就是皮肤像素,因此仅仅须要对该区域的像素进行各种颜色的漫水填充就可以。
这里处理的图像是彩色图,而对于RGB格式的图像,改变颜色的效果不会太好,因为改变颜色须要脸部图像的亮度变化,而皮肤颜色也不能变化太大。这里使用YCrCb颜色空间来进行处理。在YCrCb颜色空间中,能够直接获得亮度值,并且通常的皮肤颜色取值唯一。
// 皮肤变色器
cv::Mat YUVImage = cv::Mat(reduceSize, CV_8UC3);
cv::cvtColor(reduceImage, YUVImage, CV_BGR2YCrCb);
int sw = reduceSize.width;
int sh = reduceSize.height;
cv::Mat mask, maskPlusBorder;
maskPlusBorder = cv::Mat::zeros(sh+2, sw+2, CV_8UC1);
mask = maskPlusBorder(cv::Rect(1, 1, sw, sh));
cv::resize(edge, mask, reduceSize);
const int EDGES_THRESHOLD = 80;
cv::threshold(mask, mask, EDGES_THRESHOLD, 255, cv::THRESH_BINARY);
cv::dilate(mask, mask, cv::Mat());
cv::erode(mask, mask, cv::Mat());
// output = mask;
// 创建6个点进行漫水填充算法
cv::Point skinPoint[6];
skinPoint[0] = cv::Point(sw/2, sh/2 - sh/6);
skinPoint[1] = cv::Point(sw/2 - sw/11, sh/2 - sh/6);
skinPoint[2] = cv::Point(sw/2 + sw/11, sh/2 - sh/6);
skinPoint[3] = cv::Point(sw/2, sh/2 + sh/6);
skinPoint[4] = cv::Point(sw/2 - sw/9, sh/2 + sh/6);
skinPoint[5] = cv::Point(sw/2 + sw/9, sh/2 + sh/6);
// 设定漫水填充算法的上下限
const int MIN_Y = 60;
const int MAX_Y = 80;
const int MIN_Cr = 25;
const int MAX_Cr = 15;
const int MIN_Cb = 20;
const int MAX_Cb = 15;
cv::Scalar Min = cv::Scalar(MIN_Y, MIN_Cr, MIN_Cb);
cv::Scalar Max = cv::Scalar(MAX_Y, MAX_Cr, MAX_Cb);
// 调用漫水填充算法
const int CONNECTED_COMPONENTS = 4;
const int flag = CONNECTED_COMPONENTS | cv::FLOODFILL_FIXED_RANGE \
| cv::FLOODFILL_MASK_ONLY;
cv::Mat edgeMask = mask.clone();
//
for(int i = 0; i < 6; i++)
{
cv::floodFill(YUVImage, maskPlusBorder, skinPoint[i], cv::Scalar(), NULL,
Min, Max, flag);
}
cv::Mat BGRImage;
cv::cvtColor(YUVImage, BGRImage, CV_YCrCb2BGR);
mask -= edgeMask;
int Red = 0;
int Green = 70;
int Blue = 0;
cv::Scalar color2 = CV_RGB(Red, Green, Blue); // 颜色
cv::add(BGRImage, color2, BGRImage, mask);
cv::Mat tt;
cv::resize(BGRImage, tt, size);
cv::add(dst, tt ,dst);
output = dst; // 换肤结果
因为在脸部区域中要对很多像素使用漫水填充算法,因此为了保证人脸图像的各种颜色和阴影都能得到处理,这里设置了前额、鼻子和脸颊6个点。他们的定位依赖于先前规定的脸部轮廓坐标。输出效果例如以下:
脸部不在识别区域内时:
脸部进入识别区域内时:
以上实现了几种图片卡通化效果,接着在学有余力时要对各种算法的效果进行优化。同一时候加入GUI界面,并将应用移植到移动设备上。
參考资料:《深入理解OpenCV:有用计算机视觉项目解析》
完整代码:
cartoon.h:
#ifndef CARTOON_H
#define CARTOON_H
#include
#include
#include
class cartoon
{
public:
void cartoonTransform(cv::Mat &Frame, cv::Mat &output);
};
#endif // CARTOON_H
cartoon.cpp:
#include "cartoon.h"
void cartoon::cartoonTransform(cv::Mat &Frame, cv::Mat &output)
{
cv::Mat grayImage;
cv::cvtColor(Frame, grayImage, CV_BGR2GRAY);
// 设置中值滤波器參数
cv::medianBlur(grayImage, grayImage, 7);
// Laplacian边缘检測
cv::Mat edge; // 用于存放边缘检測输出结果
cv::Laplacian(grayImage, edge, CV_8U, 5);
// 对边缘检測结果进行二值化
cv::Mat Binaryzation; // 用于存放二值化输出结果
cv::threshold(edge, Binaryzation, 80, 255, cv::THRESH_BINARY_INV);
// 下面操作生成彩色图像和卡通效果
// 採用双边滤波器
// 因为算法复杂,因此需降低图像尺寸
cv::Size size = Frame.size();
cv::Size reduceSize;
reduceSize.width = size.width / 2;
reduceSize.height = size.height / 2;
cv::Mat reduceImage = cv::Mat(reduceSize, CV_8UC3);
cv::resize(Frame, reduceImage, reduceSize);
// 双边滤波器实现过程
cv::Mat tmp = cv::Mat(reduceSize, CV_8UC3);
int repetitions = 7;
for (int i=0 ; i < repetitions; i++)
{
int kernelSize = 9;
double sigmaColor = 9;
double sigmaSpace = 7;
cv::bilateralFilter(reduceImage, tmp, kernelSize, sigmaColor, sigmaSpace);
cv::bilateralFilter(tmp, reduceImage, kernelSize, sigmaColor, sigmaSpace);
}
// 因为图像是缩小后的图像,须要恢复
cv::Mat magnifyImage;
cv::resize(reduceImage, magnifyImage, size);
cv::Mat dst;
dst.setTo(0);
magnifyImage.copyTo(dst, Binaryzation);
//output = dst; //输出
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
// 换肤模式
// 绘制脸部区域
cv::Mat faceFrame = cv::Mat::zeros(size, CV_8UC3);
cv::Scalar color = CV_RGB(128, 0, 128); // 颜色
int thickness = 4;
// 使之占整个图像高度的70%
int width = size.width;
int height = size.height;
int faceHeight = height/2 * 70/100;
int faceWidth = faceHeight * 72/100;
cv::ellipse(faceFrame, cv::Point(width/2, height/2), cv::Size(faceWidth, faceHeight),
0, 0, 360, color, thickness, CV_AA);
// imshow("test3", faceFrame);
// 绘制眼睛区域
int eyeHeight = faceHeight * 11/100;
int eyeWidth = faceWidth * 23/100;
int eyeY = faceHeight * 13/100;
int eyeX = faceWidth * 48/100;
cv::Size eyeSize = cv::Size(eyeWidth, eyeHeight);
int eyeAngle = 15; //角度
int eyeYShift = 11;
// 画右眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画右眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
// 画左眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画左眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
char *Message = "Put your face here";
cv::putText(faceFrame, Message, cv::Point(width * 13/100, height * 10/100),
cv::FONT_HERSHEY_COMPLEX,
1.0f,
color,
2,
CV_AA);
cv::addWeighted(dst, 1.0, faceFrame, 0.7, 0, dst, CV_8UC3);
//output = dst;
// 皮肤变色器
cv::Mat YUVImage = cv::Mat(reduceSize, CV_8UC3);
cv::cvtColor(reduceImage, YUVImage, CV_BGR2YCrCb);
int sw = reduceSize.width;
int sh = reduceSize.height;
cv::Mat mask, maskPlusBorder;
maskPlusBorder = cv::Mat::zeros(sh+2, sw+2, CV_8UC1);
mask = maskPlusBorder(cv::Rect(1, 1, sw, sh));
cv::resize(edge, mask, reduceSize);
const int EDGES_THRESHOLD = 80;
cv::threshold(mask, mask, EDGES_THRESHOLD, 255, cv::THRESH_BINARY);
cv::dilate(mask, mask, cv::Mat());
cv::erode(mask, mask, cv::Mat());
// output = mask;
// 创建6个点进行漫水填充算法
cv::Point skinPoint[6];
skinPoint[0] = cv::Point(sw/2, sh/2 - sh/6);
skinPoint[1] = cv::Point(sw/2 - sw/11, sh/2 - sh/6);
skinPoint[2] = cv::Point(sw/2 + sw/11, sh/2 - sh/6);
skinPoint[3] = cv::Point(sw/2, sh/2 + sh/6);
skinPoint[4] = cv::Point(sw/2 - sw/9, sh/2 + sh/6);
skinPoint[5] = cv::Point(sw/2 + sw/9, sh/2 + sh/6);
// 设定漫水填充算法的上下限
const int MIN_Y = 60;
const int MAX_Y = 80;
const int MIN_Cr = 25;
const int MAX_Cr = 15;
const int MIN_Cb = 20;
const int MAX_Cb = 15;
cv::Scalar Min = cv::Scalar(MIN_Y, MIN_Cr, MIN_Cb);
cv::Scalar Max = cv::Scalar(MAX_Y, MAX_Cr, MAX_Cb);
// 调用漫水填充算法
const int CONNECTED_COMPONENTS = 4;
const int flag = CONNECTED_COMPONENTS | cv::FLOODFILL_FIXED_RANGE \
| cv::FLOODFILL_MASK_ONLY;
cv::Mat edgeMask = mask.clone();
//
for(int i = 0; i < 6; i++)
{
cv::floodFill(YUVImage, maskPlusBorder, skinPoint[i], cv::Scalar(), NULL,
Min, Max, flag);
}
cv::Mat BGRImage;
cv::cvtColor(YUVImage, BGRImage, CV_YCrCb2BGR);
mask -= edgeMask;
int Red = 0;
int Green = 70;
int Blue = 0;
cv::Scalar color2 = CV_RGB(Red, Green, Blue); // 颜色
cv::add(BGRImage, color2, BGRImage, mask);
cv::Mat tt;
cv::resize(BGRImage, tt, size);
cv::add(dst, tt ,dst);
output = dst; // 换肤结果
}
main函数:
#include "cartoon.h"
#include
#include
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
cartoon photo;
int cameraNumber = 0;
if(argc > 1)
cameraNumber = atoi(argv[1]);
// 开启摄像头
cv::VideoCapture camera;
camera.open(cameraNumber);
if(!camera.isOpened())
{
qDebug() << "Error: Could not open the camera.";
exit(1);
}
// 调整摄像头的分辨率
camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
while (1)
{
cv::Mat Frame;
camera >> Frame;
if(!Frame.data)
{
qDebug() << "Couldn't capture camera frame.";
exit(1);
}
// 创建一个用于存放输出图像的数据结构
cv::Mat output(Frame.size(), CV_8UC3);
photo.cartoonTransform(Frame, output);
// 使用图像处理技术将获取的帧经过处理后输入到output中
cv::imshow("Original", Frame);
cv::imshow("Carton", output);
char keypress = cv::waitKey(20);
if(keypress == 27)
{
break;
}
}
return a.exec();
}
版权声明:本文博主原创文章。博客,未经同意不得转载。