建立MySetting类

总是需要用到的一些量放入这个类,方便调用和修改。

  • 开发环境是Ubuntu。迁移Windows系统需要修改路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MySetting {
static String SECRETE_URL = "http://xk.zucc.edu.cn/CheckCode.aspx";

static String IMG_DOWN = "./data/downloads/";

// 字模位置
static String MODEL_ROOT = "./data/model";
static String MODEL = "./data/model/";

// 识别结果
static String IMG_RESULT = "./data/result/";

// 每次下载的数量
static int count = 100;
}

抓取验证码

先给项目导入HttpClient
通过HttpClient建立连接,Chrome审查元素可知验证码链接为教务系统链接后面加上“/CheckCode.aspx”
用Java文件输入输出流保存图片到本地
avatar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class getCodeIMG {
static void main() throws IOException {
System.out.println("当前单次爬取数量为:" + MySetting.count);
for (int i = 0; i < MySetting.count; i++) {
HttpGet secretCodeGet = new HttpGet(MySetting.SECRETE_URL);
CloseableHttpClient client = HttpClients.createDefault();
CloseableHttpResponse responseSecret = client.execute(secretCodeGet);
FileOutputStream fileOutputStream = new FileOutputStream(new File(MySetting.IMG_DOWN + "Code" + i + ".gif"));
responseSecret.getEntity().writeTo(fileOutputStream);
fileOutputStream.close();
System.out.println("Code" + i + ".gif ");
}
System.out.println("Finish!");
}
}

字模的生成与识别

思路是用现有的验证码生成OCR字模。再抓取一批验证码,使用字模打标。人工处理一遍,把错误的验证码找出来重新手工打标。打标用再生成字模。如此循环多次,模型不断完善,准确率达到85%以上即可

获取标签数据集

ZUCC_ZhenFangHelper下zf_train.tar.xz里面有32283张已经打好标签的验证码,选择1000张放入result

生成字模

验证码预处理(去噪、二值化)

观察验证码文本都是蓝色。遍历图片所有像素点,所有蓝色点变为黑色,其他为白色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static BufferedImage removeBackgroud(String picFile) throws Exception {
BufferedImage img = ImageIO.read(new File(picFile));
int width = img.getWidth();
int height = img.getHeight();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (isBlue(img.getRGB(x, y)) == 1) {
img.setRGB(x, y, Color.BLACK.getRGB());
} else {
img.setRGB(x, y, Color.WHITE.getRGB());
}
}
}
return img;
}

private static int isBlue(int colorInt) {
Color color = new Color(colorInt);
int rgb = color.getRed() + color.getGreen() + color.getBlue();
if (rgb == 153) {
return 1;
}
return 0;
}

切割验证码

观察验证码,发现每次文本位置不变。直接等快分割,不借助切割算法。高度在0-23像素之间,宽度在5-53像素之间。每个字符占据1/4的像素,因此规定第一个字符的像素在5-17之间,第二个在17-29之间,第三个在29-41之间,第四个在41-53之间。

1
2
3
4
5
6
7
8
private static List<BufferedImage> splitImage(BufferedImage img){
List<BufferedImage> subImgs = new ArrayList<>();
subImgs.add(img.getSubimage(5, 0, 12, 23));
subImgs.add(img.getSubimage(17, 0, 12, 23));
subImgs.add(img.getSubimage(29, 0, 12, 23));
subImgs.add(img.getSubimage(41, 0, 12, 23));
return subImgs;
}

传统的验证码识别中“9”,“0”,“o”出错率很高。但正方教务系统不会生成“9”,“o”,提高了准确率。只不过对于“l”,“i”,“1”这三个字符上出错率依旧很高,可以通过扩充字模降低

保存切割图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* 生成字模入口
*/
private static void trainModel() throws Exception {
File dir = new File(MySetting.IMG_RESULT);
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
BufferedImage img = removeBackgroud(MySetting.IMG_RESULT + file.getName());
List<BufferedImage> listImg = splitImage(img);
if (listImg.size() == 4) {
for (int j = 0; j < listImg.size(); j++) {
ImageIO.write(listImg.get(j), "gif",
new File(MySetting.MODEL + file.getName().charAt(j) + "-" + (index++) + ".gif"));
System.out.println(file.getName() + "\t" + file.getName().charAt(j) + "-" + (index++) + ".gif");
}
}
}
}
}

avatar

ZUCC_ZhenFangHelper/zfgetcode/data下有已经训练好的模型“model”和“model1”。其中“model1”是用三万多张那个数据集生成出来的,虽然识别率高,但大大降低了识别速率。“model”内的字模已经够用使用上已经够了

现有字模识别验证码

先去背景,二值化,分割成单个图片后,与字模库分别对比获得结果

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
/*
* 识别切割的单个字符
*/
private static String getSingleCharOcr(BufferedImage img, Map<BufferedImage, String> map) {
String result = "#";
int width = img.getWidth();
int height = img.getHeight();
int min = width * height;
for (BufferedImage bi : map.keySet()) {
int count = 0;
if (Math.abs(bi.getWidth() - width) > 2)
continue;
int widthmin = width < bi.getWidth() ? width : bi.getWidth();
int heightmin = height < bi.getHeight() ? height : bi.getHeight();
Label1: for (int x = 0; x < widthmin; ++x) {
for (int y = 0; y < heightmin; ++y) {
if (isBlack(img.getRGB(x, y)) != isBlack(bi.getRGB(x, y))) {
count++;
if (count >= min)
break Label1;
}
}
}
if (count <= 3) {
result = map.get(bi);
break;
}
else if (count < min) {
min = count;
result = map.get(bi);
}
}
return result;
}

二值化后的验证码,文本都变为黑色,比对黑色像素不同个数,不同像素点个数最少的作为识别结果

1
2
3
4
5
6
7
8
9
10
/*
* 识别已被处理成黑色的字符
*/
private static int isBlack(int colorInt) {
Color color = new Color(colorInt);
if (color.getRed() + color.getGreen() + color.getBlue() <= 100) {
return 1;
}
return 0;
}

人工校验字模识别的结果

用JavaFX制作人工校验图形化界面。校验时结果正确则回车下一个验证码,并删除图片。结果错误则输入框内输入正确值,把修改后的验证码移到WRONG目录下,校验完成后去该目录确认一遍,无误后移入result文件夹,调用字模生成接口,重新生成字模

avatar

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 void start(Stage primaryStage) {
root.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
String name = inputTF.getText();
File file = new File(MySetting.IMG_RESULT + nowFStr);
if (name == null || name.equals("")) {
rightCount++;
final boolean delete = file.delete();
if (!delete) {
System.out.println("删除正确文件失败");
}
} else {
inputTF.setText("");
File aimA = new File("./data/wrong/" + name + ".gif");
// 修正的文件
File aimB = new File("./data/wrongOrg/" + nowFStr);
// 错误的原始文件
try {
Files.copy(file.toPath(), aimA.toPath());
Files.copy(file.toPath(), aimB.toPath());
final boolean delete = file.delete();
if (!delete) {
System.out.println("删除错误文件失败");
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(
"Total: " + doneCount + "\tRight: " + rightCount + "\tWrong: " + (doneCount - rightCount));
rightL.setText("Correct: " + (1.0 * rightCount / doneCount));
}
});

不断重复上述步骤扩充字模库可以不断提高识别准确率,但随之而来的就是识别速率的下降
完整代码较长,可参见github


完整项目包括登录、获取信息和自动选课,其中登录模块的验证码识别采用OCR

OCR验证码识别(Python实现)使用Python调用生成好的字模,进行识别,用于项目中的自动登录功能

OCR的字模生成代码参考自Java实现正方教务验证码的识别-swiftMX