目录
前言
一、资源引入
1.1引入资源
1.2加载动态库
二、基本步骤
三、代码实现
四、最终效果
主要需求就是实现发票上传时小幅度的偏移可以自动矫正,对此我使用了opencv来实现,主要原理就是识别出照片中最大四边形轮廓,然后计算这个四边形的旋转角度,这个方法也可以用来做身份证、车票、人民币等。
window环境:
把opencv-455.jar包、opencv_java455.dll文件放入项目
下载地址:
https://pan.baidu.com/s/15NdIxM2BM7Zrj6l1arZ6fw?pwd=ufmo
https://pan.baidu.com/s/1iqbXE4mxuOFa84-Lqcs1ug?pwd=fyo7
liunx环境:
需要把opencv_java455.dll换成libopencv_java455.so这个文件,这个文件根据linux版本不一样内容也不一样,需要自己生成,生成方式看我另一篇文章。
static {
//加载opencv动态库,必要
if (SystemUtils.IS_OS_WINDOWS) {
path = "C:/data/inv/";
URL url = ClassLoader.getSystemResource("lib/opencv/window/opencv_java455.dll");
System.out.println("windos加载");
System.load(url.getPath());
} else if (SystemUtils.IS_OS_LINUX) {
path = "/inv/";
System.out.println("linux加载");
System.load("/data/libopencv_java455.so");
}
}
1、灰度化
2、二值化
3、高斯模糊降噪,避免环境中的花纹影像
4、膨胀
5、腐蚀
6、边缘检测
7、找出最大轮廓
找到所有轮廓,然后找面积最大并且是四边形的轮廓
8、计算旋转角度并旋转
通过这个轮廓的4的顶点计算一个中心点,通过这个中心点旋转
public class OpenCVUtil {
//本地新建
public static String path;
public static void main(String[] args) throws IOException {
autoRectify("1.jpg", "2.jpg");
}
static {
//加载opencv动态库,必要
if (SystemUtils.IS_OS_WINDOWS) {
path = "C:/data/inv/";
URL url = ClassLoader.getSystemResource("lib/opencv/window/opencv_java455.dll");
System.out.println("windos加载");
System.load(url.getPath());
} else if (SystemUtils.IS_OS_LINUX) {
path = "/inv/";
System.out.println("linux加载");
System.load("/data/libopencv_java455.so");
}
System.out.println(path);
}
public static boolean autoRectify(String srcPath, String rotatePath) throws IOException {
// 输入图片
Mat src = Imgcodecs.imread(path + srcPath);
// 找到最大矩形
RotatedRect rect = findMaxRect(src);
if (rect != null) {
Point[] rectPoint = new Point[4];
rect.points(rectPoint);
saveImg("画出集合逼近的点", src, rectPoint, null);
// 得到旋转角度
double angle = rect.angle;
if (angle > 45) {
angle = 90 - angle;
} else {
angle = -angle;
}
if (angle == 90) {
angle = 0.0;
}
// 矫正图片
src = Imgcodecs.imread(path + srcPath);
// 定义中心
Point center = new Point();
center.x = src.cols() / 2.0;
center.y = src.rows() / 2.0;
Mat matrix = Imgproc.getRotationMatrix2D(center, -angle, 1);
Imgproc.warpAffine(src, src, matrix, src.size(), 1, 0, new Scalar(255, 255, 255));
} else {
src = Imgcodecs.imread(path + srcPath);
}
Imgcodecs.imwrite(path + rotatePath, src);
return true;
}
//得到图片参数
public static Map getPictureParam(String srcPath) {
// 输入图片
Mat src = Imgcodecs.imread(srcPath);
// 找到最大矩形
RotatedRect rect = findMaxRect(src.clone());
Map map = new HashMap<>(16);
if (rect != null) {
//Point[] rectPoint = findFourPoint(srcPath);
Point[] rectPoint = new Point[4];
rect.points(rectPoint);
saveImg("画出集合逼近的点", src, rectPoint, null);
// 得到对应参数
int maxX = 0, minX = (int) rectPoint[0].x, maxY = 0, minY = (int) rectPoint[0].y;
for (Point point : rectPoint) {
if (point.x > 0) {
if (point.x > maxX) {
maxX = (int) point.x;
}
if (point.x < minX) {
minX = (int) point.x;
}
} else {
minX = 0;
}
if (point.y > 0) {
if (point.y > maxY) {
maxY = (int) point.y;
}
if (point.y < minY) {
minY = (int) point.y;
}
} else {
minY = 0;
}
}
if (src.width() < maxX) {
maxX = src.width();
}
if (src.height() < maxY) {
maxY = src.height();
}
if (0 > minX) {
minX = 0;
}
if (0 > minY) {
minY = 0;
}
map.put("x", minX);
map.put("y", minY);
map.put("w", maxX - minX);
map.put("h", maxY - minY);
map.put("width", src.width());
map.put("height", src.height());
//查看效果(测试)
saveImg("裁剪", src, null, map);
}
return map;
}
//裁切图片
public static boolean rect(int x, int y, int w, int h, String source, String target) {
Mat mat = Imgcodecs.imread(OpenCVUtil.path + source);//原始图片
Mat m = new Mat(mat, new Rect(x, y, w, h));//剪辑之后的图片
Imgcodecs.imwrite(OpenCVUtil.path + target, m);
return true;
}
//图片压缩
public static void reduce(String source, double size) {
File srcFile = new File(OpenCVUtil.path + source);
while (srcFile.length() > (long) (1024 * size)) {
try {
Thumbnails.of(srcFile).scale(0.5f).toFile(srcFile);
} catch (IOException e) {
throw new ClientCustomException("图片缩略图压缩失败");
}
srcFile = new File(OpenCVUtil.path + source);
}
}
/*显示图片 测试专用*/
public static void saveImg(String rotatePath, Mat src, Point[] rectPoint, Map map) {
if(rectPoint!=null){
// 画点
Mat approxPolyMat2 = src.clone();
Scalar color=new Scalar(0,0,255);//设置线的颜色
Imgproc.line(src,rectPoint[0],rectPoint[1],color);
Imgproc.line(src,rectPoint[1],rectPoint[2],color);
Imgproc.line(src,rectPoint[2],rectPoint[3],color);
Imgproc.line(src,rectPoint[3],rectPoint[0],color);
for( int i = 0; i < rectPoint.length ; i++) {
setPixel(approxPolyMat2, (int)rectPoint[i].y, (int) rectPoint[i].x, 255);
}
HighGui.imshow(rotatePath, approxPolyMat2);
HighGui.waitKey();
}else if(map!=null){
// 裁剪
src=src.submat(new Rect(map.get("x"),map.get("y"),map.get("w"),map.get("h")));
HighGui.imshow(rotatePath, src);
HighGui.waitKey();
}else{
HighGui.imshow(rotatePath, src);
HighGui.waitKey();
}
}
//画点
public static void setPixel(Mat areaMat, int y, int x, int color) {
Imgproc.circle(areaMat, new Point(x, y), 4, new Scalar(color, 100, 100), 1, -1);
}
/**
* 判断是否是大于一定面积四边形
*/
public static boolean isQuadrangle(MatOfPoint contour) {
boolean flag = false;
MatOfInt hull = new MatOfInt();
MatOfPoint2f approx = new MatOfPoint2f();
approx.convertTo(approx, CvType.CV_32F);
// 边框的凸包
Imgproc.convexHull(contour, hull);
// 用凸包计算出新的轮廓点
Point[] contourPoints = contour.toArray();
int[] indices = hull.toArray();
List newPoints = new ArrayList<>();
for (int ind : indices) {
newPoints.add(contourPoints[ind]);
}
MatOfPoint2f contourHull = new MatOfPoint2f();
contourHull.fromList(newPoints);
// 多边形拟合凸包边框(此时的拟合的精度较低)
Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true) * 0.02, true);
MatOfPoint approxf1 = new MatOfPoint();
approx.convertTo(approxf1, CvType.CV_32S);
// 四边形的各个角度都接近直角的凸四边形
int row = approx.rows();
if (row == 4) {
if (Imgproc.isContourConvex(approxf1)) {
Point[] point = approxf1.toArray();
int num = 0;
for (int j = 0; j < row; j++) {
double cosine = getAngle(point[j % row], point[(j + 2) % row], point[(j + 3) % row]);
cosine = Math.abs(cosine);
// 角度大概72度
if (cosine < 0.3) {
num++;
}
}
//3个角在72到90度
if (num >= 2) {
flag = true;
}
}
}
return flag;
}
/**
* 找最大轮廓
*/
public static RotatedRect findMaxRect(Mat src) {
//1.灰度化
Imgproc.cvtColor(src, src, Imgproc.COLOR_BGR2GRAY);
saveImg("灰度化", src, null, null);
//2.二值化处理
Imgproc.threshold(src, src, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);//灰度图像二值化,效果不是很好,阴影部分大面积全黑
saveImg("二值化处理", src, null, null);
//3..高斯模糊降噪,避免环境中的花纹影响边缘检测
Imgproc.GaussianBlur(src, src, new Size(5, 5), 1.0);
saveImg("高斯模糊降噪", src, null, null);
double num = src.width() * 0.001;
Mat structImage = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size((int) (num*3>1?num*3:1) , (int) (num*2>1?num*2:1)));
//4.膨胀
Imgproc.dilate(src, src, structImage, new Point(-1, -1),(int) (num*4>1?num*4:1) );
saveImg("膨胀",src,null,null);
//5.腐蚀
Imgproc.erode(src, src, structImage, new Point(-1,-1), (int) (num*4>1?num*4:1) );
saveImg("腐蚀",src,null,null);
//6.边缘检测
Mat cannyMat = new Mat();
Imgproc.Canny(src, cannyMat, num * 50, num * 100);
saveImg("边缘检测", cannyMat, null, null);
List contours = new ArrayList();
Mat hierarchy = new Mat();
//7.寻找轮廓
Imgproc.findContours(cannyMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE, new Point(0, 0));
//8.找出匹配到的最大轮廓
int areaMax = src.width() * src.height();
double areaMin = areaMax * 0.05;
double area = 0;
int index = -1;
for (int i = 0; i < contours.size(); i++) {
double tempArea = Imgproc.boundingRect(contours.get(i)).area();
// 筛选出面积大于某一阈值的,且四边形的各个角度都接近直角的凸四边形
if (tempArea > areaMin && isQuadrangle(contours.get(i))) {
if (tempArea > area) {
area = tempArea;
index = i;
}
} else {
continue;
}
}
//6.获取轮廓内最大的矩形
RotatedRect rect = null;
if (index != -1) {
MatOfPoint2f matOfPoint2f = new MatOfPoint2f(contours.get(index).toArray());
rect = Imgproc.minAreaRect(matOfPoint2f);
}
return rect;
}
// 根据三个点计算中间那个点的夹角 pt1 pt0 pt2 cos值
private static double getAngle(Point pt1, Point pt2, Point pt0) {
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return (dx1 * dx2 + dy1 * dy2) / Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2) + 1e-10);
}
}