From 1b5ef1e51b064b5376ccd205ff27cb528452dbeb Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Fri, 2 Sep 2022 13:55:58 -0500 Subject: [PATCH] GlobalHistogramBinarizer ported --- src/common/GlobalHistogramBinarizer.java | 203 ----------------- src/common/mod.rs | 270 ++++++++++++++++++++++- src/lib.rs | 6 +- 3 files changed, 266 insertions(+), 213 deletions(-) delete mode 100644 src/common/GlobalHistogramBinarizer.java diff --git a/src/common/GlobalHistogramBinarizer.java b/src/common/GlobalHistogramBinarizer.java deleted file mode 100644 index 1856744..0000000 --- a/src/common/GlobalHistogramBinarizer.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2009 ZXing authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.zxing.common; - -import com.google.zxing.Binarizer; -import com.google.zxing.LuminanceSource; -import com.google.zxing.NotFoundException; - -/** - * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable - * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding - * algorithm. However, because it picks a global black point, it cannot handle difficult shadows - * and gradients. - * - * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead. - * - * @author dswitkin@google.com (Daniel Switkin) - * @author Sean Owen - */ -public class GlobalHistogramBinarizer extends Binarizer { - - private static final int LUMINANCE_BITS = 5; - private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; - private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; - private static final byte[] EMPTY = new byte[0]; - - private byte[] luminances; - private final int[] buckets; - - public GlobalHistogramBinarizer(LuminanceSource source) { - super(source); - luminances = EMPTY; - buckets = new int[LUMINANCE_BUCKETS]; - } - - // Applies simple sharpening to the row data to improve performance of the 1D Readers. - @Override - public BitArray getBlackRow(int y, BitArray row) throws NotFoundException { - LuminanceSource source = getLuminanceSource(); - int width = source.getWidth(); - if (row == null || row.getSize() < width) { - row = new BitArray(width); - } else { - row.clear(); - } - - initArrays(width); - byte[] localLuminances = source.getRow(y, luminances); - int[] localBuckets = buckets; - for (int x = 0; x < width; x++) { - localBuckets[(localLuminances[x] & 0xff) >> LUMINANCE_SHIFT]++; - } - int blackPoint = estimateBlackPoint(localBuckets); - - if (width < 3) { - // Special case for very small images - for (int x = 0; x < width; x++) { - if ((localLuminances[x] & 0xff) < blackPoint) { - row.set(x); - } - } - } else { - int left = localLuminances[0] & 0xff; - int center = localLuminances[1] & 0xff; - for (int x = 1; x < width - 1; x++) { - int right = localLuminances[x + 1] & 0xff; - // A simple -1 4 -1 box filter with a weight of 2. - if (((center * 4) - left - right) / 2 < blackPoint) { - row.set(x); - } - left = center; - center = right; - } - } - return row; - } - - // Does not sharpen the data, as this call is intended to only be used by 2D Readers. - @Override - public BitMatrix getBlackMatrix() throws NotFoundException { - LuminanceSource source = getLuminanceSource(); - int width = source.getWidth(); - int height = source.getHeight(); - BitMatrix matrix = new BitMatrix(width, height); - - // Quickly calculates the histogram by sampling four rows from the image. This proved to be - // more robust on the blackbox tests than sampling a diagonal as we used to do. - initArrays(width); - int[] localBuckets = buckets; - for (int y = 1; y < 5; y++) { - int row = height * y / 5; - byte[] localLuminances = source.getRow(row, luminances); - int right = (width * 4) / 5; - for (int x = width / 5; x < right; x++) { - int pixel = localLuminances[x] & 0xff; - localBuckets[pixel >> LUMINANCE_SHIFT]++; - } - } - int blackPoint = estimateBlackPoint(localBuckets); - - // We delay reading the entire image luminance until the black point estimation succeeds. - // Although we end up reading four rows twice, it is consistent with our motto of - // "fail quickly" which is necessary for continuous scanning. - byte[] localLuminances = source.getMatrix(); - for (int y = 0; y < height; y++) { - int offset = y * width; - for (int x = 0; x < width; x++) { - int pixel = localLuminances[offset + x] & 0xff; - if (pixel < blackPoint) { - matrix.set(x, y); - } - } - } - - return matrix; - } - - @Override - public Binarizer createBinarizer(LuminanceSource source) { - return new GlobalHistogramBinarizer(source); - } - - private void initArrays(int luminanceSize) { - if (luminances.length < luminanceSize) { - luminances = new byte[luminanceSize]; - } - for (int x = 0; x < LUMINANCE_BUCKETS; x++) { - buckets[x] = 0; - } - } - - private static int estimateBlackPoint(int[] buckets) throws NotFoundException { - // Find the tallest peak in the histogram. - int numBuckets = buckets.length; - int maxBucketCount = 0; - int firstPeak = 0; - int firstPeakSize = 0; - for (int x = 0; x < numBuckets; x++) { - if (buckets[x] > firstPeakSize) { - firstPeak = x; - firstPeakSize = buckets[x]; - } - if (buckets[x] > maxBucketCount) { - maxBucketCount = buckets[x]; - } - } - - // Find the second-tallest peak which is somewhat far from the tallest peak. - int secondPeak = 0; - int secondPeakScore = 0; - for (int x = 0; x < numBuckets; x++) { - int distanceToBiggest = x - firstPeak; - // Encourage more distant second peaks by multiplying by square of distance. - int score = buckets[x] * distanceToBiggest * distanceToBiggest; - if (score > secondPeakScore) { - secondPeak = x; - secondPeakScore = score; - } - } - - // Make sure firstPeak corresponds to the black peak. - if (firstPeak > secondPeak) { - int temp = firstPeak; - firstPeak = secondPeak; - secondPeak = temp; - } - - // If there is too little contrast in the image to pick a meaningful black point, throw rather - // than waste time trying to decode the image, and risk false positives. - if (secondPeak - firstPeak <= numBuckets / 16) { - throw NotFoundException.getNotFoundInstance(); - } - - // Find a valley between them that is low and closer to the white peak. - int bestValley = secondPeak - 1; - int bestValleyScore = -1; - for (int x = secondPeak - 1; x > firstPeak; x--) { - int fromFirst = x - firstPeak; - int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]); - if (score > bestValleyScore) { - bestValley = x; - bestValleyScore = score; - } - } - - return bestValley << LUMINANCE_SHIFT; - } - -} diff --git a/src/common/mod.rs b/src/common/mod.rs index fa05703..28178e2 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -8,8 +8,10 @@ use std::collections::HashMap; use std::fmt; use std::rc::Rc; +use crate::Binarizer; use crate::DecodeHintType; use crate::Exceptions; +use crate::LuminanceSource; use crate::RXingResultPoint; use encoding::Encoding; @@ -3286,7 +3288,11 @@ impl MinimalECIInput { fn addEdge(edges: &mut Vec>>>, to: usize, edge: Rc) { if edges[to][edge.encoderIndex].is_none() - || edges[to][edge.encoderIndex].clone().unwrap().cachedTotalSize > edge.cachedTotalSize + || edges[to][edge.encoderIndex] + .clone() + .unwrap() + .cachedTotalSize + > edge.cachedTotalSize { edges[to][edge.encoderIndex] = Some(edge.clone()); } @@ -3313,7 +3319,7 @@ impl MinimalECIInput { for i in start..end { // for (int i = start; i < end; i++) { - if ch as u16== fnc1 || encoderSet.canEncode(ch, i) { + if ch as u16 == fnc1 || encoderSet.canEncode(ch, i) { Self::addEdge( edges, from + 1, @@ -3364,14 +3370,18 @@ impl MinimalECIInput { if minimalJ < 0 { panic!("Internal error: failed to encode \"{}\"", stringToEncode); } - let mut intsAL:Vec = Vec::new(); + let mut intsAL: Vec = Vec::new(); let mut current = edges[inputLength][minimalJ as usize].clone(); while current.is_some() { let c = current.unwrap().clone(); if c.isFNC1() { intsAL.splice(0..0, [1000]); } else { - let bytes:Vec = encoderSet.encode_char(c.c as u8 as char, c.encoderIndex).iter().map(|x| *x as u16).collect(); + let bytes: Vec = encoderSet + .encode_char(c.c as u8 as char, c.encoderIndex) + .iter() + .map(|x| *x as u16) + .collect(); let mut i = bytes.len() as i32 - 1; while i >= 0 { // for (int i = bytes.length - 1; i >= 0; i--) { @@ -3385,7 +3395,10 @@ impl MinimalECIInput { c.previous.clone().unwrap().encoderIndex }; if previousEncoderIndex != c.encoderIndex { - intsAL.splice(0..0, [256 as u16+ encoderSet.getECIValue(c.encoderIndex) as u16]); + intsAL.splice( + 0..0, + [256 as u16 + encoderSet.getECIValue(c.encoderIndex) as u16], + ); } current = c.previous.clone(); } @@ -3412,7 +3425,7 @@ impl InputEdge { previous: Option>, fnc1: u16, ) -> Self { - let mut size = if c == 1000 { + let mut size = if c == 1000 { 1 } else { encoderSet.encode_char(c as u8 as char, encoderIndex).len() @@ -3426,7 +3439,7 @@ impl InputEdge { size += prev.cachedTotalSize; Self { - c: if c as u16== fnc1 { 1000 } else { c as u16 }, + c: if c as u16 == fnc1 { 1000 } else { c as u16 }, encoderIndex, previous: Some(prev.clone()), cachedTotalSize: size, @@ -3499,3 +3512,246 @@ impl fmt::Display for MinimalECIInput { write!(f, "{}", result) } } + +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// package com.google.zxing.common; + +// import com.google.zxing.Binarizer; +// import com.google.zxing.LuminanceSource; +// import com.google.zxing.NotFoundException; + +/** + * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable + * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding + * algorithm. However, because it picks a global black point, it cannot handle difficult shadows + * and gradients. + * + * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +pub struct GlobalHistogramBinarizer { + luminances: Vec, + buckets: Vec, + width: usize, + height: usize, + source: Box, +} + +impl Binarizer for GlobalHistogramBinarizer { + fn getLuminanceSource(&self) -> &Box { + &self.source + } + + // Applies simple sharpening to the row data to improve performance of the 1D Readers. + fn getBlackRow(&self, y: usize, row: &mut BitArray) -> Result { + let source = self.getLuminanceSource(); + let width = source.getWidth(); + let mut row = if row.getSize() < width { + BitArray::with_size(width) + } else { + let mut z = row.clone(); + z.clear(); + z + }; + + // self.initArrays(width); + let localLuminances = source.getRow(y, &self.luminances); + let mut localBuckets = self.buckets.clone(); + for x in 0..width { + // for (int x = 0; x < width; x++) { + localBuckets + [((localLuminances[x]) >> GlobalHistogramBinarizer::LUMINANCE_SHIFT) as usize] += 1; + } + let blackPoint = self.estimateBlackPoint(&localBuckets)?; + + if width < 3 { + // Special case for very small images + for x in 0..width { + // for (int x = 0; x < width; x++) { + if (localLuminances[x] as u32) < blackPoint { + row.set(x); + } + } + } else { + let mut left = localLuminances[0]; // & 0xff; + let mut center = localLuminances[1]; // & 0xff; + for x in 1..width - 1 { + // for (int x = 1; x < width - 1; x++) { + let right = localLuminances[x + 1] & 0xff; + // A simple -1 4 -1 box filter with a weight of 2. + if ((center * 4) - left - right) as u32 / 2 < blackPoint { + row.set(x); + } + left = center; + center = right; + } + } + Ok(row) + } + + // Does not sharpen the data, as this call is intended to only be used by 2D Readers. + fn getBlackMatrix(&self) -> Result { + let source = self.getLuminanceSource(); + let width = source.getWidth(); + let height = source.getHeight(); + let mut matrix = BitMatrix::new(width as u32, height as u32)?; + + // Quickly calculates the histogram by sampling four rows from the image. This proved to be + // more robust on the blackbox tests than sampling a diagonal as we used to do. + // self.initArrays(width); + let mut localBuckets = self.buckets.clone(); + for y in 1..5 { + // for (int y = 1; y < 5; y++) { + let row = height * y / 5; + let localLuminances = source.getRow(row, &self.luminances); + let right = (width * 4) / 5; + let mut x = width / 5; + while x < right { + // for (int x = width / 5; x < right; x++) { + let pixel = localLuminances[x]; + localBuckets[(pixel >> GlobalHistogramBinarizer::LUMINANCE_SHIFT) as usize] += 1; + x += 1; + } + } + let blackPoint = self.estimateBlackPoint(&localBuckets)?; + + // We delay reading the entire image luminance until the black point estimation succeeds. + // Although we end up reading four rows twice, it is consistent with our motto of + // "fail quickly" which is necessary for continuous scanning. + let localLuminances = source.getMatrix(); + for y in 0..height { + // for (int y = 0; y < height; y++) { + let offset = y * width; + for x in 0..width { + // for (int x = 0; x < width; x++) { + let pixel = localLuminances[offset + x] & 0xff; + if (pixel as u32) < blackPoint { + matrix.set(x as u32, y as u32); + } + } + } + + Ok(matrix) + } + + fn createBinarizer(&self, source: Box) -> Box { + return Box::new(GlobalHistogramBinarizer::new(source)); + } + + fn getWidth(&self) -> usize { + self.width + } + + fn getHeight(&self) -> usize { + self.height + } +} + +impl GlobalHistogramBinarizer { + const LUMINANCE_BITS: usize = 5; + const LUMINANCE_SHIFT: usize = 8 - GlobalHistogramBinarizer::LUMINANCE_BITS; + const LUMINANCE_BUCKETS: usize = 1 << GlobalHistogramBinarizer::LUMINANCE_BITS; + const EMPTY: [u8; 0] = [0; 0]; + + pub fn new(source: Box) -> Self { + Self { + luminances: vec![0; source.getWidth()], + buckets: vec![0; GlobalHistogramBinarizer::LUMINANCE_BUCKETS], + width: source.getWidth(), + height: source.getHeight(), + source: source, + } + } + + // fn initArrays(&mut self, luminanceSize: usize) { + // // if self.luminances.len() < luminanceSize { + // // self.luminances = ; + // // } + // // // for x in 0..GlobalHistogramBinarizer::LUMINANCE_BUCKETS { + // // // for (int x = 0; x < LUMINANCE_BUCKETS; x++) { + // // self.buckets[x] = 0; + // // } + // } + + fn estimateBlackPoint(&self, buckets: &[u32]) -> Result { + // Find the tallest peak in the histogram. + let numBuckets = buckets.len(); + let mut maxBucketCount = 0; + let mut firstPeak = 0; + let mut firstPeakSize = 0; + for x in 0..numBuckets { + // for (int x = 0; x < numBuckets; x++) { + if buckets[x] > firstPeakSize { + firstPeak = x; + firstPeakSize = buckets[x]; + } + if buckets[x] > maxBucketCount { + maxBucketCount = buckets[x]; + } + } + + // Find the second-tallest peak which is somewhat far from the tallest peak. + let mut secondPeak = 0; + let mut secondPeakScore = 0; + for x in 0..numBuckets { + // for (int x = 0; x < numBuckets; x++) { + let distanceToBiggest = x - firstPeak; + // Encourage more distant second peaks by multiplying by square of distance. + let score = buckets[x] * distanceToBiggest as u32 * distanceToBiggest as u32; + if score > secondPeakScore { + secondPeak = x; + secondPeakScore = score; + } + } + + // Make sure firstPeak corresponds to the black peak. + if firstPeak > secondPeak { + let temp = firstPeak; + firstPeak = secondPeak; + secondPeak = temp; + } + + // If there is too little contrast in the image to pick a meaningful black point, throw rather + // than waste time trying to decode the image, and risk false positives. + if secondPeak - firstPeak <= numBuckets / 16 { + return Err(Exceptions::NotFoundException( + "secondPeak - firstPeak <= numBuckets / 16 ".to_owned(), + )); + } + + // Find a valley between them that is low and closer to the white peak. + let mut bestValley = secondPeak - 1; + let mut bestValleyScore = -1i32; + let mut x = secondPeak; + while x > firstPeak { + // for (int x = secondPeak - 1; x > firstPeak; x--) { + let fromFirst = x - firstPeak; + let score = + fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]) as usize; + if score as i32 > bestValleyScore { + bestValley = x; + bestValleyScore = score as i32; + } + x -= 1; + } + + Ok((bestValley as u32) << GlobalHistogramBinarizer::LUMINANCE_SHIFT) + } +} diff --git a/src/lib.rs b/src/lib.rs index 714fc30..54f096f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1023,7 +1023,7 @@ pub trait Binarizer { //private final LuminanceSource source; //fn new(source:dyn LuminanceSource) -> Self; - fn getLuminanceSource(&self) -> &dyn LuminanceSource; + fn getLuminanceSource(&self) -> &Box; /** * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return @@ -1039,7 +1039,7 @@ pub trait Binarizer { * @return The array of bits for this row (true means black). * @throws NotFoundException if row can't be binarized */ - fn getBlackRow(&self, y: usize, row: BitArray) -> Result; + fn getBlackRow(&self, y: usize, row: &mut BitArray) -> Result; /** * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive @@ -1129,7 +1129,7 @@ impl BinaryBitmap { * @return The array of bits for this row (true means black). * @throws NotFoundException if row can't be binarized */ - pub fn getBlackRow(&self, y: usize, row: BitArray) -> Result { + pub fn getBlackRow(&self, y: usize, row: &mut BitArray) -> Result { return self.binarizer.getBlackRow(y, row); }