前段时间自己在研究行人检测方面的东西,用的是OpenCV,在网上查询相关资料的时候看到一些文章介绍如何使用OpenCV实现类似“全能扫描王”的功能。最近正好有空闲时间,于是花了几天时间研究了下,现在记录下在研究过程中自己的一些心得,希望对各位有些许帮助。
先分享一篇相关文章,大家可以看下:http://www.cnblogs.com/skyfsm/p/7324346.html
org.bytedeco.javacpp-presets
opencv
3.4.3-1.4.3
可以在OpenCV官网下载3.4.3的完整安装包,从里面找到opencv_java343.dll,也可以在https://download.csdn.net/download/rwzhang/10887211这里下载单独的dll
如图所示,选择你存放dll的文件夹
注:在项目启动入口加代码 System.loadLibrary(Core.NATIVE_LIBRARY_NAME),否则无法运行
整个过程步骤大致如下:
1、图像预处理:对图像进行高斯模糊、二值化、闭运算等操作,使我们可以更方便的找到需要的轮廓
2、定位4个角的坐标:找到轮廓后定位轮廓的4个角
3、透视变换:得到轮廓的4个角后对目标进行透视变换
以上3个步骤想要实现得完美还是非常复杂的,例如以下情况:
如果我们要定位下面这张图片的4个角
原图
一种办法就是图像处理得非常好,每个角都非常清晰,这样就比较方便定位,但是很多情况下处理后得到的轮廓是这样的
很明显,左下角缺了一块,这样的话我们定位出来的角就有可能不准确,见下图圆圈圈住的地方
我们实际需要的是这样的
还有一个比较复杂的问题就是如何获取目标对象真实的高度和宽度,比如下面这张图
我们很难得到这张纸真实的高度,无法按照真实的比例进行透视变换。网上有文章说需要通过“相机标定”去解决这个问题,暂时没有时间去研究。
等会下面我会着重说一下4角定位,相机标定这个问题以后有时间了再看下。
//读取原始图像
Mat src = Imgcodecs.imread(input);
Mat dst = new Mat();
//Imgproc.pyrMeanShiftFiltering(src, dst, 50, 10);//均值偏移
//output(TMP_FOLDER + "/0_meanshift.jpg", dst);
Mat kernel = new Mat(3, 3, CvType.CV_32F,new Scalar(-1));
kernel.put(1, 1, 8.9);
Imgproc.filter2D(src, dst, src.depth(),kernel);//锐化
output(TMP_FOLDER + "/" + (i++) + "_sharpening.jpg", dst);
Imgproc.cvtColor(dst, dst, Imgproc.COLOR_RGB2GRAY);//灰度化
output(TMP_FOLDER + "/" + (i++) + "_gray.jpg", dst);
//Imgproc.equalizeHist(dst, dst);//直方图均衡化
//output(TMP_FOLDER + "/" + (i++) + "_equalizeHist.jpg", dst);
ImageUtil.gammaCorrection(dst, dst, 0.8f);//gamma校正
output(TMP_FOLDER + "/" + (i++) + "_gamma.jpg", dst);
Imgproc.GaussianBlur(dst, dst, new Size(5, 5), 0, 0);//高斯滤波
output(TMP_FOLDER + "/" + (i++) + "_gaussianBlur.jpg", dst);
Imgproc.threshold(dst, dst, 0, 255, Imgproc.THRESH_OTSU + Imgproc.THRESH_BINARY);//二值化
output(TMP_FOLDER + "/" + (i++) + "_thresholding.jpg", dst);
Mat element = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
//Imgproc.dilate(dst, dst, element);//膨胀
//output(TMP_FOLDER + "/" + (i++) + "_dilate.jpg", dst);
Imgproc.morphologyEx(dst, dst, Imgproc.MORPH_CLOSE, element);//闭运算
output(TMP_FOLDER + "/" + (i++) + "_morph_close.jpg", dst);
//有些图像多做次腐蚀检测边缘的效果感觉更好些
Imgproc.erode(dst, dst, element);//腐蚀
output(TMP_FOLDER + "/" + (i++) + "_erode.jpg", dst);
Imgproc.Canny(dst, dst, 30, 120, 3);//边缘检测
output(TMP_FOLDER + "/" + (i++) + "_canny.jpg", dst);
//查找轮廓
List contours = new ArrayList();
Mat hierarchy = new Mat();
Imgproc.findContours(dst, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE);
//加粗增强所有找到的轮廓
Imgproc.drawContours(dst, contours, -1, new Scalar(255), 3);
output(TMP_FOLDER + "/" + (i++) + "_strong.jpg", dst);
//再次查找轮廓
contours.clear();
hierarchy = new Mat();
Imgproc.findContours(dst, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE);
MatOfPoint mpoint = getMaximum(contours);
contours.clear();
contours.add(mpoint);
//画出唯一轮廓
dst.setTo(new Scalar(0));//填充为黑色
Imgproc.drawContours(dst, contours, -1, new Scalar(255, 255, 255), 3);
output(TMP_FOLDER + "/" + (i++) + "_lastContours.jpg", dst);
我们通过霍夫变换获得4条边包含的所有线段,当时看了很多文章都是直接使用HoughLinesP获得线段后直接去获取线段的交点,但是自己用的时候返回的很多都是非常短的线段,上面那篇文章的作者使用改变霍夫变换参数的方法去获取长线段,但是循环去调整参数感觉运行效率非常低(至少在java中运行起来非常慢)。一开始我想的方法是将整个图像分为4个区域,因为拍照扫描的时候基本对象都是位于屏幕相对中心位置的,那么将图像分为左上、右上、左下、右下四个区域,获取每个区域包含线段的point,然后计算每个区域离中心点最远的那个point就是我们要找的角,如下图
但是后来发现这种方式有个缺陷,就是之前说过的问题,由于图像处理问题没办法精确定位到左下角坐标。
后来想到了另一个方法,大致步骤如下:
1、还是先将图像分成4个区域
2、然后将线段分别归类到对应区域的集合中
/**
* 将线段放置到对应区域的对象中
* @param centerPoint
* @param point
* @param areaLines 存放区域线段集合的对象
* @param line
*/
private AreaLines putLines(Point centerPoint, Mat lines, int imgWidth, int imgHeight){
AreaLines areaLines = new AreaLines(imgWidth, imgHeight);
for(int i = 0;i < lines.rows();i++){
double[] line = lines.get(i, 0);
Point p1 = new Point(line[0], line[1]);
Point p2 = new Point(line[2], line[3]);
putLines(centerPoint, p1, areaLines, line);
putLines(centerPoint, p2, areaLines, line);
}
return areaLines;
}
/**
* 将线段放置到对应区域的对象中
* @param centerPoint
* @param point
* @param areaLines 存放区域线段集合的对象
* @param line
*/
private void putLines(Point centerPoint, Point point, AreaLines areaLines, double[] line){
if(point.x <= centerPoint.x && point.y <= centerPoint.y){//左上区域
if(!areaLines.getLeft_top_area().getLines().contains(line)){
areaLines.getLeft_top_area().getLines().add(line);
}
}else if(point.x > centerPoint.x && point.y <= centerPoint.y){//右上区域
if(!areaLines.getRight_top_area().getLines().contains(line)){
areaLines.getRight_top_area().getLines().add(line);
}
}else if(point.x <= centerPoint.x && point.y > centerPoint.y){//左下区域
if(!areaLines.getLeft_bottom_area().getLines().contains(line)){
areaLines.getLeft_bottom_area().getLines().add(line);
}
}else{
if(!areaLines.getRight_bottom_area().getLines().contains(line)){//右下区域
areaLines.getRight_bottom_area().getLines().add(line);
}
}
}
3、将每个区域的线段分成2组,就是角的两边,使用斜率分组,同一边的线段斜率必然是相近的
4、将一组中的N条线段合并为一条线段,取头尾的point
5、将合并后的线段延长到整个图像两边(不延长的话某些线段可能没有交点,如上图左下区域的两条边线)后计算交点
/**
* 获取两条线段的交点
* @return
*/
public Point getCrossPoint() throws Exception{
List group1 = new ArrayList();
List group2 = new ArrayList();
//计算出每条线段水平方向的角度,按角度对线段进行分组
List- lineList = getLinesAngle();
if(lineList.isEmpty()){
throw new Exception(Utils.type2Label(type) + "没有线段");
}
double angle = lineList.get(0).getAngle();
for(Item item : lineList){
double _angle = item.getAngle();
//角度相差30以内的认为是一组
if(Math.abs(angle - _angle) <= 30){
group1.add(item.getLine());
}else{
group2.add(item.getLine());
}
}
//将多条线段合并为一条线段,角的两边一共合并出2条线段
double[] longLine1 = getLine(group1);
double[] longLine2 = getLine(group2);
//延长线段到图像两端
double[] fullLine1 = getExtendedLine(longLine1);
double[] fullLine2 = getExtendedLine(longLine2);
//计算交点
Point crossPoint = Utils.getCrossPoint(fullLine1, fullLine2);
if(crossPoint == null){
throw new Exception(Utils.type2Label(type) + "未找到交点");
}
return crossPoint;
}
这样我们就得到了4个角比较精确的point
这里虽然还没有弄清相机标定的问题,不过在大多数情况下我们拍照扫描一个文件时不太会出现图像纵深的问题,所以暂时先不考虑,简单的获取目标的高度宽度做下透视变换。
//开始做透视变换
Mat mat = new Mat();
mat.push_back(new MatOfPoint2f(ltp));
mat.push_back(new MatOfPoint2f(rtp));
mat.push_back(new MatOfPoint2f(rbp));
mat.push_back(new MatOfPoint2f(lbp));
Size outputSize = getOutputSize(ltp, rtp, rbp, lbp);
Mat size = new Mat();
size.push_back(new MatOfPoint2f(new Point(0, 0)));
size.push_back(new MatOfPoint2f(new Point(outputSize.width, 0)));
size.push_back(new MatOfPoint2f(new Point(outputSize.width, outputSize.height)));
size.push_back(new MatOfPoint2f(new Point(0, outputSize.height)));
Mat pt = Imgproc.getPerspectiveTransform(mat, size);
Imgproc.warpPerspective(src, src, pt, new Size(outputSize.width, outputSize.height));
output(TMP_FOLDER + "/" + (i++) + "_final.jpg", src);
到这里基本就结束了,运行后的结果如下:
原图1:
运行结果1:
原图2:
运行结果2:
最后可以根据需要对图像做进一步处理,比如二值化等操作。
源码可以从这里下载https://download.csdn.net/download/rwzhang/10887278,因为一边研究一边在写,修修改改的,可能代码看着稍微有一点乱,但是注释都写了,应该还是能看清的。等有时间了把图像预处理和相机标定再研究下,这两块也是非常重要的,直接决定了是否能够找到需要的轮廓以及还原比例是否正确!