Coding的痕迹

一位互联网奔跑者的网上日记

0%

Rust 中使用 tesseract 识别验证码

Rust 使用 tesseract 识别验证码

近期需要将验证码识别功能集成到一个 Rust 项目中,验证码图片大概这样:

验证码图片示例

观察

首先观察图片,发现前景文字和背景图片颜色深度差别较大,可以对图片进行二值化处理。使用 Photoshop 可以看到,转为灰度图像后,在 0 ~ 255 内,前景和背景大约以 130 为界。使用 “天若OCR” 截图、识别了若干张图片,在较大截图区域的情况下,识别率可以达到 100%,说明这个验证码不复杂。由于在线 API 有数量等限制,最终将目光锁定在了 tesseract 上。

Photoshop截图

tesseract 预训练好的 trained-data(eng)大概能达到 50% 左右的识别率,但在细节上(如字母 S 与数字 5)错误率较高。训练过程不多说,请同学帮忙用约 300 张验证码图片,训练了一个模型。准确率大约 85%,由于嫌麻烦,没有进一步框图训练了。

初次尝试

我的环境是:Windows 10,CLion 2020.2 + Rust 1.46(2020-08-24)。

Rust 下有关于 tesseract 库的封装,tesseract、tesseract-sys 和 leptess(tesseract 和 leptonica 结合),我这里使用 tesseract 库。

找一张使用 Photoshop 处理过的图片,先测试一下:

1
2
3
4
fn main() {
let result = tesseract::ocr("test.jpg", "num");
println!("{:?}", result);
}

可能存在的问题

因为依赖 leptonica-systesseract-sys 只是对应 C/C++ 库的封装,所以我们需要对应的 C/C++ 库。如若未安装,可能报这样的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 省略 */
Compiling tesseract-sys v0.5.3
error: failed to run custom build command for `leptonica-sys v0.3.2`

Caused by:
process didn't exit successfully: `..some-path\target\debug\build\leptonica-sys-15ef6d44d1ec3e46\build-script-build` (exit code: 101)
--- stderr
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: LibNotFound("package leptonica is not installed for vcpkg triplet x64-windows-static-md")', C:\Users\...\.cargo\registry\src\mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd\leptonica-sys-0.3.2\build.rs:10:62
stack backtrace:
<26 internal calls>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Process finished with exit code 101

Linux

这里注意,Linux 下解决这个问题好像很简单,只需要

1
apt install libleptonica-dev libtesseract-dev clang

再次 cargo build 即可。

Windows 下

可以看到,leptonica-sys 依赖了 vcpkg,且缺少 x64-windows-static-md 。可以去 Github 仓库先下载一个 vcpkg。可以参考 《官方快速安装指南》。注意,仓库只提供了源码,你需要保证电脑中有 Visual Studio C++ 的那一套编译工具。

然后在命令行中:

1
2
vcpkg install leptonica:x64-windows-static-md
vcpkg install tesseract:x64-windows-static-md

具体参数视报错自行决定,再次编译即可。

至此问题基本解决,输出:

1
2
Warning: Invalid resolution 0 dpi. Using 70 instead.
Ok("aYNE\n")

完善

很明显,我们需要完善一下这个程序,用 Rust 来做二值化的操作。

引入 imageimageproctesseract 库:

1
2
3
4
[dependencies]
tesseract = "0.6"
image = "0.23"
imageproc = "0.21"

首先打开图片:

1
let image = image::open("31.jpg").unwrap();

imageproc 库有一个 threshold 函数,原型为:

1
pub fn threshold(image: &GrayImage, thresh: u8) -> GrayImage

所以需要先将读入的 RGB 图像转换为灰度图像。

1
2
let image_luma = image.into_luma();
threshold(&image_luma, 130);

再调用 tesseract::ocr_from_frame 函数即可:

1
2
3
4
5
6
7
8
9
10
11
let dimension = image_luma.dimensions();
let content = image_luma.into_vec();

let result = tesseract::ocr_from_frame(
&content, // frame data
dimension.0 as i32, // width
dimension.1 as i32, // height
1, // bytes_per_pixel
1 * dimension.0 as i32, // bytes_per_line
"num" // language
);

这里面,容易理解的是,bytes_per_pixel位深度 / 8,也就是每个像素所占的字节数,但是 bytes_per_line 不知道为何需要输入,可能有些图片格式比较特殊吧。该参数按字面意思是每行像素所占的字节数,然而没有找到有关文档和描述,后来翻了下源码,发现自己并没有理解错,却在这里花费了不少时间。

完整的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use imageproc::contrast::threshold;

fn main() {
let image = image::open("test.jpg").unwrap();
let image_luma = image.into_luma();
let dimension = image_luma.dimensions();

threshold(&image_luma, 130);
let content = image_luma.into_vec();
let s = tesseract::ocr_from_frame(
&content,
dimension.0 as i32,
dimension.1 as i32,
1,
1 * dimension.0 as i32,
"num"
);
println!("{:?}", s);
}

结语

断断续续地搞了好多天,vcpkgtesseract 也都没有用过,就这么边琢磨着、折腾着,弄完了。

谨以此文,庆祝一下国庆,感谢一下自己。