Java使用OpenCV(4.6.0)进行答题卡识别

版本选择

目前支持JDK1.8的最新版本是OpenCV-4.6.0

之后的版本都需要JDK11及以上。

对于OpenCV的Jar的兼容性还是挺好的,4.5.5的方法在4.8.1上都是可用的。

安装OpenCV

Windows环境

https://github.com/opencv/opencv/releases

image-20241101175616862

下载这个exe文件,它是一个自解压文件

安装的时候选择目录的时候不用新建opencv,在解压的时候会自动创建opencv文件夹。

比如我选择的安装位置是D:\Tools,它实际上会解压到D:\Tools\opencv这个目录。

CentOS源码编译

构建

下载地址

https://github.com/opencv/opencv/releases

安装依赖

1
sudo yum install -y cmake gcc gcc-c++ gtk2-devel pkgconfig libpng-devel libjpeg-devel libtiff-devel libavcodec-devel libavformat-devel libswscale-devel libv4l-devel gstreamer-plugins-base-devel python-devel python-numpy

下载

1
wget -O opencv-4.6.0.zip https://github.com/opencv/opencv/archive/4.6.0.zip

安装

1
2
3
4
unzip opencv-4.6.0.zip
cd opencv-4.6.0 && mkdir build && cd build

cmake -D BUILD_JAVA=ON -D BUILD_SHARED_LIBS=ON -D WITH_CUDA=OFF -D WITH_OPENCL=OFF -D CMAKE_BUILD_TYPE=RELEASE -D OPENCV_GENERATE_PKGCONFIG=ON -D CMAKE_INSTALL_PREFIX=/usr/local/opencv4.6.0 ..

注意只有显示如下才能成功构建Java依赖

image-20250120182651975

如果没安装ant要先安装ant

查看是否安装ant

1
ant -version

安装ant

1
yum install -y ant

然后输入下述命令进行编译

1
make -j$(nproc)

使用make如果中间如果因为特殊原因断了,可以使用make命令再继续。

编译100%完成后最后输入下述命令进行安装:

1
make install

输入以下命令:

1
2
3
4
cd /etc/ld.so.conf.d/
sudo touch opencv.conf
sudo sh -c 'echo "/usr/local/opencv4.6.0/lib64" > opencv.conf'
sudo ldconfig

然后,复制.pc文件:

1
sudo cp -f /usr/local/opencv4.6.0/lib64/pkgconfig/opencv4.pc /usr/lib64/pkgconfig/opencv4.pc

注意,我安装的是4.6.0,生成的是opencv4.pc,不是opencv.pc
测试一下:

1
pkg-config --modversion opencv4

升级cmake

注意

本文中使用的cmake3就不用再升级cmake了。

cmake需要3.5.1以上的版本

1
2
3
4
5
6
sudo yum install build-essential libssl-dev
wget http://www.cmake.org/files/v3.16/cmake-3.16.6.tar.gz
tar xf cmake-3.16.6.tar.gz
sudo chmod -R 777 cmake-3.16.6
cd cmake-3.16.6
./bootstrap && make && make install

通过yum命令移除原先的CMake。

1
yum remove cmake -y

建立目标版本CMake的软连接。

1
ln -s /usr/local/bin/cmake /usr/bin

查看版本

1
cmake --version

升级libstdc

升级cmake的时候又报错

version `GLIBCXX_3.4.20‘ not found 解决方法

解决方法

1
2
3
4
5
6
7
8
9
10
su root
cd /usr/local/lib64
# 下载最新版本的libstdc.so_.6.0.26
sudo wget http://www.vuln.cn/wp-content/uploads/2019/08/libstdc.so_.6.0.26.zip
unzip libstdc.so_.6.0.26.zip
# 将下载的最新版本拷贝到 /usr/lib64
cp libstdc++.so.6.0.26 /usr/lib64
cd /usr/lib64
# 查看 /usr/lib64下libstdc++.so.6链接的版本
ls -l | grep libstdc++

可以看到

libstdc++.so.6 ->libstdc++.so.6.0.19

1
2
3
4
5
6
# 删除/usr/lib64原来的软连接libstdc++.so.6,删除之前先备份一份
sudo rm libstdc++.so.6
# 链接新的版本
sudo ln -s libstdc++.so.6.0.26 libstdc++.so.6
# 查看新版本,成功
strings /usr/lib64/libstdc++.so.6 | grep GLIBCXX

确认库文件存在

1
ls /usr/local/opencv4.6.0/share/java/opencv4 | grep libopencv_java460

设置 java.library.path

确保 JVM 能够找到 OpenCV 的原生库。你可以在启动 Java 应用程序时通过命令行参数或环境变量设置 java.library.path

通过命令行参数设置

1
java -Djava.library.path=/usr/local/opencv4.6.0/share/java/opencv4 -cp /path/to/your/jar your.main.Class

通过环境变量设置

在启动 Java 应用程序之前,设置 LD_LIBRARY_PATH 环境变量:

1
export LD_LIBRARY_PATH=/usr/local/opencv4.6.0/share/java/opencv4:$LD_LIBRARY_PATH

代码中加载

1
2
3
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

CentOS中YUM安装

不建议使用YUM安装,YUM中的OpenCv版本太老了。

查看版本

1
yum list opencv

安装

1
2
yum install opencv opencv-devel opencv-python -y
pkg-config --modversion opencv

卸载

1
yum remove opencv opencv-devel opencv-python -y

Ubuntu中APT安装

1
2
sudo apt update
sudo apt install -y libopencv-dev python3-opencv

获取版本

1
python3 -c "import cv2; print(cv2.__version__)"

卸载

1
sudo apt remove -y libopencv-dev python3-opencv

Ubuntu源码编译

https://github.com/opencv/opencv/releases

添加环境依赖

1
2
3
4
5
6
sudo apt install -y build-essential cmake git pkg-config libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev python3-dev python3-numpy \
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev

cmake需要3.5.1以上的版本

查看cmake版本

1
cmake --version

安装:

1
2
3
4
5
6
7
unzip opencv-4.6.0.zip
cd opencv-4.6.0
mkdir build && cd build

cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local/opencv4.6.0 \
-D OPENCV_GENERATE_PKGCONFIG=ON ..

然后输入下述命令进行编译:

1
make -j12

使用make -j4或者make -j8进行多线程编译,虽然速度快但是容易出问题。

使用make如果中间如果因为特殊原因断了,可以使用make命令再继续。

编译100%完成后最后输入下述命令进行安装:

1
make install

输入以下命令:

1
2
3
4
5
cd /etc/ld.so.conf.d/
sudo touch opencv.conf
# 根据安装位置
sudo sh -c 'echo "/usr/local/opencv4.6.0/lib" > opencv.conf'
sudo ldconfig

然后,复制.pc文件:

1
sudo cp -f /usr/local/opencv4.6.0/lib/pkgconfig/opencv4.pc /usr/lib/pkgconfig/opencv4.pc

注意,我安装的是4.6.0,生成的是opencv4.pc,不是opencv.pc
测试一下:

1
pkg-config --modversion opencv4

加载依赖

项目中添加jar

jar的位置在安装目录下build\java的目录下。

下面两种方式任选其一即可。

推荐方式1更灵活。

但是

无论哪一种都要确保开发和部署时OpenCV的版本号要完全一致。

引用Jar

1
mvn install:install-file -Dfile=D:\Tools\opencv\build\java\opencv-460.jar -DgroupId="org.opencv" -DartifactId="opencv" -Dversion="4.6.0" -Dpackaging=jar

项目下引用

1
2
3
4
5
<dependency>
<groupId>org.opencv</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0</version>
</dependency>

引用本地库

方式1

添加测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;

public class Test01 {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
System.out.println("Welcome to OpenCV " + Core.VERSION);
Mat m = new Mat(5, 10, CvType.CV_8UC1, new Scalar(0));
System.out.println("OpenCV Mat: " + m);
Mat mr1 = m.row(1);
mr1.setTo(new Scalar(1));
Mat mc5 = m.col(5);
mc5.setTo(new Scalar(5));
System.out.println("OpenCV Mat data:\n" + m.dump());
}
}

加载本地库查找位置也可以通过两种方式设置。

配置运行时参数

通过菜单Run->Edit Configurations...打开Run/Debug Configurations对话框。

在对话框窗口右侧,找到VM options标签对应的文本框。

在文本框中填写参数

-Djava.library.path=D:\Tools\opencv\build\java\x64

看好自己Java是64为就选x64,32位就选x86。

添加环境变量

1
D:\Tools\opencv\build\java\x64

注意

推荐使用环境变量的方式,这样方便本地测试和服务端部署。

设置环境变量后IDEA需要重启才能生效。

方式2

这种方式就不用再指定java.library.path了。

加载项目下的库文件

把jar和dll放到项目中。

image-20220621163543761

代码中

1
2
3
4
static {
URL url = ClassLoader.getSystemResource("lib/opencv/opencv_java455.dll");
System.load(url.getPath());
}

加载指定位置的库文件

1
2
3
4
static {
String dllPath = "D:\\Tools\\opencv\\build\\java\\x64\\opencv_java455.dll"; // 绝对路径
System.load(dllPath);
}

常用方法

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
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.OpenCVUtil;


public class Test01 {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
// 以灰度方式,读取图片
Mat img = Imgcodecs.imread("D:\\Pic\\0.png", Imgcodecs.IMREAD_GRAYSCALE);
Imgcodecs.imwrite("D:\\Pic\\1.png", img);

// 转成二值化图片
Mat img2 = new Mat();
Imgproc.threshold(img, img2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
Imgcodecs.imwrite("D:\\Pic\\2.png", img2);

// 膨胀
Mat img3 = OpenCVUtil.eroding(img2);
Imgcodecs.imwrite("D:\\Pic\\3.png", img3);


}
}

如上就分别展示了

  1. 图片转灰度
  2. 灰度图片二值化
  3. 二值化图片黑色区域膨胀
  4. 图片的裁剪

灰度

1
2
3
// 以灰度方式,读取图片
Mat img = Imgcodecs.imread("D:\\Pic\\0.png", Imgcodecs.IMREAD_GRAYSCALE);
Imgcodecs.imwrite("D:\\Pic\\1.png", img);

二值化

1
2
3
4
// 转成二值化图片
Mat img2 = new Mat();
Imgproc.threshold(img, img2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
Imgcodecs.imwrite("D:\\Pic\\2.png", img2);

膨胀

1
2
3
// 膨胀
Mat img3 = OpenCVUtil.eroding(img2);
Imgcodecs.imwrite("D:\\Pic\\3.png", img3);

高斯模糊

1
2
3
Mat img01 = new Mat();
Imgproc.GaussianBlur(img, img01, new Size(1, 1), 10, 10);
Imgcodecs.imwrite("D:\\Pic\\img01.png", img01);

剪裁

1
2
3
4
// 截取左上角四分之一区域
Rect rect = new Rect(0, 0, img2.cols() / 2, img2.rows() / 2);
Mat img4 = new Mat(img2, rect);
Imgcodecs.imwrite("D:\\Pic\\4.png", img4);

拼接

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
/**
* 垂直合并多个Mat
*
* @param matList 图片列表
* @return Mat
*/
public static Mat jointMatV(List<Mat> matList) {
if (matList.isEmpty()) {
return new Mat();
}
int rows = 0;
int cols = 0;
for (Mat imgTemp : matList) {
rows += imgTemp.rows();
cols = Math.max(cols, imgTemp.cols());
}
// 创建一个白色背景的Mat对象
Mat result = new Mat(rows, cols, matList.get(0).type(), new Scalar(255, 255, 255));
int tempRows = 0;
for (Mat itemMat : matList) {
// 定义复制的区域
Mat roi = result.submat(new Rect(0, tempRows, itemMat.cols(), itemMat.rows()));
// 将itemMat的内容复制到ROI中
itemMat.copyTo(roi);
tempRows += itemMat.rows();
}
return result;
}

/**
* 水平合并多个Mat
*
* @param matList 图片列表
* @return Mat
*/
public static Mat jointMatH(List<Mat> matList) {
if (matList.isEmpty()) {
return new Mat();
}
int rows = 0;
int cols = 0;
for (Mat imgTemp : matList) {
rows = Math.max(rows, imgTemp.rows());
cols += imgTemp.cols();
}
// 创建一个白色背景的Mat对象
Mat result = new Mat(rows, cols, matList.get(0).type(), new Scalar(255, 255, 255));
int tempCols = 0;
for (Mat itemMat : matList) {
// 定义复制的区域
Mat roi = result.submat(new Rect(tempCols, 0, itemMat.cols(), itemMat.rows()));
// 将itemMat的内容复制到ROI中
itemMat.copyTo(roi);
tempCols += itemMat.cols();
}
return result;
}

Mat转Base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Mat转Base64字符串
*
* @param mat
* @param addPrefix
* @return
*/
public static String matToBase64(Mat mat, boolean addPrefix) {
// 将Mat对象转换为PNG格式的字节数组
MatOfByte matOfByte = new MatOfByte();
Imgcodecs.imencode(".jpeg", mat, matOfByte);
// 将字节数组转换为Base64字符串
byte[] byteArray = matOfByte.toArray();
String imgBase64Str = Base64.getEncoder().encodeToString(byteArray);
if (addPrefix) {
String prefix = "data:image/jpeg;base64,";
return prefix + imgBase64Str;
}
return imgBase64Str;
}

工具类

通用

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
package cn.psvmc.utils.opencv;

import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import java.util.Base64;
import java.util.List;

public class CvCommonUtils {
/**
* 逆时针旋转90度
*
* @param source 原Mat
* @return Mat
*/
public static Mat rotate90Counter(Mat source) {
Mat dst = new Mat();
Core.rotate(
source,
dst,
Core.ROTATE_90_COUNTERCLOCKWISE
);
return dst;
}

/**
* 顺时针旋转90度
*
* @param source 原Mat
* @return Mat
*/
public static Mat rotate90(Mat source) {
Mat dst = new Mat();
Core.rotate(
source,
dst,
Core.ROTATE_90_CLOCKWISE
);
return dst;
}

/**
* 旋转180度
*
* @param source 原Mat
* @return Mat
*/
public static Mat rotate180(Mat source) {
Mat dst = new Mat();
Core.rotate(
source,
dst,
Core.ROTATE_180
);
return dst;
}

/**
* 旋转任意角度
*
* @param source 原Mat
* @param angle 角度
* @return Mat
*/
public static Mat rotate(Mat source, int angle) {
angle = -angle;
// 获取图像的尺寸
Size size = source.size();
int width = (int) size.width;
int height = (int) size.height;
// 创建一个新的Mat对象来存储旋转后的图像
Mat dst;
int tempAngle = Math.abs(angle) % 180;
if (tempAngle > 45) {
dst = new Mat(
width,
height,
source.type()
);
} else {
dst = new Mat(
height,
width,
source.type()
);
}
// 获取图像的中心点
Point center = new Point((double) width / 2, (double) height / 2);
// 计算旋转矩阵
Mat rotMat = Imgproc.getRotationMatrix2D(
center,
angle,
1.0
);
// 调整旋转矩阵的平移部分以保持图像在目标矩阵中心
rotMat.put(
0,
2,
rotMat.get(
0,
2
)[0] + (dst.size().width / 2.0 - center.x)
);
rotMat.put(
1,
2,
rotMat.get(1, 2)[0] + (dst.size().height / 2.0 - center.y)
);
// 执行仿射变换(旋转)
Imgproc.warpAffine(
source,
dst,
rotMat,
dst.size()
);
return dst;
}

/**
* 缩放
*
* @param source 原Mat
* @param width 目标宽度
* @param height 目标高度
* @return Mat
*/
public static Mat resize(Mat source, int width, int height) {
// 检查输入图像是否有效
if (source.empty()) {
return new Mat();
}
// 获取源图像的宽高
int sWidth = source.cols();
int sHeight = source.rows();
// 若目标宽度和高度同时为0,直接返回源图像
if (width == 0 && height == 0) {
return source.clone(); // 深拷贝源图像
}
// 计算目标宽高
double aspectRatio = (double) sWidth / sHeight; // 原始宽高比
int targetWidth = (width > 0) ? width : (int) (height * aspectRatio);
int targetHeight = (height > 0) ? height : (int) (width / aspectRatio);
// 创建结果Mat并进行缩放
Mat resultMat = new Mat();
Imgproc.resize(source, resultMat, new Size(targetWidth, targetHeight));
return resultMat;
}

/**
* 以灰度化读取图片
*
* @param imgPath 图片路径
* @return Mat
*/
public static Mat gray(String imgPath) {
return Imgcodecs.imread(
imgPath,
Imgcodecs.IMREAD_GRAYSCALE
);
}

// 灰度化
public static Mat gray(Mat inputImage) {
Mat grayImage = new Mat();
Imgproc.cvtColor(
inputImage,
grayImage,
Imgproc.COLOR_BGR2GRAY
);
return grayImage;
}

/**
* 二值化 输入源必须是灰度化的图片
*
* @param grayImage 灰度化Mat
* @return Mat
*/
public static Mat binary(Mat grayImage) {
Mat binaryImage = new Mat();
Imgproc.threshold(
grayImage,
binaryImage,
0,
255,
Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU
);
return binaryImage;
}

/**
* 腐蚀(黑色区域变大)
*
* @param source 原Mat
* @return Mat
*/
public static Mat eroding(Mat source) {
return eroding(
source,
1
);
}

/**
* 腐蚀(黑色区域变大)
*
* @param source 原Mat
* @param erosion_size 腐蚀大小
* @return Mat
*/
public static Mat eroding(Mat source, double erosion_size) {
Mat resultMat = new Mat(
source.rows(),
source.cols(),
source.type()
);
Mat element = Imgproc.getStructuringElement(
Imgproc.MORPH_RECT,
new Size(
erosion_size + 1,
erosion_size + 1
)
);
Imgproc.erode(
source,
resultMat,
element
);
return resultMat;
}

/**
* 膨胀(白色区域变大)
*
* @param source 原Mat
* @return Mat
*/
public static Mat dilation(Mat source) {
return dilation(
source,
1
);
}

/**
* 膨胀(白色区域变大)
*
* @param source 原Mat
* @param dilation_size 膨胀因子2*x+1 里的x
* @return Mat
*/
public static Mat dilation(Mat source, double dilation_size) {
Mat resultMat = new Mat(
source.rows(),
source.cols(),
source.type()
);
Mat element = Imgproc.getStructuringElement(
Imgproc.MORPH_RECT,
new Size(
2 * dilation_size + 1,
2 * dilation_size + 1
)
);
Imgproc.dilate(
source,
resultMat,
element
);
return resultMat;
}

/**
* 获取区域Mat
*
* @param source 原Mat
* @param rect 区域
* @return Mat
*/
public static Mat getRectMat(Mat source, Rect rect) {
return new Mat(
source,
rect
);
}

/**
* 获取填涂率
*
* @param source 原Mat
* @param rect 区域
* @return Mat
*/
public static double getSmearRate(Mat source, Rect rect) {
// 创建一个新的Mat对象,它表示源图像中的指定区域
Mat matTemp = new Mat(
source,
rect
);
// 计算非零像素的数量
int count = Core.countNonZero(matTemp);
// 计算总像素数
int total = matTemp.cols() * matTemp.rows();
// 计算零像素的比率
double rate = (double) (total - count) / total;
// 释放临时Mat对象的资源
matTemp.release();
// 返回比率是否大于最大值
return rate;
}

/**
* 是否涂卡
*
* @param source 图片
* @param rect 区域
* @param min 最小的填涂率
* @return 是否填涂
*/
public static boolean isSmearCard(Mat source, Rect rect, double min) {
// 计算零像素的比率
double rate = getSmearRate(
source,
rect
);
// 返回比率是否大于最大值
return rate > min;
}

/**
* 颜色反转
*
* @param source 原Mat
* @return Mat
*/
public static Mat bitwiseNot(Mat source) {
Mat resultMat = new Mat();
Core.bitwise_not(
source,
resultMat
);
return resultMat;
}

/**
* 垂直合并多个Mat
*
* @param matList 图片列表
* @return Mat
*/
public static Mat jointMatV(List<Mat> matList) {
if (matList.isEmpty()) {
return new Mat();
}
int rows = 0;
int cols = 0;
for (Mat imgTemp : matList) {
rows += imgTemp.rows();
cols = Math.max(cols, imgTemp.cols());
}
// 创建一个白色背景的Mat对象
Mat result = new Mat(rows, cols, matList.get(0).type(), new Scalar(255, 255, 255));
int tempRows = 0;
for (Mat itemMat : matList) {
// 定义复制的区域
Mat roi = result.submat(new Rect(0, tempRows, itemMat.cols(), itemMat.rows()));
// 将itemMat的内容复制到ROI中
itemMat.copyTo(roi);
tempRows += itemMat.rows();
}
return result;
}

/**
* 水平合并多个Mat
*
* @param matList 图片列表
* @return Mat
*/
public static Mat jointMatH(List<Mat> matList) {
if (matList.isEmpty()) {
return new Mat();
}
int rows = 0;
int cols = 0;
for (Mat imgTemp : matList) {
rows = Math.max(rows, imgTemp.rows());
cols += imgTemp.cols();
}
// 创建一个白色背景的Mat对象
Mat result = new Mat(rows, cols, matList.get(0).type(), new Scalar(255, 255, 255));
int tempCols = 0;
for (Mat itemMat : matList) {
// 定义复制的区域
Mat roi = result.submat(new Rect(tempCols, 0, itemMat.cols(), itemMat.rows()));
// 将itemMat的内容复制到ROI中
itemMat.copyTo(roi);
tempCols += itemMat.cols();
}
return result;
}

/**
* Mat转Base64字符串
*
* @param mat mat
* @return Base64字符串
*/
public static String matToBase64(Mat mat) {
return matToBase64(mat, true);
}

/**
* Mat转Base64字符串
*
* @param mat mat
* @param addPrefix 是否添加前缀
* @return Base64字符串
*/
public static String matToBase64(Mat mat, boolean addPrefix) {
// 将Mat对象转换为PNG格式的字节数组
MatOfByte matOfByte = new MatOfByte();
Imgcodecs.imencode(".jpeg", mat, matOfByte);
// 将字节数组转换为Base64字符串
byte[] byteArray = matOfByte.toArray();
String imgBase64Str = Base64.getEncoder().encodeToString(byteArray);
if (addPrefix) {
String prefix = "data:image/jpeg;base64,";
return prefix + imgBase64Str;
}
return imgBase64Str;
}

/**
* 保存图片
*
* @param img 图片Mat
* @param path 保存路径
*/
public static void saveImg(Mat img, String path) {
Imgcodecs.imwrite(
path,
img
);
}
}

透视变换

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
package cn.psvmc.utils.opencv;

import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;

import java.util.Arrays;
import java.util.List;

/**
* 透视变换工具类
* 因为我透视变换做的也不是很好,就仅提供一个大概的函数...
*/
public class CvWarpPerspectiveUtils {

/**
* 透视变换
*
* @param src
* @param points
* @return
*/
public static Mat warpPerspective(Mat src, Point[] points) {
// 点的顺序[左上 ,右上 ,右下 ,左下]
List<Point> listSrcs = Arrays.asList(
points[0],
points[1],
points[2],
points[3]
);
Mat srcPoints = Converters.vector_Point_to_Mat(listSrcs, CvType.CV_32F);

List<Point> listDsts = Arrays.asList(
new Point(0, 0),
new Point(src.width(), 0),
new Point(src.width(), src.height()),
new Point(0, src.height())
);


Mat dstPoints = Converters.vector_Point_to_Mat(listDsts, CvType.CV_32F);

Mat perspectiveMmat = Imgproc.getPerspectiveTransform(dstPoints, srcPoints);

Mat dst = new Mat();

Imgproc.warpPerspective(
src,
dst,
perspectiveMmat,
src.size(),
Imgproc.INTER_LINEAR + Imgproc.WARP_INVERSE_MAP,
1,
new Scalar(0)
);

return 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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

package cn.psvmc.utils.opencv;

import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.Point;
import org.opencv.imgproc.Imgproc;

import java.util.Vector;

/**
* 轮廓工具类
*/
public class CvContoursUtils {

/**
* 获取图片的四个顶点
*
* @param img
* @return
*/
public static Point[] getAllPoints(Mat img) {
Point[] potArr = new Point[4];
for (int i = 0; i < 4; i++) {
potArr[i] = new Point(-1, -1);
}

int[] spaceArr = new int[]{-1, -1, -1, -1};
int cols = img.cols();
int rows = img.rows();
int x1 = cols / 3;
int x2 = cols * 2 / 3;
int y1 = rows / 3;
int y2 = rows * 2 / 3;
for (int x = 0; x < cols; x++) {
for (int y = 0; y < rows; y++) {
if (x > x1 && x < x2 && y > y1 && y < y2) {
continue;
}
double[] darr = img.get(y, x);
if (darr != null && darr.length >= 1 && darr[0] == 0) {
if (spaceArr[0] == -1) {
potArr[0].x = x;
potArr[0].y = y;
potArr[1].x = x;
potArr[1].y = y;
potArr[2].x = x;
potArr[2].y = y;
potArr[3].x = x;
potArr[3].y = y;
spaceArr[0] = getSpace(0, 0, x, y);
spaceArr[1] = getSpace(cols, 0, x, y);
spaceArr[2] = getSpace(cols, rows, x, y);
spaceArr[3] = getSpace(0, rows, x, y);
} else {
int s0 = getSpace(0, 0, x, y);
int s1 = getSpace(cols, 0, x, y);
int s2 = getSpace(cols, rows, x, y);
int s3 = getSpace(0, rows, x, y);
if (s0 < spaceArr[0]) {
spaceArr[0] = s0;
potArr[0].x = x;
potArr[0].y = y;
}
if (s1 < spaceArr[1]) {
spaceArr[1] = s1;
potArr[1].x = x;
potArr[1].y = y;
}
if (s2 < spaceArr[2]) {
spaceArr[2] = s2;
potArr[2].x = x;
potArr[2].y = y;
}
if (s3 < spaceArr[3]) {
spaceArr[3] = s3;
potArr[3].x = x;
potArr[3].y = y;
}
}

}
}
}
return potArr;
}

/**
* 轮廓识别,使用最外轮廓发抽取轮廓RETR_EXTERNAL,轮廓识别方法为CHAIN_APPROX_SIMPLE
*
* @param source 传入进来的图片Mat对象
* @return 返回轮廓结果集
*/
public static Vector<MatOfPoint> findContours(Mat source) {
Mat rs = new Mat();

/**
* 定义轮廓识别方法
* 边缘近似方法(除了RETR_RUNS使用内置的近似,其他模式均使用此设定的近似算法)。可取值如下:
*CV_CHAIN_CODE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。
*CHAIN_APPROX_NONE:将所有的连码点,转换成点。
*CHAIN_APPROX_SIMPLE:压缩水平的、垂直的和斜的部分,也就是,函数只保留他们的终点部分。
*CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:使用the flavors of Teh-Chin chain近似算法的一种。
*LINK_RUNS:通过连接水平段的1,使用完全不同的边缘提取算法。使用CV_RETR_LIST检索模式能使用此方法。
*/
Vector<MatOfPoint> contours = new Vector<MatOfPoint>();
Imgproc.findContours(
source,
contours,
rs,
Imgproc.RETR_LIST,
Imgproc.CHAIN_APPROX_SIMPLE
);
return contours;
}

/**
* 计算两点之间的距离
*
* @param x1
* @param y1
* @param x2
* @param y2
* @return
*/
private static int getSpace(int x1, int y1, int x2, int y2) {
int xspace = Math.abs(x1 - x2);
int yspace = Math.abs(y1 - y2);
return (int) Math.sqrt(Math.pow(xspace, 2) + Math.pow(yspace, 2));
}
}

处理全流程

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
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.opencv.ContoursUtils;
import utils.opencv.OpenCVUtil;
import utils.opencv.WarpPerspectiveUtils;

import java.util.Vector;


public class Test01 {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}

public static void main(String[] args) {
String basepath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

// 以灰度方式,读取图片
Mat img = Imgcodecs.imread(basepath + "0.jpg", Imgcodecs.IMREAD_GRAYSCALE);
Imgcodecs.imwrite(basepath + "1.png", img);

// 转成二值化图片
Mat img2 = new Mat();
Imgproc.threshold(img, img2, 0, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
Imgcodecs.imwrite(basepath + "2.png", img2);

// 透视变形
Mat img3 = WarpPerspectiveUtils.warpPerspective(img2, ContoursUtils.getAllPoints(img2));
Imgcodecs.imwrite(basepath + "3.png", img3);

// 膨胀
Mat img4 = OpenCVUtil.eroding(img3);
Imgcodecs.imwrite(basepath + "4.png", img4);

// 截取选择题区域
Rect rect = new Rect(68, 834, 1536, 220);
Mat img5 = new Mat(img4, rect);
Imgcodecs.imwrite(basepath + "5.png", img5);

// 获取边界
Vector<MatOfPoint> rectVec = ContoursUtils.findContours(img5);
Vector<MatOfPoint> rectVec2 = new Vector<>();
for (MatOfPoint matOfPoint : rectVec) {
Rect rect2 = Imgproc.boundingRect(matOfPoint);
if (rect2.width > 36 && rect2.height > 20 && rect2.width < 50 && rect2.height < 40) {
rectVec2.add(matOfPoint);
}
}
Mat img6 = new Mat(img5.rows(), img5.cols(), CvType.CV_8UC3, new Scalar(255, 255, 255));
Imgproc.drawContours(img6, rectVec2, -1, new Scalar(0, 0, 255), 1);
Imgcodecs.imwrite(basepath + "6.png", img6);
}
}

绘图

画线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String basePath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

Scalar bgColor = new Scalar(255, 255, 255, 0); // B G R 0 白色
Scalar color = new Scalar(0, 0, 255, 0); // B G R 0 红色
Mat img01 = new Mat(new Size(400, 500), CvType.CV_8UC3, bgColor);
Imgproc.line(
img01,
new Point(11, 22),
new Point(220, 330),
color,
1,
Imgproc.LINE_AA
);
Imgcodecs.imwrite(basePath + "line.png", img01);

说明

参数 解释
InputOutPutArray img 在 img 图像上绘制
Point pt1 端点1
Point pt2 端点2
Scalar& color 颜色
int thickness 线条厚度
lineType 线条边缘类型(LINE_4(边缘像素采用4连通,即上下左右),LINE_8(边缘像素采用8连通,即上下左右还有四个对角),LINE_AA(边缘像素采用高斯滤波,抗锯齿))

画椭圆

1
2
3
4
5
6
7
String basePath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

Scalar bck = new Scalar(255, 255, 255, 0); // B G R 0 白色
Scalar scalar = new Scalar(0, 0, 255, 0); // B G R 0 红色
Mat img01 = new Mat(new Size(400, 500), CvType.CV_8UC3, bck);
Imgproc.ellipse(img01, new Point(256, 256), new Size(100, 50), 0, 0, 360, scalar);
Imgcodecs.imwrite(basePath + "ellipse.png", img01);

矩形

1
2
3
4
5
6
7
String basePath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

Scalar bck = new Scalar(255, 255, 255, 0); // B G R 0 白色
Scalar scalar = new Scalar(0, 0, 255, 0); // B G R 0 红色
Mat img01 = new Mat(new Size(400, 500), CvType.CV_8UC3, bck);
Imgproc.rectangle(img01, new Point(100, 20), new Point(310, 148), scalar);
Imgcodecs.imwrite(basePath + "rectangle.png", img01);

圆圈

1
2
3
4
5
6
7
String basePath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

Scalar bck = new Scalar(255, 255, 255, 0); // B G R 0 白色
Scalar scalar = new Scalar(0, 0, 255, 0); // B G R 0 红色
Mat img01 = new Mat(new Size(400, 500), CvType.CV_8UC3, bck);
Imgproc.circle(img01, new Point(100, 100), 63, scalar);
Imgcodecs.imwrite(basePath + "circle.png", img01);

文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String basePath = "D:\\Project\\Java\\opencv-demo01\\pic\\";

Scalar bck = new Scalar(255, 255, 255, 0); // B G R 0 白色
Scalar scalar = new Scalar(0, 0, 255, 0); // B G R 0 红色
Mat img01 = new Mat(new Size(400, 500), CvType.CV_8UC3, bck);
Imgproc.putText(
img01,
"Hello Word",
new Point(10, 100),
Imgproc.FONT_HERSHEY_SIMPLEX,
2,
scalar,
2,
Imgproc.LINE_AA,
false
);
Imgcodecs.imwrite(basePath + "text.png", img01);

要将文本放入图像中,需要指定以下内容。

  • 您要写入的文字数据
  • 您要放置它的位置坐标(即文字的左下角)。
  • 字体类型
  • 字体比例(指定字体大小)
  • 线条类型 为了获得更好的外观,建议使用LINE_AA

Java加载本地库的方式

System.loadLibrary(Core.NATIVE_LIBRARY_NAME);System.load(dllPath); 都用于在 Java 程序中加载本地库,

但它们有一些关键的区别:

System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

  • 作用:这个方法加载的是基于库名称的本地库。根据操作系统,会自动将库名称转换为对应的操作系统特定格式,并从标准库路径中查找和加载库。
  • 使用场景:适用于加载系统默认路径中的库,例如:
    • 在 Windows 上,Core.NATIVE_LIBRARY_NAME 通常是 "opencv_java455",系统会自动查找并加载 opencv_java455.dll
    • 在 Linux 上,系统会查找并加载 libopencv_java455.so
  • 优点:简化了加载过程,不需要指定库文件的完整路径。
  • 缺点:依赖于系统默认库路径的配置。如果库文件不在默认路径中,可能会导致加载失败。

System.load(dllPath);

  • 作用:这个方法加载指定路径的本地库文件。你需要提供库文件的完整路径。
  • 使用场景:适用于加载不在系统默认路径中的库文件,或者是需要加载特定版本的库文件。
  • 优点:灵活性高,可以加载任何位置的库文件。
  • 缺点:需要明确指定库文件的完整路径,容易出错。