利用OpenCV中的eigen替换函数dsyev来求解特征值和特征向量

函数dsyev用于计算一个实对称矩阵的特征值和特征向量,该函数隶属于线性代数库lapack,很古老了,而且是用Fortran语言开发的。

问题背景

为什么要寻找函数dsyev的替换库?
这个库,相关资料很少,难以跨平台,主要还是用于Linux系统(其实在Linux上也不好配了)。目前有很多成熟的库,比如EigenOpenCV等,都已经能轻松解决这个问题了。

在一个开源拟合椭圆参数的代码中,存在一段特征分解的代码,我需要绕过dsyev_这个函数。

// 矩阵A是一个6x6的实对称矩阵,后续也要对它进行分解
for (i=0;i<6;i++)
  for (j=0;j<6;j++)
    for (k=0;k<4*reg_size;k++)
      A[i*6+j] += gBufferDouble[k*6+i]*gBufferDouble[k*6+j];

// 特征值分解      
#define SIZE6 6
char JOBZ = 'V';
char UPLO = 'U';    
integer M = SIZE6;
integer LDA = M;
integer LWORK = 4*SIZE6;
integer INFO;
double W[SIZE6];
double WORK[LWORK];
dsyev_(&JOBZ, &UPLO, &M, A, &LDA, W, WORK, &LWORK, &INFO);

// 保留特征值最小的特征向量作为最终拟合参数
double s[9];
s[0] = A[0];
s[1] = s[3] = A[1];
s[2] = s[6] = A[2];
s[4] = A[3];
s[5] = s[7] = A[4];
s[8] = A[5];

下面给出函数dsyev的声明:

subroutine dsyev(character 	JOBZ,
	character 	UPLO,
	integer 	N,
	double precision, dimension( lda, * ) 	A,
	integer 	LDA,
	double precision, dimension( * ) 	W,
	double precision, dimension( * ) 	WORK,
	integer 	LWORK,
	integer 	INFO)	

参数说明:

  • JOBZ:输入一个字符,N表示只计算特征值,V表示计算矩阵的特征值和特征向量。
  • UPLO:输入一个字符,考虑到矩阵 A A A是一个实对称矩阵,因此只需要利用 A A A的上三角或下三角元素即可,U表示 A A A的上三角,L表示 A A A的下三角。
  • N:输入一个整数,表示这个矩阵的阶数,6x6的矩阵就输入6。
  • A:一个double类型的数组,矩阵维度为 ( L D A , N ) (LDA, N) (LDA,N)(LDA的含义放在后面说明)。在程序结束的时候,如果JOBZ=V,且INFO=0(表示特征分解成功),则正交特征向量存在A中。如果JOBZ=N,A的上三角或下三角(包含对角线元素)都会消除,意思就是A输入前后是不一样的,注意备份
  • LDA:输入一个整数,一定要大于 m a x ( 1 , N ) max(1,N) max(1,N)
  • W:是一个double类型的数组,维度为N,里面存储一组升序的特征值。
  • WORKLWORK:WORK是一个double类型的数组,LWORK是数组长度。在一些古老的计算机中,需要尽可能避免动态分配,因此就要预计算这个函数所需的内存空间,该任务中,LWORK大于 m a x ( 1 , 3 N − 1 ) max(1,3N-1) max(1,3N1)即可,最优分配空间的计算方式后面说明
  • INFO:一个整数,表示矩阵分解的成功与否。
    • 若值为0:矩阵分解成功。
    • 若值<0:INFO=-i表示,第i个参数值错误。
    • 若值>0:INFO=i表示算法没有收敛,表示有i个元素没有收敛为0(不知道理解对不对)。

问题求解

再次对前文需要矩阵特征分解的代码进行分析。

#define SIZE6 6
char JOBZ = 'V';    // 表示计算矩阵的特征值和特征向量
char UPLO = 'U';    // 利用矩阵A的上三角元素来分解
integer M = SIZE6;
integer LDA = M;    // 表示矩阵不是从一个更大矩阵抠出来的,就是一个6*6内存连续的矩阵。
integer LWORK = 4*SIZE6; // 预分配计算空间
integer INFO;
double W[SIZE6];
double WORK[LWORK];
// 开始特征分解
// 结束后A的每6个元素都表示一个特征向量的特征值
dsyev_(&JOBZ, &UPLO, &M, A, &LDA, W, WORK, &LWORK, &INFO);

这样,我们已经完全解读了这部分代码,下面利用OpenCV的函数进行替换。

// 利用A的内存直接构建一个6x6矩阵
// 值得注意的是lapack是列优先,而opencv是行优先。
// 但A是对称矩阵,所以这个问题就不用管了。
cv::Mat matA(6, 6, CV_64FC1, A); 
cv::Mat eValuesMat, eVectorsMat;
cv::eigen(matA, eValuesMat, eVectorsMat);

// cv::eigen的特征值是降序排序,与dsyev_是反过来的
// 因此最小特征值对应的特征向量的内存地址如下,6*5=30表示偏移量。
double *_A = ((double*)eVectorsMat.data) + 30;
double s[9];
s[0] = _A[0];
s[1] = s[3] = _A[1];
s[2] = s[6] = _A[2];
s[4] = _A[3];
s[5] = s[7] = _A[4];
s[8] = _A[5];

这样,这个特征分解函数替换的问题就解决了。

知识扩展

1. LDA (Leading Dimension of Array)

正常来说,咱们定义一个矩阵,一般是分配一段连续的数组,然后把数据放在这里面。比如一个 6 × 6 6\times 6 6×6矩阵,就定义一个double A[36]数组并把数据拷贝进去即可。

但有些时候,这个 6 × 6 6\times 6 6×6矩阵来自于一个 60 × 80 60\times 80 60×80矩阵的一块,重新拷贝的话,耗时比较大。这个问题中,矩阵的位置 ( 0 , 0 ) (0,0) (0,0) ( 0 , 1 ) (0,1) (0,1)之间,内存差异是60,而不是6,这个60就是LDA值(注意Lapack中,矩阵是列优先的)。

下面我们将这个问题一般化:

假设原始矩阵维度为 M 1 × M 2 M_1\times M_2 M1×M2,对应数据指针为 A m A_m Am,要求逆的矩阵维度为 N × N N\times N N×N。假设矩阵的左上角点,即位置 ( 0 , 0 ) (0,0) (0,0)处,在原始矩阵的位置为 ( p , q ) (p, q) (p,q) p + N < M 1 p+N< M_1 p+N<M1 q + N < M 2 q+N< M_2 q+N<M2

那么这个待求逆的矩阵的起始指针为 A n = A m + q ⋅ M 1 + p A_n=A_m+q\cdot M_1+p An=Am+qM1+p L D A = M 1 LDA=M_1 LDA=M1,矩阵维度为 N N N。这样这个要求逆的矩阵,每个元素 ( i , j ) (i,j) (i,j)的内存地址为: A n + j ⋅ L D A + i A_n+j\cdot LDA +i An+jLDA+i

这个方式的目的就是避免矩阵的大量拷贝,直接从原始矩阵上进行操作,降低内存耗用并提高计算速度。

2. 函数的计算内存预分配

在早期的设备中,内存是很少的,如果不管这个问题,用时候再分配内存的话,可能存在内存溢出的问题,导致设备停止,使用时这个设备就是不稳定的,不安全的。

因此,在调用函数前,需要知道这个函数需要多少内存。

只有在设备内存不多的时候才需要考虑,目前设备发展很快,几乎不会再有人考虑这个问题了。

有需要可以参考lapack关于这部分的描述《Determining the Block Size for Block Algorithms》。

小结

这个部分的重点就是给出了函数dsyev的替换库,已经验证了其可以用OpenCV的eigen库进行替换。在研究这个问题的时候,学到了LDA和内存预分配办法,真是内存不够,技术来凑→_→。

你可能感兴趣的:(OpenCV,opencv,计算机视觉)