阅读本文可能解决的问题:
① Java识别验证码
② 使用 Tess4J 和 OCR 识别
③ JavaCV二值化和灰度测试的使用
④ Java 裁剪和缩放图片
⑤ 如何生成数字&字母验证码
⑥ ...
这里我先说几句无关紧要的话。去年毕业的时候,我帮同学搭建了一个验证码识别系统。我一步步跟着大神的操作。我对二值化的这些操作没有深入的了解。在机器登录的操作中,验证码是绕不过去的,所以翻出来供参考,总结一下,分享给大家。同时我也发现除了一些政府网站外,我下面列出的数字&字母验证码很少使用,所以这个对于项目需要的标识目前可能没有任何参考价值,但是我在学习的过程中,我也学习了很多Tess4J、JavaCV等技术,以及缩放、裁剪等图像处理。
一、常用验证码识别
1.常用验证码
这里我主要识别第四类验证码。其实经过测试,我发现前三个都是可以识别的,但是裁剪和二值化的阈值需要调整一下。
2.识别思路
输入原始验证码后,先去除图片的干扰像素,再裁剪边角,最后使用Tess4J进行识别。
3.主要代码
主方法调用具体实现方法
public static void main(String[] args){
//原始验证码地址
String OriginalImg = "C:\\mysoftware\\images\\upload\\OcrImg\\oi.jpg";
//识别样本输出地址
String ocrResult = "C:\\mysoftware\\images\\upload\\OcrResult\\or.jpg";
//去噪点
ImgUtils.removeBackground(OriginalImg, ocrResult);
//裁剪边角
ImgUtils.cuttingImg(ocrResult);
//OCR识别
String code = Tess4J.executeTess4J(ocrResult);
//输出识别结果
System.out.println("Ocr识别结果: \n" + code);
}
removeBackground 方法去除了验证码的噪音。首先,我定义了一个临界阈值,它代表像素的亮度。当我们实际扫描验证码的每个像素块时,判断像素块的亮度(获取像素块的三基色)是否超过了自定义值,从而判断是删除还是保留像素块,所以我们可以针对不同的验证码调整这个阈值。比如上面列出的几个验证码的波动一般在100到600之间,这段时间通过测试得到一个适中的阈值,可以大大提高验证码的纯度。
public static void removeBackground(String imgUrl, String resUrl){
//定义一个临界阈值
int threshold = 300;
try{
BufferedImage img = ImageIO.read(new File(imgUrl));
int width = img.getWidth();
int height = img.getHeight();
for(int i = 1;i < width;i++){
for (int x = 0; x < width; x++){
for (int y = 0; y < height; y++){
Color color = new Color(img.getRGB(x, y));
System.out.println("red:"+color.getRed()+" | green:"+color.getGreen()+" | blue:"+color.getBlue());
int num = color.getRed()+color.getGreen()+color.getBlue();
if(num >= threshold){
img.setRGB(x, y, Color.WHITE.getRGB());
}
}
}
}
for(int i = 1;i
这样就可以得到降噪后的验证码,然后通过反色显示验证码的内容,完成干扰像素的去除。接下来的非必要工作是修剪验证码。为什么我们需要切割边缘?通过观察,我们发现验证码一角没有干扰素涂片。在我们去除干扰素的过程中,这部分像素块可能会受到影响,导致最终角落出现噪点。这时候,角落会被切掉。只需降低几个像素。
public static void cuttingImg(String imgUrl){
try{
File newfile=new File(imgUrl);
BufferedImage bufferedimage=ImageIO.read(newfile);
int width = bufferedimage.getWidth();
int height = bufferedimage.getHeight();
if (width > 52) {
bufferedimage=ImgUtils.cropImage(bufferedimage,(int) ((width - 52) / 2),0,(int) (width - (width-52) / 2),(int) (height));
if (height > 16) {
bufferedimage=ImgUtils.cropImage(bufferedimage,0,(int) ((height - 16) / 2),52,(int) (height - (height - 16) / 2));
}
}else{
if (height > 16) {
bufferedimage=ImgUtils.cropImage(bufferedimage,0,(int) ((height - 16) / 2),(int) (width),(int) (height - (height - 16) / 2));
}
}
ImageIO.write(bufferedimage, "jpg", new File(imgUrl));
}catch (IOException e){
e.printStackTrace();
}
}
其中,要裁剪图像,我们需要提前读取宽度和高度,并确定要裁剪的像素宽度。在这里,我将 60*20 的图像宽度裁剪为每边 4 个像素,上下裁剪高度为 2 个像素验证码看不清楚怎么办,输出将得到清晰的图像。至此,验证码的简单处理基本完成。如无意外,验证码由
变成了
这里很简单。我们可以使用cropping将图片平均切割成四个部分验证码看不清楚怎么办,然后通过简单的训练比较完成匹配。详细操作可以看这里,不再赘述。这里我再提供一个思路,就是Tess4J文本识别工具。通过这个工具,我可以直接对当前生成的验证码进行OCR识别,准确率接近100%。注意我的项目中使用的是Tess4J作为最终的验证码。图像内容识别。
二、Tess4J 的使用
1.首先需要从官网下载Tess4J的压缩包。下载后先将解压包中的tessdata文件复制到项目根目录下,然后导入Tess4J\lib包,最后导入Tess4J\dist\tess4j-3.4.8. jar包
2.编写简单的识别代码
public static String executeTess4J(String imgUrl){
String ocrResult = "";
try{
ITesseract instance = new Tesseract();
//instance.setLanguage("chi_sim");
File imgDir = new File(imgUrl);
//long startTime = System.currentTimeMillis();
ocrResult = instance.doOCR(imgDir);
}catch (TesseractException e){
e.printStackTrace();
}
return ocrResult;
}
ocrResult 是最终结果。代码非常简单。我注释掉了 instance.setLanguage("chi_sim"); 这部分是指定要识别的文本的语言。如果是识别中文,填我评论的chi_sim就行了。当然,这是一个中文包。需要在这里找到中文包下载下来放到Tess4J\tessdata目录下才能使用。
至此,我基本完成了这个验证码的简单识别。在这个过程中,我基本上没有遇到任何困难的工作。毕竟,我是在大家伙的肩膀上运作的。事实上,我还是学到了很多东西。我需要这个 某天可能会遇到。接下来在研究验证码识别的过程中学习了JavaCV。如果我有兴趣,我会记录下来,以备后用。
三、JavaCV的使用
1.首先去官网下载包,点这里,介绍很简单,解压后可以将javacv-bin包导入到项目中(可以看到封装了OpenCV)
2. 基本操作:获取灰度图,获取二值化图
public static void main(String[] args) {
//图片地址
String imgUrl = "C:\\mysoftware\\images\\upload\\OcrImg\\20180607004153.png";
//得到灰度图像
getHuidu(imgUrl);
//得到二值化处理图像
getErzhihua(imgUrl);
}
//得到灰度图像
public static void getHuidu(String imgUrl){
Mat image=imread(imgUrl,CV_LOAD_IMAGE_GRAYSCALE);
//读入一个图像文件并转换为灰度图像(由无符号字节构成)
Mat image1=imread(imgUrl,CV_LOAD_IMAGE_COLOR);
//读取图像,并转换为三通道彩色图像,这里创建的图像中每个像素有3字节
//如果输入图像为灰度图像,这三个通道的值就是相同的
System.out.println("image has "+image1.channels()+" channel(s)");
//channels方法可用来检查图像的通道数
flip(image,image,1);//就地处理,参数1表示输入图像,参数2表示输出图像
//在一窗口显示结果
namedWindow("输入图片显示窗口");//定义窗口
imshow("输入图片显示窗口",image);//显示窗口
waitKey(0);//因为他是控制台窗口,会在mian函数结束时关闭;0表示永远的等待按键,正数表示等待指定的毫秒数
}
//得到二值化处理图像
public static void getErzhihua(String imgUrl){
// TODO Auto-generated method stub
Mat image=imread(imgUrl); //加载图像
if(image.empty())
{
System.out.println("图像加载错误,请检查图片路径!");
return ;
}
imshow("原始图像",image);
Mat gray=new Mat();
cvtColor(image,gray,COLOR_RGB2GRAY); //彩色图像转为灰度图像
imshow("灰度图像",gray);
Mat bin=new Mat();
threshold(gray,bin,120,255,THRESH_TOZERO); //图像二值化
imshow("二值图像",bin);
waitKey(0);
}
看看效果:
四、生成验证码
生成验证码的大致思路:
在加载登录界面时,会从后台请求一个随机的验证码存储在会话中,并以图形的形式显示在页面上。用户输入登录信息点击登录后,后台读取session的验证码是否一致,不一致则返回错误信息。如果相同,则进行登录账号和密码验证。
1. 首先在登录页面添加一个img
为了方便用户使用,需要点击图片自动获取新的验证码,所以changeValidateCode()方法是动态到后台请求验证码并在页面设置
function changeValidateCode() {
var timestamp = new Date().getTime();
$("#randImg").attr('src','../recordHome/getRand?flag='+timestamp);
}
请求时加时间戳是为了防止浏览器缓存导致抓取失败
2. 后台生成验证码
/**
* 随机生成4位验证码
* @param httpSession
* @param request
* @param response
*/
@RequestMapping(value="/getRand")
public void rand(HttpSession httpSession, HttpServletRequest request, HttpServletResponse response){
// 在内存中创建图象
int width = 65, height = 20;
BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
// 生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
// 设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 取随机产生的认证码(6位数字)
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random
.nextInt(110), 20 + random.nextInt(110)));
// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
g.drawString(rand, 13 * i + 6, 16);
}
// 将认证码存入SESSION
ContextHolderUtils.getSession().setAttribute("rand", sRand);
//httpSession.setAttribute("rand", sRand);
// 图象生效
g.dispose();
try{
ImageIO.write(image, "JPEG", response.getOutputStream());
response.getOutputStream().flush();
}catch (Exception e){
}
}
/**
* 给定范围获得随机颜色
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
使用 ContextHolderUtils.getSession().setAttribute("rand", sRand); 将验证码存储在会话中
@RequestMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, @RequestParam String username, @RequestParam String password, @RequestParam String vcode) {
if(!vcode.equals(ContextHolderUtils.getSession().getAttribute("rand"))){
request.setAttribute("failMsg", "验证码不正确!");
return "/base/login";
}
logger.info("用户登录用户:" + username );
...
}
登录时查看,很简单
最后是所有代码。我已经把它集成到这里提到的项目中了,直接下载就可以运行了。欢迎大家指正。
文件下载
附:项目源码【可运行】
文件大小:48M
更新时间:
立即下载