C#调用封装Opencv函数的dll文件之KNN算法调用
最近将opencv(版本为2.4.13.6)中的KNN函数(KNN算法相关介绍见后续文章。。。)封装为dll文件,提供函数接口来给C#调用。后来发现opencv有C#版本叫opencvsharp,可以直接使用,多走了不少弯路,好了,回到正题。我自己最开始写了一个dll,C++中函数声明如下,函数功能是实现KNN算法,对待测试数据进行分类。
KNN_Process(double* sampleData, int* lableData, int sample_num,
int sample_length, double* TestData, int* result)
{
//创建样本数据cv矩阵
CvMat sampleDataCvMat = cvMat(sample_num, sample_length, CV_32FC1, sampleData);
//创建标签数据cv矩阵
CvMat responsesCvMat = cvMat(1, sample_num, CV_32SC1, lableData);
//创建knn模型
CvKNearest knn(&sampleDataCvMat, &responsesCvMat, 0, false, 32);
int K = 1; //参数K值
//创建待测试数据cv矩阵
CvMat TestDataCvMat = cvMat(1, sample_length, CV_32FC1, TestData);
//nearests表示K个最邻近样本的响应值
CvMat* nearests = cvCreateMat(1, K, CV_32FC1);
//对测试数据进行分类,并返回标签
float r = knn.find_nearest(&TestDataCvMat, K, 0, 0, nearests, 0);
result[0] = r;
for (int i = 0; i<K; i++)
result[i + 1] = (int)nearests->data.fl[i];
return (int)r;
}
C#测试程序
static void Main(string[] args)
{
double[] trainFeaturesData =
{
2,2,2,2,
3,3,3,3,
4,4,4,4,
5,5,5,5,
6,6,6,6,
7,7,7,7
};
int[] trainLabelsData = { 2, 3, 4, 5, 6, 7 };
double[] testFeatureData = { 7, 7, 7, 7 };
float[] kresult = new float[5];
int Label = KNN_Process(trainFeaturesData, trainLabelsData, 6, 4,testFeatureData, kresult);
Console.WriteLine("result: {0}",Label);
正常情况下,分类标签Label应该是7,但是出来的结果要么是0,要么是个不相关的数。问题来了,输入没问题,函数没问题(单独在C++平台上测试过),输出为啥不对。在线Debug,两个int型参数sample_num, sample_length没问题,sampleData作为指针,可以发现第一个数是正确的。开始以为是C#传入的数组有问题,于是写了下面的测试函数:
void __declspec(dllexport) test(short n[], int N, int& Z)
{
for (int i = 0; i<N; i++)
{
Z += n[i];
}
}
到C#中去测试,测试结果正确。此时判断会不会是由于double类型的原因,导致传入的数据无法建立模型。将所有double换为int型后,测试结果正确如下:
为什么double类型传入就就会有问题?于是在dll中写了打印函数,查看每个CvMat的值:
终于找到问题所在:之所以分类不成功,是因为数据矩阵的值全部有问题,所以出来的标签值一直是0。
百度opencv的数据结构参数后,发现double是64bits,在CvMat数据结构参数:CV_64FC1,CV_64FC2,CV_64FC3,CV_64FC4,于是将此处的CV_32FC1改为CV_64FC1。
CvMat sampleDataCvMat = cvMat(sample_num, sample_length, CV_32FC1, sampleData);
再次出错:训练数据只能是float型的矩阵
继续改,所有的double型改为float型。测试结果如下:
我的标签最开始设置为int型,float测试完成后,将trainLabelsData的类型设置为int型,并将CvMat格式化为int格式CV_32SC1,测试结果如下:
不明白为什么样本标签为int型的时候,分类的结果不对。于是去查看opencv中K-Nearest Neighbors Classifier的源码。
在源代码中
CV_CALL( cvPrepareTrainData( "CvKNearest::train", _train_data, CV_ROW_SAMPLE,
_responses, CV_VAR_ORDERED, 0, _sample_idx, true, (const float***)&_data,
&_count, &_dims, &_dims_all, &responses, 0, 0 ));
创建KNN模型的时候和训练的时候,首先用cvPrepareTrainData和cvCheckTrainData对输入的各种数据进行了预处理。其中cvCheckTrainData对_train_data进行了判断
if( !CV_IS_MAT(train_data) || CV_MAT_TYPE(train_data->type) != CV_32FC1 )
CV_ERROR( CV_StsBadArg, "train data must be floating-point matrix" );
这解决了问题1:为什么训练数据一定要为CV_32FC1格式(CV_32F也可以),我用double转CV_64FC1,用int转CV_32SC1均会出错的原因。
问题2:样本标签为int型为什么会分类错误?
在cvPreprocessOrderedResponses中对标签数据进行了处理,这里有判断标签的类型是否CV_32SC1或者CV_32FC1
r_type = CV_MAT_TYPE(responses->type);
if( r_type != CV_32FC1 && r_type != CV_32SC1 )
CV_ERROR( CV_StsUnsupportedFormat, "Unsupported response type" );
然后将返回的值传给函数cvPrepareTrainData中的responses,再返回到CvKNearest::train函数中,在该函数中所有的样本数据都被转为了单精度浮点型。
_rsize = _count*sizeof(float);
CV_CALL( _samples = (CvVectors*)cvAlloc( sizeof(*_samples) + _rsize ));
_samples->next = samples;
_samples->type = CV_32F;//数据类型为float
_samples->data.fl = _data;
_samples->count = _count;
total += _count;
samples = _samples;
memcpy( _samples + 1, responses->data.fl, _rsize );//标签也以float的形式copy到整个样本数据集
看了里面的源码,大部分地方的数据均用的是float,至于为什么在调用封装的dll时候传入int类型的标签,分类会错。看了官方的例程后发现,发现与这里的标签数据格式化有关。若格式化为一维的行向量的形式,则int型的标签会分类出错;若分类为一维的列向量,则int型的标签会分类正确;但是float型的标签,无论是行列向量均可。
CvMat responsesCvMat = cvMat(1, sample_num, CV_32SC1, lableData);
花时间看了一下OpenCV API Reference,里面关于KNN的CvStatModel::train的说明很详细
这里明确说明了,训练数据必须是32bit的float型,标签数据通常是32bit的int型或float型。没看API文档,浪费了不少时间,以后还是要多看手册,不能一上来就百度!!!
参考资料:
1.[OpenCV K-Nearest Neighbors说明]:https://docs.opencv.org/2.4.13.6/modules/ml/doc/k_nearest_neighbors.html
2.[OpenCV Mat数据类型及位数总结]: http://blog.sina.com.cn/s/blog_662c7859010105za.html
如有不对的地方,欢迎大家批评指正