最近在弄验证码识别,由于本人对那些深度学习、AI 之类的都不了解,无意中接触到【完美验证码】这款软件(我只找到在win下使用的dll,其他系统下不知道怎么弄)。
关于完美验证码的训练和使用,网上教程很多,这里只说java如何对接,以及使用可能出现的问题,下面直接放 java 代码:
一、 外部依赖
jna (net.java.dev.jna:jna:5.6.0)可自行下载 github-jna
1. java代码
(1) WmOcr.java
package cn.ido2.ocr;
import com.sun.jna.Library;
public interface WmOcr extends Library {
boolean LoadWmFromFile(String path, String pwd);
/**
* 函数功能说明:从内存中载入识别库文件,成功返回True,否则返回False。
* 函数参数说明:
* FileBuffer :整数型,一个记录了识别库文件的二进制数据的字节数组,或一块同样功能的内存区域。这里请提供数组第一个成员的地址,或内存区域的地址。
* FileBufLen :整数型,上述字节数组的数组成员数,或内存区域大小。
* Password :文本型,识别库调用密码
*
* @param FileBuffer
* @param FileBufLen
* @param Password
* @return
*/
boolean LoadWmFromBuffer(byte[] FileBuffer, int FileBufLen, String Password);
boolean GetImageFromFile(String path, char[] result);
boolean GetImageFromBuffer(byte[] buffer, int length, byte[] result);
/**
* Private Declare Function SetWmOption Lib "WmCode.dll" (ByVal OptionIndex As Long,ByVal OptionValue As Long) As Boolean
* 函数功能说明:设定识别库选项。设定成功返回真,否则返回假。
* 函数参数说明:
* OptionIndex :整数型,选项索引,取值范围1~7
* OptionValue :整数型,选项数值。
*
* 参数详解:
* OptionIndex OptionValue
* 1. 返回方式 取值范围:0~1 默认为0,直接返回验证码,为1返回验证码字符和矩形范围形如:S,10,11,12,13|A,1,2,3,4 表示识别到文本 S 左边横坐标10,左边纵坐标11,右边横坐标,右边纵坐标12
*
* 2. 识别方式 取值范围:0~4 默认为0,0整体识别,1连通分割识别,2纵分割识别,3横分割识别,4横纵分割识别。可以进行分割的验证码,建议优先使用分割识别,因为分割后不仅能提高识别率,而且还能提高识别速度
*
* 3. 识别模式 取值范围:0~1 默认为0,0识图模式,1为识字模式。识图模式指的是背景白色视为透明不进行对比,识字模式指的是白色不视为透明,也加入对比。绝大多数我们都是使用识图模式,但是有少数部分验证码,使用识字模式更佳。
*
* 4. 识别加速 取值范围:0~1 默认为0,0为不加速,1为使用加速。一般我们建议开启加速功能,开启后对识别率几乎不影响。而且能提高3-5倍识别速度。
*
* 5. 加速返回 取值范围:0~1 默认为0,0为不加速返回,1为使用加速返回。使用加速返回一般用在粗体字识别的时候,可以大大提高识别速度,但是使用后,会稍微影响识别率。识别率有所下降。一般不是粗体字比较耗时的验证码,一般不用开启
*
* 6. 最小相似度 取值范围:0~100 默认为90
*
* 7. 字符间隙 取值范围:-10~0 默认为0,如果字符重叠,根据实际情况填写,如-3允许重叠3像素,如果不重叠的话,直接写0,注意:重叠和粘连概念不一样,粘连的话,其实字符间隙为0.
*
* @param OptionIndex
* @param OptionValue
* @return
*/
boolean SetWmOption(int OptionIndex, int OptionValue);
/**
* 设置传入传出dll的各个文本类型参数是否使用unicode格式,一次设置在程序运行期间有效。设置成功返回真,失败返回假
*
* 1. 传入是否使用unicode格式 取值范围:0~1 默认为0使用ansi格式,为1使用unicode文本
* 2. 传出是否使用unicode格式 取值范围:0~1 默认为0使用ansi格式,为1使用unicode文本
*
* @return
*/
boolean UseUnicodeString(int OptionIndex, int OptionValue);
/**
* Private Declare Function Calculator Lib "WmCode.dll" (ByVal Expression As String,ByVal CalcResult As String) As Boolean
* 函数功能说明:计算数学表达式。失败返回空文本,成功返回计算结果文本。功能简单,只是用来计算那些需要填写计算结果的验证码。计算完成返回真,否则返回假。
* 函数参数说明:
* @param Expression :文本型,数学表达式,只能计算加,减,乘,除,次方运算,支持小括号,中括号,大括号运算,支持负数运算。
* @param result :文本型,计算结果,使用需要将一个足够长的空白字符串赋值给它。
*/
boolean Calculator(byte[] Expression, byte[] result);
}
主要是要构造一个与dll对应的java对象,注释都来自于wmcode里面提供的
(2) OcrPredictWmCode.java
package cn.ido2.ocr.impl;
import com.sun.jna.Native;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public final class OcrPredictWmCode {
private static final String USER_HOME = System.getProperty("user.home");
/**
* 完美验证码中的 WmCode.dll 的路径,可以指定到其他路径,放在jar包中其实也没问题,但是使用spring-boot时加载不到,原因未知
*/
private static final String OCR_DLL = new File(USER_HOME, "WmCode3.2.1.dll").getAbsolutePath();
/**
* 使用完美验证码中训练出的文件
*/
private static final String OCR_DAT = new File(USER_HOME, "OCR2.dat").getAbsolutePath();
/**
* 训练文件的密码
*/
private static final String OCR_DAT_PWD = "123456";
public final static WmOcr ocr;
static {
ocr = Native.load(OCR_DLL, WmOcr.class);
loadFromAssets();
}
private static void loadFromAssets() {
try (
// 将文件放在 jar包内部的加载方式,使用spring-boot无法读取到文件
// InputStream stream = OcrPredict.class.getClassLoader().getResourceAsStream(OCR_DAT);
InputStream stream = new FileInputStream(OCR_DAT);
ByteArrayOutputStream output = new ByteArrayOutputStream();
) {
byte[] buffer = new byte[4096];
int n;
while (-1 != (n = stream.read(buffer))) {
output.write(buffer, 0, n);
}
byte[] fileBuffer = output.toByteArray();
boolean loadResult = ocr.LoadWmFromBuffer(fileBuffer, fileBuffer.length, OCR_DAT_PWD);
// 加载成功
if (loadResult) {
// 设置识别方式, 2纵分割识别
ocr.SetWmOption(2, 3);
// 设置字符间隙,负数是有连接部分,给3px,根据实际情况设置
ocr.SetWmOption(7, -3);
System.out.println("OcrPredict load Success.");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String predict(InputStream stream) throws Exception {
// 图片处理
BufferedImage image = OcrProcess.imageProcessing(stream);
byte[] buffer = OcrProcess.imageToBytes(image, "jpg");
// 存放结果的,根据实际情况设置长度
byte[] result = new byte[8];
boolean b = ocr.GetImageFromBuffer(buffer, buffer.length, result);
// 识别成功
if (b) {
// 截取 buffer中有数据的部分
int length = result.length;
for (int i = 0; i < result.length; i++) {
if (result[i] == 0) {
length = i;
break;
}
}
return new String(result, 0, length);
}
return null;
}
}
(2) OcrProcess.java
这个是图片处理类,自行根据情况处理
package cn.ido2.ocr.impl;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class OcrProcess {
/**
* 转换BufferedImage 数据为byte数组
*
* @param image Image对象
* @param format image格式字符串.如"gif","png"
* @return byte数组
*/
public static byte[] imageToBytes(BufferedImage image, String format) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
ImageIO.write(image, format, out);
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
public static BufferedImage imageProcessing(InputStream stream) throws IOException {
BufferedImage bufferedImage = ImageIO.read(stream);
if (bufferedImage == null) {
System.out.println("error");
return null;
}
int h = bufferedImage.getHeight();
int w = bufferedImage.getWidth();
// 灰度化
int[][] gray = new int[w][h];
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int argb = bufferedImage.getRGB(x, y);
// 图像加亮(调整亮度识别率非常高)
int r = (int) (((argb >> 16) & 0xFF) * 1.1 + 30);
int g = (int) (((argb >> 8) & 0xFF) * 1.1 + 30);
int b = (int) (((argb) & 0xFF) * 1.1 + 30);
if (r >= 255) {
r = 255;
}
if (g >= 255) {
g = 255;
}
if (b >= 255) {
b = 255;
}
//此处根据实际需要进行设定阈值
gray[x][y] = (int) Math.pow((
Math.pow(r, 2.2) * 0.2973
+ Math.pow(g, 2.2) * 0.6274
+ Math.pow(b, 2.2) * 0.0753), 1 / 2.2);
}
}
// 二值化
int threshold = ostu(gray, w, h);
BufferedImage binaryBufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY);
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
if (gray[x][y] > threshold) {
gray[x][y] |= 0x00FFFF;
} else {
gray[x][y] &= 0xFF0000;
}
binaryBufferedImage.setRGB(x, y, gray[x][y]);
}
}
//去除干扰点 或 干扰线(运用八领域,即像素周围八个点判定,根据实际需要判定)
for (int y = 1; y < h - 1; y++) {
for (int x = 1; x < w - 1; x++) {
boolean lineFlag = false;//去除线判定
int pointflagNum = 0;//去除点判定
if (isBlack(binaryBufferedImage.getRGB(x, y))) {
//左右像素点为"白"即空时,去掉此点
if (isWhite(binaryBufferedImage.getRGB(x - 1, y)) && isWhite(binaryBufferedImage.getRGB(x + 1, y))) {
lineFlag = true;
pointflagNum += 2;
}
//上下像素点为"白"即空时,去掉此点
if (isWhite(binaryBufferedImage.getRGB(x, y + 1)) && isWhite(binaryBufferedImage.getRGB(x, y - 1))) {
lineFlag = true;
pointflagNum += 2;
}
//斜上像素点为"白"即空时,去掉此点
if (isWhite(binaryBufferedImage.getRGB(x - 1, y + 1)) && isWhite(binaryBufferedImage.getRGB(x + 1, y - 1))) {
lineFlag = true;
pointflagNum += 2;
}
if (isWhite(binaryBufferedImage.getRGB(x + 1, y + 1)) && isWhite(binaryBufferedImage.getRGB(x - 1, y - 1))) {
lineFlag = true;
pointflagNum += 2;
}
//去除干扰线
if (lineFlag) {
binaryBufferedImage.setRGB(x, y, -1);
}
//去除干扰点
if (pointflagNum > 3) {
binaryBufferedImage.setRGB(x, y, -1);
}
}
}
}
return binaryBufferedImage;
}
public static boolean isBlack(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() <= 300;
}
public static boolean isWhite(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() > 300;
}
public static int isBlackOrWhite(int colorInt) {
if (getColorBright(colorInt) < 30 || getColorBright(colorInt) > 730) {
return 1;
}
return 0;
}
public static int getColorBright(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue();
}
public static int ostu(int[][] gray, int w, int h) {
int[] histData = new int[w * h];
// Calculate histogram
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int red = 0xFF & gray[x][y];
histData[red]++;
}
}
// Total number of pixels
int total = w * h;
float sum = 0;
for (int t = 0; t < 256; t++)
sum += t * histData[t];
float sumB = 0;
int wB = 0;
int wF = 0;
float varMax = 0;
int threshold = 0;
for (int t = 0; t < 256; t++) {
wB += histData[t]; // Weight Background
if (wB == 0)
continue;
wF = total - wB; // Weight Foreground
if (wF == 0)
break;
sumB += (float) (t * histData[t]);
float mB = sumB / wB; // Mean Background
float mF = (sum - sumB) / wF; // Mean Foreground
// Calculate Between Class Variance
float varBetween = (float) wB * (float) wF * (mB - mF) * (mB - mF);
// Check if new maximum found
if (varBetween > varMax) {
varMax = varBetween;
threshold = t;
}
}
return threshold;
}
}
注意 只能使用32位的jdk,我使用jdk版本是jdk8