rxing/common/
global_histogram_binarizer.rs

1/*
2 * Copyright 2009 ZXing authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// package com.google.zxing.common;
18
19// import com.google.zxing.Binarizer;
20// import com.google.zxing.LuminanceSource;
21// import com.google.zxing.NotFoundException;
22
23use std::borrow::Cow;
24
25use once_cell::sync::OnceCell;
26
27use crate::common::Result;
28use crate::{Binarizer, Exceptions, LuminanceSource};
29
30use super::{BitArray, BitMatrix, LineOrientation};
31
32const LUMINANCE_BITS: usize = 5;
33const LUMINANCE_SHIFT: usize = 8 - LUMINANCE_BITS;
34const LUMINANCE_BUCKETS: usize = 1 << LUMINANCE_BITS;
35
36/**
37 * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable
38 * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding
39 * algorithm. However, because it picks a global black point, it cannot handle difficult shadows
40 * and gradients.
41 *
42 * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead.
43 *
44 * @author dswitkin@google.com (Daniel Switkin)
45 * @author Sean Owen
46 */
47pub struct GlobalHistogramBinarizer<LS: LuminanceSource> {
48    //_luminances: Vec<u8>,
49    width: usize,
50    height: usize,
51    source: LS,
52    black_matrix: OnceCell<BitMatrix>,
53    black_row_cache: Vec<OnceCell<BitArray>>,
54    black_column_cache: Vec<OnceCell<BitArray>>,
55}
56
57impl<LS: LuminanceSource> Binarizer for GlobalHistogramBinarizer<LS> {
58    type Source = LS;
59
60    fn get_luminance_source(&self) -> &Self::Source {
61        &self.source
62    }
63
64    // Applies simple sharpening to the row data to improve performance of the 1D Readers.
65    fn get_black_row(&'_ self, y: usize) -> Result<Cow<'_, BitArray>> {
66        let row = self.black_row_cache[y].get_or_try_init(|| {
67            let source = self.get_luminance_source();
68            let width = source.get_width();
69            let mut row = BitArray::with_size(width);
70
71            // self.initArrays(width);
72            let localLuminances = source
73                .get_row(y)
74                .ok_or(Exceptions::index_out_of_bounds_with("row out of bounds"))?;
75            let mut localBuckets = [0; LUMINANCE_BUCKETS]; //self.buckets.clone();
76            for x in 0..width {
77                // for (int x = 0; x < width; x++) {
78                localBuckets[((localLuminances[x]) >> LUMINANCE_SHIFT) as usize] += 1;
79            }
80            let blackPoint = Self::estimateBlackPoint(&localBuckets)?;
81
82            if width < 3 {
83                // Special case for very small images
84                for (x, &lum) in localLuminances.iter().enumerate().take(width) {
85                    // for x in 0..width {
86                    //   for (int x = 0; x < width; x++) {
87                    if (lum as u32) < blackPoint {
88                        row.set(x);
89                    }
90                }
91            } else {
92                let mut left = localLuminances[0]; // & 0xff;
93                let mut center = localLuminances[1]; // & 0xff;
94                for x in 1..width - 1 {
95                    //   for (int x = 1; x < width - 1; x++) {
96                    let right = localLuminances[x + 1];
97                    // A simple -1 4 -1 box filter with a weight of 2.
98                    if ((center as i64 * 4) - left as i64 - right as i64) / 2 < blackPoint as i64 {
99                        row.set(x);
100                    }
101                    left = center;
102                    center = right;
103                }
104            }
105
106            Ok(row)
107        })?;
108
109        Ok(Cow::Borrowed(row))
110    }
111
112    fn get_black_line(&'_ self, l: usize, lt: LineOrientation) -> Result<Cow<'_, BitArray>> {
113        if lt == LineOrientation::Row {
114            self.get_black_row(l)
115        } else {
116            let col = self.black_column_cache[l].get_or_try_init(|| {
117                let source = self.get_luminance_source();
118                let width = source.get_height();
119                let mut col = BitArray::with_size(width);
120
121                // self.initArrays(width);
122                let localLuminances = source.get_column(l);
123                let mut localBuckets = [0; LUMINANCE_BUCKETS]; //self.buckets.clone();
124                for x in 0..width {
125                    // for (int x = 0; x < width; x++) {
126                    localBuckets[((localLuminances[x]) >> LUMINANCE_SHIFT) as usize] += 1;
127                }
128                let blackPoint = Self::estimateBlackPoint(&localBuckets)?;
129
130                if width < 3 {
131                    // Special case for very small images
132                    for (x, lum) in localLuminances.iter().enumerate().take(width) {
133                        // for x in 0..width {
134                        //   for (int x = 0; x < width; x++) {
135                        if (*lum as u32) < blackPoint {
136                            col.set(x);
137                        }
138                    }
139                } else {
140                    let mut left = localLuminances[0]; // & 0xff;
141                    let mut center = localLuminances[1]; // & 0xff;
142                    for x in 1..width - 1 {
143                        //   for (int x = 1; x < width - 1; x++) {
144                        let right = localLuminances[x + 1];
145                        // A simple -1 4 -1 box filter with a weight of 2.
146                        if ((center as i64 * 4) - left as i64 - right as i64) / 2
147                            < blackPoint as i64
148                        {
149                            col.set(x);
150                        }
151                        left = center;
152                        center = right;
153                    }
154                }
155
156                Ok(col)
157            })?;
158
159            Ok(Cow::Borrowed(col))
160        }
161    }
162
163    // Does not sharpen the data, as this call is intended to only be used by 2D Readers.
164    fn get_black_matrix(&self) -> Result<&BitMatrix> {
165        let matrix = self
166            .black_matrix
167            .get_or_try_init(|| Self::build_black_matrix(&self.source))?;
168        Ok(matrix)
169    }
170
171    fn create_binarizer(&self, source: LS) -> Self {
172        Self::new(source)
173    }
174
175    fn get_width(&self) -> usize {
176        self.width
177    }
178
179    fn get_height(&self) -> usize {
180        self.height
181    }
182
183    fn get_black_row_from_matrix(&'_ self, y: usize) -> Result<Cow<'_, BitArray>> {
184        if let Some(matrix) = self.black_matrix.get() {
185            Ok(Cow::Owned(matrix.getRow(y as u32)))
186        } else {
187            self.get_black_row(y)
188        }
189    }
190}
191
192impl<LS: LuminanceSource> GlobalHistogramBinarizer<LS> {
193    // const EMPTY: [u8; 0] = [0; 0];
194
195    pub fn new(source: LS) -> Self {
196        Self {
197            //_luminances: vec![0; source.getWidth()],
198            width: source.get_width(),
199            height: source.get_height(),
200            black_matrix: OnceCell::new(),
201            black_row_cache: vec![OnceCell::default(); source.get_height()],
202            black_column_cache: vec![OnceCell::default(); source.get_width()],
203            source,
204        }
205    }
206
207    fn build_black_matrix(source: &LS) -> Result<BitMatrix> {
208        // let source = source.getLuminanceSource();
209        let width = source.get_width();
210        let height = source.get_height();
211        let mut matrix = BitMatrix::new(width as u32, height as u32)?;
212
213        // Quickly calculates the histogram by sampling four rows from the image. This proved to be
214        // more robust on the blackbox tests than sampling a diagonal as we used to do.
215        // self.initArrays(width);
216        let mut localBuckets = [0; LUMINANCE_BUCKETS]; //self.buckets.clone();
217        for y in 1..5 {
218            // for (int y = 1; y < 5; y++) {
219            let row = height * y / 5;
220            let localLuminances = source
221                .get_row(row)
222                .ok_or(Exceptions::index_out_of_bounds_with("row out of bounds"))?;
223            let right = (width * 4) / 5;
224            for pixel in &localLuminances[(width / 5)..right] {
225                // for x in (width / 5)..right {
226                // let pixel = localLuminances[x];
227                localBuckets[(pixel >> LUMINANCE_SHIFT) as usize] += 1;
228            }
229        }
230        let blackPoint = Self::estimateBlackPoint(&localBuckets)?;
231
232        // We delay reading the entire image luminance until the black point estimation succeeds.
233        // Although we end up reading four rows twice, it is consistent with our motto of
234        // "fail quickly" which is necessary for continuous scanning.
235        let localLuminances = source.get_matrix();
236        for y in 0..height {
237            let offset = y * width;
238            for x in 0..width {
239                let pixel = localLuminances[offset + x];
240                if (pixel as u32) < blackPoint {
241                    matrix.set(x as u32, y as u32);
242                }
243            }
244        }
245
246        Ok(matrix)
247    }
248
249    fn estimateBlackPoint<const BUCKET_COUNT: usize>(buckets: &[u32; BUCKET_COUNT]) -> Result<u32> {
250        // Find the tallest peak in the histogram.
251        let mut maxBucketCount = 0;
252        let mut firstPeak = 0;
253        let mut firstPeakSize = 0;
254        for (x, &bucket) in buckets.iter().enumerate() {
255            if bucket > firstPeakSize {
256                firstPeak = x;
257                firstPeakSize = bucket;
258            }
259            if bucket > maxBucketCount {
260                maxBucketCount = bucket;
261            }
262        }
263
264        // Find the second-tallest peak which is somewhat far from the tallest peak.
265        let mut secondPeak = 0;
266        let mut secondPeakScore = 0;
267        for (x, bucket) in buckets.iter().enumerate() {
268            let distanceToBiggest = (x as i32 - firstPeak as i32).unsigned_abs();
269            // Encourage more distant second peaks by multiplying by square of distance.
270            let score = *bucket * distanceToBiggest * distanceToBiggest;
271            if score > secondPeakScore {
272                secondPeak = x;
273                secondPeakScore = score;
274            }
275        }
276
277        // Make sure firstPeak corresponds to the black peak.
278        if firstPeak > secondPeak {
279            std::mem::swap(&mut firstPeak, &mut secondPeak);
280        }
281
282        // If there is too little contrast in the image to pick a meaningful black point, throw rather
283        // than waste time trying to decode the image, and risk false positives.
284        if secondPeak - firstPeak <= BUCKET_COUNT / 16 {
285            return Err(Exceptions::not_found_with(
286                "secondPeak - firstPeak <= numBuckets / 16 ",
287            ));
288        }
289
290        // Find a valley between them that is low and closer to the white peak.
291        let mut bestValley = secondPeak as isize - 1;
292        let mut bestValleyScore = -1;
293
294        let mut x = secondPeak as isize;
295        while x > firstPeak as isize {
296            let fromFirst = x - firstPeak as isize;
297            let score = fromFirst
298                * fromFirst
299                * (secondPeak as isize - x)
300                * (maxBucketCount - buckets[x as usize]) as isize;
301            if score as i32 > bestValleyScore {
302                bestValley = x;
303                bestValleyScore = score as i32;
304            }
305            x -= 1;
306        }
307
308        Ok((bestValley as u32) << LUMINANCE_SHIFT)
309    }
310}