验证码识别之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
.
验证码识别之w3cschool字符图片验证码(easy级别)的更多相关文章
- iOS-仿智联字符图片验证码
概述 随机字符组成的图片验证码, 字符位数可改变, 字符可斜可正排列. 详细 代码下载:http://www.demodashi.com/demo/10850.html 项目中有时候会有这种需求: 获 ...
- 手机app有了短信验证码还有没必要有图片验证码?
当然有必要,这里我们来聊一个恶意短信验证的案例,通过这个案例我们就能更好理解短信验证码和图片验证码这两者的关系了. 讨论防止恶意短信验证之前,我们先来看看什么是恶意短信验证及出现的原因. 恶意短信验证 ...
- python验证码识别(2)极验滑动验证码识别
目录 一:极验滑动验证码简介 二:极验滑动验证码识别思路 三:极验验证码识别 一:极验滑动验证码简介 近些年来出现了一些新型验证码,不想旧的验证码对人类不友好,但是这种验证码对于代码来说识别难度上 ...
- 字符型图片验证码识别完整过程及Python实现
字符型图片验证码识别完整过程及Python实现 1 摘要 验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的 防火墙 功能,但是随时OCR技术的发展,验证码暴露出来的安全问题也越 ...
- 字符识别Python实现 图片验证码识别
字符型图片验证码识别完整过程及Python实现 1 摘要 验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的 防火墙 功能,但是随时OCR技术的发展,验证码暴露出来的安全问题也越 ...
- python3图片验证码识别
http://my.cnki.net/elibregister/CheckCode.aspx每次刷新该网页可以得到新的验证码进行测试 以我本次查看的验证码图片为例,右键保存图片为image.jpg 下 ...
- 【java+selenium3】Tesseract-OCR识别图片验证码 (十六)
[java+selenium+Tesseract-OCR(图片识别)+AutoIt(windows窗口识别)]完成自动化图片验证码识别! 一.AutoIt(windows窗口识别)参考:https:/ ...
- 验证码识别--type5
验证码识别--type5 每一种验证码都是由人设计出来.在设计过程中,可能由于多个方面的原因,造成了这样或那样的可以被利用的漏洞.验证码识别,首先需要解决的问题就是发现这些漏洞--然后利用漏洞解决问题 ...
- python验证码识别
关于利用python进行验证码识别的一些想法 用python加“验证码”为关键词在baidu里搜一下,可以找到很多关于验证码识别的文章.我大体看了一下,主要方法有几类:一类是通过对图片进行处 理,然后 ...
随机推荐
- JQuery Layer的应用实例
参考以上链接:https://blog.csdn.net/zlj_blog/article/details/24994799 sql面试题:https://www.cnblogs.com/qixuej ...
- Django ORM创建数据库
Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为:大而全,框架本身集成了ORM.模型绑定.模板引擎.缓存.Session等诸多功能. ...
- HTML常用布局---新浪布局
MarkdownPad Document/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) *//* Author: Nicol ...
- assert后面如果是假则程序崩溃
assert后面如果是假,则程序崩溃.
- issubclass判断前面是不是后面的子类
issubclass(sub,sup) 判断前面是不是后面的子类
- PHP 抓取网页内容的几个函数
<?php //获取所有内容url保存到文件 function get_index($save_file, $prefix="index_"){ $count = 68; $ ...
- 【H5-移动端开发】外部唤起本机APP的解决方法
太长时间没来博客园,原因很简单啊--太懒了!罪过罪过~ 最近公司的APP项目开始运行,采用的是原生框架+内嵌H5页面.作为一个菜鸡前端,开始入手学习移动端的界面制作加载性能优化.由于公司开始推广软件, ...
- 《跟我学Shiro》学习笔记 第一章:Shiro简介
前言 现在在学习Shiro,参照着张开涛老师的博客进行学习,然后自己写博客记录一下学习中的知识点,一来可以加深理解,二来以后遗忘了可以查阅.没有学习过Shiro的小伙伴,也可以和我一起学习,大家共同进 ...
- 使用Navicat Premium 链接本地数据库的方法(二)
最早一篇:http://www.cnblogs.com/zhengyeye/p/6363179.html 现在又重新装了电脑系统,需遇到了同样的问题.恰巧记得之前自己写的文档,没准可以帮助自己解决掉这 ...
- [SPOJ 4155]OTOCI
Description 题库链接 给你 \(n\) 个节点,让你兹磁以下操作,维护一棵树: 动态加边: 修改点权: 询问路径上点权和. \(1\leq n\leq 30000\) Solution 好 ...