OpenCV处理拍照表格(一)

环境配置

https://www.learn2crack.com/2016/03/setup-opencv-sdk-android-studio.html

非常新的一篇在AS中安装OpenCV的教程,按教程装好了环境并测试通过。

注意教程中没有讲到的是想要使用OpenCV的相关功能,需要安装下载包中apk目录下的对应处理器的OpenCV manager。并在使用OpenCV的活动中加入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {

//Connect to OpenCV manager service and initialize
@Override
public void onManagerConnected(int status) {
switch (status){
case BaseLoaderCallback.SUCCESS:
Log.i(TAG, "OpenCV Success");
break;
default:
super.onManagerConnected(status);
Log.i(TAG, "OpenCV Fail");
break;
}

}
};
1
2
3
4
5
6
7
//Initialize at every resume
@Override
protected void onResume() {
super.onResume();
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, getApplicationContext(), mLoaderCallback);
Log.d(TAG, "On Resume OK");
}

http://docs.opencv.org/java/3.1.0/>
opencv的官方文档。
更正:上面的3.1版的文档并没有详细的方法解释!

所以只能看

http://docs.opencv.org/java/2.4.11/

2.4.11的。

处理的大致思路

目前想到的思路是:

  1. 图像彩色转灰度
  2. 灰度图像设置阈值后二值化即变成完全黑白
  3. 去除多余的噪点
  4. 边缘识别
  5. 透视变形
  6. 矩形识别
  7. 分割识别出的矩形
  8. OCR对矩形进行识别,读取数据。

实现

灰度与二值化

开始采用OpenCV中BitmapToMat方法,将文件以Bitmap的形式读取,再转换为Mat格式再进行处理。

后面发现了OpenCV自带的imread方法,传入文件路径和Mat的格式后就可以方便地获得一个Mat对象。

另外如果在这里采用Imgcodecs.CV_LOAD_IMAGE_GRAYSCALE标签,就可以直接以灰度的形式读取图像,省去了颜色转化的步骤。并且这个Mat的格式就是下面一步二值化要求传入的8UC1(8位单通道)格式。

下面就是二值化步骤,OpenCV提供了两个函数,第一个是普通的Threshold函数:

threshold

public static double threshold(Mat src,
Mat dst,double thresh,double maxval,int type)
Parameters:
src - input array (single-channel, 8-bit or 32-bit floating point).
dst - output array of the same size and type as src.
thresh - threshold value.
maxval - maximum value to use with the THRESH_BINARY and THRESH_BINARY_INV thresholding types.
type - thresholding type (see the details below).

传入图像,传出图像,阈值,填充的最深颜色,填充方法(达到阈值就填充最深颜色或相反),就可以根据每个像素的灰度值与阈值进行比较来决定填充的值为0或是最深。

定阈值的方法虽然可以对一张图像通过调整达到最优的效果,但是对于不同光照条件下拍摄出来的照片,因为整体亮度的不同,定阈值显然无法适应所有的情况。

所以就有了第二种函数,adaptiveThreshold,除了传入上面的这些参数外,增加了三个重要的参数

adaptiveThreshold

public static void adaptiveThreshold(Mat src,Mat dst,double maxValue,int adaptiveMethod,int thresholdType,int blockSize,double C)
Parameters:
src - Source 8-bit single-channel image.
dst - Destination image of the same size and the same type as src.
maxValue - Non-zero value assigned to the pixels for which the condition is satisfied. See the details below.
adaptiveMethod - Adaptive thresholding algorithm to use, ADAPTIVE_THRESH_MEAN_C or ADAPTIVE_THRESH_GAUSSIAN_C. See the details below.
thresholdType - Thresholding type that must be either THRESH_BINARY or THRESH_BINARY_INV.
blockSize - Size of a pixel neighborhood that is used to calculate a threshold value for the pixel: 3, 5, 7, and so on.
C - Constant subtracted from the mean or weighted mean (see the details below). Normally, it is positive but may be zero or negative as well.

blockSize:对某个像素周围进行采样的范围。
adaptiveMethod:根据上面的范围求阈值的方法,有两种:

  1. mean平均,简单地取采样范围内的平均值作为阈值。
  2. gaussian高斯,以高斯函数为基础,简单地说就是近的地方权重更高、远的地方权重低,来求阈值。

C:求出来的阈值减去的常量。

这三个参数就是决定二值化效果的关键,我找最优值的方法比较暴力,写了一个嵌套的循环设置这两个值,再输出到文件导出到电脑上用肉眼比较。最后确定的值为17-10。

adaptiveMethod我用的是mean,因为表格相对来说黑白比较明显,并不需要去根据距离的远近来决定阈值。

17这个值在我的手机拍摄出来的效果里面是最好的,但由于不同拍摄设备的分辨率不同,就造成了笔画所占据的像素的数量的不同,可以想到的是在高分辨率的情况下这个值应该要相应地增大,打个比方说我一个笔画的粗细就有17个像素,那么这个范围内检测出的阈值就会非常高从而导致笔画的残缺不全。

C这个值还是需要经过测试来得出的,设置的不同对最后效果的影响是最大的,直接会决定最后出来的图片是笔画过粗或是笔画残缺。

下面是处理前后的效果:

qq%e6%88%aa%e5%9b%be20161115205902qq%e6%88%aa%e5%9b%be20161115205922

可以看到二值化以后的图像只有黑白两色,但是明显有许多的噪点。

去噪

二值化之后就是去噪了,去噪的目的是把图像中的独立的点去掉。

去噪的方法是腐蚀,跟字面意思一样,就是缩小图案的范围,当图像的范围本身就很小时(噪点就是一个个这样的独立点),缩小后自然就不见了。

可以想到,在去噪后,部分笔画也随之缩小甚至细的地方会直接消失,所以腐蚀之后要再进行一步膨胀,即把图案的边缘扩大。

因为噪点已经消失,所以也不会因扩大而回来,但笔画依然存在,就会膨胀而得到弥补,也顺便可以补一下残缺的地方。

原理大概就是这样,但是由于OpenCV的这两个操作针对的是图像中的亮点(白色的地方被认为是亮点),而我们的表格又是白底黑字的,实际上黑色的部分是我们想要处理的部分,所以我将这两步交换了,相当于是对黑色的地方先腐蚀后膨胀。

下面是代码实现:

1
2
3
4
5
6
Mat kernelDilate = Imgproc.getStructuringElement(Imgproc.MORPH_DILATE, new Size(2, 2));
Imgproc.dilate(srcPic, srcPic, kernelDilate);
Imgcodecs.imwrite("/storage/sdcard/pic/test/afterErode.jpg", srcPic);
Mat kernelErode = Imgproc.getStructuringElement(Imgproc.MORPH_ERODE, new Size(2, 2));
Imgproc.erode(srcPic, srcPic, kernelErode);
Imgcodecs.imwrite("/storage/sdcard/pic/test/afterDilate.jpg", srcPic);

参数中有一个Kernel,这个就是处理图像的核,具体的内容不展开,我们利用getStructuringElement函数可以构建特定的处理核,这个函数第一个参数是构建的核的类型,除了用于代码中用到的膨胀和腐蚀的类型,还有ractcrossellipse等不同的形状,后面的size就是我们设置的重点了,指的是核的大小,可以理解成检测的范围,对于大的噪点自然需要大的范围,但是也意味着笔画细节丢失也更加严重。对于膨胀操作,则可以理解成膨胀的像素数,这个数值越大,最后的结果中的笔画也就越粗。

下面是上述代码的结果对比:

qq%e6%88%aa%e5%9b%be20161115205922之前

qq%e6%88%aa%e5%9b%be20161115202014 qq%e6%88%aa%e5%9b%be20161115202029 qq%e6%88%aa%e5%9b%be20161115202050 qq%e6%88%aa%e5%9b%be20161115202107

设置了四种不同的参数,可以看到噪点基本都被去除,最后的细节不尽相同。