amazon_captcha_rs/
lib.rs

1/*!
2Amazon Captcha Solver
3
4This library has been highly inspired by:
5- a-maliarov/amazoncaptcha (Python)
6- gopkg-dev/amazoncaptcha (Go)
7
8Some methods are re-used from these libraries,
9the dataset is also re-used from gopkg but
10converted to uncompressed bincode format which makes
11it much faster to load.
12
13# Example
14
15```
16use amazon_captcha_rs::Solver;
17
18let image = image::open("examples/dataset/aatmag.jpg").unwrap();
19
20let solver = Solver::new().unwrap();
21let response = solver.resolve_image(&image);
22
23assert_eq!(response, "aatmag");
24```
25*/
26
27use std::collections::HashMap;
28
29use image::{imageops, DynamicImage, GenericImage, GrayImage};
30
31/// Solver implementation
32pub struct Solver {
33    training_data: HashMap<String, char>,
34}
35
36impl Solver {
37    /**
38    Creates a new [`Solver`] using the training data from
39    `dataset.bin` (included in the crate)
40
41    # Errors
42
43    Refer to [`bincode::Error`]
44    */
45    pub fn new() -> Result<Self, bincode::Error> {
46        Ok(Solver {
47            training_data: bincode::deserialize(include_bytes!(
48                "../dataset.bin"
49            ))?,
50        })
51    }
52
53    /**
54    Resolves a captcha image using training data
55
56    # Example
57
58    ```
59    use amazon_captcha_rs::Solver;
60
61    let image = image::open("examples/dataset/cxkgmg.jpg").unwrap();
62
63    let solver = Solver::new().unwrap();
64    let response = solver.resolve_image(&image);
65
66    assert_eq!(response, "cxkgmg");
67    ```
68    */
69    pub fn resolve_image(&self, image: &DynamicImage) -> String {
70        let mut letters = extract_letters(image);
71
72        if letters.len() == 7 {
73            letters[6] = merge_images(&letters[6], &letters[0]);
74            letters.remove(0);
75        }
76
77        let mut resolved = String::new();
78
79        for img in letters {
80            let binary = img
81                .pixels()
82                .map(|pixel| if pixel.0[0] <= 1 { '1' } else { '0' })
83                .collect::<String>();
84
85            resolved.push(
86                *self
87                    .training_data
88                    .get(&binary)
89                    .unwrap_or_else(|| self.most_similar_letter(&binary)),
90            );
91        }
92
93        resolved.to_lowercase()
94    }
95
96    /// Returns most similar letter
97    fn most_similar_letter(&self, letter: &str) -> &char {
98        let mut max_letter = &' ';
99        let mut max_score = usize::MIN;
100
101        for (key, value) in &self.training_data {
102            let score = letter
103                .chars()
104                .zip(key.chars())
105                .filter(|(a, b)| a == b)
106                .count();
107
108            if score > max_score {
109                max_score = score;
110                max_letter = value;
111            }
112        }
113
114        max_letter
115    }
116}
117
118/// Merge two images side by side
119fn merge_images(img1: &GrayImage, img2: &GrayImage) -> GrayImage {
120    let (width1, height1) = img1.dimensions();
121    let (width2, height2) = img2.dimensions();
122
123    let mut merged_image =
124        GrayImage::new(width1 + width2, height1.max(height2));
125
126    merged_image.copy_from(img1, 0, 0).unwrap();
127    merged_image.copy_from(img2, width1, 0).unwrap();
128
129    merged_image
130}
131
132/// Extracts letters from an image
133fn extract_letters(img: &DynamicImage) -> Vec<GrayImage> {
134    let img = imageops::grayscale(img);
135    let (width, height) = img.dimensions();
136
137    let mut rects = Vec::new();
138    let mut start = None;
139
140    for x in 0..width {
141        if (0..height).any(|y| img.get_pixel(x, y)[0] <= 1) {
142            if start.is_none() {
143                start = Some(x);
144            }
145        } else if let Some(point) = start {
146            rects.push((point, x));
147            start = None;
148        }
149    }
150
151    if let Some(point) = start {
152        rects.push((point, width));
153    }
154
155    rects
156        .iter()
157        .map(|(x1, x2)| {
158            imageops::crop_imm(&img, *x1, 0, x2 - x1, height).to_image()
159        })
160        .collect()
161}