验证码识别之旅(一)

时间:2022-09-25 09:02:09

当我们拿到一份验证码时,我们首先应该对它进行观察.

验证码识别之旅(一)

可以看到,它的长和高分别为250和60.不过由于它内容区域的周边含有许多白色的"无效内容"区域,会对我们进行识别造成干扰,所以第一步,就是应该进行噪音数据的过滤.

即将内容区域提取出来.

这个验证码的无效区域为白色,而且是纯白,没有噪音数据,所以rgb会为>=(240,240,240).处理起来就比较容易了.我们只需要分别找出:

  • 最左的<(240,240,240)像素点leftX
  • 最右的<(240,240,240)像素点rightX
  • 最上的<(240,240,240)像素点topY
  • 最下的<(240,240,240)像素点bottomY

那么即可以得到边框的(x,y,width,height)数值为(leftX,topY,rightX-leftX+1,bottomY-topY+1).(注:(x,y)为边框左上角坐标)

代码如下:

/**
* @param rgb
* @return false非白色,true白色
*/
public boolean isWhite(int rgb){
int r = (rgb & 16711680) >> 16;
int g = (rgb & 65280) >> 8;
int b = (rgb & 255);
if(r >= 240 && g>= 240 && b>=240){
return true;
}
return false;
}

/**
*
* @return 获取图片的像素边缘
*/
public int[] getBorder(){
int[] border = new int[4];
try{
int leftX = -1;
int rightX = -1;
int topY = -1;
int bottomY = -1;

//获取最左侧的非白色像素位置
for(int i=0;i<width&&leftX==-1;i++){
for(int j=0;j<height;j++){
int rgb = bimg.getRGB(i,j);
if(!isWhite(rgb)){
leftX = i;
break;
}
}
}
//获取最右侧的非白色像素位置
for(int i=width-1;i>=0&&rightX==-1;i--){
for(int j=0;j<height;j++){
int rgb = bimg.getRGB(i,j);
if(!isWhite(rgb)){
rightX = i;
break;
}
}
}
//获取最上的非白色像素位置
for(int i=0;i<height&&topY==-1;i++){
for(int j=0;j<width;j++){
int rgb = bimg.getRGB(j,i);
if(!isWhite(rgb)){
topY = i;
break;
}
}
}
//获取最下方的非白色像素的位置
for(int i=height-1;i>=0&&bottomY==-1;i--){
for(int j=0;j<width;j++){
int rgb = bimg.getRGB(j,i);
if(!isWhite(rgb)){
bottomY = i;
break;
}
}
}

int x = leftX;
int y = topY;
int width = rightX - leftX + 1;
int height = bottomY - topY + 1;
border = new int[]{x,y,width,height};
}catch (Exception ex){
ex.printStackTrace();
}
return border;
}

为了方便我们查看边框是否正确的框住了内容区域,我们可以绘制一下识别图片,将边框识别的区域用蓝色矩形框住.

public static void main(String[] args){
String[] filePaths = new String[]{
"/home/pijing/comp/validcode/train2/type2_train_1.jpg",
"/home/pijing/comp/validcode/train2/type2_train_2.jpg",
"/home/pijing/comp/validcode/train2/type2_train_3.jpg",
"/home/pijing/comp/validcode/train2/type2_train_4.jpg",
"/home/pijing/comp/validcode/train2/type2_train_5.jpg",
"/home/pijing/comp/validcode/train2/type2_train_6.jpg",
"/home/pijing/comp/validcode/train2/type2_train_7.jpg",
"/home/pijing/comp/validcode/train2/type2_train_8.jpg",
"/home/pijing/comp/validcode/train2/type2_train_9.jpg",
"/home/pijing/comp/validcode/train2/type2_train_10.jpg"
};
int index = -1;
for(String filePath:filePaths){
index++;
Type2ImageModel type2ImageModel = new Type2ImageModel(filePath);
int[] border = type2ImageModel.getBorder();
for(int i=0;i<border.length;i++){
System.out.print(border[i]+" ");
}
System.out.println();
//绘制边框以供验证
Graphics2D g2 = (Graphics2D) type2ImageModel.getBimg().createGraphics();
g2.setColor(Color.BLUE);
g2.setStroke(new BasicStroke(3.0f));
g2.drawRect(border[0], border[1], border[2], border[3]);
g2.dispose();
try {
ImageIO.write(type2ImageModel.getBimg(),"jpg",
new File("/home/pijing/comp/validcode/train2/reco_"+index+".jpg"));
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("done!");
}

可以看到,输出的reco图片中蓝色框是比较准确的框住了内容区域的:

验证码识别之旅(一)

验证码识别之旅(一)

但是,这时我们就发现了一个问题,由于内容区域大小不是绝对相等,所以边框大小时不相等的.这时我们可以打印训练集中的10份验证码(训练集有10000份图片,所以只能抽样)的边框大小:

验证码识别之旅(一)

可以看到,width处于[160,168],height处于[40,48].取最大的with和height作为边框长宽(168,48),另外可以看到验证码含有5个字符,而168不能被5整除,所以定width=170.

最终确定的标准的边框长宽为(170,48)

绘制边框当然不是我们的主要目的,我们的主要目的是获取内容区域的图片.所以按照边框对于图片进行截取,然后再缩放为统一的长宽(170,48):

/**
*
* @return 返回按边框截取的图片,并按照统一的长宽(170,480)进行缩放
*/
public BufferedImage cutImage(){
int[] border = getBorder();
Image scaleImage = bimg.getSubimage(border[0],border[1],border[2],border[3]).
getScaledInstance(borderWidth,borderHeight,Image.SCALE_SMOOTH);
BufferedImage newImg = new BufferedImage(borderWidth,borderHeight,BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) newImg.getGraphics();
g.drawImage(scaleImage,0,0,borderWidth,borderHeight,null);
g.dispose();
return newImg;
}

这样我们就可以得到内容区域的图片,而且图片大小是大小一致的.

验证码识别之旅(一)

接着,我们需要将它按照5等分进行切割,代码如下:

/**
* @return 由于待识别的验证码的图片的数字就为5个,所以可以按照5对于图片进行切割
*/
public BufferedImage[] divideImage(){
BufferedImage curImg = cutImage();
BufferedImage[] imgArr = new BufferedImage[5];
try{
int divideWidth = borderWidth/5;
int divideHeight = borderHeight;
for(int i=0;i<5;i++){
int x = divideWidth*i;
int y = 0;
imgArr[i] = curImg.getSubimage(x,y,divideWidth,divideHeight);
}
}catch(Exception ex){
ex.printStackTrace();
}
return imgArr;
}

切割后的图片如下图所示:

验证码识别之旅(一)

验证码识别之旅(一)

所以可以得到每一个被切割图片的灰度值数组:

/**
* @param rgb
* @return 根据rgb计算灰度值
*/
public double getGrayValue(int rgb){
int r = (rgb & 16711680) >> 16;
int g = (rgb & 65280) >> 8;
int b = (rgb & 255);
return ( r*38 + g * 75 + b * 15 )>>7;
}

/**
* 得到5张图片的灰度值数组
*/
public void calGrayArray(){
BufferedImage[] arr = divideImage();
for(int i=0;i<arr.length;i++){
grayValue[i] = new double[arr[i].getWidth()*arr[i].getHeight()];
for(int m=0;m<arr[i].getHeight();m++){
for(int n=0;n<arr[i].getWidth();n++){
grayValue[i][m*arr[i].getWidth()+n] = getGrayValue(arr[i].getRGB(n,m));
}
}
}
}

可以知道,每个验证码对象的灰度数组都长度为5,包含对象为34*48=1632长度的double数组的数组.

完整代码如下:

package com.fetching.validcode.model;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
/**
* Created by pijing on 17-4-29.
* 处理类型为2的验证码图片模型类
*/
public class Type2ImageModel implements Serializable{
private String filePath = null;
private BufferedImage bimg = null;
private int width = 0;
private int height = 0;
//根据经验取得边框大小
private int borderWidth = 170;
private int borderHeight = 48;
//灰度值的数组
private double[][] grayValue = new double[5][];

public Type2ImageModel(String filePath){
this.filePath = filePath;
try{
bimg = ImageIO.read(new File(filePath));
this.width = bimg.getWidth();
this.height = bimg.getHeight();
}catch(Exception ex){
ex.printStackTrace();
}
}

public BufferedImage getBimg(){
return this.bimg;
}

/**
* @param rgb
* @return false非白色,true白色
*/
public boolean isWhite(int rgb){
int r = (rgb & 16711680) >> 16;
int g = (rgb & 65280) >> 8;
int b = (rgb & 255);
if(r >= 240 && g>=240 && b>= 240){
return true;
}
return false;
}

/**
*
* @return 获取图片的像素边缘
*/
public int[] getBorder(){
int[] border = new int[4];
try{
int leftX = -1;
int rightX = -1;
int topY = -1;
int bottomY = -1;

//获取最左侧的非白色像素位置
for(int i=0;i<width&&leftX==-1;i++){
for(int j=0;j<height;j++){
int rgb = bimg.getRGB(i,j);
if(!isWhite(rgb)){
leftX = i;
break;
}
}
}
//获取最右侧的非白色像素位置
for(int i=width-1;i>=0&&rightX==-1;i--){
for(int j=0;j<height;j++){
int rgb = bimg.getRGB(i,j);
if(!isWhite(rgb)){
rightX = i;
break;
}
}
}
//获取最上的非白色像素位置
for(int i=0;i<height&&topY==-1;i++){
for(int j=0;j<width;j++){
int rgb = bimg.getRGB(j,i);
if(!isWhite(rgb)){
topY = i;
break;
}
}
}
//获取最下方的非白色像素的位置
for(int i=height-1;i>=0&&bottomY==-1;i--){
for(int j=0;j<width;j++){
int rgb = bimg.getRGB(j,i);
if(!isWhite(rgb)){
bottomY = i;
break;
}
}
}

int x = leftX;
int y = topY;
int width = rightX - leftX + 1;
int height = bottomY - topY + 1;
border = new int[]{x,y,width,height};
}catch (Exception ex){
ex.printStackTrace();
}
return border;
}

/**
*
* @return 返回按边框截取的图片,并按照统一的长宽(170,480)进行缩放
*/
public BufferedImage cutImage(){
int[] border = getBorder();
Image scaleImage = bimg.getSubimage(border[0],border[1],border[2],border[3]).
getScaledInstance(borderWidth,borderHeight,Image.SCALE_SMOOTH);
BufferedImage newImg = new BufferedImage(borderWidth,borderHeight,BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) newImg.getGraphics();
g.drawImage(scaleImage,0,0,borderWidth,borderHeight,null);
g.dispose();
return newImg;
}

/**
* @return 由于待识别的验证码的图片的数字就为5个,所以可以按照5对于图片进行切割
*/
public BufferedImage[] divideImage(){
BufferedImage curImg = cutImage();
BufferedImage[] imgArr = new BufferedImage[5];
try{
int divideWidth = borderWidth/5;
int divideHeight = borderHeight;
for(int i=0;i<5;i++){
int x = divideWidth*i;
int y = 0;
imgArr[i] = curImg.getSubimage(x,y,divideWidth,divideHeight);
}
}catch(Exception ex){
ex.printStackTrace();
}
return imgArr;
}

/**
* @param rgb
* @return 根据rgb计算灰度值
*/
public double getGrayValue(int rgb){
int r = (rgb & 16711680) >> 16;
int g = (rgb & 65280) >> 8;
int b = (rgb & 255);
return ( r*38 + g * 75 + b * 15 )>>7;
}

/**
* 得到5张图片的灰度值数组
*/
public void calGrayArray(){
BufferedImage[] arr = divideImage();
for(int i=0;i<arr.length;i++){
grayValue[i] = new double[arr[i].getWidth()*arr[i].getHeight()];
for(int m=0;m<arr[i].getHeight();m++){
for(int n=0;n<arr[i].getWidth();n++){
grayValue[i][m*arr[i].getWidth()+n] = getGrayValue(arr[i].getRGB(n,m));
}
}
}
}

public double[][] getGrayValue(){
return this.grayValue;
}

public static void main(String[] args){
String[] filePaths = new String[]{
"/home/pijing/comp/validcode/train2/type2_train_1.jpg",
"/home/pijing/comp/validcode/train2/type2_train_2.jpg",
"/home/pijing/comp/validcode/train2/type2_train_3.jpg",
"/home/pijing/comp/validcode/train2/type2_train_4.jpg",
"/home/pijing/comp/validcode/train2/type2_train_5.jpg",
"/home/pijing/comp/validcode/train2/type2_train_6.jpg",
"/home/pijing/comp/validcode/train2/type2_train_7.jpg",
"/home/pijing/comp/validcode/train2/type2_train_8.jpg",
"/home/pijing/comp/validcode/train2/type2_train_9.jpg",
"/home/pijing/comp/validcode/train2/type2_train_10.jpg"
};
int index = -1;
for(String filePath:filePaths){
index++;
Type2ImageModel type2ImageModel = new Type2ImageModel(filePath);
/*
int[] border = type2ImageModel.getBorder();
for(int i=0;i<border.length;i++){
System.out.print(border[i]+" ");
}
System.out.println();*/
//绘制边框以供验证
/*
Graphics2D g2 = (Graphics2D) type2ImageModel.getBimg().createGraphics();
g2.setColor(Color.BLUE);
g2.setStroke(new BasicStroke(3.0f));
g2.drawRect(border[0], border[1], border[2], border[3]);
g2.dispose();
try {
ImageIO.write(type2ImageModel.getBimg(),"jpg",
new File("/home/pijing/comp/validcode/train2/reco_"+index+".jpg"));
} catch (IOException e) {
e.printStackTrace();
}*/
/*
BufferedImage cutImage = type2ImageModel.cutImage();
try {
ImageIO.write(cutImage,"jpg",
new File("/home/pijing/comp/validcode/train2/cut_"+index+".jpg"));
} catch (IOException e) {
e.printStackTrace();
}*/
/*
BufferedImage[] tempArr = type2ImageModel.divideImage();
for(int m=0;m<tempArr.length;m++){
try {
ImageIO.write(tempArr[m],"jpg",
new File("/home/pijing/comp/validcode/train2/divide_"+index+"_"+m+".jpg"));
} catch (IOException e) {
e.printStackTrace();
}
}*/
type2ImageModel.calGrayArray();
double[][] grayArr = type2ImageModel.getGrayValue();
for(int i=0;i<grayArr.length;i++){
for(int j=0;j<grayArr[i].length;j++){
System.out.print(grayArr[i][j]+" ");
}
System.out.println();
}
System.out.println();
}
System.out.println("done!");
}
}


唉,自从废寝忘食的让验证码识别的准确率到了99.1%,就丧失了完成文章的动力。周末补上所有文章。