Opencv学习笔记 - 使用opencvsharp和支持向量机
以统计学习理论为基础的支持向量机被广泛应用于机器学习的各个领域,是最通用的万能分类器。20世纪90年代,针对当时的神经网络在小样本条件下的不良表现,人们试图从更本质的层次上寻求一种更好的学习机器。在这种需求的激发下,产生了统计学习理论,即研究小样本条件下机器学习规律的理论。1995年,出现了基于统计学习理论的支持向量机(Support Vector Machine,SVM)。与神经网络相比,对于有限样本的学习问题,统计学习理论具有更坚实的数学理论基础,因此SVM取得了很大的成功。
一、支持向量机原理
1、统计学习理论概述
统计学习理论(Statistical Learning Theory,SLT)在解决机器学习问题中起到了基础性的作用。传统统计学研究的主要是渐近理论,基于传统方法的学习机器在样本数无穷时,往往表现出非常不错的性能,但在样本数有限时却表现出很差的推广能力。在实际应用中,我们面对的通常是有限样本的情况。统计学习理论就是在研究小样本统计估计和预测的过程中发展起来的一种理论,它建立了一套较好的有限样本下机器学习的理论框架,既有严格的理论基础,又能较好地解决小样本、非线性、高维度和局部极小点等实际问题。
统计学习理论中的SVM是执行SRM原则的典型学习机器[。其基本思想是,在线性分类问题中,选择具有最小VC维的元素中的函数(称为“最优分类超平面”)把训练数据分开。
SVM主要包括以下三种模型:
◎ 线性可分支持向量机。
◎ 线性支持向量机。
◎ 非线性支持向量机。
在上述三种模型中,简单模型是复杂模型的基础。
针对不同的样本类型,应使用不同类型的SVM:
◎ 当训练样本线性可分时,通过硬间隔最大化,学习一个线性分类器,即线性可分支持向量机,又称为硬间隔支持向量机。
◎ 当训练样本近似线性可分时,通过软间隔最大化,学习一个线性分类器,又称为软间隔支持向量机。
◎ 当训练样本线性不可分时,通过核技巧和软间隔最大化,学习一个非线性支持向量机。
2、线性SVM算法的基本原理
(1)硬间隔支持向量分类器
(2)软间隔支持向量分类器
(3)支持向量分类器的对偶形式解
3、非线性SVM算法的基本原理
前文介绍的是样本线性可分或近似线性可分情况下的SVM分类器。我们可以用硬间隔SVM解决线性可分数据的分类问题,用软间隔SVM分类器解决只有少量样本不在线性可分范围内的分类问题。
(1)特征变换
对于非线性不可分的数据,是无法直接用上述线性SVM方法的。但是人们发现,低维空间的数据在通过特征变换到高维空间后,是有可能变成线性可分数据的,或者提高了线性可分性。
(2)核方法
核方法的基本思想是:给定原始空间中的向量x,通过非线性映射Ф(∙)将x映射到高维特征空间(也称为核空间),然后在Ф(x)中构建线性算法,使它对应于原始空间中非线性问题的解。如果x各坐标分量间的相互作用仅限于内积,则可以使用满足Mercer条件的核函数代替内积运算,从而越过本来需要计算的非线性映射Ф(x),避免“维数灾难”。
4、SVM回归算法的基本原理
最初的SVM是用于求解分类问题的,Vapnik通过ε不敏感(ε−Insensitive)损失函数将SVM扩展到了求解回归问题。
用于回归的支持向量机被称为SVR(SupportVector Regression)。SVR和SVM在本质上是相同的,不同的是,在SVM中最小化的误差函数只等于+1或者−1;而SVR对于每个输入样本,都有不同的误差。
二、OpenCV函数实现
1、OpenCV中的SVM算法
OpenCV实现了5种类型的SVM算法。
(1)OpenCV中的支持向量分类
在OpenCV中有3种SVM分类算法,分别为C-SVM分类算法、C-SVM分类算法和One-class SVM分类算法。前两种分类算法可以完成多分类(Nc>2)任务,第三种分类算法用于一分类,它们都支持离群点分类。离群点是指无法分配给正确类的数据点(或不能由核空间中的超平面分隔的数据点)。
(2)OpenCV中的支持向量回归
OpenCV支持两种SVM的变体算法:C-SVM算法和v-SVM算法。这两个拓展的方法都假设了“离群点”的存在。“离群点”指不能被正确分类的数据点。
(3)OpenCV支持的核函数
为了解决非线性数据的分类和回归问题,常将原始数据x通过某个非线性映射Ф投影到高维特征空间,再在特征空间构建超平面完成分类或回归任务。非线性映射Ф(x)可能非常复杂,造成特征空间维度很高,无法计算。核方法在某些SVM文献中经常被称为核技巧(Kernel Trick),其本质上是一种计算方法,它通过满足一定条件的核函数K(x, z)来计算两样本点映射的内积,即K(x, z)=〈Ф(x), Ф(z)〉=Ф(x)TФ(z),从而避免直接计算映射Ф(x)和Ф(z)。
OpenCV提供了6个不同的核函数供SVM使用。
2、创建SVM模型
在OpenCV中,SVM分类器接口通过ml库中的SVM类定义。与其他机器学习算法一样,SVM类是从StatModel类派生的。
以下是创建、训练和使用SVM模型时的主要步骤。
(1)创建SVM实例
Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
(2)设置SVM算法的类型
svm->setType(SVM::C_SVC)
(3)设置SVM参数
svn->setC(10);
svn->setP();
svn->setNu();
(4)设置核函数与核参数
svn->setKernel(SVM::RBF);
svn->setGamma(1);
svn->setCoef0(0);
svn->setDegree(2);
3、训练模型
(1)常规训练
对SVM模型的训练及预测与ml模块中其他由StatModel类派生的算法相同。
(2)自动训练(opencvsharp没有实现这个函数)
OpenCV还提供了trainAuto函数,分类或回归模型都可以使用。该函数用于自动寻找最佳参数C、γ、p、nu、c0和d来训练模型,使用的方法是k折叠交叉验证。数据集会被自动分为k个子集,然后运行k次;每次运行时,都对k–1个子集进行训练,用保留的另一个子集进行验证。当k折叠交叉验证误差最小时,参数组合被认为是最佳的。如果是ONE_CLASS函数,则不会进行优化,而是执行指定参数的常规训练。
virtual bool trainAuto( const Ptr<TrainData>& data, int kFold = 10,
ParamGrid Cgrid = getDefaultGrid(C),
ParamGrid gammaGrid = getDefaultGrid(GAMMA),
ParamGrid pGrid = getDefaultGrid(P),
ParamGrid nuGrid = getDefaultGrid(NU),
ParamGrid coeffGrid = getDefaultGrid(COEF),
ParamGrid degreeGrid = getDefaultGrid(DEGREE),
bool balanced=false) = 0;
函数参数:
◎ samples:训练样本。
◎ layout:训练样本集中特征向量的排列方式,如ROW_SAMPLE或COL_SAMPLE。
◎ responses:与训练样本相关的响应(标签)向量。
◎ kFold:交叉验证参数,默认为10折交叉验证。训练集分为k个子集。一个子集用于测试模型,其他子集构成训练集。
◎ Cgrid:参数C的网格。
◎ gammaGrid:参数γ的网格。
◎ pGrid:参数p的网格。
◎ nuGrid:参数v的网格。
◎ coeffGrid:参数c0的网格。
◎ degreeGrid:参数d的网格。
◎ balanced:如果为真,并且是二分类问题,则该方法将创建更平衡的交叉验证子集,该子集中子类之间的比例接近整个训练数据集中的比例。
三、使用HOG特征与SVM算法识别手写数字
1. c++代码参考图像校正预处理
#include "opencv_svm.h"#include <opencv.hpp>
#include <opencv2/ml/ml.hpp>
#include <iostream>
#include <ctime>
#include <io.h>using namespace std;
using namespace cv;
using namespace cv::ml;
using namespace cv::dnn;bool hog_falg = true;cv::HOGDescriptor hog(cv::Size(28, 28),cv::Size(14, 14),cv::Size(7, 7),cv::Size(7, 7),9/*, 1,-1,HOGDescriptor::HistogramNormType::L2Hys,0.2, true,HOGDescriptor::DEFAULT_NLEVELS, true*/);/// <summary>
/// 计算并校正倾斜度,这里没用到,有兴趣
/// 可以把图像处理一遍,观察效果
/// </summary>
/// <param name="img"></param>
/// <returns></returns>
cv::Mat deskew(cv::Mat& img)
{cv::Moments m = moments(img);int SZ = img.rows; //图像大小if (abs(m.mu02) < 1e-2)return img.clone();//基于中心距计算倾斜度double skew = m.mu11 / m.mu02;//计算仿射变换矩阵并校正偏斜度cv::Mat wrapMat = (cv::Mat_<double>(2, 3) << 1, -skew, 0.5*SZ* skew, 0,1,0);cv::Mat imgOut = cv::Mat::zeros(img.rows, img.cols, img.type());cv::warpAffine(img, imgOut, wrapMat, imgOut.size());return imgOut;
}void convert_to_ml(const vector<Mat>& train_samples, Mat& trainData)
{//--Convert dataconst int rows = (int)train_samples.size();const int cols = (int)std::max(train_samples[0].cols,train_samples[0].rows);Mat tmp(1, cols, CV_32FC1);trainData = Mat(rows, cols, CV_32FC1);for (size_t i = 0; i < train_samples.size(); ++i){CV_Assert(train_samples[i].cols == 1 || train_samples[i].rows == 1);if (train_samples[i].cols == 1){transpose(train_samples[i], tmp);tmp.copyTo(trainData.row((int)i));}else if (train_samples[i].rows == 1){train_samples[i].reshape(0, 1).copyTo(trainData.row((int)i));}}
}void convert_to_ml_for(const vector<Mat>& train_samples, Mat& trainData)
{const int rows = (int)train_samples.size();const int cols = (int)std::max(train_samples[0].cols, train_samples[0].rows);trainData = Mat(rows, cols, CV_32FC1);for (size_t i = 0; i < train_samples.size(); ++i){train_samples[i].copyTo(trainData.row((int)i));}
}/// <summary>
/// 生成模型的训练集和测试集
/// </summary>
/// <param name="img">输入的灰度图</param>
/// <param name="trainDataVec">输出</param>
/// <param name="testDataVec">输出</param>
/// <param name="testLabel">输出</param>
/// <param name="train_rows">输出</param>
/// <param name="HOG_flag">输入。true:提取HOG特征描述子作为特征量。false:直接使用图像原始像素</param>
void generateDataSet(cv::Mat& img, vector<Mat>& trainDataVec, vector<Mat>& testDataVec,vector<int>& testLabel, int train_rows, bool HOG_flag)
{//初始化图像中切片图像与其他参数int width_slice = 20; //单个数字切片图像的宽度int height_slice = 20; //单个数字切片图像的高度int row_sample = 100; //每行样本数int col_sample = 50; //每列样本数int row_single_number = 5; //单个数字占5行int test_rows = row_single_number - train_rows; //测试样本所占行数Mat trainMat(train_rows * 20 * 10, img.cols, CV_8UC1);//存放所有训练图片trainMat = Scalar::all(0);Mat testMat(test_rows * 20 * 10, img.cols, CV_8UC1);//存放所有测试图片testMat = Scalar::all(0);//生成测试大图和训练大图for (int i = 1; i <= 10; i++){Mat tempTrainMat = img.rowRange((i - 1) * row_single_number * 20, (i *row_single_number - 1) * 20).clone();Mat tempTestMat = img.rowRange((i * row_single_number - 1) * 20, (i *row_single_number) * 20).clone();//traincv::Mat roi_train = trainMat(Rect(0, (i - 1) * train_rows * 20,tempTrainMat.cols, tempTrainMat.rows));Mat mask_train(roi_train.rows, roi_train.cols, roi_train.depth(),Scalar(1));//testcv::Mat roi_test = testMat(Rect(0, (i - 1) * test_rows * 20,tempTestMat.cols, tempTestMat.rows));Mat mask_test(roi_test.rows, roi_test.cols, roi_test.depth(),Scalar(1));//把提取的训练测试行分别复制到训练图片与测试图片中tempTrainMat.copyTo(roi_train, mask_train);tempTestMat.copyTo(roi_test, mask_test);}
}void getFiles1(string path, vector<string>& files)
{// 文件句柄intptr_t hFile = 0;// 文件信息struct _finddata_t fileinfo;string p;if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1) {do {// 保存文件的全路径if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)files.push_back(p.assign(path).append("\\").append(fileinfo.name));} while (_findnext(hFile, &fileinfo) == 0); //寻找下一个,成功返回0,否则-1_findclose(hFile);}
}void opencv_svm_mnist()
{//读取原始数据string trainPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\training";string testPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\testing";//校正大图,这里暂时不进行校正//制作训练集int train_sample_count = 60000;int test_sample_count = 10000;//声明变量cv::Mat trainHOGData, testHOGData, trainRawData, testRawData;vector<cv::Mat> trainHOGDataVec, testHOGDataVec, trainRawDataVec, testRawDataVec;Mat trainLabel = Mat(train_sample_count, 1, CV_32FC1);Mat testLabel = Mat(test_sample_count, 1, CV_32FC1);//组织训练数据int trainNums = 0;for (int i = 0; i < 10; i++){stringstream ss;ss << trainPath << "\\" << i;string path = ss.str();vector<string> files;getFiles1(path, files);int size = files.size();for (int a = 0; a < size; a++){Mat temp = imread(files[a].c_str(), IMREAD_GRAYSCALE);if (hog_falg){vector<float> descriptors;hog.compute(temp, descriptors, Size(1, 1), Size(0, 0));trainHOGDataVec.push_back(Mat(descriptors).reshape(0, 1).clone());}else{trainRawDataVec.push_back(temp.reshape(0, 1));}trainLabel.at<float>(trainNums) = i;trainNums++;}}//组织测试数据int testNums = 0;for (int i = 0; i < 10; i++){stringstream ss;ss << testPath << "\\" << i;string path = ss.str();vector<string> files;getFiles1(path, files);int size = files.size();for (int a = 0; a < size; a++){Mat temp = imread(files[a].c_str(), IMREAD_GRAYSCALE);if (hog_falg){vector<float> descriptors;hog.compute(temp, descriptors, Size(1, 1), Size(0, 0));testHOGDataVec.push_back(Mat(descriptors).reshape(0, 1).clone());}else{testRawDataVec.push_back(temp.reshape(0, 1));}testLabel.at<float>(testNums) = i;testNums++;}}//创建SVM分类器Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();svm->setType(cv::ml::SVM::C_SVC);svm->setKernel(cv::ml::SVM::RBF);svm->setC(10);svm->setGamma(0.5);//转换训练和测试集格式convert_to_ml_for(trainHOGDataVec, trainHOGData);convert_to_ml_for(testHOGDataVec, testHOGData);//训练printf("开始训练......\n");trainLabel.convertTo(trainLabel, CV_32S);svm->train(trainHOGData, cv::ml::ROW_SAMPLE, trainLabel);printf("训练完成......\n");svm->save("digits_hog_svm.xml");//测试cv::Mat result;printf("开始测试......\n");svm->predict(testHOGData, result);//输出结果int t = 0, f = 0;for (int i=0; i< test_sample_count; i++){int predict = (int)(result.at<float>(i));int actual = (int)(testLabel.at<float>(i));if (predict == actual){t++;}else{f++;}}printf("测试完成......\n");//输出结果float accuracy = (t * 1.0) / (t + f);printf("测试总数 %d \n", test_sample_count);printf("正确 %d \n", t);printf("错误 %d \n", f);
}
测试完成......
测试总数 10000
正确 9932
错误 68
2、c#、opencvsharp代码参考
OpenCvSharp.HOGDescriptor hog = new OpenCvSharp.HOGDescriptor(new OpenCvSharp.Size(28, 28),new OpenCvSharp.Size(14, 14),new OpenCvSharp.Size(7, 7),new OpenCvSharp.Size(7, 7),9);string trainPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\training";
string testPath = "D:\\Project\\deeplearn\\dataset\\mnist_png_full\\testing";//制作训练集
int train_sample_count = 60000;
int test_sample_count = 10000;//声明变量
Mat trainData, testData;
List<Mat> trainHOGDataVec = new List<Mat>();
List<Mat> testHOGDataVec = new List<Mat>();Mat trainLabel = new Mat(train_sample_count, 1, MatType.CV_32FC1);
Mat testLabel = new Mat(test_sample_count, 1, MatType.CV_32FC1);//组织训练数据
int trainNums = 0;
for (int i = 0; i < 10; i++)
{string path = trainPath + "\\" + i;DirectoryInfo TheFolder = new DirectoryInfo(path);foreach (FileInfo NextFile in TheFolder.GetFiles()){Mat temp = new Mat(NextFile.FullName, ImreadModes.Grayscale);float[] descriptors = hog.Compute(temp, new OpenCvSharp.Size(1, 1), new OpenCvSharp.Size(0, 0));Mat des = OpenCvSharp.InputArray.Create(descriptors).GetMat();trainHOGDataVec.Add(des.Reshape(0, 1).Clone());trainLabel.Set<float>(trainNums, i);trainNums++;}
}//组织测试数据
int testNums = 0;
for (int i = 0; i < 10; i++)
{string path = testPath + "\\" + i;DirectoryInfo TheFolder = new DirectoryInfo(path);foreach (FileInfo NextFile in TheFolder.GetFiles()){Mat temp = new Mat(NextFile.FullName, ImreadModes.Grayscale);float[] descriptors = hog.Compute(temp, new OpenCvSharp.Size(1, 1), new OpenCvSharp.Size(0, 0));Mat des = OpenCvSharp.InputArray.Create(descriptors).GetMat();testHOGDataVec.Add(des.Reshape(0, 1).Clone());testLabel.Set<float>(testNums, i);testNums++;}
}//创建SVM分类器
OpenCvSharp.ML.SVM svm = OpenCvSharp.ML.SVM.Create();
svm.Type = OpenCvSharp.ML.SVM.Types.CSvc;
svm.KernelType = OpenCvSharp.ML.SVM.KernelTypes.Rbf;
svm.C = 10;
svm.Gamma = 0.5;trainData = new Mat(train_sample_count, hog.GetDescriptorSize(), MatType.CV_32FC1);
for (int i = 0; i < train_sample_count; ++i)
{trainHOGDataVec[i].CopyTo(trainData.Row((int)i));
}testData = new Mat(test_sample_count, hog.GetDescriptorSize(), MatType.CV_32FC1);
for (int i = 0; i < test_sample_count; ++i)
{testHOGDataVec[i].CopyTo(testData.Row((int)i));
}//训练
Console.WriteLine("开始训练......\n");
trainLabel.ConvertTo(trainLabel, MatType.CV_32S);
svm.Train(trainData, OpenCvSharp.ML.SampleTypes.RowSample, trainLabel);
Console.WriteLine("训练完成......\n");
svm.Save("digits_hog_svm.xml");//测试
Mat result = new Mat();
Console.WriteLine("开始测试......\n");
svm.Predict(testData, result);
//输出结果
int t = 0, f = 0;
for (int i = 0; i < test_sample_count; i++)
{int predict = (int)(result.At<float>(i));int actual = (int)(testLabel.At<float>(i));if (predict == actual){t++;}else{f++;}
}
Console.WriteLine("测试完成......\n");//输出结果
double accuracy = (t * 1.0) / (t + f);
Console.WriteLine("测试总数:" + test_sample_count);
Console.WriteLine("正确:" + t);
Console.WriteLine("错误:" + f);
测试完成......
测试总数 10000
正确 9927
错误 73
四、小结
SVM的类型非常多。
针对线性可分数据,可以使用软间隔SVM;
针对非线性可分数据,可以使用引入核技巧的非线性SVM;
针对回归任务可以使用SVR。
在具体使用时有两个重要的问题,一是模型选择问题(即参数搜索),二是特征提取问题。
如果SVM用于机器视觉的分类任务,则通常会先提取原始数据的特征并进行一定的归一化,再使用SVM进行分类,而不是直接使用原始数据(图像)进行分类。传统特征多为基于直方图的特征,也可以使用神经网络来提取特征。
Opencv学习笔记 - 使用opencvsharp和支持向量机相关推荐
- Opencv学习笔记 - 使用opencvsharp和Boosting算法处理分类问题
决策树非常有用,但单独使用时它并不是表现最佳的分类器.改进的方法随机森林和Boosting算法.随机森林与Boosting算法都是在内部循环中使用决策树的,因此继承了决策树的许多优良属性,它们通常是机 ...
- Opencv学习笔记 - 使用opencvsharp和决策树进行训练和预测
一.决策树 决策树是最早的机器学习算法之一,起源于对人类某些决策过程的模仿,属于监督学习算法.决策树的优点是易于理解,有些决策树既可以做分类,也可以做回归.在排名前十的数据挖掘算法中有两种是决策树[1 ...
- Opencv学习笔记 - 使用opencvsharp和期望最大化
一.期望最大化概述 期望最大化的受欢迎程度在很大程度上是因为它是从观察中学习参数的有效且稳健的程序.然而,通常可用于训练概率模型的唯一数据是不完整的.例如,在医学诊断中可能会出现缺失值,其中患者病史通 ...
- Opencv学习笔记 - OpenCV 4机器学习算法简介
在机器学习中,一些比较流行方法的包括:支持向量机(SVM).人工神经网络(ANN).聚类.k-最近邻.决策树和深度学习.OpenCV支持并实现几乎所有这些方法,并有详细的文档说明(包含在Main mo ...
- OpenCV学习笔记(二十六)——小试SVM算法ml OpenCV学习笔记(二十七)——基于级联分类器的目标检测objdect OpenCV学习笔记(二十八)——光流法对运动目标跟踪Video Ope
OpenCV学习笔记(二十六)--小试SVM算法ml 总感觉自己停留在码农的初级阶段,要想更上一层,就得静下心来,好好研究一下算法的东西.OpenCV作为一个计算机视觉的开源库,肯定不会只停留在数字图 ...
- OpenCV学习笔记(二十一)——绘图函数core OpenCV学习笔记(二十二)——粒子滤波跟踪方法 OpenCV学习笔记(二十三)——OpenCV的GUI之凤凰涅槃Qt OpenCV学习笔记(二十
OpenCV学习笔记(二十一)--绘图函数core 在图像中,我们经常想要在图像中做一些标识记号,这就需要绘图函数.OpenCV虽然没有太优秀的GUI,但在绘图方面还是做得很完整的.这里就介绍一下相关 ...
- 某人写的openCV学习笔记
原文地址:某人写的openCV学习笔记作者:拔剑 http://blog.csdn.net/thefutureisour 我的OpenCV学习笔记(25):c++版本的高斯混合模型的源代码完全注释 之 ...
- 某人写的openCV学习笔记_拔剑-浆糊的传说_新浪博客
http://blog.csdn.net/thefutureisour 我的OpenCV学习笔记(25):c++版本的高斯混合模型的源代码完全注释 之前看到过C版本的,感觉写的很长,没有仔细看,但是C ...
- OpenCV学习笔记(十七)——K均值聚类
当我们要预测的是一个离散值时,做的工作就是"分类".机器学习模型还可以将训练集中的数据划分为若干个组,每个组被称为一个"簇(cluster)".它的重要特点是在 ...
- OpenCV 学习笔记03 boundingRect、minAreaRect、minEnclosingCircle、boxPoints、int0、circle、rectangle函数的用法...
函数中的代码是部分代码,详细代码在最后 1 cv2.boundingRect 作用:矩形边框(boundingRect),用于计算图像一系列点的外部矩形边界. cv2.boundingRect(arr ...
最新文章
- 不对全文内容进行索引的 Loki 到底优秀在哪里
- 操作系统习题6—存储管理2
- inode与ln命令
- JS监听DOM宽高的变化
- cpu负载过高问题处理
- 汇编在嵌入式编程中的作用_如何在嵌入式Power BI报表中以编程方式传递凭据
- 逗号,句号。问号?叹号!顿号、冒号:人名分隔·
- matlab2c使用c++实现matlab函数系列教程-cumprod函数
- 团队第二次冲刺第一天
- We7CMS内容管理系统助阵政府完善信息公开制度
- mysql的dql_Mysql-DQL
- 以组播流方式替换运营商IPTV直播频道
- ubuntu 10.04 trackpoint
- windows开机启动项(​仅限Win10,Win7)​
- Aras Innovator PLM二次开发
- 安防摄像头无法接入国标GB28181视频平台EasyGBS问题排查与解决方案
- 被骂了十年的国产软件,却成了世界之最...
- LeetCode·每日一题·757.设置交集大小至少为2·贪心
- 数字IC笔面试(一)——联发科提前批笔试题记录
- Android studio 3.0 Appt2的异常问题 不一定需要关闭才能通过编译