GlobalHistogramBinarizer ported

This commit is contained in:
Henry Schimke
2022-09-02 13:55:58 -05:00
parent b2e0fd74e9
commit 1b5ef1e51b
3 changed files with 266 additions and 213 deletions

View File

@@ -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;
}
}

View File

@@ -8,8 +8,10 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use crate::Binarizer;
use crate::DecodeHintType; use crate::DecodeHintType;
use crate::Exceptions; use crate::Exceptions;
use crate::LuminanceSource;
use crate::RXingResultPoint; use crate::RXingResultPoint;
use encoding::Encoding; use encoding::Encoding;
@@ -3286,7 +3288,11 @@ impl MinimalECIInput {
fn addEdge(edges: &mut Vec<Vec<Option<Rc<InputEdge>>>>, to: usize, edge: Rc<InputEdge>) { fn addEdge(edges: &mut Vec<Vec<Option<Rc<InputEdge>>>>, to: usize, edge: Rc<InputEdge>) {
if edges[to][edge.encoderIndex].is_none() 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()); edges[to][edge.encoderIndex] = Some(edge.clone());
} }
@@ -3313,7 +3319,7 @@ impl MinimalECIInput {
for i in start..end { for i in start..end {
// for (int i = start; i < end; i++) { // 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( Self::addEdge(
edges, edges,
from + 1, from + 1,
@@ -3364,14 +3370,18 @@ impl MinimalECIInput {
if minimalJ < 0 { if minimalJ < 0 {
panic!("Internal error: failed to encode \"{}\"", stringToEncode); panic!("Internal error: failed to encode \"{}\"", stringToEncode);
} }
let mut intsAL:Vec<u16> = Vec::new(); let mut intsAL: Vec<u16> = Vec::new();
let mut current = edges[inputLength][minimalJ as usize].clone(); let mut current = edges[inputLength][minimalJ as usize].clone();
while current.is_some() { while current.is_some() {
let c = current.unwrap().clone(); let c = current.unwrap().clone();
if c.isFNC1() { if c.isFNC1() {
intsAL.splice(0..0, [1000]); intsAL.splice(0..0, [1000]);
} else { } else {
let bytes:Vec<u16> = encoderSet.encode_char(c.c as u8 as char, c.encoderIndex).iter().map(|x| *x as u16).collect(); let bytes: Vec<u16> = 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; let mut i = bytes.len() as i32 - 1;
while i >= 0 { while i >= 0 {
// for (int i = bytes.length - 1; i >= 0; i--) { // for (int i = bytes.length - 1; i >= 0; i--) {
@@ -3385,7 +3395,10 @@ impl MinimalECIInput {
c.previous.clone().unwrap().encoderIndex c.previous.clone().unwrap().encoderIndex
}; };
if previousEncoderIndex != c.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(); current = c.previous.clone();
} }
@@ -3412,7 +3425,7 @@ impl InputEdge {
previous: Option<Rc<InputEdge>>, previous: Option<Rc<InputEdge>>,
fnc1: u16, fnc1: u16,
) -> Self { ) -> Self {
let mut size = if c == 1000 { let mut size = if c == 1000 {
1 1
} else { } else {
encoderSet.encode_char(c as u8 as char, encoderIndex).len() encoderSet.encode_char(c as u8 as char, encoderIndex).len()
@@ -3426,7 +3439,7 @@ impl InputEdge {
size += prev.cachedTotalSize; size += prev.cachedTotalSize;
Self { Self {
c: if c as u16== fnc1 { 1000 } else { c as u16 }, c: if c as u16 == fnc1 { 1000 } else { c as u16 },
encoderIndex, encoderIndex,
previous: Some(prev.clone()), previous: Some(prev.clone()),
cachedTotalSize: size, cachedTotalSize: size,
@@ -3499,3 +3512,246 @@ impl fmt::Display for MinimalECIInput {
write!(f, "{}", result) 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<u8>,
buckets: Vec<u32>,
width: usize,
height: usize,
source: Box<dyn LuminanceSource>,
}
impl Binarizer for GlobalHistogramBinarizer {
fn getLuminanceSource(&self) -> &Box<dyn LuminanceSource> {
&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<BitArray, Exceptions> {
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<BitMatrix, Exceptions> {
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<dyn crate::LuminanceSource>) -> Box<dyn Binarizer> {
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<dyn LuminanceSource>) -> 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<u32, Exceptions> {
// 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)
}
}

View File

@@ -1023,7 +1023,7 @@ pub trait Binarizer {
//private final LuminanceSource source; //private final LuminanceSource source;
//fn new(source:dyn LuminanceSource) -> Self; //fn new(source:dyn LuminanceSource) -> Self;
fn getLuminanceSource(&self) -> &dyn LuminanceSource; fn getLuminanceSource(&self) -> &Box<dyn LuminanceSource>;
/** /**
* Converts one row of luminance data to 1 bit data. May actually do the conversion, or return * 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). * @return The array of bits for this row (true means black).
* @throws NotFoundException if row can't be binarized * @throws NotFoundException if row can't be binarized
*/ */
fn getBlackRow(&self, y: usize, row: BitArray) -> Result<BitArray, Exceptions>; fn getBlackRow(&self, y: usize, row: &mut BitArray) -> Result<BitArray, Exceptions>;
/** /**
* Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive * 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). * @return The array of bits for this row (true means black).
* @throws NotFoundException if row can't be binarized * @throws NotFoundException if row can't be binarized
*/ */
pub fn getBlackRow(&self, y: usize, row: BitArray) -> Result<BitArray, Exceptions> { pub fn getBlackRow(&self, y: usize, row: &mut BitArray) -> Result<BitArray, Exceptions> {
return self.binarizer.getBlackRow(y, row); return self.binarizer.getBlackRow(y, row);
} }