Skip to main content

scan_core/cv/
threshold.rs

1use image::GrayImage;
2
3/// Applies Adaptive Thresholding using Stack Box Blur.
4/// Port of CV.adaptiveThreshold from the JS codebase.
5///
6/// `kernel_size`: Radius for the stack box blur (JS default: 2).
7/// `threshold`: Constant for comparison (JS default: 7).
8pub fn adaptive_threshold(img: &GrayImage, kernel_size: usize, threshold: i32) -> GrayImage {
9    let (width, height) = img.dimensions();
10    let mut blurred = stack_box_blur(img, kernel_size);
11    let src = img.as_raw();
12    let dst = blurred.as_mut();
13
14    // Build lookup table identical to JS:
15    // tab[i] = (i - 255 <= -threshold) ? 255 : 0
16    let mut tab = [0u8; 768];
17    for i in 0..768 {
18        tab[i] = if (i as i32 - 255) <= -threshold { 255 } else { 0 };
19    }
20
21    // Apply: dst[i] = tab[ src[i] - dst[i] + 255 ]
22    for i in 0..src.len() {
23        let idx = src[i] as i32 - dst[i] as i32 + 255;
24        dst[i] = tab[idx as usize];
25    }
26
27    GrayImage::from_raw(width, height, dst.to_vec()).unwrap()
28}
29
30/// Stack Box Blur — exact port of CV.stackBoxBlur from JS.
31/// Uses a circular linked-list stack for O(1) per-pixel blur.
32fn stack_box_blur(img: &GrayImage, kernel_size: usize) -> GrayImage {
33    let (width, height) = img.dimensions();
34    let w = width as usize;
35    let h = height as usize;
36    let src = img.as_raw();
37    let mut dst = src.clone();
38
39    // Lookup tables from JS (indices 0..15)
40    let mult_table: [u32; 16] = [1, 171, 205, 293, 57, 373, 79, 137, 241, 27, 391, 357, 41, 19, 283, 265];
41    let shift_table: [u32; 16] = [0, 9, 10, 11, 9, 12, 10, 11, 12, 9, 13, 13, 10, 9, 13, 13];
42
43    let ks = kernel_size.min(15);
44    let mult = mult_table[ks];
45    let shift = shift_table[ks];
46    let size = ks + ks + 1;
47    let radius = ks + 1;
48
49    // Build circular stack
50    let mut stack = vec![0u32; size];
51
52    // Horizontal pass
53    for y in 0..h {
54        let row_start = y * w;
55
56        // Initialize stack and sum for this row
57        let first_color = dst[row_start] as u32;
58        let mut sum = radius as u32 * first_color;
59
60        // Fill first `radius` slots with border color
61        for i in 0..radius {
62            stack[i] = first_color;
63        }
64        // Fill remaining slots with actual pixels
65        for i in 1..radius {
66            let px = if i < w { dst[row_start + i] as u32 } else { dst[row_start + w - 1] as u32 };
67            stack[radius - 1 + i] = px;
68            sum += px;
69        }
70
71        let mut stack_idx = 0;
72
73        for x in 0..w {
74            dst[row_start + x] = ((sum * mult) >> shift) as u8;
75
76            // Remove outgoing, add incoming
77            let p = x + radius;
78            let p = if p < w { row_start + p } else { row_start + w - 1 };
79            let incoming = src[p] as u32;
80
81            sum -= stack[stack_idx];
82            sum += incoming;
83            stack[stack_idx] = incoming;
84
85            stack_idx += 1;
86            if stack_idx >= size { stack_idx = 0; }
87        }
88    }
89
90    // Vertical pass (operate on dst in-place)
91    let src_v = dst.clone();
92    for x in 0..w {
93        let first_color = src_v[x] as u32;
94        let mut sum = radius as u32 * first_color;
95
96        for i in 0..radius {
97            stack[i] = first_color;
98        }
99        for i in 1..radius {
100            let py = if i < h { i } else { h - 1 };
101            let px = src_v[py * w + x] as u32;
102            stack[radius - 1 + i] = px;
103            sum += px;
104        }
105
106        let mut stack_idx = 0;
107
108        for y in 0..h {
109            dst[y * w + x] = ((sum * mult) >> shift) as u8;
110
111            let p = y + radius;
112            let py = if p < h { p } else { h - 1 };
113            let incoming = src_v[py * w + x] as u32;
114
115            sum -= stack[stack_idx];
116            sum += incoming;
117            stack[stack_idx] = incoming;
118
119            stack_idx += 1;
120            if stack_idx >= size { stack_idx = 0; }
121        }
122    }
123
124    GrayImage::from_raw(width, height, dst).unwrap()
125}
126
127/// Applies global thresholding.
128/// If pixel > thresh -> 255 else 0
129pub fn global_threshold(img: &GrayImage, thresh: u8) -> GrayImage {
130    let mut out = GrayImage::new(img.width(), img.height());
131    for (x, y, p) in img.enumerate_pixels() {
132        let val = if p[0] > thresh { 255 } else { 0 };
133        out.put_pixel(x, y, image::Luma([val]));
134    }
135    out
136}
137
138/// Computes Otsu's threshold for bimodal distributions.
139pub fn otsu_threshold(img: &GrayImage) -> u8 {
140    let mut hist = [0u32; 256];
141    for p in img.pixels() {
142        hist[p[0] as usize] += 1;
143    }
144    let total = img.len() as f32;
145
146    let mut sum = 0.0f32;
147    for i in 0..256 {
148        sum += i as f32 * hist[i] as f32;
149    }
150
151    let mut sum_b = 0.0f32;
152    let mut w_b = 0u32;
153    let mut max = 0.0f32;
154    let mut threshold = 0u8;
155
156    for i in 0..256 {
157        w_b += hist[i];
158        if w_b == 0 { continue; }
159        let w_f = total as u32 - w_b;
160        if w_f == 0 { break; }
161
162        sum_b += i as f32 * hist[i] as f32;
163
164        let m_b = sum_b / w_b as f32;
165        let m_f = (sum - sum_b) / w_f as f32;
166
167        let between = w_b as f32 * w_f as f32 * (m_b - m_f).powi(2);
168        if between > max {
169            max = between;
170            threshold = i as u8;
171        }
172    }
173    threshold
174}