Denua 博客

识别简单验证码, Java 实现

发布时间: 2018-01-15 01:52   分类 : Java    标签: 图像识别 Java 浏览: 644   


之前写过一个 Python PIL 识别验证码, 由于最近需求, 需要在 Android 中识别类似验证码,于是就用 Java 实现了一遍. 大概实现方法: 1, 获取图片, 分析验证码中每个数字的位置, 得到各个验证码块的 x, y, width, height. 2, 采集一定量的样本切割, 打上标签, 编码后生成字典. 3, 将要识别的验证码转换为灰度图, 降噪, 切片, 编码. 4 对比字典中各个值, 获取相似度, 返回每个切片与字典值相似度最高的值的下标. 验证码样本 ![image](https://www.denua.cn/media/kindeditor/2018-1/15/4f4a0b8e-f953-11e7-b568-00163e020e5d.gif) 识别过程 ![image](https://www.denua.cn/media/kindeditor/2018-1/15/89396b96-f953-11e7-b568-00163e020e5d.jpg) ``` package captcha; import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.ColorConvertOp; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; public class Scan { // 切割开始位置 x 轴坐标 private final int cropStartX = 7; // 切割开始位置 y 轴坐标 private final int cropStartY = 7; private final int cropWidth = 8; private final int cropHeight = 12; // 切割数字间隔填充 private final int cropPad = 1; // 过滤噪音阈值 ARGB 值小于这个的都为背影噪音, 大于这个的都为数字 private final int threshold = 0xff777777; private String[] dict; // 验证码默认大小 private int height = 22; private int width = 63; private BufferedImage bufferedImage; public Scan(InputStream input){ try { this.bufferedImage = ImageIO.read(input); } catch (IOException e) { e.printStackTrace(); } } /** * 给图片降噪, 再此之前必须先将图片转换为 灰度图 BYTE_GRAY 模式 \n * 将图片 低于阈值的像素点灰度变为 0 高于阈值的像素点灰度变为 255 \n * 这样图片就只有黑白两种颜色了 * */ public void denoise(){ BufferedImage res = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); int[] rgb = new int[width * height]; this.bufferedImage.getRGB(0, 0, width, height, rgb, 0, width); for(int index=0; index小于 rgb.length; index++){ int pixel = rgb[index]; // 小于阈值则变白否则变黑 if(pixel 小于 this.threshold) rgb[index] = 0xff000000; else rgb[index] = 0xffffffff; } res.setRGB(0, 0, width, height, rgb, 0, width); this.bufferedImage = res; } /** * 将传入的灰度图转换为一个 0, 1 二值数组. * 灰度图需已经降噪 * * @param img 需要二值化的灰度图 * * @return 灰度值对应的二值数组 */ public byte[] getBin(BufferedImage img){ int w = img.getWidth(); int h = img.getHeight(); byte[] bin = new byte[w*h]; for(int x=0; x小于h; x++){ for(int y=0; y小于w; y++){ // 获取像素 int pixel = img.getRGB(y, x); // 纯黑则为1 if(pixel == 0xffffffff) bin[x*w + y] = 1; else bin[x*w + y] = 0; } } return bin; } /** * 将图片转换为灰度图 * */ public void convertGrayMode(){ ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); ColorConvertOp op = new ColorConvertOp(cs, null); this.bufferedImage = op.filter(bufferedImage, null); } /** * 将验证码图片切割为五部分 * 将数字部分切割 * * @param x1 第一个数字的右上角 x 坐标 * @param y1 第一个数字的右上角 y 坐标 * @param h 各部分的高度 * @param w 各部分的宽度 * @param pad 各部分间隔 * * @return 验证码的五个数字部分 */ private BufferedImage[] getPart(int x1, int y1, int w, int h, int pad){ // 用于保存和返回切割的四部分 BufferedImage[] imagePart = new BufferedImage[5]; // 五个部分的位置, 各个部分的 x 等于 x1 + n * pad, n=位置 Rectangle[] part = new Rectangle[5]; part[0] = new Rectangle(x1 + 0 * pad + w * 0, y1, h, w); part[1] = new Rectangle(x1 + 1 * pad + w * 1, y1, h, w); part[2] = new Rectangle(x1 + 2 * pad + w * 2, y1, h, w); part[3] = new Rectangle(x1 + 3 * pad + w * 3, y1, h, w); part[4] = new Rectangle(x1 + 4 * pad + w * 4, y1, h, w); // 用于存放 rgb 值 int[] rgbTamp = new int[w*h]; for(int index=0; index 小于 5; index++){ int x = part[index].x; int y = part[index].y; // 将每个部分存放到临时数组中 this.bufferedImage.getRGB(x, y, w, h, rgbTamp, 0, w); // 新建图像 imagePart[index] = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY); // 将数 rgb 组数据填入 新图像 imagePart[index].setRGB(0, 0, w, h, rgbTamp, 0, w); } return imagePart; } /** * 从文件中读取已保存的验证码特征值 * * * @param path */ public void setDict(File dict){ try { Reader in = new FileReader(dict); BufferedReader reader = new BufferedReader(in); String str = ""; String temp; while((temp = reader.readLine()) != null){ str += temp; } this.dict = str.split("#");//Arrays.copyOfRange(str.split("#"), 0, 10); in.close(); } catch (IOException e) { e.printStackTrace(); } } /** * 获取所选数字的所有字典值 * * @param number 要获得字典的数字 * * @return byte[10][imagePixelCount] 字典 */ public byte[][] getDict(int number){ byte[][] dictx = new byte[10][cropHeight * cropWidth]; String str = this.dict[number]; String[] each = str.split(",\\|"); for(int i=0; i 小于 10; i++){ String[] part = each[i].split(","); byte[] data = new byte[cropHeight * cropWidth]; for(int index=0; index 小于 data.length; index++) data[index] = Byte.valueOf(part[index]); dictx[i] = data; } return dictx; } /** * 获取验证码切片的结果 * 验证码切片先转换为 二值 数组, 再与字典中的每个值对比 * 得出每个数字的像素点相似率, 相似率最大的就为结果 * * @param img 验证码切片对应二值数组 * * @return 验证码切片识别的数字 */ public int getResult(byte[] binImg){ // 十个数字的相似度 double[] same = new double[10]; // 结果 int res = -1; // 对比每个数字 for(int n=0; n 小于 10; n++){ // 获取当前数字的字典值 byte[][] dictx = getDict(n); // 用于统计值一致的像素点数量 double sm = 0; // 与每组值对比 for(int r=0; r 小于 10; r++){ // 当前组的值 byte[] now = dictx[r]; // 与当前组每个像素点对比 for(int index=0; index 小于 (cropWidth*cropHeight); index++){ // 如果相似则统计加一 if(now[index] == binImg[index]){ sm += 1; }; } } // 与当前数字的相似度 12*8*10=960 => 1000 same[n] = sm/1000;// (cropWidth*cropHeight*10); } double max = 0; // 获取最大值的下标 for(int i=0; i 小于 10; i++){ if(same[i] 大于 max){ res = i; max = same[i]; } } return res; } /** * 从已打好标签的图片中生成字典 * 目录中包含数字 0-9 命名的文件夹, 每个文件夹里是对应的验证码中数字的切片 * * @param LabeledDir 标记图片的目录 * @param saveFile 字典储存文件 * @throws IOException */ public void generaterDict(File LabeledDir, File saveFile) throws IOException{ String s = ""; // 枚举每个文件夹 for(File n:LabeledDir.listFiles()){ File[] imgs = n.listFiles(); int size = 0; // 枚举每个数字 for(File im:imgs){ // 只生成 10 个值 if(size++大于9) break; BufferedImage bim = ImageIO.read(im); // 获取对应的二值数组 byte[] bin = getBin(bim); // 转换并添加分隔符 for(byte b:bin) s += String.valueOf(b) + ","; // 每组值的分隔符 s += "|"; } // 每个数字的分隔符 s += "\n#"; } OutputStream out = new FileOutputStream(saveFile); out.write(s.getBytes()); out.close(); } /** * 开始识别验证码内容 * 1, 转换为灰度图 * 2, 降噪 * 3, 识别每个切片 * * @return 结果 */ private String scan(){ String result = ""; convertGrayMode(); denoise(); BufferedImage[] parts = getPart(cropStartX, cropStartY, cropWidth, cropHeight, cropPad); for(BufferedImage part : parts){ byte[] binImg = getBin(part); printBin(binImg); p(getResult(binImg)); } return result; } public static void main(String[] args) throws IOException { File f = new File("H:\\temp\\0.gif"); InputStream in = new FileInputStream(f); Scan scan = new Scan(in); scan.setDict(new File("H:\\Desktop\\Python\\school_data_spider\\sc.dict")); scan.scan(); in.close(); } /** * 打印二值验证码切片 * * @param bin 二值验证码切片 */ public void printBin(byte[] bin){ for(int x=0; x小于cropHeight; x++){ for(int y=0; y小于cropWidth; y++){ if(bin[x*8 + y] == 1) System.out.print(". "); else System.out.print("# "); } System.out.println(""); } } public void show(BufferedImage im){ JFrame frame = new JFrame("IMAGE"); JLabel l = new JLabel(new ImageIcon(im)); frame.add(l); frame.setBounds(600, 300, 200, 100); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } public void p(Object obj){ System.out.println(obj); } } ``` (完)

评论    

Copyright denua denua.cn