OpenCV中有两个程序可以训练级联分类器: opencv_haartraining 和opencv_traincascade。opencv_traincascade 是一个新程序,使用OpenCV 2.x API 以C++ 编写。这二者主要的区别是 opencv_traincascade 支持 Haar、Hog和 LBP(Local Binary Patterns) 三种特征,并易于增加其他的特征。与Haar特征相比,LBP特征是整数特征,因此训练和检测过程都会比Haar特征快几倍。LBP和Haar特征用于检测的准确率,是依赖训练过程中的训练数据的质量和训练参数。训练一个与基于Haar特征同样准确度的LBP的分类器是可能的。
opencv_traincascade and opencv_haartraining 所输出的分类器文件格式并不相同。注意,新的级联检测接口(参考 objdetect 模块中的 CascadeClassifier 类)支持这两种格式。 opencv_traincascade 可以旧格式导出训练好的级联分类器。但是在训练过程被中断后再重启训练过程, opencv_traincascade and opencv_haartraining 不能装载与中断前不同的文件格式。
opencv_traincascade 程序使用TBB来处理多线程。如果希望使用多核并行运算加速,请使用TBB来编译OpenCV。还有一些与训练相关的辅助程序。
opencv_createsamples 用来准备训练用的正样本数据和测试数据。 opencv_createsamples 能够生成能被 opencv_haartraining 和 opencv_traincascade 程序支持的正样本数据。它的输出为以 *.vec 为扩展名的文件,该文件以二进制方式存储图像。
opencv_performance 可以用来评估分类器的质量。它读入一组标注好的图像,运行分类器并报告性能,如检测到物体的数目,漏检的数目,误检的数目,以及其他信息。
http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/user_guide/ug_traincascade.html
http://blog.csdn.net/delltdk/article/details/9186875
1 建立样本集pos和neg
pos中存放正样本的图像,可以是一张图像或多张图像。neg中存放包含负样本的图像,可以是任意图像,但是这些图像中不能包含待检测的物体。
注意:
一般需要一个很大的负样本库送给训练程序进行训练。如果是绝对刚性的物体,如OpenCV的标志,只有一张正样本图像,那么可以通过对物体图像的随机旋转、改变标志亮度以及将标志放在任意的背景上而获得大量的正样本;如果是人脸,需要几百甚至几千个正样本。在待检测物体是人脸的情况下,需要考虑所有的人种、年龄、表情甚至胡子的样式。
2 生成正\负样本描述文件negdata.txt
(1) 负样本描述文件negdata.txt
在命令行窗口输入“cd d:\%....%\pos”路径切换到neg文件夹下,输入“dir/b>negdata.txt”就会在neg文件夹下生成描述文件negdata.txt,存放neg中所有图片的文件名,注意要去掉最后的negdata.txt 。
注意:
如果把negdata.txt保存在neg文件夹外面,需要在每一行外面添加相对路径或绝对路径来指出样本的所在位置。一种方法是将negdata.txt 文件中的内容复制到word中,使用替换功能实现快速修改。neg替换为neg\neg
(2) 正样本描述文件posdata.txt
与建立negdata.txt一样建立posdata.txt,只不过在正样本描述文件中需要指出目标在每个样本中的数量和位置如pos\1.bmp 1 x1 y1 x2 y2,其中(x1,y1,x2,y2)为目标所在的矩形框,又如pos\1.bmp2 x1 y1 x2 y2 x1’ y1’ x2’ y2’
因为我们准备的正样本基本都是目标,因此只需在文件名后增加1 0 0 width height即可。
注意:
1若正样本图像是不同尺寸的,一方面可以使用ImageResize或matlab将图像统一成同一尺寸在生成posdata.txt,或程序ImageToTxt直接生成具有不同尺寸图像的正样本描述文件。在此阶段不归一化的话,后续生成.vec文件的时候在程序中自动归一化。
2样本描述文件与图像要一致,可以存在图像但没有写在描述文件中,即有多余的图像,但千万不要在描述文件中写不存在的图像。
问题:
归一化尺寸的方法是否对训练结果有影响? opencv_createsamples 采用何种方法进行归一化?
sample = cvCreateImage( cvSize(winwidth, winheight ), IPL_DEPTH_8U, 1 );
fscanf( info, "%d %d %d%d", &x, &y, &width, &height )
cvSetImageROI( src, cvRect( x, y, width,height ) );
cvResize( src, sample,
width >=sample->width && height >= sample->height ? CV_INTER_AREA :CV_INTER_LINEAR );
CV_INTER_NN 最近邻插值
CV_INTER_LINER 双线性插值,缺省情况
CV_INTER_AREA 使用像素关系重采样,当图像缩小时候可以避免波纹出现,当图像方法时类似CV_INTER_NN
CV_INTER_CUBIC 立方插值
3 生成.vec文件
生成的正样本数目以及随机的程度都可以通过 opencv_createsamples 的命令行参数控制。
使用create.dat调用%Opencv%\vs2008\bin\Release\ opencv_createsamples.exe
在createsamples.cpp中查阅参数设置
-info 输入正样本描述文件,默认NULL
-img 输入图像文件名,默认NULL
-bg 负样本描述文件,文件中包含一系列的被随机选作物体背景的图像文件名,默认NULL
-num 生成正样本的数目,默认1000
-bgcolor 背景颜色,表示透明颜色,默认0
-bgthresh 颜色容差,所有处于bgcolor-bgthresh和bgcolor+bgthresh之间的像素被置为透明像素,也就是将白噪声加到前景图像上,默认80
-inv 前景图像颜色翻转标志,如果指定颜色翻转,默认0(不翻转)
-randinv 如果指定颜色将随机翻转,默认0
-maxidev 前景图像中像素的亮度梯度最大值,默认40
-maxxangle X轴最大旋转角度,以弧度为单位,默认1.1
-maxyangle Y轴最大旋转角度,以弧度为单位,默认1.1
-maxzangle Z轴最大旋转角度,以弧度为单位,默认0.5
输入图像沿着三个轴进行旋转,旋转角度由上述3个值限定。
-show 如果指定,每个样本都将被显示,按下Esc键,程序将继续创建样本而不在显示,默认为0(不显示)
-scale 显示图像的缩放比例,默认4.0
-w 输出样本宽度,默认24
-h 输出样本高度,默认24
-vec 输出用于训练的.vec文件,默认NULL
将正样本描述文件中的正样本转换为灰度图像并缩放到-w-h尺寸存入vec文件中。
(1)如果设置-img和-vec
调用cvCreateTrainingSamples,采用一张图像创建训练样本
(2)如果设置-img、-bg和-info
调用cvCreateTestSamples,采用一张图像创建测试样本。-bg在这里又有什么作用?目的是作为背景创建测试图像。
(3)如果设置-info和-vec(采用正样本描述文件中的图像创建训练样本)
调用cvCreateTrainingSamplesFromInfo,在cvCreateTrainingSamplesFromInfo中将读取样本,并resize后调用icvWriteVecHeader和icvWriteVecSample创建vec文件。
(4)如果只设置-vec(只显示vec文件中的样本)
调用cvShowVecSamples查看和检查保存在vec文件中正样本
上述参数在create.dat中设置好,最后有一个pause,等待显示结果:Done.Created num samples
4 训练过程
使用train.dat调用%Opencv%\vs2008\bin\Release\ opencv_traincascade.exe
在traincascade.cpp中查阅参数设置
1. 基本参数
-data 目录名,存放训练好的分类器,如果不存在训练程序自行创建
-vec 正样本.vec文件,由opencv_createsamples生成
-bg 负样本描述文件
-numPos 每级分类器训练时所用到的正样本数目
-numNeg 每级分类器训练时所用到的负样本数目,可以大于-bg指定的图片数目
-numStages 训练分类器的级数
-precalcValBufSize 缓存大小,用于存储预先计算的特征值,单位MB
-precalcIdxBufSize 缓存大小,用于存储预先计算的特征索引,单位M币
-baseFormatSave 仅在使用Haar特征时有效,如果指定,级联分类器将以老格式存储
2. 级联参数cascadeParams
-stageType 级联类型,staticconst char* stageTypes[] = { CC_BOOST };
-featureType 特征类型,staticconst char* featureTypes[] = { CC_HAAR, CC_LBP, CC_HOG };
-w
-h 训练样本的尺寸,必须跟使用opencv_createsamples创建的训练样本尺寸保持一致
3. Boosted分类器参数stageParams
-bt Boosted分类器类型
DAB-discrete Adaboost, RAB-RealAdaboost, LB-LogiBoost, GAB-Gentle Adaboost
-minHitRate 分类器的每一级希望得到的最小检测率,总的最大检测率大约为
min_hit_rate^number_of_stages
-maxFalseAlarmRate 分类器的每一级希望得到的最大误检率,总的误检率大约为
max_false_rate^number_of_stages
-weightTrimRate Specifies whether trimming should beused and its weight. 一个还不错的数值是0.95
-maxDepth 弱分类器的最大深度,一个不错数值是1,二叉树
-maxWeightCount 每一级中弱分类器的最大数目
4. Haar特征参数featureParams
-mode 训练过程使用的Haar特征类型,CORE-Allupright ALL-All Features BASIC-Viola
上述参数设置好后调用CvCascadeClassifier::train进行训练
将上述内容在train.dat中编辑好,运行即可。训练最终生成一个-data指定级联分类器的文件夹和一个cascade.xml文件,其余文件都是中间结果,当训练程序被中断之后,再重新运行训练程序将读入之前的训练结果,无需从头重新训练,训练结束后可以删除这些文件。
在cascade.xml文件中主要有stageType,featureType,width,height,stageParams,featureParams,stageNum,stages和features节点。
stages中的stage数目是自己设定的,每个stage又包含多个weakClassifiers,每个weakClassifier又包含一个internalNodes和一个leafValues。internalNodes中四个变量代表一个node,分别为node中的left/right标记,特征池中的ID和threshold。leafValues中两个变量代表一个node,分别为leftleaf和right leaf值。
features是分类器的特征池,每个Haar特征包含一个矩形rect和要提取的特征序号,每个Hog特征/LBP特征包含一个矩形。
注意:
1. 包含负样本的图像一定不小于在create中设置的尺寸
负样本图像可以是不同的尺寸,但是图像尺寸应该比训练窗口的尺寸大,在使用负样本图像时,OpenCV自动从负样本图像中抠出一块和正样本同样大小的区域作为负样本。具体可查阅icvGetNextFromBackgroundData,具体抠图过程为:
1) 确定抠图区域的左上角坐标(Point.x, Point.y)
2) 确定一个最小缩放比例,使得原负样本图像缩放后恰好包含选中负样本区域
3) 对原负样本图象按计算好的缩放比例进行缩放
4) 在缩放后的图像上抠出负样本。
2. –numPos一般比实际正样本数量少200-300,-numNeg是否存在同样的情况?正负样本选择规则?
如果出现:训练停留在一个分类器长达几小时没有相应,问题出现在取负样本的那个函数 icvGetHaarTrainingDataFromBG中;只有当之前的强分类器对负样本集内的样本全部分类正确时才会出现死循环,因为只要有一个样本会被错分为正样本,那么通过count次扫描整个负样本集就能得到count个负样本,当然这count个负样本实际上就是一个负样本的count个拷贝。为避免这种情况,负样本集中的样本数需要足够多 。
不过此时的分类器已经完全额、可以使用,因为它的误检率已经很低,从实用性上时没有任何问题的。所以我们可以通过设置-nstages 这个参数来限制分类器级数,适当时候停止并生成xml文件。
从CvCascadeBoost::train中去查阅
函数 poscount= icvGetHaarTrainingDataFromVec( training_data, 0, npos,
(CvIntHaarClassifier*)tcc, vecfilename, &consumed )负责从正样本集*.vec 文件中载入 count(npos)个正样本。在程序第一次运行到此(即训练第一个分类器之前)时,只要正样本集中有 count 个样本,就一定能取出 count 个正样本。在以后运行到此时,有可能取不到 count 个样本,因为
必须是用前面的级联强分类器((CvIntHaarClassifier*) tcc)分类为正样本(即分类正确的样本)的样本才会被取出作为下一个强分类器训练样本,具体可参考 icvGetHaarTrainingData和icvEvalTreeCascadeClassifierFilter函数。
训练负样本,具体可参考icvGetHaarTrainingDataFromBG和icvEvalTreeCascadeClassifierFilter函数。
int icvGetHaarTrainingDataFromBG(CvHaarTrainingData* data, int first, int count,
CvIntHaarClassifier*cascade, double* acceptance_ratio, const char * filename = NULL )
传递返回值的 acceptance_ratio 参数记录的是实际取出的负样本数与查询过的负样本数(如果通过前面级联stage强分类器的负样本数很少时,那么程序会循环重复读取负样本,并用thread_consumed_count计数)之比(acceptance_ratio = ((double) count) / consumed_count),也就是虚警率,用于判断已训练的级联分类器是否达到指标,若达到指标,则停止训练过程。
注意函数 icvGetHaarTrainingData中一个主要的 For 循环:
for( i = first; i < first +count; i++ ) //共读取 count 个负样本,当读取不到
{ //这么多负样本时将出现死循环!
对上面代码中的注释有必要进一步说明一下:只有当之前的强分类器对负样本集内的样本全部分类正确时才会出现死循环。因为只要有一个样本会被错分为正样本,那么通过 count次扫描整个负样本集就能得到 count 个负样本,当然这 count 个负样本实际上就是一个负样本的 count 个拷贝。为避免这些情况的发生,负样本集中的样本数需要足够多。
在负样本图像大小与正样本大小完全一致时,假设最终的分类器虚警率要求是falsealarm,参加训练的负样本要求是 count 个,则需要的负样本总数可计算如下: TotalCount = count / falsealarm
以 Rainer Lienhart 的文章中的一些参数为例,falsealarm=0.5^20=9.6e-07, count=3000,
则 TotalCount=3000/(0.5^20)= 3,145,728,000=31 亿。
函数 icvGetHaarTrainingDataFromBG ()负责从负样本集中载入 count 个负样本。在程序第一次运行到此(即训练第一个分类器之前)时,只要负样本集中有 count 个样本,就一定能取出 count 个负样本。在以后运行到此时,有可能取不到 count 个样本,因为必须是用前面的级联强分类器分类为正样本的样本(即分类错误的样本)才会被取出作为下一个强分类器的负样本输入。
对于int icvGetHaarTrainingData( CvHaarTrainingData* data,int first, int count,
CvIntHaarClassifier*cascade,
CvGetHaarTrainingDataCallbackcallback, void* userdata,
int*consumed, double* acceptance_ratio )
这个函数的解释:
这是个对于读取正负样本通用的函数,区别在于callback的调用。在这个函数中有个变量thread_getcount,表示将样本分为正样本的数目(不论这个样本是负样本还是正样本)。
传递返回值的 Consumed 参数表示为取 count 个正样本,查询过的正样本总数。对于负样本为空(null),没有返回值。
3. 之前遇到过10*20的不能训练Hog特征的分类器?Hog特征是否存在尺寸限制?
查阅Hog特征的计算方法,Opencv中HogDescriptor
训练方法大同小异,只是提取的特征是不一样的,流程是一样的
4. 在用opencv_traincascade训练分类器的时候,遇到了报错如下:
Train dataset for temp stage can not be filled. Branch training terminated.
在stackoverflow上查了一下,问题的根源在于负样本读取失败,导致问题的原因有:
1)负样本描述文件neg.txt不能带路径名,即 : -bg neg.txt 是合法的, -bg negdata/neg.txt是非法的。所以必须把neg.txt文件跟exe文件放在同一个目录下
2)当切换了操作系统时,会因为txt文件的格式问题而导致了负样本读取失败。比如:在windows操作系统下生出了neg.txt,但是在ubuntu下进行训练,这样就会导致错误,这是因为windows下txt文件换行符'\r'在ubuntu下无法识别
3)负样本图片的问题。我遇到的报错原因就是用opencv处理负样本后保存时没有检验负样本是否为空,即直接把空的mat写入了文件,导致了报错
5. 其它注意
1 关于正样本,首先正样本不是有些人说的,你实际有300个正样本,在traincascade的时候可以写成3000的,这种思路是没有用的。在采集正样本的时候你一定要注意保持所有样本宽高比大致相同,如果你自己截图,推荐使用光影魔术手。或者牛逼的你自己写个gui截图工具。为了避免出现opencv error,在用opencv_traincascade.exe的时候,-numPos要稍微低于实际的正样本数目,比如你有2100个你就可以将numpos设为1900-2000,
2 无论正样本负样本,图片命名时不要用特殊字符,你就规规矩矩的命名pos1.jpg 。。。。。等等,特殊字符包括(),会出现opencv error,或者无法识别。
3 正负样本比例1:2.5~1:3,曾经有篇文章中说,为了减小false positive ,可以加大负样本数目。
4 当出现内存不够的情况时,有几种方法:1 你可以在64为pc上跑,2 减小正负样本的数目。3 减小正样本的宽高。
参考链接:
https://blog.csdn.net/xidianzhimeng/article/details/10470839
opencv训练自己的xml分类器以及如何获取opencv_createsamples.exe和opencv_traincascade.exe_Lizaozao96的博客-CSDN博客
(终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测_opencv 物体识别_ζั͡ ั͡雾 ั͡狼 ั͡✾的博客-CSDN博客
【OpenCV学习记录】级联分类器训练与测试_Silence涂的博客-CSDN博客
【cascades训练】 使用Cascade Trainer GUI进行级联分类器训练_十年一梦实验室的博客-CSDN博客
[Opencv]Cascade级联分类器_cascade分类器_Chris_Liu_的博客-CSDN博客
OpenCV中级联分类器Cascade训练方法(Linux)_opencv级联分类器_灼灼其华R的博客-CSDN博客
常见问题
我尝试使用opencv_traincascade.exe,但它启动后几秒钟崩溃。这是我如何进行:opencv_traincascade崩溃没有解释
这是因为您选择的窗口大小。 traincascade.exe用于训练分类器的内存随着窗口大小成指数增长,并且有几台家用计算机可以处理traincascade中的100x100窗口大小。
执行此练习:打开任务管理器并监视内存使用情况当你开始训练时,你会注意到当程序不能分配更多内存时程序崩溃。 要解决这个问题,你必须选择一个更小的窗口大小。
您可以使用参数-precalcValBufSize 1024和-precalcIdxBufSize 1024.如果喜欢,可以增加数字,但需要将值放在RAM容量以下
楼主你好。我也在训练分类器,目前的问题是但是只训练了一层,识别率就到了1,FA=0,然后训练stage1就无限卡住,问题究竟出在哪里?
训练过程命令如下:
opencv_traincascade -data classifier -vec pos.vec -vg neg.txt -numPos 11000 -numNeg 26545 -numStage 15 -precalcValBufSize 10240 -precalcIdxBufSize 10240 -w 64 -h 64 -mode ALL
正样本12519张、负样本26545张、都是64x64尺寸,训练stage设置为15层。
参数都是比较正常的,为什么训练完stage0 结果就是HR=1,FA=0
再就卡到stage1了,头疼??
-
-
负样本数目设置不对,设定为总数的一半,或者其他值,反正要少与实际有的数目
-
-
你好...我看设置训练参数那...只是设置了每个强分类器包含的最大的弱分类器个数maxWeakCount...那每个强分离包含的弱分类器个数是怎么设定的?...
-
-
样本路径不对
-
vCascadeClassifier::train()这个函数概述了整个Cascade的执行过程。包括训练前的初始化,各Stage的强分类器间的样本集更新及强分类器训练都可看到其踪影,最显眼的还是其中的Stage训练的for大循环。
更新stage间的样本, 要做到训练新的stage的时候仍保障正负样本数足够.
if ( !updateTrainingSet( tempLeafFARate ) )
{
样本数不够, 退出训练
cout << "Train dataset for temp stage can not be filled. Branch training terminated." << endl;
break;
}
if( tempLeafFARate <= requiredLeafFARate )
{
虚警率已经达标 不再继续训练
cout << "Required leaf false alarm rate achieved. Branch training terminated." << endl;
break;
}
编辑 网页链接里面的函数解析这么说的
-
-
zhi_jin2016.05.20
楼主你好噻,我把参数改来改去都一直出现这样的错误,可以帮忙看看问题出在哪里么~?
正:18587 负:10926
根据公式:numpos<=(18587-10926)/(1+15*0.001) numpos<=7547.78...
E:\train>opencv_traincascade.exe -data xml -vec pos1.vec -bg negdata\neg.txt -numpos 7547 -numneg 10926 -numstages 16 -minHitRate 0.999 -featureType HAAR -w 20
-h 20
PARAMETERS:
cascadeDirName: xml
vecFileName: pos1.vec
bgFileName: negdata\neg.txt
numPos: 2000
numNeg: 1000
numStages: 20
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HAAR
sampleWidth: 20
sampleHeight: 20
boostType: GAB
minHitRate: 0.999
maxFalseAlarmRate: 0.5
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
===== TRAINING 0-stage =====
<BEGIN
POS count : consumed 2000 : 2000
Train dataset for temp stage can not be filled. Branch training terminated.
Cascade classifier can't be trained. Check the used training parameters. -
-
请问一下,你最后改成功了吗?咋改的啊,我也遇到同样的问题
-
你好。能过能发我一份正样本和反样本。我的邮箱1079666237@qq.com 谢谢
-
-
我现在遇到的问题是:
1、虚警率有些高,检测时经常会误检;
我使用正样本大约4000个,负样本大约8000个,minHitRate=0.997,maxFalseRate=0.5
我现在的命中率很高,请问怎么降低误检率?
2、第二个问题是如何调各个参数?如何提高样本的质量?如何评价样本的质量?谢谢! -
-
回复wangyuezhuiyi2016.11.08
我的一直出错OpenCV Error: Bad argument (Can not get new positive sample. The most possible reason is insufficient count of samples in given vec-file.
能不能给我个无限制条件能直接下载的地址,或者发我邮箱1079666237@qq.com 谢谢
-
-
kissmemissme2016.05.09
你好楼主,我要用随机森林算法训练分类器怎么用呢
-
楼主,能不能麻烦发一份源代码呢,非常感谢!!
-
-
回复第二个半价2016.04.29
源码可以从opencv中找哈,在sources/apps/traincascade目录下
-
-
为什么我把-numPos改成3200,程序自己只能识别2000呢,这是为什么啊,每次都是这样,楼主请您解答啊,非诚感谢。负样本改成什么数都可以识别
-
-
浅浅_mo回复lee3333332016.11.28
有可能是你-numPos的参数名中的p没有写成大写
-
-
opencv_traincascade.exe,楼主能不能发给我一份呢,我的是2.4.6版本,为啥电脑就是找不到,用2.4.9版本出现很多问题,我的邮箱是xinxueab@163.com
-
-
西电之梦作者回复天使之一2014.10.13
给你发过去了,你看一下。
-
-
chen123123_sh2014.06.11
HOG和HAAR训练的模式是否相同?
-
-
西电之梦作者回复chen123123_sh2014.10.13
训练方法大同小异,只是提取的特征是不一样的,流程是一样的
-
-
楼主你好,我想问下xml里的leafvalue,它leftvalue和rightvalue是怎么算出来的呢?还有featureIdex和featcomponent的区别是啥呀
-
-
西电之梦作者回复hwl567892014.10.13
featureIndex表示特征池中的Id号(与features中的特征个数相对应),featureComponent表示HOG特征的特征序号(0-35)
-
-
a201c501ys2014.02.23
楼主你好,我的正样本250张10*40大小的,负样本1000张跟正样本一样大小且不包含正样本图像的,但训练到第四层就卡住,不知道怎么回事?
-
-
你好,请问训练的正负样本有没有上限呢
我觉得是训练样本数太少了,陷入了极小值而不能跳出,你试着多增加点样本试试。采用adaboost训练的时候样本数量最好足够多。
-
-
楼主您好,刚开始用opencv的分类器,看您的文章的时候有些疑惑。“使用create.dat调用%Opencv%\vs2008\bin\Release\ opencv_createsamples.exe”这句话什么意思??文章来源:https://www.toymoban.com/news/detail-581571.html
-
那句话的意思就是自己写一个扩展名为dat的文件,里面包含下面的参数设置,相信看看opencv_createsamples程序的源文件您就明白了,dat文件就是main函数中argv参数,argv[0]=应用程序名,也就是 opencv_createsamples.exe文章来源地址https://www.toymoban.com/news/detail-581571.html
到了这里,关于OpenCV分类检测器训练的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!