开发环境
本次处理图片主要使用的依赖是opencv
,具体的方法可以去参照官网。
首先去opencv官网去下载与系统对应的jar包和依赖文件。(这里以windows举例)
选择windows版本opencv-4.7.0-windows.exe
,然后进行安装。(linux的版本需要先进行编译后获取动态库和jar包)
在安装目录的opencv/build/java
的目录下,获得jar
包和动态库文件。(注意是安装目录里)
在maven项目引入opencv
依赖。
1 2 3 4 5 6 7 8
| <dependency> <groupId>org.opencv</groupId> <artifactId>opencv</artifactId> <version>4.7.0</version> <scope>system</scope> <systemPath>${basedir}/src/main/resources/lib/opencv-470.jar</systemPath> </dependency>
|
加载opencv
动态库
由于opencv的开发语言问题,所以Java在使用opencv相关类的时候,需要先加载对应的动态库才行,不然直接使用会出现异常。需要注意linux和windows的动态库是不一样的。
1 2 3 4 5 6 7 8
|
public static void load() { System.load(ResourceUtil.getResource("lib/opencv_java470.dll").getPath()); }
|
好像最新版本的opencv(4.7.0)不支持Java8,只能有Java11,如果用Java8的话,可以使用4.6.0的版本。
如果觉得opencv配置环境过于麻烦,可以使用Javacv,Javacv其实就是调用opencv进行操作的,虽然官方缺少文档,不过和opencv的调用方法都差不多。
opencv基础操作
读入图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Test {
public static void main(String[] args) { OpencvUtil.load(); String filePath = "C:/Users/11419/Desktop/project/image-process/src/main/resources/test/1.jpg"; Mat mat = Imgcodecs.imread(filePath);
System.out.println(mat.rows()); System.out.println(mat.cols()); for (int i = 0, rows = mat.rows(); i < rows; i++) { for (int j = 0, cols = mat.cols(); j < cols; j++) { double[] data = mat.get(i, j); System.out.println(Arrays.toString(data)); } } }
}
|
写出图片
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Test {
public static void main(String[] args) { OpencvUtil.load(); String filePath = "C:/Users/yww/Desktop/project/image-process/src/main/resources/test/1.jpg"; String output = "C:/Users/yww/Desktop/project/image-process/src/main/resources/test/2.jpg";
Mat mat = Imgcodecs.imread(filePath); Imgcodecs.imwrite(output, mat); }
}
|
Imgcodecs.imread()
这个方法,偶然碰见过一次保存失败的情况,成功运行却没有保存文件,所以还可以通过字节方式保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public static void saveImage(Mat mat, String filePath) { saveImage(mat, filePath, "png"); }
public static void saveImage(Mat mat, String filePath, String ext) { MatOfByte matOfByte = new MatOfByte(); Imgcodecs.imencode("." + ext, mat, matOfByte); byte[] byteArray = matOfByte.toArray(); FileUtil.writeBytes(byteArray, filePath); }
|
图片处理方法
图片信息
获取图片DPI
暂时有两种方法可以获取图片的DPI。
commons-imaging依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
public static int getDpi1(String filePath) { ImageInfo imageInfo = null; try { imageInfo = Imaging.getImageInfo(FileUtil.file(filePath)); } catch (ImageReadException | IOException e) { throw new RuntimeException("获取图片信息出错!"); } if (null == imageInfo) { return -1; } else { return imageInfo.getPhysicalWidthDpi(); } }
|
metadata-extractor依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public static int getDpi2(String filePath) { Metadata metadata = null; int res = -1; try { metadata = ImageMetadataReader.readMetadata(FileUtil.file(filePath)); for (Directory directory : metadata.getDirectories()) { for (Tag tag : directory.getTags()) { if ("X Resolution".equals(tag.getTagName())) { res = Convert.toInt(tag.getDescription()); } } } } catch (ImageProcessingException | IOException e) { throw new RuntimeException("获取图片信息出错!"); } return res; }
|
获取图片分辨率
1 2 3 4 5 6 7 8 9 10 11 12
|
public static int[] getResolution(String filePath) { Mat mat = Imgcodecs.imread(filePath); int widthResolution = mat.width(); int heightResolution = mat.height(); return new int[]{widthResolution, heightResolution}; }
|
这些操作实质是获取图片的基本信息得到的,所以图片大部分的基本信息都可以通过这些方法修改得到。
图片旋转
这里的图片旋转使用了Graphics2D
进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public static void rotateImage(String src, String dst, double degree) { BufferedImage image = ImgUtil.read(src);
int width = image.getWidth(); int height = image.getHeight(); int type = image.getType(); BufferedImage res = new BufferedImage(width, height, type); Graphics2D graphics = res.createGraphics(); graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics.setBackground(Color.WHITE); graphics.fillRect(0, 0, width, height); graphics.rotate(Math.toRadians(degree), width >> 1, height >> 1); graphics.drawImage(image, 0, 0, null); graphics.dispose();
ImgUtil.write(res, FileUtil.file(dst)); }
|
纠正图片旋转
有些时候,电脑会自动对一些图片进行旋转显示(源文件不变),比如手机拍的一些图片,竖着拍的,电脑显示是竖着的,但是源文件其实是横着的,进行图片旋转,其实是按横着的源文件图片进行旋转,所以有时候会感觉旋转角度不对,所以需要先纠正图片旋转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
public static void correctImg(String srcImgPath) { FileOutputStream fos = null; try { File srcFile = new File(srcImgPath); int angle = getAngle(srcFile); if (angle != 90 && angle != 270) { return; } BufferedImage srcImg = ImageIO.read(srcFile); int imgWidth = srcImg.getHeight(); int imgHeight = srcImg.getWidth(); double centerWidth = ((double) imgWidth) / 2; double centerHeight = ((double) imgHeight) / 2; BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB); Graphics2D graphics = targetImg.createGraphics(); graphics.rotate(Math.toRadians(angle), centerWidth, centerHeight); graphics.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null); graphics.rotate(Math.toRadians(-angle), centerWidth, centerHeight); graphics.dispose(); fos = new FileOutputStream(srcFile); ImageIO.write(targetImg, "jpg", fos); } catch (Exception e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } } } }
private static int getAngle(File file) throws Exception { Metadata metadata = ImageMetadataReader.readMetadata(file); for (Directory directory : metadata.getDirectories()) { for (Tag tag : directory.getTags()) { if ("Orientation".equals(tag.getTagName())) { String orientation = tag.getDescription(); if (orientation.contains("90")) { return 90; } else if (orientation.contains("180")) { return 180; } else if (orientation.contains("270")) { return 270; } } } } return 0; }
|
灰度化
大多数彩色图片的每个像素都是由红绿蓝三个通道组成的,为了更方便的处理图像,将这三个通道的值按一定比例进行加权平均,得到一个单通道的灰度值,用来表示该像素的颜色亮度。大多数图片处理都会先进行图片灰度化,因为只有一个通道,处理起来方便很多。opencv的灰度化操作,只需要调用方法即可。灰度化操作的话,主要是要考虑图片通道数量的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public static Mat gray(Mat mat) { Mat gray = new Mat(); int channel = mat.channels(); if (channel == 1) { gray = mat.clone(); } else if (channel == 2) { Mat temp = new Mat(); Core.addWeighted(mat, 0.5, mat, 0.5, 0, temp); Imgproc.cvtColor(temp, gray, Imgproc.COLOR_BGR2GRAY); } else if (channel == 4 || channel == 3) { Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY); } else { throw new UnsupportedOperationException("不支持灰度化的通道数: -->" + channel); } return gray; }
|
高斯滤波
处理图像时,往往需要对图像进行平滑操作以去除噪声,同时也可以模糊图像以达到柔化的效果。高斯滤波(Gaussian blur)就是一种常用的平滑滤波器,其通过对图像像素进行加权平均的方式来实现平滑和模糊的效果。高斯滤波的原理是使用一个高斯核(Gaussian kernel)对图像像素进行加权平均。一般来说,高斯核的大小或标准差越大,平滑效果越明显,图像的细节丢失也会越多。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public static Mat gaussianBlur(Mat mat) { Mat blurred = mat.clone(); Imgproc.GaussianBlur(mat, blurred, new Size(3, 3), 0, 0); return blurred; }
|
边缘检测
边缘是指图像中颜色或亮度发生急剧变化的区域,通常是物体之间的边界或物体内部的纹理。边缘检测算法旨在在图像中找到这些边缘并将其标记出来。具体的步骤如下:
- 高斯滤波平滑,去除图像噪声,提高边缘检测的准确性
- 图像灰度化
- 进行边缘检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
public static Mat canny(Mat mat) { return canny(mat, 60, 200, 3); }
public static Mat canny(Mat mat, int threshold1, int threshold2, int apertureSize) { Mat blurred = new Mat(); Imgproc.GaussianBlur(mat, blurred, new Size(3, 3), 0, 0);
Mat gray = new Mat(); Imgproc.cvtColor(blurred, gray, Imgproc.COLOR_BGR2GRAY);
Mat cannyMat = new Mat(); Imgproc.Canny(gray, cannyMat, threshold1, threshold2, apertureSize);
return cannyMat; }
|
一些图片的实际应用
图片亮度处理
网上关于图片亮度检测的方法,大概有两种。
计算图片平均亮度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
public static double brightness(String filePath) { Mat src = Imgcodecs.imread(filePath); Mat gray = ImageUtil.gray(src.clone()); Scalar mean = Core.mean(gray); return mean.val[0]; }
|
计算图片亮度异常值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
private double[] brightness2(String filePath) { Mat src = Imgcodecs.imread(filePath); Mat gray = ImageUtil.gray(src.clone()); double a = 0; int[] hist = new int[256]; for (int i = 0; i < gray.rows(); i++) { for (int j = 0; j < gray.cols(); j++) { a += gray.get(i, j)[0] - 128; int index = (int) gray.get(i, j)[0]; hist[index]++; } } double da = a / (gray.rows() * gray.cols()); double ma = 0; for (int i = 0; i < hist.length; i++) { ma += Math.abs(i - 128 - da) * hist[i]; } ma = ma / (gray.rows() * gray.cols()); double cast = Math.abs(da) / Math.abs(ma); return new double[] {cast, da}; }
|
亮度调整主要是调整平均亮度值到一个自定义的值当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public void adjustBrightness(String src, String dst) { Mat mat = Imgcodecs.imread(src); Mat gray = ImageUtil.gray(mat.clone()); Scalar mean = Core.mean(gray); double brightness = mean.val[0];
Mat adjustedImage = mat.clone(); if (brightness < 100 || brightness > 250) { double alpha = 175 / brightness; gray.convertTo(adjustedImage, -1, alpha, 0); } Imgcodecs.imwrite(dst, adjustedImage); }
|
图片纠偏
图片纠偏是一个比较难的问题,主要的难点在于如何计算出图片的倾斜角度,最常见的方法就是霍夫变换检测直线了,以下是具体步骤。
- 图片边缘检测
- 霍夫变换,获取检测的直线
- 计算每条直线的倾斜角度
- 获取所有直线倾斜角度的平均值或者是众数来作为图片的倾斜角度。(关于平均值和众数,总会在某些情况下十分异常,我认为平均数比较可靠,但需要严格筛选需要内容的直线才行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
|
public static void deskew(String src, String dst) { Mat mat = Imgcodecs.imread(src); double angle = getDeskewAngle(mat); ImageUtil.rotateImage(src, dst, angle); }
public static double getDeskewAngle(Mat mat) { Mat canny = ImageUtil.canny(mat, 60, 200, 3);
Mat lines = new Mat(); int threshold = 30; double minLineLength = 0; double maxLineGap = 200; Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);
List<Integer> angelList = new ArrayList<>(); for (int i = 0; i < lines.rows(); i++) { double[] line = lines.get(i, 0); int k = calculateAngle(line[0], line[1], line[2], line[3]);
angelList.add(k); } if (angelList.isEmpty()) { return 0.0; } return most(angelList); }
private static int most(List<Integer> angelList) { if (angelList.isEmpty()) { return 0; } int res = 0; int max = Integer.MIN_VALUE; Map<Integer, Integer> map = new HashMap<>(); for (int i : angelList) { map.put(i, map.getOrDefault(i, 0) + 1); } for (Integer i : map.keySet()) { int count = map.get(i); if (count > max) { max = count; res = i; } } return res; }
private static int calculateAngle(double x1, double y1, double x2, double y2) { if (Math.abs(x2 - x1) < 1e-4) { return 90; } else if (Math.abs(y2 - y1) < 1e-4) { return 0; } else { double k = -(y2 - y1) / (x2 - x1); double res = 360 * Math.atan(k) / (2 * Math.PI); return Convert.toInt(Math.round(res)); } }
|
去除红色印章
有很多场景,比如说发票,白底黑字文件这种,在OCR识别的过程中,会受到红色印章的影响,所以需要先去除红色印章,在进行OCR识别,这样可以提高OCR的识别率。
红色印章检测
我碰到比较多的场景是白底黑字的图片,所以我这里就使用了检测红色像素,痛过红色像素的数量,来检测是否该图片有红色印章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
public static Boolean recognizeRed(String filePath) { Mat mat = Imgcodecs.imread(filePath); Mat hsv = new Mat(); Imgproc.cvtColor(mat, hsv, Imgproc.COLOR_BGR2HSV); int nums = 0; for (int i = 0; i < hsv.rows(); i++) { for (int j = 0; j < hsv.cols(); j++) { double[] clone = hsv.get(i, j).clone(); double h = clone[0]; double s = clone[1]; double v = clone[2]; if ((h > 0 && h < 10) || (h > 156 && h < 180)) { if (s > 43 && s < 255) { if (v < 255 && v > 46) { nums++; } } } } } return nums > 8000; }
|
有些图片看着是没有红色像素的,但是其实会存在有,所以先使用一个只有红色印章的图片进行检测,然后选择一个比较合理的值进行判断就可以。
这种方法不适合除了印章外还有很多红色的图片,那种情况可以考虑使用霍夫圆检测,那种可能会耗费很多时间,所以我没有使用。
去除红色印章
我这里去除红色印章的方法,主要的原理就是使用图片二值化,让印章的变成白色即可,但是会出现一个问题,就是红色比黑色更难二值化,所以需要先把图片的红色通道分离出来,红色通道的红色印章的颜色会变淡很多,从而二值化能去除掉红色印章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public static void removeRed(String filePath, String dst) { Mat mat = Imgcodecs.imread(filePath);
List<Mat> matList = new ArrayList<>(); Core.split(mat, matList); Mat red = matList.get(2);
Mat redThresh = new Mat(); Imgproc.threshold(red, redThresh, 120, 255, Imgproc.THRESH_BINARY);
Imgcodecs.imwrite(dst, redThresh); }
|
这种方法的结果图片,因为去除了蓝绿通道,所以会变得灰色,不过因为主要的目的是给OCR识别,所以无所谓,如果想不改变图片的话,还是挺麻烦的,可能还得识别出椭圆印章,然后精确的去除。