OpenCV OCR预处理

需求

需要识别出 标签中的 76046 58420

Tesseract 识别大图 速度特别慢

并且 Tesseract 本身也是默认你传递给它的图片是预处理过的

目标把图片处理成这样

旋转一下角度

实现

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
package org.lqs1848.cv.test;

import java.util.ArrayList;
import java.util.List;

import org.bytedeco.javacpp.indexer.IntIndexer;
import org.bytedeco.javacpp.indexer.UByteRawIndexer;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.MatVector;
import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_core.Size;

public class OpenCV_LableCatch {

public static void main(String[] args) {
//加载图片为灰度图
Mat src = opencv_imgcodecs.imread("1.jpg", opencv_imgcodecs.IMREAD_GRAYSCALE);

int x = src.cols(); // 列
int y = src.rows(); // 行

if (x != y) { // 非正方形的图 截取中心部分为正方形
if (x < y) {
int j = (y - x) / 2;
src = src.apply(new Rect(0, j, x, x));
} else {
int j = (x - y) / 2;
src = src.apply(new Rect(j, 0, y, y));
}
} // if

Mat gray = new Mat();
//把大小不确定的图片 转换为固定 3000 * 3000
opencv_imgproc.resize(src, gray, new Size(3000, 3000));

Mat target = new Mat();
// 二值化
opencv_imgproc.threshold(gray, target, 0, 255, opencv_imgproc.THRESH_OTSU);
opencv_imgcodecs.imwrite("E:\\cv\\ezh.jpg", target);

// new Scalar(255) 初始matrix的数值 不定义每次腐蚀膨胀的结果都会不一样 坑爹
Mat kernel = new Mat(new Size(20, 20), opencv_core.CV_8UC1, new Scalar(255));
// 腐蚀膨胀 修改迭代次数 和 kernel 的 size 保证数字有被处理成可识别的轮廓
opencv_imgproc.morphologyEx(target, src, opencv_imgproc.MORPH_OPEN, kernel, new Point(-1, -1), 2,
opencv_imgproc.MORPH_RECT, opencv_imgproc.morphologyDefaultBorderValue());
opencv_imgcodecs.imwrite("E:\\cv\\fspz.jpg", src);

// 反色
opencv_core.bitwise_not(src, src);
opencv_imgcodecs.imwrite("E:\\cv\\fs.jpg", src);

// 存放轮廓
MatVector contours = new MatVector();
Mat hierarchy = new Mat();
// 查找轮廓
opencv_imgproc.findContours(src, contours, hierarchy, opencv_imgproc.RETR_TREE,
opencv_imgproc.CHAIN_APPROX_NONE);

Mat contours_img = new Mat(src.size(), opencv_core.CV_8U, new Scalar(255));
List<Rect> rects = new ArrayList<>();
IntIndexer hie = hierarchy.createIndexer();
for (int i = 0; i < contours.size(); i++) {
if (hie.get(0, i, 2) == -1) {
Mat contour = contours.get(i);
double area = opencv_imgproc.contourArea(contour);
double mins = 100;
if (area > mins) {
Rect rect = opencv_imgproc.boundingRect(contour);
rects.add(rect);
opencv_imgproc.drawContours(contours_img, contours, i, new Scalar(0), -1, opencv_imgproc.LINE_8,
hierarchy, Integer.MAX_VALUE, new Point());
// opencv_imgcodecs.imwrite("E:\\cv\\x_" + i + ".jpg", target.apply(rect));
} // if
}
} // for
// opencv_imgcodecs.imwrite("E:\\cv\\x_.jpg", contours_img);

kernel = new Mat(new Size(7, 7), opencv_core.CV_8UC1, new Scalar(255));

int s1_id = 0, s2_id = 0, s1_jump = 0, s2_jump = 0;
for (int i = 0; i < rects.size(); i++) {
Rect rect = rects.get(i);
Mat roi = target.apply(rect);
if (roi.rows() <= roi.cols())
opencv_imgproc.resize(roi, roi, new Size(300, 100), 0, 0, opencv_imgproc.INTER_NEAREST);
else
opencv_imgproc.resize(roi, roi, new Size(100, 300), 0, 0, opencv_imgproc.INTER_NEAREST);

opencv_imgproc.morphologyEx(roi, roi, opencv_imgproc.MORPH_GRADIENT, kernel, new Point(-1, -1), 1,
opencv_imgproc.MORPH_RECT, opencv_imgproc.morphologyDefaultBorderValue());

//opencv_imgproc.medianBlur(roi, roi, 7);

int t_jump = stringJudge(roi);

// 选出跳变次数最多的两个轮廓
//if (t_jump < 20) {
if (t_jump >= s1_jump) {
s2_id = s1_id;
s2_jump = s1_jump;
s1_id = i;
s1_jump = t_jump;
} else if (t_jump < s1_jump && t_jump >= s2_jump) {
s2_id = i;
s2_jump = t_jump;
}
//} // if
} // for

// 结果排序,坐标最左为第一串数字
if (rects.get(s1_id).width() < rects.get(s1_id).height()
&& rects.get(s1_id).tl().y() < rects.get(s2_id).tl().y()) {
s1_id = s1_id + s2_id;
s2_id = s1_id - s2_id;
s1_id = s1_id - s2_id;
}

Rect ns1 = rects.get(s1_id);
if (isCentre(target, ns1))
ns1 = new Rect(ns1.x() - 10, ns1.y() - 10, ns1.width() + 20, ns1.height() + 20);
Rect ns2 = rects.get(s2_id);
if (isCentre(target, ns2))
ns2 = new Rect(ns2.x() - 10, ns2.y() - 10, ns2.width() + 20, ns2.height() + 20);

Mat dst1 = gray.apply(ns1);
Mat dst2 = gray.apply(ns2);

if (dst1.cols() < dst1.rows()) {
opencv_core.transpose(dst1, dst1);
opencv_core.flip(dst1, dst1, 1);
}
if (dst2.cols() < dst2.rows()) {
opencv_core.transpose(dst2, dst2);
opencv_core.flip(dst2, dst2, 1);
}

opencv_imgproc.resize(dst1, dst1, new Size(300, 100), 0, 0, opencv_imgproc.INTER_NEAREST);
opencv_imgcodecs.imwrite("E:\\cv\\r_111.jpg", dst1);
dst1 = OpenCVUtils.adjustAngle(dst1);
opencv_imgcodecs.imwrite("E:\\cv\\r_111_jz.jpg", dst1);
opencv_imgproc.resize(dst2, dst2, new Size(300, 100), 0, 0, opencv_imgproc.INTER_NEAREST);
opencv_imgcodecs.imwrite("E:\\cv\\r_222.jpg", dst2);
dst2 = OpenCVUtils.adjustAngle(dst2);
opencv_imgcodecs.imwrite("E:\\cv\\r_222_jz.jpg", dst2);
}


// 判断轮廓 是否在中心范围
public static boolean isCentre(Mat mat, Rect rect) {
int x = Math.abs((mat.cols() / 2) - (mat.cols() - rect.x()));
int y = Math.abs((mat.rows() / 2) - (mat.rows() - rect.y()));
return x < mat.cols() / 3 && y < mat.rows() / 3;
}//method isCentre

// 判断区域是否为数字
public static int stringJudge(Mat img) {
UByteRawIndexer intIndexer = img.createIndexer();
int rows = img.rows();
int cols = img.cols();
int jump = 0;
// 数字横着放,优先遍历列
if (rows < cols) {
for (int row = 0; row < rows; row++) {
boolean wb_flag = false;// 白色到黑色
boolean bw_flag = false;
int t_jump = 0;
for (int col = 0; col < cols; col++) {
if (col + 1 < cols) {
int now_point = intIndexer.get(row, col);
int next_point = intIndexer.get(row, col + 1);
if (now_point == 255 && next_point == 0) {
wb_flag = true;
}
if (now_point == 0 && next_point == 255 && wb_flag) {
bw_flag = true;
}
if (wb_flag && bw_flag) {
++t_jump;
wb_flag = false;
bw_flag = false;
}
}
if (t_jump > jump)
jump = t_jump;
}
}
} else {
for (int col = 0; col < cols; col++) {
boolean wb_flag = false;
boolean bw_flag = false;
int t_jump = 0;
for (int row = 0; row < rows; row++) {
if (row + 1 < rows) {
int now_point = intIndexer.get(row, col);
int next_point = intIndexer.get(row + 1, col);
if (now_point == 255 && next_point == 0) {
wb_flag = true;
}
if (now_point == 0 && next_point == 255 && wb_flag) {
bw_flag = true;
}
if (wb_flag && bw_flag) {
++t_jump;
wb_flag = false;
bw_flag = false;
}
}
if (t_jump > jump)
jump = t_jump;
}
}
}
return jump;
}//method

}//class

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
package org.lqs1848.cv.test;

import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.MatVector;
import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Point2f;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.RotatedRect;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_core.Size;

public class OpenCVUtils {

/**
* 矩形 矫正角度
*
* 非直线矫正!!!!
*
* @param gray
* @return
*/
public static Mat adjustAngle(Mat gray){
//Mat gray = opencv_imgcodecs.imread("E:\\test\\1.jpg", opencv_imgcodecs.IMREAD_GRAYSCALE);

Mat binImg = new Mat();
// 二值化
opencv_imgproc.threshold(gray, binImg, 100, 255, opencv_imgproc.CV_THRESH_BINARY_INV);
opencv_imgcodecs.imwrite("E:\\test\\ezh222.jpg", binImg);

Mat target = new Mat();
opencv_imgproc.threshold(gray, target, 0, 255, opencv_imgproc.THRESH_OTSU);

// new Size(15, 15) 可以慢慢调整
Mat kernel = new Mat(new Size(15, 15), opencv_core.CV_8UC1, new Scalar(255));
Mat morphologyDst = new Mat();
//1 迭代次数可以修改
opencv_imgproc.morphologyEx(binImg, morphologyDst, opencv_imgproc.MORPH_GRADIENT, kernel, new Point(-1, -1), 1,
opencv_imgproc.MORPH_RECT, opencv_imgproc.morphologyDefaultBorderValue());
//查看腐蚀膨胀的信息 根据这个图调整 size 保证腐蚀膨胀出来的轮廓是数字连在一起的轮廓
opencv_imgcodecs.imwrite("E:\\test\\morphology.jpg", morphologyDst);

Mat cannyDst = new Mat();
opencv_imgproc.Canny(morphologyDst, cannyDst, 150, 200);
//opencv_imgcodecs.imwrite("E:\\test\\canny.jpg", cannyDst);

// 获取最大矩形
RotatedRect rect = findMaxRect(cannyDst);
// 旋转矩形
Mat CorrectImg = rotation(target , rect);
opencv_imgcodecs.imwrite("E:\\test\\xz1.jpg", CorrectImg);
return CorrectImg;
//尝试二值化看效果会不会更好
/*
kernel = new Mat(new Size(0, 0), opencv_core.CV_8UC1, new Scalar(255));
opencv_imgproc.morphologyEx(CorrectImg, CorrectImg, opencv_imgproc.MORPH_GRADIENT, kernel, new Point(-1, -1), 1,
opencv_imgproc.MORPH_RECT, opencv_imgproc.morphologyDefaultBorderValue());
opencv_imgcodecs.imwrite("E:\\test\\xz2.jpg", CorrectImg);
return CorrectImg;
*/
}

public static RotatedRect findMaxRect(Mat cannyMat) {
MatVector contours = new MatVector();
Mat hierarchy = new Mat();
// 寻找轮廓
opencv_imgproc.findContours(cannyMat, contours, hierarchy, opencv_imgproc.RETR_EXTERNAL,
opencv_imgproc.CHAIN_APPROX_NONE, new Point(0, 0));
// 找出匹配到的最大轮廓
double area = opencv_imgproc.boundingRect(contours.get(0)).area();
int index = 0;
// 找出匹配到的最大轮廓
for (int i = 0; i < contours.size(); i++) {
double tempArea = opencv_imgproc.boundingRect(contours.get(i)).area();
if (tempArea > area) {
area = tempArea;
index = i;
}
}
Mat matOfPoint2f = new Mat(contours.get(index));
RotatedRect rect = opencv_imgproc.minAreaRect(matOfPoint2f);
Rect rect2 = opencv_imgproc.boundingRect(contours.get(index));
opencv_imgcodecs.imwrite("E:\\test\\lk.jpg", cannyMat.apply(rect2));
return rect;
}

public static Mat rotation(Mat cannyMat, RotatedRect rect) {
double angle = rect.angle();
Point2f center = rect.center();
Mat CorrectImg = new Mat(cannyMat.size(), cannyMat.type());
cannyMat.copyTo(CorrectImg);
// 得到旋转矩阵算子
Mat matrix = opencv_imgproc.getRotationMatrix2D(center, angle, 0.8);
opencv_imgproc.warpAffine(CorrectImg, CorrectImg, matrix, CorrectImg.size(), 1, 0, new Scalar(0, 0, 0, 0));
return CorrectImg;
}

}// class

fspz.jpg

fspz.jpg 出来的图要一点点调整参数 保证 两块数字区域被正确的处理成可识别的轮廓

如果数字区域 二值化后不可见 那就是无法识别

旋转要保证 数字的轮廓是正确的

首先 bytedeco 的 API 资料是真的少

网上的资料使用的 查找轮廓 返回的轮廓信息都是 二维数组

bytedeco 给的是个 Mat ???

找了好久才发现原来是用 Indexer 读取 (https://github.com/bytedeco/javacv/issues/276)

腐蚀膨胀的 kernel 不传递 Scalar 每次腐蚀膨胀出来的结果都不一样…

最后

如果对焦不准的话 处理后效果还是很一般 关键还是要原图有对上焦

线上代码还是同事进行了多种处理

比如 同时本地请求 并发查询百度

并且本地做多种处理

源码和上方代码有差异 以源码为准

源码