验证码识别之w3cschool字符图片验证码(easy级别)
起因:
最近在练习解析验证码,看到了这个网站的验证码比较简单,于是就拿来解析一下攒攒经验值,并无任何冒犯之意...
验证码所在网页: https://www.w3cschool.cn/checkmphone?type=findpwd
验证码地址: https://www.w3cschool.cn/scode
1. 分析规律
打开这个页面: https://www.w3cschool.cn/scode,不断的按F5刷新观察,可以发现,虽然每次字符内容、位置会变化,但是字体的样式是一直不变的,对于这种字体样式不变的,去噪去的好是可以做到识别率100%的。
然后再看噪音,下载下来一张图在Windows自带的画图中打开:
基本上都是噪点,对于噪点只需要判断8邻域判断就可以了,观察了几幅图像应该都是噪点,但是我并不确定到底有没有噪块,还有鉴于对于8邻域我已经快写吐了,所以这里采用连通域来去除噪音。(没有看到噪块的情况下可以使用8邻域试下,比较简单这里就不展开讲啦。在我写这段话的时候我觉得我真是太蠢了为什么放着简单的8邻域不用而非要用连通域呢...)
然后就是注意到背景色还会变化,所以没办法直接确定背景色到底是啥色,这需要程序能够自动识别出背景色。这个比较简单,只需要在计算连通域的时候将最大连通域标记为背景色就可以了。
总结:
1. 字体样式无变化,意味着特征极其稳定,识别率高
2. 有噪音,可以使用连通域来过滤
3. 背景色随机,需要能够识别并统一白色,最大连通域标记为背景色
提示:一般验证码的链接地址都没有UA检查,访问次数限制之类的,可以直接打开其所在链接快速刷新观察规律。
2. 下载样本
不管三七二十一,先下载一些样本到本地来慢慢观察再说:
/** * 验证码下载路径 */ public static final String CAPTCHA_URL = "https://www.w3cschool.cn/scode?rand="; public static void download(String saveDirectory, int howMany) { Random random = new Random(); ExecutorService executorService = Executors.newFixedThreadPool(10); while (howMany-- > 0) { executorService.submit(() -> { Response response = null; try { long currentMillis = System.currentTimeMillis(); Request request = Request.Get(CAPTCHA_URL + currentMillis); response = request.connectTimeout(2000).socketTimeout(2000).execute(); response.saveContent(new File(saveDirectory + random.nextLong() + ".png")); System.out.println("download..."); } catch (IOException e) { e.printStackTrace(); } finally { if (response != null) { response.discardContent(); } } }); } try { executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } }
这里下载了5000张图片:
这里下这么多是因为等下我要从这些图片中自动生成一个字典,如果下得少了我怕会漏掉某些字符。
3. 过滤噪音
然后就是对下载下来的图片进行处理,把噪音去掉:
/** * 去噪点,使用连通域大小来判断 * * @param originalCaptcha 原始的验证码图片 * @param areaSizeFilter 连通域小于等于此大小的将被过滤掉 * @return */ public static BufferedImage noiseClean(BufferedImage originalCaptcha, int areaSizeFilter) { // 会有一些干扰边,把边缘部分切割丢掉 int edgeDropWidth = 15; BufferedImage captcha = originalCaptcha.getSubimage(edgeDropWidth / 2, edgeDropWidth / 2, // originalCaptcha.getWidth() - edgeDropWidth, originalCaptcha.getHeight() - edgeDropWidth); int w = captcha.getWidth(); int h = captcha.getHeight(); int[][] book = new int[w][h]; // 连通域最大的色块将被认为是背景色,这样实现了自动识别背景色 Map<Integer, Integer> flagAreaSizeMap = new HashMap<>(); int currentFlag = 1; int maxAreaSizeFlag = currentFlag; int maxAreaSizeColor = 0XFFFFFFFF; // 标记 for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { if (book[i][j] != 0) { continue; } book[i][j] = currentFlag; int currentColor = captcha.getRGB(i, j); int areaSize = waterFlow(captcha, book, i, j, currentColor, currentFlag); if (areaSize > flagAreaSizeMap.getOrDefault(maxAreaSizeFlag, 0)) { maxAreaSizeFlag = currentFlag; maxAreaSizeColor = currentColor; } flagAreaSizeMap.put(currentFlag, areaSize); currentFlag++; } } // 复制 BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int currentColor = captcha.getRGB(i, j); if (book[i][j] == maxAreaSizeFlag // || (currentColor & 0XFFFFFF) == (maxAreaSizeColor & 0XFFFFFF) // || flagAreaSizeMap.get(book[i][j]) <= areaSizeFilter) { resultImage.setRGB(i, j, 0XFFFFFFFF); } else { resultImage.setRGB(i, j, currentColor); } } } return resultImage; } /** * 将图像抽象为颜色矩阵 * * @param img * @param book * @param x * @param y * @param color * @param flag * @return */ private static int waterFlow(BufferedImage img, int[][] book, int x, int y, int color, int flag) { if (x < 0 || x >= img.getWidth() || y < 0 || y >= img.getHeight()) { return 0; } // 这个1统计的是当前点 int areaSize = 1; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { int nextX = x + i; int nextY = y + j; if (nextX < 0 || nextX >= img.getWidth() || nextY < 0 || nextY >= img.getHeight()) { continue; } // 如果这一点没有被访问过,并且颜色相同 // if (book[nextX][nextY] == 0 && isSimilar(img.getRGB(nextX, nextY), color, 0)) { if (book[nextX][nextY] == 0 && (img.getRGB(nextX, nextY) & 0XFFFFFF) == (color & 0XFFFFFF)) { book[nextX][nextY] = flag; areaSize += waterFlow(img, book, nextX, nextY, color, flag); } } } return areaSize; }
这是前面那张图经过去噪音之后的效果,因为噪音比较少,所以效果还可以:
4. 分割字符
接下来就是将上面干净的图片切割为单个字符了,但是切割出来的结果会有很多,难道我要一个一个的去挑出来我需要的字典吗,感觉有点蠢,所以我决定让程序自动推举出字典来,只需要在切割出字符之后保存之前对字符图片进行一个去重操作就可以了,这里为了方便对图片进行一个压缩,将小图压缩为了一个整数:
/** * 切割字符 * * @param img * @return */ public static List<BufferedImage> mattingCharacter(BufferedImage img) { List<BufferedImage> list = new ArrayList<>(); int w = img.getWidth(); int h = img.getHeight(); boolean lastColumnIsBlack = true; int beginColumn = -1; for (int i = 0; i < w; i++) { boolean currentColumnIsBlack = true; for (int j = 0; j < h; j++) { if ((img.getRGB(i, j) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } // 进入字符区域 if (lastColumnIsBlack && !currentColumnIsBlack) { beginColumn = i; } else if (!lastColumnIsBlack && currentColumnIsBlack) { // 离开字符区域 BufferedImage charImage = img.getSubimage(beginColumn, 0, i - beginColumn, h); BufferedImage trimCharImage = trimUpAndDown(charImage); list.add(trimCharImage); } lastColumnIsBlack = currentColumnIsBlack; } return list; } private static BufferedImage trimUpAndDown(BufferedImage img) { int w = img.getWidth(); int h = img.getHeight(); // 计算上方空白 int upBeginLine = -1; for (int i = 0; i < h; i++) { boolean currentColumnIsBlack = true; for (int j = 0; j < w; j++) { if ((img.getRGB(j, i) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } if (!currentColumnIsBlack) { upBeginLine = i; break; } } // 计算下方空白 int downBeginLine = -1; for (int i = h - 1; i >= 0; i--) { boolean currentColumnIsBlack = true; for (int j = 0; j < w; j++) { if ((img.getRGB(j, i) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } if (!currentColumnIsBlack) { downBeginLine = i; break; } } return img.getSubimage(0, upBeginLine, w, downBeginLine - upBeginLine + 1); } /** * 计算图像的哈希值,即将图片内容压缩为一个整数 * <p> * NOTE: 适用于小图像 * * @param img * @return */ public static int imgHashCode(BufferedImage img) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < img.getWidth(); i++) { for (int j = 0; j < img.getHeight(); j++) { sb.append(i).append("|").append(j).append("|").append(img.getRGB(i, j) & 0XFFFFFF).append("|"); } } return sb.toString().hashCode(); }
下面是保存时去重的代码:
/** * 得到字符字典 * * @param srcDirectory * @param destDirectory */ public static void splitCharacter(String srcDirectory, String destDirectory) { File file = new File(srcDirectory); File[] imgFileArray = file.listFiles(); Map<Integer, BufferedImage> charDictionary = new HashMap<>(); for (File imgFile : imgFileArray) { BufferedImage image = null; try { image = ImageIO.read(imgFile); } catch (IOException e) { e.printStackTrace(); } List<BufferedImage> charList = W3cSchoolCaptchaUtil.mattingCharacter(image); charList.forEach(x -> { int hashcode = W3cSchoolCaptchaUtil.imgHashCode(x); System.out.println(hashcode); charDictionary.put(hashcode, x); }); System.out.println("split..."); } charDictionary.forEach((k, v) -> { try { ImageIO.write(v, "png", new File(destDirectory + k + ".png")); System.out.println("write..."); } catch (IOException e) { e.printStackTrace(); } }); }
这是自动推举出来的字符,目前字符内容和文件名字还没有对应,等下需要手动标记:
5. 生成字典
接下来人工标记,将文件的名字改为图片所表示的字符,改好之后的效果如下:
大写字母+数字应该是36个的,这里只有34个,是因为他们在生成验证码的时候讲容易混淆的0和O去掉了,啊,看来还是考虑到了用户体验的...
然后读取这个目录下的每个文件,对每个图片的内容做hash将一个图片映射为文件名对应的整数:
/** * 根据字符图片生成字符字典 * * @param charDirectory */ public static void genDictionary(String charDirectory) { File[] charImgs = new File(charDirectory).listFiles(); for (File charImgFile : charImgs) { try { BufferedImage charBufferedImage = ImageIO.read(charImgFile); int charHashCode = W3cSchoolCaptchaUtil.imgHashCode(charBufferedImage); System.out.printf("charMapping.put(%d, '%c');\n", charHashCode, charImgFile.getName().split("\\.")[0].charAt(0)); } catch (IOException e) { e.printStackTrace(); } } }
打印内容是初始化Map的代码,直接粘过去初始化这个Map:
private static Map<Integer, Character> charMapping = new HashMap<>(); static { charMapping.put(1844796036, '1'); charMapping.put(1594429278, '2'); charMapping.put(-222305694, '3'); charMapping.put(452270032, '4'); charMapping.put(-1898118878, '5'); charMapping.put(999670338, '6'); charMapping.put(-965770966, '7'); charMapping.put(-337170896, '8'); charMapping.put(585835558, '9'); charMapping.put(-724014232, 'A'); charMapping.put(-428164778, 'B'); charMapping.put(-886387444, 'C'); charMapping.put(1946490946, 'D'); charMapping.put(416715843, 'E'); charMapping.put(-917974862, 'F'); charMapping.put(-764688176, 'G'); charMapping.put(28434468, 'H'); charMapping.put(10891004, 'I'); charMapping.put(-2084516900, 'J'); charMapping.put(259070252, 'K'); charMapping.put(1209338035, 'L'); charMapping.put(486706942, 'M'); charMapping.put(983181712, 'N'); charMapping.put(1065112842, 'P'); charMapping.put(183746070, 'Q'); charMapping.put(782513722, 'R'); charMapping.put(-984311436, 'S'); charMapping.put(-1276745734, 'T'); charMapping.put(-796848932, 'U'); charMapping.put(-967446486, 'V'); charMapping.put(331594374, 'W'); charMapping.put(1503060590, 'X'); charMapping.put(-507424510, 'Y'); charMapping.put(468466871, 'Z'); }
并基于之前写的代码编写解析验证码图片的方法:
/** * 解析传入的验证码 * * @param captcha * @return */ public static String ocr(BufferedImage captcha) { BufferedImage noiseCleaned = noiseClean(captcha, 20); List<BufferedImage> charImageList = mattingCharacter(noiseCleaned); return charImageList.stream().map(x -> charMapping.get(imgHashCode(x)).toString()).collect(joining()); }
6. 验证解析效果
再写点代码验证之前的解析算法的正确性:
package bar.ocr.w3cschool; import org.apache.http.client.fluent.Request; import org.apache.http.client.fluent.Response; import org.apache.http.message.BasicNameValuePair; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; /** * 用来验证之前写的代码的正确性 * * @author CC11001100 */ public class VerifyAccuracy { /** * 发起一次验证,将结果是否成功返回,这里的结果只是为了验证验证码识别的结果 * * @return */ private static boolean once() { Request request = Request.Get(DownloadCaptcha.CAPTCHA_URL + System.currentTimeMillis()); Response response = null; String captchaString = ""; try { response = request.connectTimeout(2000).socketTimeout(2000).execute(); BufferedImage captchaImg = ImageIO.read(response.returnContent().asStream()); captchaString = W3cSchoolCaptchaUtil.ocr(captchaImg); System.out.printf("captcha is: %s\n", captchaString); } catch (IOException e) { e.printStackTrace(); return false; } finally { if (response != null) { response.discardContent(); } } Request postSms = Request.Post("https://www.w3cschool.cn/sendsmscode"); // 手机号改为不合法的,后端会有校验这样短信就不会被发出去,否则.... - - postSms.bodyForm(new BasicNameValuePair("mphone", "123456789"), // new BasicNameValuePair("type", "findpwd"), // new BasicNameValuePair("scode", captchaString)); try { response = postSms.socketTimeout(2000).connectTimeout(2000).execute(); String json = response.returnContent().asString(); System.out.printf("response is: %s\n", json); return !json.contains("验证码错误"); } catch (IOException e) { e.printStackTrace(); } finally { if (response != null) { response.discardContent(); } } return false; } public static void main(String[] args) { int totalTimes = 100; int successCount = 0; for (int i = 0; i < totalTimes; i++) { System.out.printf("%d :\n", i + 1); if (once()) { successCount++; System.out.println("ocr success"); } else { System.out.println("ocr failed"); } System.out.println(); } System.out.printf("success times %d, accuracy is %g%%\n", successCount, 1.0 * successCount / totalTimes * 100); } }
跑一下看看效果:
因为字体并没有任何的变化,所以通过直接比对是可以做到准确率100%的。
总结: 对于字体样式等没有变化的,不应该炫技搞训练啥的,直接比对就可以做到准确率100%了,当然去噪要做得好。
下面贴上完整代码:
DownloadCaptcha.java:
package bar.ocr.w3cschool; import org.apache.http.client.fluent.Request; import org.apache.http.client.fluent.Response; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @author CC11001100 */ public class DownloadCaptcha { /** * 验证码下载路径 */ public static final String CAPTCHA_URL = "https://www.w3cschool.cn/scode?rand="; public static void download(String saveDirectory, int howMany) { Random random = new Random(); ExecutorService executorService = Executors.newFixedThreadPool(10); while (howMany-- > 0) { executorService.submit(() -> { Response response = null; try { long currentMillis = System.currentTimeMillis(); Request request = Request.Get(CAPTCHA_URL + currentMillis); response = request.connectTimeout(2000).socketTimeout(2000).execute(); response.saveContent(new File(saveDirectory + random.nextLong() + ".png")); System.out.println("download..."); } catch (IOException e) { e.printStackTrace(); } finally { if (response != null) { response.discardContent(); } } }); } try { executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 处理噪点噪块等 * * @param srcDirectory * @param destDirectory */ public static void processNoise(String srcDirectory, String destDirectory) { File file = new File(srcDirectory); File[] imgFileArray = file.listFiles(); for (File imgFile : imgFileArray) { try { BufferedImage image = ImageIO.read(imgFile); BufferedImage noiseCleanImage = W3cSchoolCaptchaUtil.noiseClean(image, 20); ImageIO.write(noiseCleanImage, "png", new File(destDirectory + imgFile.getName())); System.out.println("process noise..."); } catch (IOException e) { e.printStackTrace(); } } } /** * 得到字符字典 * * @param srcDirectory * @param destDirectory */ public static void splitCharacter(String srcDirectory, String destDirectory) { File file = new File(srcDirectory); File[] imgFileArray = file.listFiles(); Map<Integer, BufferedImage> charDictionary = new HashMap<>(); for (File imgFile : imgFileArray) { BufferedImage image = null; try { image = ImageIO.read(imgFile); } catch (IOException e) { e.printStackTrace(); } List<BufferedImage> charList = W3cSchoolCaptchaUtil.mattingCharacter(image); charList.forEach(x -> { int hashcode = W3cSchoolCaptchaUtil.imgHashCode(x); System.out.println(hashcode); charDictionary.put(hashcode, x); }); System.out.println("split..."); } charDictionary.forEach((k, v) -> { try { ImageIO.write(v, "png", new File(destDirectory + k + ".png")); System.out.println("write..."); } catch (IOException e) { e.printStackTrace(); } }); } /** * 根据字符图片生成字符字典 * * @param charDirectory */ public static void genDictionary(String charDirectory) { File[] charImgs = new File(charDirectory).listFiles(); for (File charImgFile : charImgs) { try { BufferedImage charBufferedImage = ImageIO.read(charImgFile); int charHashCode = W3cSchoolCaptchaUtil.imgHashCode(charBufferedImage); System.out.printf("charMapping.put(%d, '%c');\n", charHashCode, charImgFile.getName().split("\\.")[0].charAt(0)); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { // download("D:/test/ocr/w3cschool/original/", 5000); // processNoise("D:/test/ocr/w3cschool/original", "D:/test/ocr/w3cschool/stage01/"); // splitCharacter("D:/test/ocr/w3cschool/stage01", "D:/test/ocr/w3cschool/stage02/"); genDictionary("D:/test/ocr/w3cschool/stage03"); } }
W3cSchoolCaptchaUtil.java:
package bar.ocr.w3cschool; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.util.stream.Collectors.joining; /** * @author CC11001100 */ public class W3cSchoolCaptchaUtil { private static Map<Integer, Character> charMapping = new HashMap<>(); static { charMapping.put(1844796036, '1'); charMapping.put(1594429278, '2'); charMapping.put(-222305694, '3'); charMapping.put(452270032, '4'); charMapping.put(-1898118878, '5'); charMapping.put(999670338, '6'); charMapping.put(-965770966, '7'); charMapping.put(-337170896, '8'); charMapping.put(585835558, '9'); charMapping.put(-724014232, 'A'); charMapping.put(-428164778, 'B'); charMapping.put(-886387444, 'C'); charMapping.put(1946490946, 'D'); charMapping.put(416715843, 'E'); charMapping.put(-917974862, 'F'); charMapping.put(-764688176, 'G'); charMapping.put(28434468, 'H'); charMapping.put(10891004, 'I'); charMapping.put(-2084516900, 'J'); charMapping.put(259070252, 'K'); charMapping.put(1209338035, 'L'); charMapping.put(486706942, 'M'); charMapping.put(983181712, 'N'); charMapping.put(1065112842, 'P'); charMapping.put(183746070, 'Q'); charMapping.put(782513722, 'R'); charMapping.put(-984311436, 'S'); charMapping.put(-1276745734, 'T'); charMapping.put(-796848932, 'U'); charMapping.put(-967446486, 'V'); charMapping.put(331594374, 'W'); charMapping.put(1503060590, 'X'); charMapping.put(-507424510, 'Y'); charMapping.put(468466871, 'Z'); } /** * 去噪点,使用连通域大小来判断 * * @param originalCaptcha 原始的验证码图片 * @param areaSizeFilter 连通域小于等于此大小的将被过滤掉 * @return */ public static BufferedImage noiseClean(BufferedImage originalCaptcha, int areaSizeFilter) { // 会有一些干扰边,把边缘部分切割丢掉 int edgeDropWidth = 15; BufferedImage captcha = originalCaptcha.getSubimage(edgeDropWidth / 2, edgeDropWidth / 2, // originalCaptcha.getWidth() - edgeDropWidth, originalCaptcha.getHeight() - edgeDropWidth); int w = captcha.getWidth(); int h = captcha.getHeight(); int[][] book = new int[w][h]; // 连通域最大的色块将被认为是背景色,这样实现了自动识别背景色 Map<Integer, Integer> flagAreaSizeMap = new HashMap<>(); int currentFlag = 1; int maxAreaSizeFlag = currentFlag; int maxAreaSizeColor = 0XFFFFFFFF; // 标记 for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { if (book[i][j] != 0) { continue; } book[i][j] = currentFlag; int currentColor = captcha.getRGB(i, j); int areaSize = waterFlow(captcha, book, i, j, currentColor, currentFlag); if (areaSize > flagAreaSizeMap.getOrDefault(maxAreaSizeFlag, 0)) { maxAreaSizeFlag = currentFlag; maxAreaSizeColor = currentColor; } flagAreaSizeMap.put(currentFlag, areaSize); currentFlag++; } } // 复制 BufferedImage resultImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int currentColor = captcha.getRGB(i, j); if (book[i][j] == maxAreaSizeFlag // || (currentColor & 0XFFFFFF) == (maxAreaSizeColor & 0XFFFFFF) // || flagAreaSizeMap.get(book[i][j]) <= areaSizeFilter) { resultImage.setRGB(i, j, 0XFFFFFFFF); } else { resultImage.setRGB(i, j, currentColor); } } } return resultImage; } /** * 将图像抽象为颜色矩阵 * * @param img * @param book * @param x * @param y * @param color * @param flag * @return */ private static int waterFlow(BufferedImage img, int[][] book, int x, int y, int color, int flag) { if (x < 0 || x >= img.getWidth() || y < 0 || y >= img.getHeight()) { return 0; } // 这个1统计的是当前点 int areaSize = 1; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { int nextX = x + i; int nextY = y + j; if (nextX < 0 || nextX >= img.getWidth() || nextY < 0 || nextY >= img.getHeight()) { continue; } // 如果这一点没有被访问过,并且颜色相同 // if (book[nextX][nextY] == 0 && isSimilar(img.getRGB(nextX, nextY), color, 0)) { if (book[nextX][nextY] == 0 && (img.getRGB(nextX, nextY) & 0XFFFFFF) == (color & 0XFFFFFF)) { book[nextX][nextY] = flag; areaSize += waterFlow(img, book, nextX, nextY, color, flag); } } } return areaSize; } // /** // * 判断两个像素的相似性 // * // * @param rgb1 // * @param rgb2 // * @param distance // * @return // */ // private static boolean isSimilar(int rgb1, int rgb2, int distance) { // int r1 = rgb1 & 0XFF0000 >> 16; // int g1 = rgb1 & 0X00FF00 >> 8; // int b1 = rgb1 & 0X0000FF; // // int r2 = rgb2 & 0XFF0000 >> 16; // int g2 = rgb2 & 0X00FF00 >> 8; // int b2 = rgb2 & 0X0000FF; // // return (Math.abs(r1 - r2) <= distance) && (Math.abs(g1 - g2) <= distance) && (Math.abs(b1 - b2) <= distance); // } /** * 切割字符 * * @param img * @return */ public static List<BufferedImage> mattingCharacter(BufferedImage img) { List<BufferedImage> list = new ArrayList<>(); int w = img.getWidth(); int h = img.getHeight(); boolean lastColumnIsBlack = true; int beginColumn = -1; for (int i = 0; i < w; i++) { boolean currentColumnIsBlack = true; for (int j = 0; j < h; j++) { if ((img.getRGB(i, j) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } // 进入字符区域 if (lastColumnIsBlack && !currentColumnIsBlack) { beginColumn = i; } else if (!lastColumnIsBlack && currentColumnIsBlack) { // 离开字符区域 BufferedImage charImage = img.getSubimage(beginColumn, 0, i - beginColumn, h); BufferedImage trimCharImage = trimUpAndDown(charImage); list.add(trimCharImage); } lastColumnIsBlack = currentColumnIsBlack; } return list; } private static BufferedImage trimUpAndDown(BufferedImage img) { int w = img.getWidth(); int h = img.getHeight(); // 计算上方空白 int upBeginLine = -1; for (int i = 0; i < h; i++) { boolean currentColumnIsBlack = true; for (int j = 0; j < w; j++) { if ((img.getRGB(j, i) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } if (!currentColumnIsBlack) { upBeginLine = i; break; } } // 计算下方空白 int downBeginLine = -1; for (int i = h - 1; i >= 0; i--) { boolean currentColumnIsBlack = true; for (int j = 0; j < w; j++) { if ((img.getRGB(j, i) & 0XFFFFFF) != 0XFFFFFF) { currentColumnIsBlack = false; } } if (!currentColumnIsBlack) { downBeginLine = i; break; } } return img.getSubimage(0, upBeginLine, w, downBeginLine - upBeginLine + 1); } /** * 计算图像的哈希值,即将图片内容压缩为一个整数 * <p> * NOTE: 适用于小图像 * * @param img * @return */ public static int imgHashCode(BufferedImage img) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < img.getWidth(); i++) { for (int j = 0; j < img.getHeight(); j++) { sb.append(i).append("|").append(j).append("|").append(img.getRGB(i, j) & 0XFFFFFF).append("|"); } } return sb.toString().hashCode(); } /** * 解析传入的验证码 * * @param captcha * @return */ public static String ocr(BufferedImage captcha) { BufferedImage noiseCleaned = noiseClean(captcha, 20); List<BufferedImage> charImageList = mattingCharacter(noiseCleaned); return charImageList.stream().map(x -> charMapping.get(imgHashCode(x)).toString()).collect(joining()); } }
参考资料:
1. https://www.w3cschool.cn/checkmphone?type=findpwd
2. https://www.w3cschool.cn/scode
.