最近用Java和openCV做了一个项目,之所以不用C++而用Java就是不愿意舍弃java这强大的编程语言,不过,说回来OpenCV对java的支持真不是一般的,呵呵。虽然现在的OpenCV原生支持Java了,可文档呐,绝对是不看不懂,看了更糊涂。万幸OpenCV有原码,没文档看原码还不行吗,于是通过看文档,源码和网上搜索(资源太少了),积累了一点经验,记录在此供自己和同行参考。
(1)创建矩阵
OpenCV Java中矩阵就是一个类 Mat,和它扩展出来的MatOfDouble之类的类。通常创建一个矩阵对象的方法是:
Mat A = new Mat(3, 4, CvType.CV_64FC1);
或者分两步:
Mat A = new Mat();
A.create(3,4,CvType.CV_64FC1);
矩阵使用完毕不用了,要记得销毁:
A.release();
另外还有些特殊矩阵的创建方法,这个文档里倒是有,大家可以看看。这些方法一般是静态方法,可以通过类来调用。例如:
A = Mat.eye(3,3, CvType.CV_64FC1);
A = Mat.zero(3,3,CvType.CV_64FC1);
诸如MatOfDouble之类的矩阵,还支持将矩阵转换为List或者从List(array)转换成矩阵。所以可以直接这样创建矩阵:
MatOfDouble A = new MatOfDouble(1,2,3,4,5,6,7,8,9);
或者
MatOfDouble A = new MatOfDouble();
A.fromArray(1,2,3,4,5,6,7,8,9);
注意,这样得到的矩阵实际上是一个列向量,也就是9*1的矩阵。如果想得到一个3*3的矩阵,可以用reshape,
A.reshape(1,3);
reshape这个方法的参数是很坑爹的,我好长时间都以为参数是行和列,后来翻文档才发现,第一个参数是通道数,第二参数是行数。不怪别人,是我自己想当然了。
另外,Mat还有一个方法是获得矩阵尺寸的,size()。该方法也十分坑爹,返回值居然是列*行。比方说上面那个矩阵A,
System.out.println(A.size().toString());
结果是1x9。
(2) 对矩阵进行操作是很简单的,因为OpenCV有专门的方法,其他的不说了,有一个特别要命,主要是Java开发人员特别要小心的,就是矩阵的乘法。对于C++开发者而言,矩阵乘法可以直接用*搞定,例如A=B*C。Java语言是不支持运算符重载的,因此也就不可能用这么简单的形式实现,只能用方法实现。
不过你找javadoc的时候会发现,Mat类里也提供了multi的方法,哈哈……且慢欢喜,这个不是我们说的矩阵乘法,而是矩阵对应元素的乘法,真正的矩阵乘法在Core类里,需要用Core.gemm()的形式调用。我写了一个简单那的函数实现了简单的矩阵乘法:
/**
* 计算矩阵A和B的乘,得到新的矩阵C。即 C=A*B;
* 注意:A,B,C调用前必需要初始化完毕。
* @param A 被乘矩阵
* @param B 乘矩阵
* @param C 结果矩阵
* @return 矩阵C
*/
public Mat matMul(Mat A, Mat B, Mat C)
{
Core.gemm(A, B, 1.0, Mat.zeros(A.size(), A.type()), 0.0, C);
return C;
}
这个方法是有文档的,不说啥了,目前还没有找到其他更好的方法。注意Mat类里还有dot()方法和cross()方法,都是给向量准备的,一个点乘,一个叉乘,其中叉乘还只能是三元素的向量。
(3) Java矩阵类的粗浅解析
我的理解Java矩阵实际上是两个部分。一个部分是Java的矩阵说明,包括尺寸,通道数,数据类型等;另一部分是实际存储数据的区域,在Java中这部分应该是用JNI调用其C++版本里的功能实现的,所以需要create和release。因此在做矩阵复制,赋值,转换等的各种操作的时候,有的时候是完整的创建了一个新的对象,有的时候只是创建了一个Java头,数据仍然存储在原来的地方没动,所以就有可能我们创建了好多个Mat对象,而实际存储的区域可能没那么多,会有多个Mat对象引用同一块数据存储区域。release方法实际上内部也有一个类似计数的变量,每次调用release的时候就会将这个变量减一,用这种方法来保证在合适的时候释放内存。所以这么说适时的调用release还是很重要的。但实际上什么时候应该release确实是个难题。我感觉一个标准是,谁new的对象,谁就负责release。在函数内部创建的矩阵对象,当做返回值的,必须要告诉调用者去release。
(4)矩阵元素的读取和存储
Java里读取矩阵的元素和改变某位置元素的值非常麻烦。对于C++而言,A(0,0)就可以直接取出(0,0)的值,同时也可以直接赋值。Java专门有这么两个方法,就是put()和get()。例如:
double[] x = A.get(0, 0)
这个操作可以获得(0,0)位置上的元素的值。为什么是返回的数组呢?因为OpenCV的矩阵可以是多维的,也就是在(0,0)位置上的值可以有多个通道(典型的例子是图像,其元素可能是3个值的数组(R,G,B)或者4个值的数组),所以返回值是数组。对于单通道的数据类型的矩阵而言,x[0]就是它的值。
A.put(0, 0, 1.0)
这个操作将(0,0)的值设置为1.0。当然本之上也应该是一个数组的,因为这里Java定义为可变数量参数,所以也可以只输入一个1.0。
但是这样的操作太麻烦了,大家可以想象一下最简单的常用的4*3矩阵如果这么赋起值来,那出错的概率是杠杠的。
还有一个办法就是把矩阵先转换为数组,然后进行操作,操作完毕后再转换会矩阵。也就是用数组作为中转,因为Java中操作数组还是很容易的。
Mat没有提供一个专为数组的方法,只能自己写一个了。其实也很简单,还是用的上面的两个方法。
double[] value = new double[9];
A.get(0,0, value); //从(0,0)位置开始读取数据来填充数组,一直到数组满或者矩阵结尾
或者
double[] value = new double[]{1,2,3,4,5,6,7,8,9};
A.put(0,0,value); //从(0,0)位置开始填充数组里的数据,一直到数组结束
很方便吧。注意计算好数组的大小。用这种方法也可以修改矩阵的部分数据。
(未完待续)