SVM简单实用教程——从采集到训练再到应用

暑假的时候因为参加机器人类的比赛所以用到了机器视觉,在准备的过程中,我们发现,如果只是应用简单的反投影(之前的博客中写过的Opencv直方图反投影检测颜色),无论是调整颜色通道HSV,RGB,Lab,还是通过代码自动调整对比度,亮度,黑白图都无法取得想要的结果。最终,我们采用了利用SVM训练模型进行识别的方案。
本文仅整理SVM相关代码和用法,不涉及机器人的控制和策略等。而且,因为是封装代码,所以加入了许多我们团队用得到但是其他人不一定用得到的代码,所以请各位斟酌自行跳过。
一、配置要求
仅考虑SVM的话,为了集成部署方便,我们采用Opencv库中自带的相关库,使用Opencv320版本。
二、样本采集
数据采集很简单,可以直接利用摄像头读取,并传回终端(电脑),利用系统自带的截图就可以了。不过要注意在采样的时候综合考虑各种情况,例如光照,对比度。个人建议是尽量把所有现场颜色都囊括进去,训练的样本越丰富,相对应的效果肯定相对更好一些。至于采集的代码,可以直接用Opencv自带的VideoCapture调用。
三、训练集制作
这份代码有一个缺点是只能处理一张图片(其实也可以写一段代码处理多个图片,但是因为不知道具体每次几张所以就单写了一份代码用于做一张特别大的图片),所以我写了一份代码用于将多张图片合成一张(其实就是并排放,而且实现方法特别low)。这样就可以同时将几份样本一起训练了


#include <opencv2/opencv.hpp>
#include <iostream>
#include <cstdio>
using namespace cv;
using namespace std;
#define filenum 3
int main()
{
  
  Mat finalimage(240,filenum*320, CV_8UC3);
  Mat image1=imread("/home/Pictures/pu1.png");
  Mat image2=imread("/home/Pictures/pu2.png");
  Mat image3=imread("/home/Pictures/pu3.png");
  //读取图片
  int num=0;
  //每一段双重循环都是一次将图片重新读取并在新图中复现的过程
  num++;
  for(int j=0;j<240;j++)
  {
  	for(int i=(num-1)*320;i<num*320;i++)
  	{
  		
  		finalimage.at<Vec3b>(j,i)[0]=image1.at<Vec3b>(j,i%320)[0];
  		finalimage.at<Vec3b>(j,i)[1]=image1.at<Vec3b>(j,i%320)[1];
  		finalimage.at<Vec3b>(j,i)[2]=image1.at<Vec3b>(j,i%320)[2];
  	}
  }
  
  num++;
  for(int j=0;j<240;j++)
  {
  	for(int i=(num-1)*320;i<num*320;i++)
  	{
  		finalimage.at<Vec3b>(j,i)[0]=image2.at<Vec3b>(j,i%320)[0];
  		finalimage.at<Vec3b>(j,i)[1]=image2.at<Vec3b>(j,i%320)[1];
  		finalimage.at<Vec3b>(j,i)[2]=image2.at<Vec3b>(j,i%320)[2];
  	}
  }
  num++;
  for(int j=0;j<240;j++)
  {
  	for(int i=(num-1)*320;i<num*320;i++)
  	{
  		finalimage.at<Vec3b>(j,i)[0]=image3.at<Vec3b>(j,i%320)[0];
  		finalimage.at<Vec3b>(j,i)[1]=image3.at<Vec3b>(j,i%320)[1];
  		finalimage.at<Vec3b>(j,i)[2]=image3.at<Vec3b>(j,i%320)[2];
  	}
  }
  imshow("final",finalimage);
  waitKey();
  imwrite("finalimage.png",finalimage);
  return 0;
}

好吧,这确实是一个特别low的方法,上面是一个把三张图片合成一张的代码,如果想放n张,首先要修改Mat finalimage(240,filenum*320, CV_8UC3);中对于图片最终长宽的设置,240和320是原图片的宽和长,然后复制Mat image3=imread("/home/Pictures/pu3.png");和双重循环那一段,有几张图片就复制几次…好吧,写到这里有点后悔当时为什么不换个实现方法。
总之,通过这段代码就可以将图片合成了。
四、训练SVM模型
先贴一段训练代码,具体讲解附在注释中

#include "opencv2/opencv.hpp"
using namespace cv;
using namespace cv::ml;
using namespace std;
/*
之所以说每次只能处理一张图片是因为在这一段代码执行过程中,首先选择目标颜色,也就是正样本,按‘c’键切换以后选择的都是负样本,所以正样本只有一种颜色。选择完毕后,按‘q’键开始训练,并将结果保存在filename中。
*/
#define filename "svmPurple_bi.xml"

Mat img, image, orig;
Mat targetData, backData;
bool flag = true;
string wdname = "image";

//这里都是函数声明,具体定义在下面

//鼠标选择函数定义
void on_mouse(int event, int x, int y, int flags, void* ustc);

//
void getTrainData(Mat &train_data, Mat &train_label);
//训练代码
Mat svm();

//训练结束后用于展示训练成果
void svmFind(Mat bimage);


int main(int argc, char** argv)
{
//首先选择样本
	string path = "/home/worktree2018/combine/finalimage.png";
	orig = imread(path);
	//转换为Lab通道进行训练
	cvtColor(orig, img, COLOR_BGR2Lab);
//无论原图像大小如何,统一转换成下面大小
	resize(img, img, Size(800, 400));
	resize(orig, orig, Size(800, 400));
	img.copyTo(image);
	//无法读取的判断
	if (img.empty())
	{
		cout << "Image load error";
		return 0;
	}
	namedWindow(wdname);
	//人工取点
	setMouseCallback(wdname, on_mouse, 0);

    //这是为了能在各种运行状态随时切换
	for (;;)
	{
		imshow("image", orig);

		int c = waitKey(0);
		if ((c & 255) == 27)
		{
			cout << "Exiting ...\n";
			break;
		}
		if ((char)c == 'c')
		{
			flag = false;
		}
		if ((char)c == 'q')
		{
			destroyAllWindows();
			break;
		}
	}
	//进行训练
	Mat result1 = svm();
	//展示训练结果
	svmFind(result1);
	return 0;
}

//关于如何相应鼠标单击可以从网上寻找具体解释
void on_mouse(int event, int x, int y, int flags, void* ustc)
{
	if (event == CV_EVENT_LBUTTONDOWN)
	{
		Point pt = Point(x, y);
		Vec3b point = img.at<Vec3b>(y, x);
		Mat tmp = (Mat_<float>(1, 3) << point[0], point[1], point[2]);
		if (flag)
		{
			targetData.push_back(tmp);
			circle(orig, pt, 2, Scalar(0, 255, 255), -1, 8);//

		}

		else
		{
			backData.push_back(tmp);
			circle(orig, pt, 2, Scalar(255, 0, 0), -1, 8);//

		}
		imshow(wdname, orig);//
	}
}
//简单处理
void getTrainData(Mat &train_data, Mat &train_label)
{
	int m = targetData.rows;
	int n = backData.rows;
	cout << "target:" << m << endl;
	cout << "back:" << n << endl;
	vconcat(targetData, backData, train_data);
	train_label = Mat(m + n, 1, CV_32S, Scalar::all(1));
	for (int i = m; i < m + n; i++)
		train_label.at<int>(i, 0) = -1;
}
//训练模块
Mat svm()
{
	Mat train_data, train_label;
	getTrainData(train_data, train_label);

	Ptr<SVM> svm = SVM::create();
	svm->setType(SVM::C_SVC);
	svm->setKernel(SVM::RBF);
	svm->setGamma(0.01);
	svm->setC(10.0);
	svm->setTermCriteria(TermCriteria(CV_TERMCRIT_EPS, 1000, FLT_EPSILON));

	Ptr<TrainData> tData = TrainData::create(train_data, ROW_SAMPLE, train_label);
	//svm->train(tData);
	svm->trainAuto(tData, 10,
		SVM::getDefaultGrid(SVM::C),
		SVM::getDefaultGrid(SVM::GAMMA),
		SVM::getDefaultGrid(SVM::P),
		SVM::getDefaultGrid(SVM::NU),
		SVM::getDefaultGrid(SVM::COEF),
		SVM::getDefaultGrid(SVM::DEGREE),
		true);
	svm->save(filename);


	Mat newImge(Size(800, 400), CV_8UC1, Scalar(0));
	int step = 3;

	for (int i = 0; i < image.rows - step; i += step) {
		Vec3b *data = image.ptr<Vec3b>(i);
		uchar *newData = newImge.ptr<uchar>(i);
		for (int j = 0; j < image.cols - step; j += step)
		{
			Mat sampleMat = (Mat_<float>(1, 3) << data[j][0], data[j][1], data[j][2]);
			float response = svm->predict(sampleMat);

			if ((int)response == -1) {
				newData[j] = 0;
			}
			else {
				newData[j] = 255;
			}
		}
	}
	imshow("SVM New Simple Example", newImge);
	Mat element = getStructuringElement(MORPH_RECT, Size(7, 7));
	morphologyEx(newImge, newImge, MORPH_CLOSE, element);
	//imshow("SVM Simple Example", image);
	return newImge;
}
//训练结果展示模块
void svmFind(Mat bimage) {
	Mat bimg;
	//imshow("bimg", bimage);
	bimg = bimage;
	vector<std::vector<cv::Point> > contours;
	findContours(bimg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //只检测外轮廓。忽略轮廓内部的洞。
	Mat contoursimg(bimg.size(), CV_8U, Scalar(0));					  //黑图
	Mat conimg = contoursimg.clone();
	//drawContours(conimg, contours, -1, Scalar(0), 2); //-1:如果是负数,则绘制所有的轮廓 用黑色绘制图像
	int cmax;
	int s[1000], i = 1, position = 160;
	//找最长轮廓 用最小的矩形将它包围起来
	if (contours.size() > 0)
	{
		vector<std::vector<cv::Point> >::iterator itc = contours.begin();
		while (itc != contours.end())
		{
			s[i] = itc->size();
			//cout << s[i] << endl;
			++i;
			++itc;
		}
		for (int l = 2; l <= i - 1; l++)
			if (s[l - 1] > s[l])
				s[l] = s[l - 1];
		cmax = s[i - 1];
		itc = contours.begin();
		while (itc != contours.end())
		{
			if (itc->size() < cmax)
			{
				itc = contours.erase(itc);
			}
			else
				++itc;
		}
		drawContours(contoursimg, contours, -1, Scalar(255), 2);
		Mat result = contoursimg.clone();
		Rect r = boundingRect(Mat(contours[0])); //矩形将轮廓包围
		rectangle(result, r, Scalar(255), 2);
		imshow("result", result);
		waitKey(0);

	}
	else
		cout << "haven't deal it sucessfully" << endl;
		waitKey(0);
}

}

上述是代码,下面稍微展示一下操作过程。
取样
该图片是在代码运行后的界面。图片就用了比赛现场做的图片(不小心被我拍进去的小哥哥小姐姐们如果看到了不要打我,侵删),其中黄色的是正样本,蓝色的点代表负样本,只是展示效果,所以随便点了几下。
原图
结果展示
上面两张图是训练的原图和训练结果(如果点的仔细一些效果可以更好,这里就随意搞了下),紫色的基本都是可以分辨出来的。因为有压缩,所以大小看起来不是完全一致。
而结果,则保存在代码中所示的文件内,方便后续调用。
保存格式如下:

<?xml version="1.0"?>
<opencv_storage>
<opencv_ml_svm>
  <format>3</format>
  <svmType>C_SVC</svmType>
  <kernel>
    <type>RBF</type>
    <gamma>1.5000000000000001e-04</gamma></kernel>
  <C>5.0000000000000000e-01</C>
  <term_criteria><epsilon>1.1920928955078125e-07</epsilon></term_criteria>
  <var_count>3</var_count>
  <class_count>2</class_count>
  <class_labels type_id="opencv-matrix">
    <rows>2</rows>
    <cols>1</cols>
    <dt>i</dt>
    <data>
      -1 1</data></class_labels>
  <sv_total>69</sv_total>
  <support_vectors>
    <_>
      37. 148. 105.</_>
    <_>
      42. 147. 106.</_>
    <_>
      41. 148. 105.</_>
    <_>
      44. 149. 104.</_>
    <_>
      42. 150. 106.</_>
    <_>
      65. 154. 101.</_>
    <_>
      33. 147. 105.</_>
    <_>
      59. 152. 103.</_>
    <_>
      48. 150. 104.</_>
    <_>
      48. 150. 104.</_>
    <_>
      38. 148. 106.</_>
    <_>
      31. 144. 109.</_>
    <_>
      34. 146. 112.</_>
    <_>
      42. 145. 112.</_>
    <_>
      52. 148. 100.</_>
    <_>
      41. 148. 108.</_>
    <_>
      40. 147. 108.</_>
    <_>
      18. 145. 105.</_>
    <_>
      29. 147. 106.</_>
    <_>
      35. 148. 106.</_>
    <_>
      40. 149. 107.</_>
    <_>
      39. 147. 107.</_>
    <_>
      42. 147. 109.</_>
    <_>
      39. 147. 107.</_>
    <_>
      33. 148. 106.</_>
    <_>
      111. 167. 75.</_>
    <_>
      106. 168. 76.</_>
    <_>
      110. 172. 80.</_>
    <_>
      104. 173. 77.</_>
    <_>
      105. 169. 75.</_>
    <_>
      104. 170. 73.</_>
    <_>
      87. 157. 99.</_>
    <_>
      84. 160. 85.</_>
    <_>
      7. 128. 128.</_>
    <_>
      250. 116. 168.</_>
    <_>
      15. 128. 128.</_>
    <_>
      20. 128. 128.</_>
    <_>
      6. 128. 128.</_>
    <_>
      7. 128. 128.</_>
    <_>
      9. 128. 128.</_>
    <_>
      91. 132. 92.</_>
    <_>
      82. 133. 95.</_>
    <_>
      43. 131. 107.</_>
    <_>
      70. 131. 103.</_>
    <_>
      73. 132. 103.</_>
    <_>
      54. 165. 149.</_>
    <_>
      51. 162. 150.</_>
    <_>
      46. 160. 149.</_>
    <_>
      44. 160. 149.</_>
    <_>
      17. 130. 114.</_>
    <_>
      70. 134. 97.</_>
    <_>
      85. 134. 95.</_>
    <_>
      42. 158. 144.</_>
    <_>
      49. 161. 152.</_>
    <_>
      255. 128. 128.</_>
    <_>
      88. 133. 95.</_>
    <_>
      81. 132. 98.</_>
    <_>
      76. 132. 99.</_>
    <_>
      78. 132. 100.</_>
    <_>
      36. 129. 128.</_>
    <_>
      71. 133. 99.</_>
    <_>
      70. 137. 97.</_>
    <_>
      79. 132. 97.</_>
    <_>
      81. 133. 96.</_>
    <_>
      77. 130. 102.</_>
    <_>
      51. 142. 100.</_>
    <_>
      79. 131. 99.</_>
    <_>
      136. 148. 74.</_>
    <_>
      240. 121. 176.</_></support_vectors>
  <decision_functions>
    <_>
      <sv_count>69</sv_count>
      <rho>-8.2782848332732450e-01</rho>
      <alpha>
        8.1885663858543123e-02 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 1.4269005298462256e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 1.7608948166951635e-01
        5.0000000000000000e-01 4.4780397830801466e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        1.4877604321770961e-02 5.0000000000000000e-01
        6.9877155978621944e-02 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        5.0000000000000000e-01 5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -3.1957181100387066e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -1.1365212611721892e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01 -5.0000000000000000e-01
        -5.0000000000000000e-01</alpha>
      <index>
        54 60 61 62 55 56 57 58 59 40 41 42 43 44 50 45 46 47 48 49 63
        51 52 53 66 64 65 67 68 33 34 35 37 38 39 36 8 9 10 11 18 17 12
        16 15 14 13 4 3 2 1 0 7 6 5 32 31 30 29 28 27 26 25 24 23 22 21
        20 19</index></_></decision_functions></opencv_ml_svm>
</opencv_storage>

如果是在Linux中使用,编译的时候可能会出错,所以可以用如下代码编译:

g++ main.cpp `pkg-config --libs opencv` `pkg-config --cflags opencv`

为了方便起见,可以以.sh的方式直接用,不用每次写这么一长串代码。
五、训练集使用
这里贴一下我们封装的.h头文件和.cpp源文件

#include "opencv2/opencv.hpp"
using namespace cv;
using namespace cv::ml;
using namespace std;
//头文件,简单定义视觉中用到的参数
class wxzSVM
{
  private:
	int center_Y;
	int center_X;
	int down_Y;
	int up_Y;
	int right_X;
	int left_X;
	int Area;
	int leftup_X;
	int leftup_Y;
	int leftdown_X;
	int leftdown_Y;
	int rightup_X;
	int rightup_Y;
	int rightdown_X;
	int rightdown_Y;


  public:
	Mat svm(Mat &image, int type); //svm鍒嗙被
	Mat SVMFind(int type, Mat image);
	void printInfo();
	int center_y();
	int center_x();
	int down_y();
	int up_y();
	int area();
	int right_x();
	int left_x();
	int leftup_x();
	int leftup_y();
	int leftdown_x();
	int leftdown_y();
	int rightup_x();
	int rightup_y();
	int rightdown_x();
	int rightdown_y();
};

上面是头文件,简单定义了处理结束后希望得到的参数。

#include "wxzSVM.h"
using namespace std;
using namespace cv;
using namespace cv::ml;

Mat wxzSVM::svm(Mat &image, int type)
{
	Ptr<SVM> svm;
	switch (type)
	{
	case 2:
		//cout << "00000000000000000000000" << endl;
		svm = SVM::load("/home/liyang/worktree2018/avoidobstacle/svmRed_bi.xml");
		cvtColor(image, image, COLOR_BGR2Lab);
		break;
	case 1:
		svm = SVM::load("/home/liyang/worktree2018/avoidobstacle/svmOrange_bi.xml");
		cvtColor(image, image, COLOR_BGR2Lab);
		break;
	case 3:
		svm = SVM::load("/home/liyang/worktree2018/avoidobstacle/svmPurple_bi.xml");
		cvtColor(image, image, COLOR_BGR2Lab);
		break;
	}

	// Show the decision regions given by the SVM

	Mat newImge(image.size(), CV_8UC1, Scalar(0));
	int step = 3;

	for (int i = 0; i < image.rows - step; i += step)
	{
		Vec3b *data = image.ptr<Vec3b>(i);
		uchar *newData = newImge.ptr<uchar>(i);
		for (int j = 0; j < image.cols - step; j += step)
		{
			Mat sampleMat = (Mat_<float>(1, 3) << data[j][0], data[j][1], data[j][2]);
			float response = svm->predict(sampleMat);

			if ((int)response == -1)
			{
				newData[j] = 0;
			}
			else
			{
				newData[j] = 255;
			}
		}
	}
	//cout << "444444444444444444444" << endl;
	imshow("SVM New Simple Example", newImge);
	Mat element = getStructuringElement(MORPH_RECT, Size(7, 7));
	morphologyEx(newImge, newImge, MORPH_CLOSE, element);
	//imshow("SVM Simple Example", image);
	return newImge;
}
Mat wxzSVM::SVMFind(int type, Mat image)
{
	Mat bimg;
	bimg = svm(image, type);
	// waitKey(50);
	//cout << "555555555555555555555" << endl;
	vector<std::vector<cv::Point> > contours;
	findContours(bimg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); 
	Mat contoursimg(bimg.size(), CV_8U, Scalar(0));					  
	Mat conimg = contoursimg.clone();
	//cout << "666666666666666666666666" << endl;
	//drawContours(conimg, contours, -1, Scalar(0), 2);
	int cmax;
	int s[1000], i = 1, position = 160;
	
	if (contours.size() > 0)
	{
		vector<std::vector<cv::Point> >::iterator itc = contours.begin();
		//cout << "777777777777777777777" << endl;
		while (itc != contours.end())
		{
			s[i] = itc->size();
			//cout << s[i] << endl;
			++i;
			++itc;
		}
		for (int l = 2; l <= i - 1; l++)
			if (s[l - 1] > s[l])
				s[l] = s[l - 1];
		cmax = s[i - 1];
		itc = contours.begin();
		while (itc != contours.end())
		{
			if (itc->size() < cmax)
			{
				itc = contours.erase(itc);
			}
			else
				++itc;
		}
		drawContours(contoursimg, contours, -1, Scalar(255), 2);
		Mat result = contoursimg.clone();
		Rect r = boundingRect(Mat(contours[0])); 
		rectangle(result, r, Scalar(255), 2);
		imshow("result", result);

		center_Y = r.y + r.height / 2; 
		center_X = r.x + r.width / 2;
		down_Y = r.y + r.height;
		up_Y = r.y;
		left_X = r.x;
		right_X = r.x + r.width;
		leftup_X = r.x;
		leftup_Y = r.y;
		leftdown_X = r.x ;
		leftdown_Y = r.y + r.height;
		rightup_X = r.x + r.width;
		rightup_Y = r.y;
		rightdown_X = r.x + r.width;
		rightdown_Y = r.y + r.height;
		Area = r.height * r.width;
		//printInfo();
		return result;
	}
	else
	{
		cout << "haven't deal it sucessfully" << endl;
		center_Y = 0;
		center_X = 0;
		down_Y = 0;
		up_Y = 0;
		left_X = 0;
		right_X = 0;
		leftup_X = 0;
		leftup_Y = 0;
		leftdown_X = 0;
		leftdown_Y = 0;
		rightup_X = 0;
		rightup_Y = 0;
		rightdown_X = 0;
		rightdown_Y = 0;
		Area = 0;

		return bimg;
	}
}
void wxzSVM::printInfo()
{
	cout << "center_y: " << center_Y << "||"
	     << "center_x: " << center_X << "||"
	     << "up_y: " << up_Y << "||"
	     << "down_y: " << down_Y << "||"
	     << "area :" << Area << endl;
}
int wxzSVM::center_y()
{
	return center_Y;
}
int wxzSVM::center_x()
{
	return center_X;
}
int wxzSVM::down_y()
{
	return down_Y;
}
int wxzSVM::up_y()
{
	return up_Y;
}
int wxzSVM::area()
{
	return Area;
}

int wxzSVM::left_x()
{
	return left_X;
}
int wxzSVM::right_x()
{
	return right_X;
}

int wxzSVM::leftup_x()
{
	return leftup_X;
}
int wxzSVM::leftup_y()
{
	return leftup_Y;
}
int wxzSVM::leftdown_x()
{
	return leftdown_X;
}
int wxzSVM::leftdown_y()
{
	return leftdown_Y;
}
int wxzSVM::rightup_x()
{
	return rightup_X;
}
int wxzSVM::rightup_y()
{
	return rightup_Y;
}
int wxzSVM::rightdown_x()
{
	return rightdown_X;
}
int wxzSVM::rightdown_y()
{
	return rightdown_Y;
}

核心代码其实很少,就是load和predict,而且都是封装好的,没什么好说的,至此,就可以在需要的地方直接调用了。
各位看官如果有什么更好的方法还望不吝赐教!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值