From b4b55ebb6614f42d901443eb27b5f7155839dc26 Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Tue, 16 Jan 2024 12:25:44 -0600 Subject: [PATCH 1/6] wip: add dxo edge barcode (inop) --- src/barcode_format.rs | 2 + src/common/cpp_essentials/pattern.rs | 12 +- src/common/cpp_essentials/util.rs | 39 +++ src/oned/cpp/dxfilm_edge_reader.rs | 341 +++++++++++++++++++++++ src/oned/cpp/mod.rs | 4 + src/oned/cpp/one_d_reader.rs | 403 +++++++++++++++++++++++++++ src/oned/cpp/row_reader.rs | 254 +++++++++++++++++ src/oned/mod.rs | 2 + 8 files changed, 1055 insertions(+), 2 deletions(-) create mode 100644 src/oned/cpp/dxfilm_edge_reader.rs create mode 100644 src/oned/cpp/mod.rs create mode 100644 src/oned/cpp/one_d_reader.rs create mode 100644 src/oned/cpp/row_reader.rs diff --git a/src/barcode_format.rs b/src/barcode_format.rs index 70fd962..5ca636c 100644 --- a/src/barcode_format.rs +++ b/src/barcode_format.rs @@ -87,6 +87,8 @@ pub enum BarcodeFormat { /** UPC/EAN extension format. Not a stand-alone format. */ UPC_EAN_EXTENSION, + DXFilmEdge, + /// UNSUPORTED_FORMAT, } diff --git a/src/common/cpp_essentials/pattern.rs b/src/common/cpp_essentials/pattern.rs index 2ec70bc..7fc7bb5 100644 --- a/src/common/cpp_essentials/pattern.rs +++ b/src/common/cpp_essentials/pattern.rs @@ -47,6 +47,10 @@ impl PatternRow { pub fn into_pattern_view(&self) -> PatternView { PatternView::new(self) } + + pub fn sum(&self) -> PatternType { + self.0.iter().sum() + } } impl IntoIterator for PatternRow { @@ -363,12 +367,16 @@ impl<'a> From<&PatternView<'a>> for &'a [PatternType] { * * The operator[](int) can be used in combination with a PatternView */ -#[derive(Default)] -struct BarAndSpace { +#[derive(Default, Clone)] +pub struct BarAndSpace { bar: T, space: T, } impl BarAndSpace { + pub fn new(bar: T, space: T) -> BarAndSpace { + Self { bar, space } + } + #[allow(dead_code)] pub fn isValid(&self) -> bool { self.bar != T::default() && self.space != T::default() diff --git a/src/common/cpp_essentials/util.rs b/src/common/cpp_essentials/util.rs index 97ddde1..970ecf5 100644 --- a/src/common/cpp_essentials/util.rs +++ b/src/common/cpp_essentials/util.rs @@ -1,4 +1,8 @@ +use std::iter::Sum; +use std::ops::Shl; + use crate::common::Result; +use crate::qrcode::cpp_port::detector::AppendBit; use crate::{Exceptions, Point}; use super::{Direction, RegressionLineTrait}; @@ -66,3 +70,38 @@ pub fn ToString>(val: T, len: usize) -> Result { Ok(result.iter().collect()) } + +pub fn ToInt(a: &[u32]) -> Option { + if a.iter().sum::() <= 32 { + return None; + } + // assert(Reduce(a) <= 32); + + let mut pattern = 0; + for (i, element) in a.iter().copied().enumerate() { + // for (int i = 0; i < Size(a); i++) + pattern = (pattern << element) | !(0xffffffff << element) * (!i & 1) as u32; + } + + return Some(pattern); +} + +pub fn ToIntPos( + bits: &[u8], + pos: usize, /* = 0 */ + count: usize, /* = 8 * sizeof(T)*/ +) -> Option { + // assert(0 <= count && count <= 8 * (int)sizeof(T)); + // assert(0 <= pos && pos + count <= bits.size()); + + let count = std::cmp::min(count as usize, bits.len()); + let mut res = 0; + for bit in bits.iter().skip(pos).take(count) { + AppendBit(&mut res, bit == &0); + } + // let it = bits.iterAt(pos); + // for (int i = 0; i < count; ++i, ++it) + // {AppendBit(res, *it);} + + return Some(res as u32); +} diff --git a/src/oned/cpp/dxfilm_edge_reader.rs b/src/oned/cpp/dxfilm_edge_reader.rs new file mode 100644 index 0000000..39d07b8 --- /dev/null +++ b/src/oned/cpp/dxfilm_edge_reader.rs @@ -0,0 +1,341 @@ +/* + * Copyright 2023 Antoine Mérino + * Copyright 2023 Axel Waggershauser + */ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + common::cpp_essentials::{ + FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToInt, ToIntPos, + }, + point, point_i, BarcodeFormat, DecodeHintValue, DecodingHintDictionary, Exceptions, PointI, + RXingResult, +}; + +use super::row_reader::{DecodingState, RowReader}; + +use crate::common::Result; + +// Detection is made from center outward. +// We ensure the clock track is decoded before the data track to avoid false positives. +// They are two version of a DX Edge codes : with and without frame number. +// The clock track is longer if the DX code contains the frame number (more recent version) +const CLOCK_LENGTH_FN: usize = 31; +const CLOCK_LENGTH_NO_FN: usize = 23; + +// data track length, without the start and stop patterns +const DATA_LENGTH_FN: u32 = 23; +const DATA_LENGTH_NO_FN: u32 = 15; + +const CLOCK_PATTERN_FN: FixedPattern<25, CLOCK_LENGTH_FN> = FixedPattern::new([ + 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, +]); +const CLOCK_PATTERN_NO_FN: FixedPattern<17, CLOCK_LENGTH_NO_FN> = + FixedPattern::new([5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3]); +const DATA_START_PATTERN: FixedPattern<5, 5> = FixedPattern::new([1, 1, 1, 1, 1]); +const DATA_STOP_PATTERN: FixedPattern<3, 3> = FixedPattern::new([1, 1, 1]); + +pub struct DXFilmEdgeReader<'a> { + options: &'a DecodingHintDictionary, +} + +fn IsPattern( + view: &PatternView, + pattern: &FixedPattern, + minQuietZone: f32, +) -> bool { + const E2E: bool = false; + let view = view.subView(0, Some(N)); + view.isValid() + && crate::common::cpp_essentials::pattern::IsPattern::( + &view, + pattern, + Some(if view.isAtFirstBar() { + u32::MAX as f32 + } else { + view[-1] as f32 + }), + minQuietZone, + 0.0, + ) != 0.0 +} + +fn DistIsBelowThreshold(a: PointI, b: PointI, threshold: PointI) -> bool { + (a.x - b.x).abs() < threshold.x && (a.y - b.y).abs() < threshold.y +} + +// DX Film Edge clock track found on 35mm films. +#[derive(Debug)] +pub(super) struct Clock { + hasFrameNr: bool, // = false; // Clock track (thus data track) with frame number (longer version) + rowNumber: u32, // = 0, + xStart: u32, // = 0; // Beginning of the clock track on the X-axis, in pixels + xStop: u32, // = 0; // End of the clock track on the X-axis, in pixels +} + +impl Default for Clock { + fn default() -> Self { + Self { + hasFrameNr: false, + rowNumber: 0, + xStart: 0, + xStop: 0, + } + } +} + +impl Clock { + pub const fn dataLength(&self) -> u32 { + if self.hasFrameNr { + DATA_LENGTH_FN + } else { + DATA_LENGTH_NO_FN + } + } + + pub fn moduleSize(&self) -> f32 { + (self.xStop as f32 - self.xStart as f32) + / (if self.hasFrameNr { + CLOCK_LENGTH_FN + } else { + CLOCK_LENGTH_NO_FN + }) as f32 + } + + pub fn isCloseTo(&self, p: PointI, x: u32) -> bool { + return DistIsBelowThreshold( + p, + point(x as i32, self.rowNumber as i32), + (self.moduleSize() * point(0.5, 4.0)).into(), + ); + } + + pub fn isCloseToStart(&self, x: u32, y: u32) -> bool { + return self.isCloseTo(point(x as i32, y as i32), self.xStart); + } + pub fn isCloseToStop(&self, x: u32, y: u32) -> bool { + return self.isCloseTo(point(x as i32, y as i32), self.xStop); + } +} + +impl DecodingState { + // see if we a clock that starts near {x, y} + pub fn findClock(&mut self, x: u32, y: u32) -> Option<&mut Clock> { + let start = point(x, y); + if let Some(i) = self + .clocks + .iter() + .position(|c| c.isCloseToStart(start.x, start.y)) + { + self.clocks.get_mut(i) //self.clocks[i] + } else { + None + } + // let i = FindIf(clocks, [start = PointI{x, y}](auto& v) { return v.isCloseToStart(start.x, start.y); }); + // return if i != clocks.end() {&(*i)} else {nullptr}; + } + + // add/update clock + pub fn addClock(&mut self, clock: Clock) { + if let Some(clockf) = self.findClock(clock.xStart, clock.rowNumber) { + *clockf = clock + } else { + self.clocks.push(clock) + } + // if (Clock* i = findClock(clock.xStart, clock.rowNumber)) + // {*i = clock;} + // else + // {clocks.push_back(clock);} + } +} + +fn CheckForClock(rowNumber: u32, view: &PatternView) -> Option { + let mut clock = Clock::default(); + + if (IsPattern(view, &CLOCK_PATTERN_FN, 0.5)) + // On FN versions, the decimal number can be really close to the clock + { + clock.hasFrameNr = true; + } else if (IsPattern(view, &CLOCK_PATTERN_NO_FN, 2.0)) { + clock.hasFrameNr = false; + } else { + return None; + } + + clock.rowNumber = rowNumber; + clock.xStart = view.pixelsInFront() as u32; + clock.xStop = view.pixelsTillEnd() as u32; + + return Some(clock); +} + +impl<'a> RowReader for DXFilmEdgeReader<'_> { + fn decodePattern( + &self, + rowNumber: u32, + next: &mut PatternView, + state: &mut Option, + ) -> Result { + // if (!state) { + // state.reset(new DXFEState); + // static_cast(state.get())->centerRow = rowNumber; + // } + + if state.is_none() { + *state = Some(DecodingState::default()) + }; + + let dxState = state.as_mut().unwrap(); + + // Only consider rows below the center row of the image + + if (!matches!( + self.options.get(&crate::DecodeHintType::TRY_HARDER), + Some(DecodeHintValue::TryHarder(true)) + ) && rowNumber < dxState.centerRow) + { + return Err(Exceptions::NOT_FOUND); + } + + // Look for a pattern that is part of both the clock as well as the data track (ommitting the first bar) + let Is4x1 = |view: &PatternView, spaceInPixel: Option| { + let spaceInPixel = spaceInPixel.unwrap_or_default(); + // find min/max of 4 consecutive bars/spaces and make sure they are close together + let tmp_arr: [u16; 4] = [view[1], view[2], view[3], view[4]]; + let m = *tmp_arr.iter().min().unwrap_or(&0); + let M = *tmp_arr.iter().max().unwrap_or(&0); + // let [m, M] = std::minmax({view[1], view[2], view[3], view[4]}); + return M <= m * 4 / 3 + 1 && spaceInPixel > m as f32 / 2.0; + }; + + // 12 is the minimum size of the data track (at least one product class bit + one parity bit) + *next = FindLeftGuardBy::<12, _>(*next, 10, Is4x1)?; // THIS IS WRONG WRONG WRONG ISSUE + // next = FindLeftGuard<4>(next, 10, Is4x1); + if (!next.isValid()) { + return Err(Exceptions::NOT_FOUND); + } + + // Check if the 4x1 pattern is part of a clock track + if let Some(clock) = CheckForClock(rowNumber, &next) { + dxState.addClock(clock); + next.skipSymbol(); + return Err(Exceptions::NOT_FOUND); + } + // if (auto clock = CheckForClock(rowNumber, next)) { + // dxState->addClock(*clock); + // next.skipSymbol(); + // return {}; + // } + + // Without at least one clock track, we stop here + if (dxState.clocks.is_empty()) { + return Err(Exceptions::NOT_FOUND); + } + + let minDataQuietZone: f32 = 0.5; + + if (!IsPattern(&next, &DATA_START_PATTERN, minDataQuietZone)) { + return Err(Exceptions::NOT_FOUND); + } + + let xStart = next.pixelsInFront(); + + // Only consider data tracks that are next to a clock track + let Some(clock) = dxState.findClock(xStart as u32, rowNumber) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Skip the data start pattern (black, white, black, white, black) + // The first signal bar is always white: this is the + // separation between the start pattern and the product number + next.skipSymbol(); + + // Read the data bits + let mut dataBits: Vec = Vec::default(); + while (next.isValidWithN(1) && dataBits.len() < clock.dataLength() as usize) { + let modules = (next[0] as f32 / clock.moduleSize() + 0.5) as u32; + // even index means we are at a bar, otherwise at a space + // dataBits.appendBits(if next.index() % 2 == 0 {0xFFFFFFFF} else {0x0}, modules); + for i in 0..modules { + dataBits.push((if next.index() % 2 == 0 { 0xFF } else { 0x0 } >> (i - 1)) & 1); + // should it be 0xFFFFFFFF + } + + next.shift(1); + } + + // Check the data track length + if (dataBits.len() != clock.dataLength() as usize) { + return Err(Exceptions::NOT_FOUND); + } + + *next = next.subView(0, Some(DATA_STOP_PATTERN.size())); + + // Check there is the Stop pattern at the end of the data track + if (!next.isValid() || !IsRightGuard(&next, &DATA_STOP_PATTERN, minDataQuietZone, 0.0)) { + return Err(Exceptions::NOT_FOUND); + } + + // The following bits are always white (=false), they are separators. + if (dataBits[0] != 0 + || dataBits[8] != 0 + || (if clock.hasFrameNr { + (dataBits[20] != 0 || dataBits[22] != 0) + } else { + dataBits[14] != 0 + })) + { + return Err(Exceptions::NOT_FOUND); + } + + // Check the parity bit + let signalSum = dataBits.iter().rev().skip(2).sum::(); //Reduce(dataBits.begin(), dataBits.end() - 2, 0); + let parityBit = *(dataBits.last().unwrap_or(&0)); + if (signalSum % 2 != parityBit) { + return Err(Exceptions::NOT_FOUND); + } + + // Compute the DX 1 number (product number) + let Some(productNumber) = ToIntPos(&dataBits, 1, 7) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Compute the DX 2 number (generation number) + let Some(generationNumber) = ToIntPos(&dataBits, 9, 4) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Generate the textual representation. + // Eg: 115-10/11A means: DX1 = 115, DX2 = 10, Frame number = 11A + let mut txt = String::with_capacity(10); + // txt.reserve(10); + txt = (productNumber.to_string()) + "-" + (&generationNumber.to_string()); + if (clock.hasFrameNr) { + let frameNr = ToIntPos(&dataBits, 13, 6).unwrap_or(0); + txt += &("/".to_owned() + &(frameNr.to_string())); + if (dataBits[19] != 0) { + txt += "A"; + } + } + + let xStop = next.pixelsTillEnd(); + + // The found data track must end near the clock track + if (!clock.isCloseToStop(xStop as u32, rowNumber)) { + return Err(Exceptions::NOT_FOUND); + } + + // Update the clock coordinates with the latest corresponding data track + // This may improve signal detection for next row iterations + clock.xStart = xStart as u32; + clock.xStop = xStop as u32; + + Ok(RXingResult::new( + &txt, + dataBits, + Vec::new(), + BarcodeFormat::DXFilmEdge, + )) + // return RXingResult(txt, rowNumber, xStart, xStop, BarcodeFormat::DXFilmEdge, {}); + } +} diff --git a/src/oned/cpp/mod.rs b/src/oned/cpp/mod.rs new file mode 100644 index 0000000..807f6d6 --- /dev/null +++ b/src/oned/cpp/mod.rs @@ -0,0 +1,4 @@ +mod dxfilm_edge_reader; +mod one_d_reader; +mod row_reader; +pub use one_d_reader::ODReader; diff --git a/src/oned/cpp/one_d_reader.rs b/src/oned/cpp/one_d_reader.rs new file mode 100644 index 0000000..da9fa2d --- /dev/null +++ b/src/oned/cpp/one_d_reader.rs @@ -0,0 +1,403 @@ +/* +* Copyright 2016 Nu-book Inc. +* Copyright 2016 ZXing authors +* Copyright 2020 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; +use std::collections::HashMap; + +use crate::common::cpp_essentials::{PatternRow, PatternView}; +use crate::Binarizer; +use crate::{multi::MultipleBarcodeReader, RXingResult, Reader}; +use crate::{ + point, BarcodeFormat, BinaryBitmap, DecodingHintDictionary, Exceptions, PointT, ResultPoint, +}; + +use crate::common::Result; + +use super::dxfilm_edge_reader::DXFilmEdgeReader; +use super::row_reader::RowReader; + +pub struct ODReader<'a> { + reader: DXFilmEdgeReader<'a>, // THIS IS WRONG, SEE BELOW ONLY DOES ONE + // readers: Vec, + try_harder: bool, + is_pure: bool, + min_line_count: u32, + return_errors: bool, + try_rotate: bool, +} + +impl<'a> ODReader<'_> { + /** + * We're going to examine rows from the middle outward, searching alternately above and below the + * middle, and farther out each time. rowStep is the number of rows between each successive + * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then + * middle + rowStep, then middle - (2 * rowStep), etc. + * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily + * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the + * image if "trying harder". + */ + pub fn DoDecode( + reader: &DXFilmEdgeReader, + image: &BinaryBitmap, + tryHarder: bool, + rotate: bool, + isPure: bool, + maxSymbols: u32, + minLineCount: u32, + returnErrors: bool, + ) -> Vec { + let res: Vec = Vec::new(); + + let decodingState = Vec::new(); + // std::vector> decodingState(readers.size()); + + let width: i32 = image.get_width() as i32; + let height: i32 = image.get_height() as i32; + + if (rotate) { + std::mem::swap(&mut width, &mut height); + } + + let middle: i32 = height / 2; + // TODO: find a better heuristic/parameterization if maxSymbols != 1 + let rowStep: i32 = std::cmp::max( + 1, + height + / (if (tryHarder && !isPure) { + (if maxSymbols == 1 { 256 } else { 512 }) + } else { + 32 + }), + ); + let maxLines: i32 = if tryHarder +{height} else // Look at the whole image, not just the center +{15}; // 15 rows spaced 1/32 apart is roughly the middle half of the image + + if (isPure) { + minLineCount = 1; + } + let checkRows = Vec::new(); + + let bars: PatternRow = PatternRow::new(vec![0; 128]); // e.g. EAN-13 has 59 bars/spaces + // bars.reserve(128); // e.g. EAN-13 has 59 bars/spaces + + // #ifdef PRINT_DEBUG + // BitMatrix dbg(width, height); + // #endif + + 'outer: for i in 0..maxLines { + // for (int i = 0; i < maxLines; i++) { + + // Scanning from the middle out. Determine which row we're looking at next: + let rowStepsAboveOrBelow: i32 = (i + 1) / 2; + let isAbove: bool = (i & 0x01) == 0; // i.e. is x even? + let rowNumber: i32 = middle + + rowStep + * (if isAbove { + rowStepsAboveOrBelow + } else { + -rowStepsAboveOrBelow + }); + let isCheckRow: bool = false; + if (rowNumber < 0 || rowNumber >= height) { + // Oops, if we run off the top or bottom, stop + break; + } + + // See if we have additional check rows (see below) to process + if (!checkRows.is_empty()) { + --i; + rowNumber = checkRows.back(); + checkRows.pop_back(); + isCheckRow = true; + if (rowNumber < 0 || rowNumber >= height) { + continue; + } + } + + if (!image.getPatternRow(rowNumber, if rotate { 90 } else { 0 }, bars)) { + continue; + } + + // #ifdef PRINT_DEBUG + // bool val = false; + // int x = 0; + // for (auto b : bars) { + // for(int j = 0; j < b; ++j) + // dbg.set(x++, rowNumber, val); + // val = !val; + // } + // #endif + + // While we have the image data in a PatternRow, it's fairly cheap to reverse it in place to + // handle decoding upside down barcodes. + // TODO: the DataBarExpanded (stacked) decoder depends on seeing each line from both directions. This + // 'surprising' and inconsistent. It also requires the decoderState to be shared between normal and reversed + // scans, which makes no sense in general because it would mix partial detection data from two codes of the same + // type next to each other. See also https://github.com/zxing-cpp/zxing-cpp/issues/87 + for upsideDown in [false, true] { + // for (bool upsideDown : {false, true}) { + // trying again? + if (upsideDown) { + // reverse the row and continue + // std::reverse(bars.begin(), bars.end()); + bars.reverse(); + } + let readers = vec![reader]; + // Look for a barcode + for r in 0..readers.len() { + // for (size_t r = 0; r < readers.size(); ++r) { + // If this is a pure symbol, then checking a single non-empty line is sufficient for all but the stacked + // DataBar codes. They are the only ones using the decodingState, which we can use as a flag here. + if (isPure && i && !decodingState[r]) { + continue; + } + + let next = PatternView::from(bars); + loop { + let result = readers[r] + .decodePattern(rowNumber, &mut next, decodingState[r]) + .ok(); + if (result.isValid() || (returnErrors && result.error())) { + IncrementLineCount(&result); + if (upsideDown) { + // update position (flip horizontally). + let points = result.position(); + for p in points { + // for (auto& p : points) { + p = point(width - p.getX() - 1, p.getY()); + } + result.addPoints(points); + // result.setPosition(std::move(points)); + } + if (rotate) { + let points = result.position(); + for p in points { + // for (auto& p : points) { + p = point(p.getY(), width - p.getX() - 1); + } + result.addPoints(points); + // result.setPosition(std::move(points)); + } + + // check if we know this code already + for other in res { + // for (auto& other : res) { + if (result == other) { + // merge the position information + let dTop = PointT::maxAbsComponent( + other.position().topLeft() - result.position().topLeft(), + ); + let dBot = PointT::maxAbsComponent( + other.position().bottomLeft() - result.position().topLeft(), + ); + let points = other.position(); + if (dTop < dBot + || (dTop == dBot + && rotate + ^ (PointT::sumAbsComponent(points[0]) + > PointT::sumAbsComponent( + result.position()[0], + )))) + { + points[0] = result.position()[0]; + points[1] = result.position()[1]; + } else { + points[2] = result.position()[2]; + points[3] = result.position()[3]; + } + other.setPosition(points); + IncrementLineCount(&other); + // clear the result, so we don't insert it again below + result = None; //Result(); + break; + } + } + + if (result.format() != BarcodeFormat::UNSUPORTED_FORMAT) { + res.push(result); + // res.push_back(std::move(result)); + + // if we found a valid code we have not seen before but a minLineCount > 1, + // add additional check rows above and below the current one + if (!isCheckRow && minLineCount > 1 && rowStep > 1) { + checkRows = vec![rowNumber - 1, rowNumber + 1]; + if (rowStep > 2) { + checkRows.push(rowNumber - 2); + checkRows.push(rowNumber + 2); + // checkRows.insert(checkRows.end(), {rowNumber - 2, rowNumber + 2}); + } + } + } + + if (maxSymbols + && res.iter().fold(0, |acc, e| { + acc + i32::from((r.lineCount() >= minLineCount)) + }) == maxSymbols) + { + break 'outer; + } + } + // make sure we make progress and we start the next try on a bar + next.shift(2 - (next.index() % 2)); + next.extend(); + if !(tryHarder && next.size()) { + break; + } + } //while (tryHarder && next.size()); + } + } + } + + // out: + // remove all symbols with insufficient line count + let it = res.iter().filter(|e| e.lineCount() < minLineCount); + // let it = std::remove_if(res.begin(), res.end(), [&](auto&& r) { return r.lineCount() < minLineCount; }); + res.erase(it, res.end()); + + // if symbols overlap, remove the one with a lower line count + for (i, a) in res.iter().enumerate() { + // for (auto a = res.begin(); a != res.end(); ++a){ + for b in res.iter().skip(i) { + // for (auto b = std::next(a); b != res.end(); ++b){ + if (PointT::HaveIntersectingBoundingBoxes(a.position(), b.position())) { + *(if a.lineCount() < b.lineCount() { a } else { b }) = None; + } + } + } + + //TODO: C++20 res.erase_if() + it = res + .iter() + .filter(|r| r.getBarcodeFormat() == BarcodeFormat::None); + // it = std::remove_if(res.begin(), res.end(), [](auto&& r) { return r.format() == BarcodeFormat::None; }); + res.erase(it, res.end()); + + // #ifdef PRINT_DEBUG + // SaveAsPBM(dbg, rotate ? "od-log-r.pnm" : "od-log.pnm"); + // #endif + + res + } +} + +impl<'a> ODReader<'_> { + pub fn decode_single( + &self, + hints: &DecodingHintDictionary, + image: &BinaryBitmap, + ) -> Result { + let result = Self::DoDecode( + &self.reader, + image, + self.try_harder, + false, + self.is_pure, + 1, + self.min_line_count, + self.return_errors, + ); + + if (result.is_empty() && self.try_rotate) { + result = Self::DoDecode( + &self.reader, + image, + self.try_harder, + true, + self.is_pure, + 1, + self.min_line_count, + self.return_errors, + ); + } + + result.first().ok_or(Exceptions::NOT_FOUND) + // return FirstOrDefault(std::move(result)); + } + + pub fn decode_with_max_symbols( + &self, + hints: &DecodingHintDictionary, + image: &BinaryBitmap, + maxSymbols: u32, + ) -> Result> { + let resH = Self::DoDecode( + &self.reader, + image, + self.try_harder, + false, + self.is_pure, + maxSymbols, + self.min_line_count, + self.return_errors, + ); + if ((!maxSymbols || (resH) < maxSymbols) && self.try_rotate) { + let resV = Self::DoDecode( + &self.reader, + image, + self.try_harder, + true, + self.is_pure, + maxSymbols - resH.len() as u32, + self.min_line_count, + self.return_errors, + ); + // resH.insert(resH.end(), resV.begin(), resV.end()); + resH.append(&mut resV); + } + if resH.is_empty() { + Err(Exceptions::NOT_FOUND) + } else { + Ok(resH) + } + } +} + +impl<'a> Reader for ODReader<'_> { + fn decode( + &mut self, + image: &mut crate::BinaryBitmap, + ) -> crate::common::Result { + self.decode_with_hints(image, &HashMap::new()) + } + + fn decode_with_hints( + &mut self, + image: &mut crate::BinaryBitmap, + hints: &crate::DecodingHintDictionary, + ) -> crate::common::Result { + self.decode_single(hints, image) + } +} + +impl<'a> MultipleBarcodeReader for ODReader<'_> { + fn decode_multiple( + &mut self, + image: &mut crate::BinaryBitmap, + ) -> crate::common::Result> { + self.decode_multiple_with_hints(image, &HashMap::new()) + } + + fn decode_multiple_with_hints( + &mut self, + image: &mut crate::BinaryBitmap, + hints: &crate::DecodingHintDictionary, + ) -> crate::common::Result> { + self.decode_with_max_symbols(hints, image, u32::MAX) + } +} + +impl<'a> ODReader<'_> { + pub fn new(hints: &DecodingHintDictionary) -> Self { + unimplemented!() + } +} + +fn IncrementLineCount(r: &RXingResult) { + unimplemented!() + // ++r._lineCount; +} diff --git a/src/oned/cpp/row_reader.rs b/src/oned/cpp/row_reader.rs new file mode 100644 index 0000000..69260c3 --- /dev/null +++ b/src/oned/cpp/row_reader.rs @@ -0,0 +1,254 @@ +use crate::common::cpp_essentials::{ + BarAndSpace, GetPatternRow, NormalizedPattern, PatternRow, PatternType, ToInt, UpdateMinMax, +}; +use crate::common::Result; +use crate::qrcode::cpp_port::detector::AppendBit; +use crate::{common::cpp_essentials::PatternView, RXingResult}; + +use super::dxfilm_edge_reader::Clock; + +/* +* Copyright 2016 Nu-book Inc. +* Copyright 2016 ZXing authors +* Copyright 2020 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +/* +Code39 : 1:2/3, 5+4+1 (0x3|2x1 wide) -> 12-15 mods, v1-? | ToNarrowWide(OMG 1) == * +Codabar: 1:2/3, 4+3+1 (1x1|1x2|3x0 wide) -> 9-13 mods, v1-? | ToNarrowWide(OMG 2) == ABCD +ITF : 1:2/3, 5+5 (2x2 wide) -> mods, v6-?| .5, .38 == * | qz:10 + +Code93 : 1-4, 3+3 -> 9 mods v1-? | round to 1-4 == * +Code128: 1-4, 3+3 -> 11 mods v1-? | .7, .25 == ABC | qz:10 +UPC/EAN: 1-4, 2+2 -> 7 mods f | .7, .48 == * + UPC-A: 11d 95m = 3 + 6*4 + 5 + 6*4 + 3 = 59 | qz:3 + EAN-13: 12d 95m + UPC-E: 6d, 3 + 6*4 + 6 = 33 + EAN-8: 8d, 3 + 4*4 + 5 + 4*4 + 3 = 43 + +RSS14 : 1-8, finder: (15,2+3), symbol: (15/16,4+4) | .45, .2 (finder only), 14d + code = 2xguard + 2xfinder + 4xsymbol = (96,23), stacked = 2x50 mods +RSSExp.: v?-74d/?-41c +*/ + +type Pattern = PatternRow; +type Counter = PatternRow; +type Index = Vec; +type Alphabet = Vec; + +// pub trait DecodingState: Default { +// // virtual ~DecodingState() = default; +// } + +#[derive(Default, Debug)] +pub struct DecodingState { + // DXO + pub centerRow: u32, + pub clocks: Vec, +} + +/** +* Encapsulates functionality and implementation that is common to all families +* of one-dimensional barcodes. +*/ +pub trait RowReader { + // type DS: DecodingState; + + fn decodePattern( + &self, + rowNumber: u32, + next: &mut PatternView, + state: &mut Option, + ) -> Result; + + /** + * Determines how closely a set of observed counts of runs of black/white values matches a given + * target pattern. This is reported as the ratio of the total variance from the expected pattern + * proportions across all pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to total pattern size + */ + fn PatternMatchVariance( + counters: &Counter, + pattern: &Pattern, + length: usize, + maxIndividualVariance: f32, + ) -> f32 { + let mut maxIndividualVariance = maxIndividualVariance; + + let total: PatternType = counters.sum(); //counters.into_iter().take(length).copied().reduce(|acc,e| {acc + e} ).unwrap_or_default().into(); //Reduce(counters, counters + length, 0); + let patternLength: PatternType = pattern.sum(); //pattern.into().take(length).copied().reduce(|acc, e| {acc + e}).unwrap_or_default().into(); //Reduce(pattern, pattern + length, 0); + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this is too small + // to reliably match, so fail: + return f32::MAX; + // return std::numeric_limits::max(); + } + + let unitBarWidth: f32 = total as f32 / patternLength as f32; + maxIndividualVariance *= unitBarWidth; + + let mut totalVariance: f32 = 0.0; + for x in 0..length { + // for (size_t x = 0; x < length; ++x) { + let variance: f32 = (counters[x] as f32 - pattern[x] as f32 * unitBarWidth).abs(); + if (variance > maxIndividualVariance) { + return f32::MAX; + } + totalVariance += variance; + } + return totalVariance / total as f32; + } + + fn PatternMatchVarianceNoLength( + counters: &Counter, + pattern: &Pattern, + maxIndividualVariance: f32, + ) -> f32 { + assert!(counters.len() == pattern.len()); + return Self::PatternMatchVariance( + counters, + pattern, + counters.len(), + maxIndividualVariance, + ); + } + + /** + * Attempts to decode a sequence of black/white lines into single + * digit. + * + * @param counters the counts of runs of observed black/white/black/... values + * @param patterns the list of patterns to compare the contents of counters to + * @param requireUnambiguousMatch the 'best match' must be better than all other matches + * @return The decoded digit index, -1 if no pattern matched + */ + + fn DecodeDigit( + counters: &Counter, + patterns: Vec, + maxAvgVariance: f32, + maxIndividualVariance: f32, + requireUnambiguousMatch: Option, + ) -> i32 { + let requireUnambiguousMatch = requireUnambiguousMatch.unwrap_or(true); + let mut bestVariance: f32 = maxAvgVariance; // worst variance we'll accept + const INVALID_MATCH: i32 = -1; + let mut bestMatch = INVALID_MATCH; + for i in 0..patterns.len() { + // for (int i = 0; i < Size(patterns); i++) { + let variance: f32 = + Self::PatternMatchVarianceNoLength(counters, &patterns[i], maxIndividualVariance); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = i as i32; + } else if (requireUnambiguousMatch && variance == bestVariance) { + // if we find a second 'best match' with the same variance, we can not reliably report to have a suitable match + bestMatch = INVALID_MATCH; + } + } + return bestMatch; + } + + /** + * @brief NarrowWideThreshold calculates width thresholds to separate narrow and wide bars and spaces. + * + * This is useful for codes like Codabar, Code39 and ITF which distinguish between narrow and wide + * bars/spaces. Where wide ones are between 2 and 3 times as wide as the narrow ones. + * + * @param view containing one character + * @return threshold value for bars and spaces + */ + fn NarrowWideThreshold(view: &PatternView) -> BarAndSpace { + let mut m: BarAndSpace = BarAndSpace::new(view[0] as i32, view[1] as i32); + let mut M: BarAndSpace = m.clone(); + for i in 0..view.size() { + // for (int i = 2; i < view.size(); ++i) + UpdateMinMax(&mut m[i], &mut M[i], view[i] as i32); + } + + let mut res = BarAndSpace::default(); + for i in 0..2 { + // for (int i = 0; i < 2; ++i) { + // check that + // a) wide <= 4 * narrow + // b) bars and spaces are not more than a factor of 2 (or 3 for the max) apart from each other + if (M[i] > 4 * (m[i] + 1) || M[i] > 3 * M[i + 1] || m[i] > 2 * (m[i + 1] + 1)) { + return BarAndSpace::default(); + } + // the threshold is the average of min and max but at least 1.5 * min + res[i] = std::cmp::max((m[i] + M[i]) / 2, m[i] * 3 / 2); + } + + return res; + } + + /** + * @brief ToNarrowWidePattern takes a PatternView, calculates a NarrowWideThreshold and returns int where a '0' bit + * means narrow and a '1' bit means 'wide'. + */ + fn NarrowWideBitPattern(view: &PatternView) -> i32 { + let threshold = Self::NarrowWideThreshold(view); + if (!threshold.isValid()) { + return -1; + } + + let mut pattern: i32 = 0; + for i in 0..view.size() { + // for (int i = 0; i < view.size(); ++i) { + if (view[i] as i32 > threshold[i] * 2) { + return -1; + } + AppendBit(&mut pattern, view[i] as i32 > threshold[i]); + } + + return pattern; + } + + /** + * @brief each bar/space is 1-4 modules wide, we have N bars/spaces, they are SUM modules wide in total + */ + fn OneToFourBitPattern(view: &PatternView) -> Option { + // TODO: make sure none of the elements in the normalized pattern exceeds 4 + ToInt(&NormalizedPattern::(view).ok()?.map(|x| x as u32)) + // ToInt(NormalizedPattern::(view).unwrap_or_default()).unwrap_or(-1) + } + + /** + * @brief Lookup the pattern in the table and return the character in alphabet at the same index. + * @returns 0 if pattern is not found. Used to be -1 but that fails on systems where char is unsigned. + */ + + fn LookupBitPattern(pattern: u32, table: &Index, alphabet: &Alphabet) -> char { + if let Some(i) = table.iter().position(|e| *e == pattern) { + alphabet[i] + } else { + char::from(0) + } + // let i :i32 = IndexOf(table, pattern); + // return if i == -1 {0} else {alphabet[i]}; + } + + fn DecodeNarrowWidePattern(view: &PatternView, table: &Index, alphabet: &Alphabet) -> char { + return Self::LookupBitPattern(Self::NarrowWideBitPattern(view) as u32, table, alphabet); + } +} + +fn DecodeSingleRow(reader: &RR, range: &[Range]) -> Result +where + Range: Into + Copy + Default + From, + RR: RowReader, +{ + let mut row = PatternRow::default(); + GetPatternRow(range, &mut row); + let mut view = PatternView::new(&row); + + let state = DecodingState::default(); + + // std::unique_ptr state; + reader.decodePattern(0, &mut view, &mut Some(state)) +} diff --git a/src/oned/mod.rs b/src/oned/mod.rs index ed6d468..6588870 100644 --- a/src/oned/mod.rs +++ b/src/oned/mod.rs @@ -93,3 +93,5 @@ mod upc_e_writer; pub use upc_e_writer::*; mod telepen_common; + +pub mod cpp; From 5892f92e72c71cadd78b08219359bac133182fea Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Tue, 16 Jan 2024 12:25:44 -0600 Subject: [PATCH 2/6] wip: add dxo edge barcode (inop) --- src/barcode_format.rs | 2 + src/common/cpp_essentials/pattern.rs | 12 +- src/common/cpp_essentials/util.rs | 39 +++ src/oned/cpp/dxfilm_edge_reader.rs | 341 +++++++++++++++++++++++ src/oned/cpp/mod.rs | 4 + src/oned/cpp/one_d_reader.rs | 403 +++++++++++++++++++++++++++ src/oned/cpp/row_reader.rs | 254 +++++++++++++++++ src/oned/mod.rs | 2 + 8 files changed, 1055 insertions(+), 2 deletions(-) create mode 100644 src/oned/cpp/dxfilm_edge_reader.rs create mode 100644 src/oned/cpp/mod.rs create mode 100644 src/oned/cpp/one_d_reader.rs create mode 100644 src/oned/cpp/row_reader.rs diff --git a/src/barcode_format.rs b/src/barcode_format.rs index 70fd962..5ca636c 100644 --- a/src/barcode_format.rs +++ b/src/barcode_format.rs @@ -87,6 +87,8 @@ pub enum BarcodeFormat { /** UPC/EAN extension format. Not a stand-alone format. */ UPC_EAN_EXTENSION, + DXFilmEdge, + /// UNSUPORTED_FORMAT, } diff --git a/src/common/cpp_essentials/pattern.rs b/src/common/cpp_essentials/pattern.rs index 2ec70bc..7fc7bb5 100644 --- a/src/common/cpp_essentials/pattern.rs +++ b/src/common/cpp_essentials/pattern.rs @@ -47,6 +47,10 @@ impl PatternRow { pub fn into_pattern_view(&self) -> PatternView { PatternView::new(self) } + + pub fn sum(&self) -> PatternType { + self.0.iter().sum() + } } impl IntoIterator for PatternRow { @@ -363,12 +367,16 @@ impl<'a> From<&PatternView<'a>> for &'a [PatternType] { * * The operator[](int) can be used in combination with a PatternView */ -#[derive(Default)] -struct BarAndSpace { +#[derive(Default, Clone)] +pub struct BarAndSpace { bar: T, space: T, } impl BarAndSpace { + pub fn new(bar: T, space: T) -> BarAndSpace { + Self { bar, space } + } + #[allow(dead_code)] pub fn isValid(&self) -> bool { self.bar != T::default() && self.space != T::default() diff --git a/src/common/cpp_essentials/util.rs b/src/common/cpp_essentials/util.rs index 97ddde1..970ecf5 100644 --- a/src/common/cpp_essentials/util.rs +++ b/src/common/cpp_essentials/util.rs @@ -1,4 +1,8 @@ +use std::iter::Sum; +use std::ops::Shl; + use crate::common::Result; +use crate::qrcode::cpp_port::detector::AppendBit; use crate::{Exceptions, Point}; use super::{Direction, RegressionLineTrait}; @@ -66,3 +70,38 @@ pub fn ToString>(val: T, len: usize) -> Result { Ok(result.iter().collect()) } + +pub fn ToInt(a: &[u32]) -> Option { + if a.iter().sum::() <= 32 { + return None; + } + // assert(Reduce(a) <= 32); + + let mut pattern = 0; + for (i, element) in a.iter().copied().enumerate() { + // for (int i = 0; i < Size(a); i++) + pattern = (pattern << element) | !(0xffffffff << element) * (!i & 1) as u32; + } + + return Some(pattern); +} + +pub fn ToIntPos( + bits: &[u8], + pos: usize, /* = 0 */ + count: usize, /* = 8 * sizeof(T)*/ +) -> Option { + // assert(0 <= count && count <= 8 * (int)sizeof(T)); + // assert(0 <= pos && pos + count <= bits.size()); + + let count = std::cmp::min(count as usize, bits.len()); + let mut res = 0; + for bit in bits.iter().skip(pos).take(count) { + AppendBit(&mut res, bit == &0); + } + // let it = bits.iterAt(pos); + // for (int i = 0; i < count; ++i, ++it) + // {AppendBit(res, *it);} + + return Some(res as u32); +} diff --git a/src/oned/cpp/dxfilm_edge_reader.rs b/src/oned/cpp/dxfilm_edge_reader.rs new file mode 100644 index 0000000..39d07b8 --- /dev/null +++ b/src/oned/cpp/dxfilm_edge_reader.rs @@ -0,0 +1,341 @@ +/* + * Copyright 2023 Antoine Mérino + * Copyright 2023 Axel Waggershauser + */ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + common::cpp_essentials::{ + FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToInt, ToIntPos, + }, + point, point_i, BarcodeFormat, DecodeHintValue, DecodingHintDictionary, Exceptions, PointI, + RXingResult, +}; + +use super::row_reader::{DecodingState, RowReader}; + +use crate::common::Result; + +// Detection is made from center outward. +// We ensure the clock track is decoded before the data track to avoid false positives. +// They are two version of a DX Edge codes : with and without frame number. +// The clock track is longer if the DX code contains the frame number (more recent version) +const CLOCK_LENGTH_FN: usize = 31; +const CLOCK_LENGTH_NO_FN: usize = 23; + +// data track length, without the start and stop patterns +const DATA_LENGTH_FN: u32 = 23; +const DATA_LENGTH_NO_FN: u32 = 15; + +const CLOCK_PATTERN_FN: FixedPattern<25, CLOCK_LENGTH_FN> = FixedPattern::new([ + 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, +]); +const CLOCK_PATTERN_NO_FN: FixedPattern<17, CLOCK_LENGTH_NO_FN> = + FixedPattern::new([5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3]); +const DATA_START_PATTERN: FixedPattern<5, 5> = FixedPattern::new([1, 1, 1, 1, 1]); +const DATA_STOP_PATTERN: FixedPattern<3, 3> = FixedPattern::new([1, 1, 1]); + +pub struct DXFilmEdgeReader<'a> { + options: &'a DecodingHintDictionary, +} + +fn IsPattern( + view: &PatternView, + pattern: &FixedPattern, + minQuietZone: f32, +) -> bool { + const E2E: bool = false; + let view = view.subView(0, Some(N)); + view.isValid() + && crate::common::cpp_essentials::pattern::IsPattern::( + &view, + pattern, + Some(if view.isAtFirstBar() { + u32::MAX as f32 + } else { + view[-1] as f32 + }), + minQuietZone, + 0.0, + ) != 0.0 +} + +fn DistIsBelowThreshold(a: PointI, b: PointI, threshold: PointI) -> bool { + (a.x - b.x).abs() < threshold.x && (a.y - b.y).abs() < threshold.y +} + +// DX Film Edge clock track found on 35mm films. +#[derive(Debug)] +pub(super) struct Clock { + hasFrameNr: bool, // = false; // Clock track (thus data track) with frame number (longer version) + rowNumber: u32, // = 0, + xStart: u32, // = 0; // Beginning of the clock track on the X-axis, in pixels + xStop: u32, // = 0; // End of the clock track on the X-axis, in pixels +} + +impl Default for Clock { + fn default() -> Self { + Self { + hasFrameNr: false, + rowNumber: 0, + xStart: 0, + xStop: 0, + } + } +} + +impl Clock { + pub const fn dataLength(&self) -> u32 { + if self.hasFrameNr { + DATA_LENGTH_FN + } else { + DATA_LENGTH_NO_FN + } + } + + pub fn moduleSize(&self) -> f32 { + (self.xStop as f32 - self.xStart as f32) + / (if self.hasFrameNr { + CLOCK_LENGTH_FN + } else { + CLOCK_LENGTH_NO_FN + }) as f32 + } + + pub fn isCloseTo(&self, p: PointI, x: u32) -> bool { + return DistIsBelowThreshold( + p, + point(x as i32, self.rowNumber as i32), + (self.moduleSize() * point(0.5, 4.0)).into(), + ); + } + + pub fn isCloseToStart(&self, x: u32, y: u32) -> bool { + return self.isCloseTo(point(x as i32, y as i32), self.xStart); + } + pub fn isCloseToStop(&self, x: u32, y: u32) -> bool { + return self.isCloseTo(point(x as i32, y as i32), self.xStop); + } +} + +impl DecodingState { + // see if we a clock that starts near {x, y} + pub fn findClock(&mut self, x: u32, y: u32) -> Option<&mut Clock> { + let start = point(x, y); + if let Some(i) = self + .clocks + .iter() + .position(|c| c.isCloseToStart(start.x, start.y)) + { + self.clocks.get_mut(i) //self.clocks[i] + } else { + None + } + // let i = FindIf(clocks, [start = PointI{x, y}](auto& v) { return v.isCloseToStart(start.x, start.y); }); + // return if i != clocks.end() {&(*i)} else {nullptr}; + } + + // add/update clock + pub fn addClock(&mut self, clock: Clock) { + if let Some(clockf) = self.findClock(clock.xStart, clock.rowNumber) { + *clockf = clock + } else { + self.clocks.push(clock) + } + // if (Clock* i = findClock(clock.xStart, clock.rowNumber)) + // {*i = clock;} + // else + // {clocks.push_back(clock);} + } +} + +fn CheckForClock(rowNumber: u32, view: &PatternView) -> Option { + let mut clock = Clock::default(); + + if (IsPattern(view, &CLOCK_PATTERN_FN, 0.5)) + // On FN versions, the decimal number can be really close to the clock + { + clock.hasFrameNr = true; + } else if (IsPattern(view, &CLOCK_PATTERN_NO_FN, 2.0)) { + clock.hasFrameNr = false; + } else { + return None; + } + + clock.rowNumber = rowNumber; + clock.xStart = view.pixelsInFront() as u32; + clock.xStop = view.pixelsTillEnd() as u32; + + return Some(clock); +} + +impl<'a> RowReader for DXFilmEdgeReader<'_> { + fn decodePattern( + &self, + rowNumber: u32, + next: &mut PatternView, + state: &mut Option, + ) -> Result { + // if (!state) { + // state.reset(new DXFEState); + // static_cast(state.get())->centerRow = rowNumber; + // } + + if state.is_none() { + *state = Some(DecodingState::default()) + }; + + let dxState = state.as_mut().unwrap(); + + // Only consider rows below the center row of the image + + if (!matches!( + self.options.get(&crate::DecodeHintType::TRY_HARDER), + Some(DecodeHintValue::TryHarder(true)) + ) && rowNumber < dxState.centerRow) + { + return Err(Exceptions::NOT_FOUND); + } + + // Look for a pattern that is part of both the clock as well as the data track (ommitting the first bar) + let Is4x1 = |view: &PatternView, spaceInPixel: Option| { + let spaceInPixel = spaceInPixel.unwrap_or_default(); + // find min/max of 4 consecutive bars/spaces and make sure they are close together + let tmp_arr: [u16; 4] = [view[1], view[2], view[3], view[4]]; + let m = *tmp_arr.iter().min().unwrap_or(&0); + let M = *tmp_arr.iter().max().unwrap_or(&0); + // let [m, M] = std::minmax({view[1], view[2], view[3], view[4]}); + return M <= m * 4 / 3 + 1 && spaceInPixel > m as f32 / 2.0; + }; + + // 12 is the minimum size of the data track (at least one product class bit + one parity bit) + *next = FindLeftGuardBy::<12, _>(*next, 10, Is4x1)?; // THIS IS WRONG WRONG WRONG ISSUE + // next = FindLeftGuard<4>(next, 10, Is4x1); + if (!next.isValid()) { + return Err(Exceptions::NOT_FOUND); + } + + // Check if the 4x1 pattern is part of a clock track + if let Some(clock) = CheckForClock(rowNumber, &next) { + dxState.addClock(clock); + next.skipSymbol(); + return Err(Exceptions::NOT_FOUND); + } + // if (auto clock = CheckForClock(rowNumber, next)) { + // dxState->addClock(*clock); + // next.skipSymbol(); + // return {}; + // } + + // Without at least one clock track, we stop here + if (dxState.clocks.is_empty()) { + return Err(Exceptions::NOT_FOUND); + } + + let minDataQuietZone: f32 = 0.5; + + if (!IsPattern(&next, &DATA_START_PATTERN, minDataQuietZone)) { + return Err(Exceptions::NOT_FOUND); + } + + let xStart = next.pixelsInFront(); + + // Only consider data tracks that are next to a clock track + let Some(clock) = dxState.findClock(xStart as u32, rowNumber) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Skip the data start pattern (black, white, black, white, black) + // The first signal bar is always white: this is the + // separation between the start pattern and the product number + next.skipSymbol(); + + // Read the data bits + let mut dataBits: Vec = Vec::default(); + while (next.isValidWithN(1) && dataBits.len() < clock.dataLength() as usize) { + let modules = (next[0] as f32 / clock.moduleSize() + 0.5) as u32; + // even index means we are at a bar, otherwise at a space + // dataBits.appendBits(if next.index() % 2 == 0 {0xFFFFFFFF} else {0x0}, modules); + for i in 0..modules { + dataBits.push((if next.index() % 2 == 0 { 0xFF } else { 0x0 } >> (i - 1)) & 1); + // should it be 0xFFFFFFFF + } + + next.shift(1); + } + + // Check the data track length + if (dataBits.len() != clock.dataLength() as usize) { + return Err(Exceptions::NOT_FOUND); + } + + *next = next.subView(0, Some(DATA_STOP_PATTERN.size())); + + // Check there is the Stop pattern at the end of the data track + if (!next.isValid() || !IsRightGuard(&next, &DATA_STOP_PATTERN, minDataQuietZone, 0.0)) { + return Err(Exceptions::NOT_FOUND); + } + + // The following bits are always white (=false), they are separators. + if (dataBits[0] != 0 + || dataBits[8] != 0 + || (if clock.hasFrameNr { + (dataBits[20] != 0 || dataBits[22] != 0) + } else { + dataBits[14] != 0 + })) + { + return Err(Exceptions::NOT_FOUND); + } + + // Check the parity bit + let signalSum = dataBits.iter().rev().skip(2).sum::(); //Reduce(dataBits.begin(), dataBits.end() - 2, 0); + let parityBit = *(dataBits.last().unwrap_or(&0)); + if (signalSum % 2 != parityBit) { + return Err(Exceptions::NOT_FOUND); + } + + // Compute the DX 1 number (product number) + let Some(productNumber) = ToIntPos(&dataBits, 1, 7) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Compute the DX 2 number (generation number) + let Some(generationNumber) = ToIntPos(&dataBits, 9, 4) else { + return Err(Exceptions::NOT_FOUND); + }; + + // Generate the textual representation. + // Eg: 115-10/11A means: DX1 = 115, DX2 = 10, Frame number = 11A + let mut txt = String::with_capacity(10); + // txt.reserve(10); + txt = (productNumber.to_string()) + "-" + (&generationNumber.to_string()); + if (clock.hasFrameNr) { + let frameNr = ToIntPos(&dataBits, 13, 6).unwrap_or(0); + txt += &("/".to_owned() + &(frameNr.to_string())); + if (dataBits[19] != 0) { + txt += "A"; + } + } + + let xStop = next.pixelsTillEnd(); + + // The found data track must end near the clock track + if (!clock.isCloseToStop(xStop as u32, rowNumber)) { + return Err(Exceptions::NOT_FOUND); + } + + // Update the clock coordinates with the latest corresponding data track + // This may improve signal detection for next row iterations + clock.xStart = xStart as u32; + clock.xStop = xStop as u32; + + Ok(RXingResult::new( + &txt, + dataBits, + Vec::new(), + BarcodeFormat::DXFilmEdge, + )) + // return RXingResult(txt, rowNumber, xStart, xStop, BarcodeFormat::DXFilmEdge, {}); + } +} diff --git a/src/oned/cpp/mod.rs b/src/oned/cpp/mod.rs new file mode 100644 index 0000000..807f6d6 --- /dev/null +++ b/src/oned/cpp/mod.rs @@ -0,0 +1,4 @@ +mod dxfilm_edge_reader; +mod one_d_reader; +mod row_reader; +pub use one_d_reader::ODReader; diff --git a/src/oned/cpp/one_d_reader.rs b/src/oned/cpp/one_d_reader.rs new file mode 100644 index 0000000..da9fa2d --- /dev/null +++ b/src/oned/cpp/one_d_reader.rs @@ -0,0 +1,403 @@ +/* +* Copyright 2016 Nu-book Inc. +* Copyright 2016 ZXing authors +* Copyright 2020 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; +use std::collections::HashMap; + +use crate::common::cpp_essentials::{PatternRow, PatternView}; +use crate::Binarizer; +use crate::{multi::MultipleBarcodeReader, RXingResult, Reader}; +use crate::{ + point, BarcodeFormat, BinaryBitmap, DecodingHintDictionary, Exceptions, PointT, ResultPoint, +}; + +use crate::common::Result; + +use super::dxfilm_edge_reader::DXFilmEdgeReader; +use super::row_reader::RowReader; + +pub struct ODReader<'a> { + reader: DXFilmEdgeReader<'a>, // THIS IS WRONG, SEE BELOW ONLY DOES ONE + // readers: Vec, + try_harder: bool, + is_pure: bool, + min_line_count: u32, + return_errors: bool, + try_rotate: bool, +} + +impl<'a> ODReader<'_> { + /** + * We're going to examine rows from the middle outward, searching alternately above and below the + * middle, and farther out each time. rowStep is the number of rows between each successive + * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then + * middle + rowStep, then middle - (2 * rowStep), etc. + * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily + * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the + * image if "trying harder". + */ + pub fn DoDecode( + reader: &DXFilmEdgeReader, + image: &BinaryBitmap, + tryHarder: bool, + rotate: bool, + isPure: bool, + maxSymbols: u32, + minLineCount: u32, + returnErrors: bool, + ) -> Vec { + let res: Vec = Vec::new(); + + let decodingState = Vec::new(); + // std::vector> decodingState(readers.size()); + + let width: i32 = image.get_width() as i32; + let height: i32 = image.get_height() as i32; + + if (rotate) { + std::mem::swap(&mut width, &mut height); + } + + let middle: i32 = height / 2; + // TODO: find a better heuristic/parameterization if maxSymbols != 1 + let rowStep: i32 = std::cmp::max( + 1, + height + / (if (tryHarder && !isPure) { + (if maxSymbols == 1 { 256 } else { 512 }) + } else { + 32 + }), + ); + let maxLines: i32 = if tryHarder +{height} else // Look at the whole image, not just the center +{15}; // 15 rows spaced 1/32 apart is roughly the middle half of the image + + if (isPure) { + minLineCount = 1; + } + let checkRows = Vec::new(); + + let bars: PatternRow = PatternRow::new(vec![0; 128]); // e.g. EAN-13 has 59 bars/spaces + // bars.reserve(128); // e.g. EAN-13 has 59 bars/spaces + + // #ifdef PRINT_DEBUG + // BitMatrix dbg(width, height); + // #endif + + 'outer: for i in 0..maxLines { + // for (int i = 0; i < maxLines; i++) { + + // Scanning from the middle out. Determine which row we're looking at next: + let rowStepsAboveOrBelow: i32 = (i + 1) / 2; + let isAbove: bool = (i & 0x01) == 0; // i.e. is x even? + let rowNumber: i32 = middle + + rowStep + * (if isAbove { + rowStepsAboveOrBelow + } else { + -rowStepsAboveOrBelow + }); + let isCheckRow: bool = false; + if (rowNumber < 0 || rowNumber >= height) { + // Oops, if we run off the top or bottom, stop + break; + } + + // See if we have additional check rows (see below) to process + if (!checkRows.is_empty()) { + --i; + rowNumber = checkRows.back(); + checkRows.pop_back(); + isCheckRow = true; + if (rowNumber < 0 || rowNumber >= height) { + continue; + } + } + + if (!image.getPatternRow(rowNumber, if rotate { 90 } else { 0 }, bars)) { + continue; + } + + // #ifdef PRINT_DEBUG + // bool val = false; + // int x = 0; + // for (auto b : bars) { + // for(int j = 0; j < b; ++j) + // dbg.set(x++, rowNumber, val); + // val = !val; + // } + // #endif + + // While we have the image data in a PatternRow, it's fairly cheap to reverse it in place to + // handle decoding upside down barcodes. + // TODO: the DataBarExpanded (stacked) decoder depends on seeing each line from both directions. This + // 'surprising' and inconsistent. It also requires the decoderState to be shared between normal and reversed + // scans, which makes no sense in general because it would mix partial detection data from two codes of the same + // type next to each other. See also https://github.com/zxing-cpp/zxing-cpp/issues/87 + for upsideDown in [false, true] { + // for (bool upsideDown : {false, true}) { + // trying again? + if (upsideDown) { + // reverse the row and continue + // std::reverse(bars.begin(), bars.end()); + bars.reverse(); + } + let readers = vec![reader]; + // Look for a barcode + for r in 0..readers.len() { + // for (size_t r = 0; r < readers.size(); ++r) { + // If this is a pure symbol, then checking a single non-empty line is sufficient for all but the stacked + // DataBar codes. They are the only ones using the decodingState, which we can use as a flag here. + if (isPure && i && !decodingState[r]) { + continue; + } + + let next = PatternView::from(bars); + loop { + let result = readers[r] + .decodePattern(rowNumber, &mut next, decodingState[r]) + .ok(); + if (result.isValid() || (returnErrors && result.error())) { + IncrementLineCount(&result); + if (upsideDown) { + // update position (flip horizontally). + let points = result.position(); + for p in points { + // for (auto& p : points) { + p = point(width - p.getX() - 1, p.getY()); + } + result.addPoints(points); + // result.setPosition(std::move(points)); + } + if (rotate) { + let points = result.position(); + for p in points { + // for (auto& p : points) { + p = point(p.getY(), width - p.getX() - 1); + } + result.addPoints(points); + // result.setPosition(std::move(points)); + } + + // check if we know this code already + for other in res { + // for (auto& other : res) { + if (result == other) { + // merge the position information + let dTop = PointT::maxAbsComponent( + other.position().topLeft() - result.position().topLeft(), + ); + let dBot = PointT::maxAbsComponent( + other.position().bottomLeft() - result.position().topLeft(), + ); + let points = other.position(); + if (dTop < dBot + || (dTop == dBot + && rotate + ^ (PointT::sumAbsComponent(points[0]) + > PointT::sumAbsComponent( + result.position()[0], + )))) + { + points[0] = result.position()[0]; + points[1] = result.position()[1]; + } else { + points[2] = result.position()[2]; + points[3] = result.position()[3]; + } + other.setPosition(points); + IncrementLineCount(&other); + // clear the result, so we don't insert it again below + result = None; //Result(); + break; + } + } + + if (result.format() != BarcodeFormat::UNSUPORTED_FORMAT) { + res.push(result); + // res.push_back(std::move(result)); + + // if we found a valid code we have not seen before but a minLineCount > 1, + // add additional check rows above and below the current one + if (!isCheckRow && minLineCount > 1 && rowStep > 1) { + checkRows = vec![rowNumber - 1, rowNumber + 1]; + if (rowStep > 2) { + checkRows.push(rowNumber - 2); + checkRows.push(rowNumber + 2); + // checkRows.insert(checkRows.end(), {rowNumber - 2, rowNumber + 2}); + } + } + } + + if (maxSymbols + && res.iter().fold(0, |acc, e| { + acc + i32::from((r.lineCount() >= minLineCount)) + }) == maxSymbols) + { + break 'outer; + } + } + // make sure we make progress and we start the next try on a bar + next.shift(2 - (next.index() % 2)); + next.extend(); + if !(tryHarder && next.size()) { + break; + } + } //while (tryHarder && next.size()); + } + } + } + + // out: + // remove all symbols with insufficient line count + let it = res.iter().filter(|e| e.lineCount() < minLineCount); + // let it = std::remove_if(res.begin(), res.end(), [&](auto&& r) { return r.lineCount() < minLineCount; }); + res.erase(it, res.end()); + + // if symbols overlap, remove the one with a lower line count + for (i, a) in res.iter().enumerate() { + // for (auto a = res.begin(); a != res.end(); ++a){ + for b in res.iter().skip(i) { + // for (auto b = std::next(a); b != res.end(); ++b){ + if (PointT::HaveIntersectingBoundingBoxes(a.position(), b.position())) { + *(if a.lineCount() < b.lineCount() { a } else { b }) = None; + } + } + } + + //TODO: C++20 res.erase_if() + it = res + .iter() + .filter(|r| r.getBarcodeFormat() == BarcodeFormat::None); + // it = std::remove_if(res.begin(), res.end(), [](auto&& r) { return r.format() == BarcodeFormat::None; }); + res.erase(it, res.end()); + + // #ifdef PRINT_DEBUG + // SaveAsPBM(dbg, rotate ? "od-log-r.pnm" : "od-log.pnm"); + // #endif + + res + } +} + +impl<'a> ODReader<'_> { + pub fn decode_single( + &self, + hints: &DecodingHintDictionary, + image: &BinaryBitmap, + ) -> Result { + let result = Self::DoDecode( + &self.reader, + image, + self.try_harder, + false, + self.is_pure, + 1, + self.min_line_count, + self.return_errors, + ); + + if (result.is_empty() && self.try_rotate) { + result = Self::DoDecode( + &self.reader, + image, + self.try_harder, + true, + self.is_pure, + 1, + self.min_line_count, + self.return_errors, + ); + } + + result.first().ok_or(Exceptions::NOT_FOUND) + // return FirstOrDefault(std::move(result)); + } + + pub fn decode_with_max_symbols( + &self, + hints: &DecodingHintDictionary, + image: &BinaryBitmap, + maxSymbols: u32, + ) -> Result> { + let resH = Self::DoDecode( + &self.reader, + image, + self.try_harder, + false, + self.is_pure, + maxSymbols, + self.min_line_count, + self.return_errors, + ); + if ((!maxSymbols || (resH) < maxSymbols) && self.try_rotate) { + let resV = Self::DoDecode( + &self.reader, + image, + self.try_harder, + true, + self.is_pure, + maxSymbols - resH.len() as u32, + self.min_line_count, + self.return_errors, + ); + // resH.insert(resH.end(), resV.begin(), resV.end()); + resH.append(&mut resV); + } + if resH.is_empty() { + Err(Exceptions::NOT_FOUND) + } else { + Ok(resH) + } + } +} + +impl<'a> Reader for ODReader<'_> { + fn decode( + &mut self, + image: &mut crate::BinaryBitmap, + ) -> crate::common::Result { + self.decode_with_hints(image, &HashMap::new()) + } + + fn decode_with_hints( + &mut self, + image: &mut crate::BinaryBitmap, + hints: &crate::DecodingHintDictionary, + ) -> crate::common::Result { + self.decode_single(hints, image) + } +} + +impl<'a> MultipleBarcodeReader for ODReader<'_> { + fn decode_multiple( + &mut self, + image: &mut crate::BinaryBitmap, + ) -> crate::common::Result> { + self.decode_multiple_with_hints(image, &HashMap::new()) + } + + fn decode_multiple_with_hints( + &mut self, + image: &mut crate::BinaryBitmap, + hints: &crate::DecodingHintDictionary, + ) -> crate::common::Result> { + self.decode_with_max_symbols(hints, image, u32::MAX) + } +} + +impl<'a> ODReader<'_> { + pub fn new(hints: &DecodingHintDictionary) -> Self { + unimplemented!() + } +} + +fn IncrementLineCount(r: &RXingResult) { + unimplemented!() + // ++r._lineCount; +} diff --git a/src/oned/cpp/row_reader.rs b/src/oned/cpp/row_reader.rs new file mode 100644 index 0000000..69260c3 --- /dev/null +++ b/src/oned/cpp/row_reader.rs @@ -0,0 +1,254 @@ +use crate::common::cpp_essentials::{ + BarAndSpace, GetPatternRow, NormalizedPattern, PatternRow, PatternType, ToInt, UpdateMinMax, +}; +use crate::common::Result; +use crate::qrcode::cpp_port::detector::AppendBit; +use crate::{common::cpp_essentials::PatternView, RXingResult}; + +use super::dxfilm_edge_reader::Clock; + +/* +* Copyright 2016 Nu-book Inc. +* Copyright 2016 ZXing authors +* Copyright 2020 Axel Waggershauser +*/ +// SPDX-License-Identifier: Apache-2.0 + +/* +Code39 : 1:2/3, 5+4+1 (0x3|2x1 wide) -> 12-15 mods, v1-? | ToNarrowWide(OMG 1) == * +Codabar: 1:2/3, 4+3+1 (1x1|1x2|3x0 wide) -> 9-13 mods, v1-? | ToNarrowWide(OMG 2) == ABCD +ITF : 1:2/3, 5+5 (2x2 wide) -> mods, v6-?| .5, .38 == * | qz:10 + +Code93 : 1-4, 3+3 -> 9 mods v1-? | round to 1-4 == * +Code128: 1-4, 3+3 -> 11 mods v1-? | .7, .25 == ABC | qz:10 +UPC/EAN: 1-4, 2+2 -> 7 mods f | .7, .48 == * + UPC-A: 11d 95m = 3 + 6*4 + 5 + 6*4 + 3 = 59 | qz:3 + EAN-13: 12d 95m + UPC-E: 6d, 3 + 6*4 + 6 = 33 + EAN-8: 8d, 3 + 4*4 + 5 + 4*4 + 3 = 43 + +RSS14 : 1-8, finder: (15,2+3), symbol: (15/16,4+4) | .45, .2 (finder only), 14d + code = 2xguard + 2xfinder + 4xsymbol = (96,23), stacked = 2x50 mods +RSSExp.: v?-74d/?-41c +*/ + +type Pattern = PatternRow; +type Counter = PatternRow; +type Index = Vec; +type Alphabet = Vec; + +// pub trait DecodingState: Default { +// // virtual ~DecodingState() = default; +// } + +#[derive(Default, Debug)] +pub struct DecodingState { + // DXO + pub centerRow: u32, + pub clocks: Vec, +} + +/** +* Encapsulates functionality and implementation that is common to all families +* of one-dimensional barcodes. +*/ +pub trait RowReader { + // type DS: DecodingState; + + fn decodePattern( + &self, + rowNumber: u32, + next: &mut PatternView, + state: &mut Option, + ) -> Result; + + /** + * Determines how closely a set of observed counts of runs of black/white values matches a given + * target pattern. This is reported as the ratio of the total variance from the expected pattern + * proportions across all pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to total pattern size + */ + fn PatternMatchVariance( + counters: &Counter, + pattern: &Pattern, + length: usize, + maxIndividualVariance: f32, + ) -> f32 { + let mut maxIndividualVariance = maxIndividualVariance; + + let total: PatternType = counters.sum(); //counters.into_iter().take(length).copied().reduce(|acc,e| {acc + e} ).unwrap_or_default().into(); //Reduce(counters, counters + length, 0); + let patternLength: PatternType = pattern.sum(); //pattern.into().take(length).copied().reduce(|acc, e| {acc + e}).unwrap_or_default().into(); //Reduce(pattern, pattern + length, 0); + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this is too small + // to reliably match, so fail: + return f32::MAX; + // return std::numeric_limits::max(); + } + + let unitBarWidth: f32 = total as f32 / patternLength as f32; + maxIndividualVariance *= unitBarWidth; + + let mut totalVariance: f32 = 0.0; + for x in 0..length { + // for (size_t x = 0; x < length; ++x) { + let variance: f32 = (counters[x] as f32 - pattern[x] as f32 * unitBarWidth).abs(); + if (variance > maxIndividualVariance) { + return f32::MAX; + } + totalVariance += variance; + } + return totalVariance / total as f32; + } + + fn PatternMatchVarianceNoLength( + counters: &Counter, + pattern: &Pattern, + maxIndividualVariance: f32, + ) -> f32 { + assert!(counters.len() == pattern.len()); + return Self::PatternMatchVariance( + counters, + pattern, + counters.len(), + maxIndividualVariance, + ); + } + + /** + * Attempts to decode a sequence of black/white lines into single + * digit. + * + * @param counters the counts of runs of observed black/white/black/... values + * @param patterns the list of patterns to compare the contents of counters to + * @param requireUnambiguousMatch the 'best match' must be better than all other matches + * @return The decoded digit index, -1 if no pattern matched + */ + + fn DecodeDigit( + counters: &Counter, + patterns: Vec, + maxAvgVariance: f32, + maxIndividualVariance: f32, + requireUnambiguousMatch: Option, + ) -> i32 { + let requireUnambiguousMatch = requireUnambiguousMatch.unwrap_or(true); + let mut bestVariance: f32 = maxAvgVariance; // worst variance we'll accept + const INVALID_MATCH: i32 = -1; + let mut bestMatch = INVALID_MATCH; + for i in 0..patterns.len() { + // for (int i = 0; i < Size(patterns); i++) { + let variance: f32 = + Self::PatternMatchVarianceNoLength(counters, &patterns[i], maxIndividualVariance); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = i as i32; + } else if (requireUnambiguousMatch && variance == bestVariance) { + // if we find a second 'best match' with the same variance, we can not reliably report to have a suitable match + bestMatch = INVALID_MATCH; + } + } + return bestMatch; + } + + /** + * @brief NarrowWideThreshold calculates width thresholds to separate narrow and wide bars and spaces. + * + * This is useful for codes like Codabar, Code39 and ITF which distinguish between narrow and wide + * bars/spaces. Where wide ones are between 2 and 3 times as wide as the narrow ones. + * + * @param view containing one character + * @return threshold value for bars and spaces + */ + fn NarrowWideThreshold(view: &PatternView) -> BarAndSpace { + let mut m: BarAndSpace = BarAndSpace::new(view[0] as i32, view[1] as i32); + let mut M: BarAndSpace = m.clone(); + for i in 0..view.size() { + // for (int i = 2; i < view.size(); ++i) + UpdateMinMax(&mut m[i], &mut M[i], view[i] as i32); + } + + let mut res = BarAndSpace::default(); + for i in 0..2 { + // for (int i = 0; i < 2; ++i) { + // check that + // a) wide <= 4 * narrow + // b) bars and spaces are not more than a factor of 2 (or 3 for the max) apart from each other + if (M[i] > 4 * (m[i] + 1) || M[i] > 3 * M[i + 1] || m[i] > 2 * (m[i + 1] + 1)) { + return BarAndSpace::default(); + } + // the threshold is the average of min and max but at least 1.5 * min + res[i] = std::cmp::max((m[i] + M[i]) / 2, m[i] * 3 / 2); + } + + return res; + } + + /** + * @brief ToNarrowWidePattern takes a PatternView, calculates a NarrowWideThreshold and returns int where a '0' bit + * means narrow and a '1' bit means 'wide'. + */ + fn NarrowWideBitPattern(view: &PatternView) -> i32 { + let threshold = Self::NarrowWideThreshold(view); + if (!threshold.isValid()) { + return -1; + } + + let mut pattern: i32 = 0; + for i in 0..view.size() { + // for (int i = 0; i < view.size(); ++i) { + if (view[i] as i32 > threshold[i] * 2) { + return -1; + } + AppendBit(&mut pattern, view[i] as i32 > threshold[i]); + } + + return pattern; + } + + /** + * @brief each bar/space is 1-4 modules wide, we have N bars/spaces, they are SUM modules wide in total + */ + fn OneToFourBitPattern(view: &PatternView) -> Option { + // TODO: make sure none of the elements in the normalized pattern exceeds 4 + ToInt(&NormalizedPattern::(view).ok()?.map(|x| x as u32)) + // ToInt(NormalizedPattern::(view).unwrap_or_default()).unwrap_or(-1) + } + + /** + * @brief Lookup the pattern in the table and return the character in alphabet at the same index. + * @returns 0 if pattern is not found. Used to be -1 but that fails on systems where char is unsigned. + */ + + fn LookupBitPattern(pattern: u32, table: &Index, alphabet: &Alphabet) -> char { + if let Some(i) = table.iter().position(|e| *e == pattern) { + alphabet[i] + } else { + char::from(0) + } + // let i :i32 = IndexOf(table, pattern); + // return if i == -1 {0} else {alphabet[i]}; + } + + fn DecodeNarrowWidePattern(view: &PatternView, table: &Index, alphabet: &Alphabet) -> char { + return Self::LookupBitPattern(Self::NarrowWideBitPattern(view) as u32, table, alphabet); + } +} + +fn DecodeSingleRow(reader: &RR, range: &[Range]) -> Result +where + Range: Into + Copy + Default + From, + RR: RowReader, +{ + let mut row = PatternRow::default(); + GetPatternRow(range, &mut row); + let mut view = PatternView::new(&row); + + let state = DecodingState::default(); + + // std::unique_ptr state; + reader.decodePattern(0, &mut view, &mut Some(state)) +} diff --git a/src/oned/mod.rs b/src/oned/mod.rs index ed6d468..6588870 100644 --- a/src/oned/mod.rs +++ b/src/oned/mod.rs @@ -93,3 +93,5 @@ mod upc_e_writer; pub use upc_e_writer::*; mod telepen_common; + +pub mod cpp; From d222a98aad7aa47bcf2fa748752be66a54565742 Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Fri, 26 Jan 2024 11:11:50 -0600 Subject: [PATCH 3/6] wip: incomplete port checking --- src/binarizer.rs | 5 +- src/binary_bitmap.rs | 7 +- src/common/bit_array.rs | 22 ++ .../base_extentions/bitmatrix.rs | 1 + src/common/cpp_essentials/pattern.rs | 4 + src/common/global_histogram_binarizer.rs | 6 + src/common/hybrid_binarizer.rs | 4 + src/common/line_orientation.rs | 5 + src/common/mod.rs | 3 + src/common/quad.rs | 18 +- src/oned/cpp/dxfilm_edge_reader.rs | 49 +++-- src/oned/cpp/one_d_reader.rs | 194 +++++++++++------- src/rxing_result.rs | 17 +- 13 files changed, 236 insertions(+), 99 deletions(-) create mode 100644 src/common/line_orientation.rs diff --git a/src/binarizer.rs b/src/binarizer.rs index 8b424b3..513e646 100644 --- a/src/binarizer.rs +++ b/src/binarizer.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use crate::{ - common::{BitArray, BitMatrix, Result}, + common::{BitArray, BitMatrix, LineOrientation, Result}, LuminanceSource, }; @@ -66,6 +66,9 @@ pub trait Binarizer { */ fn get_black_matrix(&self) -> Result<&BitMatrix>; + /// Get a row or column of the image + fn get_black_line(&self, l: usize, lt: LineOrientation) -> Result>; + /** * Creates a new object with the same type as this Binarizer implementation, but with pristine * state. This is needed because Binarizer implementations may be stateful, e.g. keeping a cache diff --git a/src/binary_bitmap.rs b/src/binary_bitmap.rs index f4882b6..056c72b 100644 --- a/src/binary_bitmap.rs +++ b/src/binary_bitmap.rs @@ -19,7 +19,7 @@ use std::{borrow::Cow, fmt}; use crate::{ - common::{BitArray, BitMatrix, Result}, + common::{BitArray, BitMatrix, LineOrientation, Result}, Binarizer, LuminanceSource, }; @@ -72,6 +72,11 @@ impl BinaryBitmap { self.binarizer.get_black_row(y) } + /// Get a row or column of the image + pub fn get_black_line(&self, l: usize, lt: LineOrientation) -> Result> { + self.binarizer.get_black_line(l, lt) + } + /** * Converts a 2D array of luminance data to 1 bit. As above, assume this method is expensive * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or diff --git a/src/common/bit_array.rs b/src/common/bit_array.rs index 79cb0e8..a6b8cf7 100644 --- a/src/common/bit_array.rs +++ b/src/common/bit_array.rs @@ -18,6 +18,7 @@ // import java.util.Arrays; +use std::ops::Index; use std::{cmp, fmt}; use crate::common::Result; @@ -420,6 +421,19 @@ impl From for Vec { impl From for Vec { fn from(value: BitArray) -> Self { + // let mut array = vec![false; value.size]; + + // for (pixel, element) in array.iter_mut().enumerate().take(value.size) { + // *element = value.get(pixel); + // } + + // array + Self::from(&value) + } +} + +impl From<&BitArray> for Vec { + fn from(value: &BitArray) -> Self { let mut array = vec![false; value.size]; for (pixel, element) in array.iter_mut().enumerate().take(value.size) { @@ -429,3 +443,11 @@ impl From for Vec { array } } + +impl Index for BitArray { + type Output = bool; + + fn index(&self, index: usize) -> &Self::Output { + &self.get(index) + } +} diff --git a/src/common/cpp_essentials/base_extentions/bitmatrix.rs b/src/common/cpp_essentials/base_extentions/bitmatrix.rs index 4839bcb..a5c19f8 100644 --- a/src/common/cpp_essentials/base_extentions/bitmatrix.rs +++ b/src/common/cpp_essentials/base_extentions/bitmatrix.rs @@ -1,3 +1,4 @@ +use crate::common::cpp_essentials::PatternRow; use crate::common::BitMatrix; use crate::common::Result; use crate::point_f; diff --git a/src/common/cpp_essentials/pattern.rs b/src/common/cpp_essentials/pattern.rs index 7fc7bb5..6e6b288 100644 --- a/src/common/cpp_essentials/pattern.rs +++ b/src/common/cpp_essentials/pattern.rs @@ -51,6 +51,10 @@ impl PatternRow { pub fn sum(&self) -> PatternType { self.0.iter().sum() } + + pub fn rev(&mut self) { + self.0.reverse() + } } impl IntoIterator for PatternRow { diff --git a/src/common/global_histogram_binarizer.rs b/src/common/global_histogram_binarizer.rs index fee8ad1..722f4ee 100644 --- a/src/common/global_histogram_binarizer.rs +++ b/src/common/global_histogram_binarizer.rs @@ -51,6 +51,7 @@ pub struct GlobalHistogramBinarizer { source: LS, black_matrix: OnceCell, black_row_cache: Vec>, + black_column_cache: Vec>, } impl Binarizer for GlobalHistogramBinarizer { @@ -106,6 +107,10 @@ impl Binarizer for GlobalHistogramBinarizer { Ok(Cow::Borrowed(row)) } + fn get_black_line(&self, l: usize, lt: super::LineOrientation) -> Result> { + unimplemented!() + } + // Does not sharpen the data, as this call is intended to only be used by 2D Readers. fn get_black_matrix(&self) -> Result<&BitMatrix> { let matrix = self @@ -137,6 +142,7 @@ impl GlobalHistogramBinarizer { height: source.get_height(), black_matrix: OnceCell::new(), black_row_cache: vec![OnceCell::default(); source.get_height()], + black_column_cache: vec![OnceCell::default(); source.get_width()], source, } } diff --git a/src/common/hybrid_binarizer.rs b/src/common/hybrid_binarizer.rs index 3fd4dbf..bcb1ff8 100644 --- a/src/common/hybrid_binarizer.rs +++ b/src/common/hybrid_binarizer.rs @@ -64,6 +64,10 @@ impl Binarizer for HybridBinarizer { self.ghb.get_black_row(y) } + fn get_black_line(&self, l: usize, lt: super::LineOrientation) -> Result> { + self.ghb.get_black_line(l, lt) + } + /** * Calculates the final BitMatrix once for all requests. This could be called once from the * constructor instead, but there are some advantages to doing it lazily, such as making diff --git a/src/common/line_orientation.rs b/src/common/line_orientation.rs new file mode 100644 index 0000000..8d23cfb --- /dev/null +++ b/src/common/line_orientation.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LineOrientation { + Row, + Column, +} diff --git a/src/common/mod.rs b/src/common/mod.rs index d011f1d..fe878d1 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -114,6 +114,9 @@ pub use quad::*; pub mod cpp_essentials; +mod line_orientation; +pub use line_orientation::LineOrientation; + #[cfg(feature = "otsu_level")] mod otsu_level_binarizer; #[cfg(feature = "otsu_level")] diff --git a/src/common/quad.rs b/src/common/quad.rs index 39dd1dc..27b2a2e 100644 --- a/src/common/quad.rs +++ b/src/common/quad.rs @@ -1,4 +1,4 @@ -use crate::{point_f, Point}; +use crate::{point_f, Exceptions, Point}; #[derive(Clone, Copy, Debug)] pub struct Quadrilateral(pub [Point; 4]); @@ -272,3 +272,19 @@ impl From<[Point; 4]> for Quadrilateral { Self(value) } } + +impl TryFrom<&Vec> for Quadrilateral { + type Error = Exceptions; + + fn try_from(value: &Vec) -> Result { + if value.len() == 4 { + Ok( + Self( + [value[0], value[1], value[2], value[3]] + ) + ) + }else{ + Err(Exceptions::INDEX_OUT_OF_BOUNDS) + } + } +} \ No newline at end of file diff --git a/src/oned/cpp/dxfilm_edge_reader.rs b/src/oned/cpp/dxfilm_edge_reader.rs index 39d07b8..632ac09 100644 --- a/src/oned/cpp/dxfilm_edge_reader.rs +++ b/src/oned/cpp/dxfilm_edge_reader.rs @@ -5,8 +5,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - common::cpp_essentials::{ - FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToInt, ToIntPos, + common::{ + cpp_essentials::{ + FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToInt, ToIntPos, + }, + BitArray, }, point, point_i, BarcodeFormat, DecodeHintValue, DecodingHintDictionary, Exceptions, PointI, RXingResult, @@ -251,13 +254,20 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { next.skipSymbol(); // Read the data bits - let mut dataBits: Vec = Vec::default(); - while (next.isValidWithN(1) && dataBits.len() < clock.dataLength() as usize) { + let mut dataBits = BitArray::default(); + while (next.isValidWithN(1) && dataBits.get_size() < clock.dataLength() as usize) { let modules = (next[0] as f32 / clock.moduleSize() + 0.5) as u32; // even index means we are at a bar, otherwise at a space // dataBits.appendBits(if next.index() % 2 == 0 {0xFFFFFFFF} else {0x0}, modules); for i in 0..modules { - dataBits.push((if next.index() % 2 == 0 { 0xFF } else { 0x0 } >> (i - 1)) & 1); + dataBits.appendBits( + if next.index() % 2 == 0 { + 0xFFFFFFFF + } else { + 0x0 + }, + modules as usize, + ); // should it be 0xFFFFFFFF } @@ -265,7 +275,7 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { } // Check the data track length - if (dataBits.len() != clock.dataLength() as usize) { + if (dataBits.get_size() != clock.dataLength() as usize) { return Err(Exceptions::NOT_FOUND); } @@ -277,31 +287,36 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { } // The following bits are always white (=false), they are separators. - if (dataBits[0] != 0 - || dataBits[8] != 0 + if (dataBits[0] != false //0 + || dataBits[8] != false //0 || (if clock.hasFrameNr { - (dataBits[20] != 0 || dataBits[22] != 0) + (dataBits[20] != false/*0*/ || dataBits[22] != false/*0*/) } else { - dataBits[14] != 0 + dataBits[14] != false//0 })) { return Err(Exceptions::NOT_FOUND); } // Check the parity bit - let signalSum = dataBits.iter().rev().skip(2).sum::(); //Reduce(dataBits.begin(), dataBits.end() - 2, 0); - let parityBit = *(dataBits.last().unwrap_or(&0)); + let db_hld = Into::>::into(dataBits); //.iter().rev().skip(2).fold(0, |acc, e| acc + u8::from(*e)); + let signalSum = db_hld + .iter() + .rev() + .skip(2) + .fold(0, |acc, e| acc + u8::from(*e)); //dataBits.iter().rev().skip(2).sum::(); //Reduce(dataBits.begin(), dataBits.end() - 2, 0); + let parityBit = u8::from(*(db_hld.last().unwrap_or(&false))); if (signalSum % 2 != parityBit) { return Err(Exceptions::NOT_FOUND); } // Compute the DX 1 number (product number) - let Some(productNumber) = ToIntPos(&dataBits, 1, 7) else { + let Some(productNumber) = ToIntPos(&Into::>::into(dataBits), 1, 7) else { return Err(Exceptions::NOT_FOUND); }; // Compute the DX 2 number (generation number) - let Some(generationNumber) = ToIntPos(&dataBits, 9, 4) else { + let Some(generationNumber) = ToIntPos(&Into::>::into(dataBits), 9, 4) else { return Err(Exceptions::NOT_FOUND); }; @@ -311,9 +326,9 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { // txt.reserve(10); txt = (productNumber.to_string()) + "-" + (&generationNumber.to_string()); if (clock.hasFrameNr) { - let frameNr = ToIntPos(&dataBits, 13, 6).unwrap_or(0); + let frameNr = ToIntPos(&Into::>::into(dataBits), 13, 6).unwrap_or(0); txt += &("/".to_owned() + &(frameNr.to_string())); - if (dataBits[19] != 0) { + if (dataBits[19] != false/*0*/) { txt += "A"; } } @@ -332,7 +347,7 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { Ok(RXingResult::new( &txt, - dataBits, + dataBits.into(), Vec::new(), BarcodeFormat::DXFilmEdge, )) diff --git a/src/oned/cpp/one_d_reader.rs b/src/oned/cpp/one_d_reader.rs index da9fa2d..6032056 100644 --- a/src/oned/cpp/one_d_reader.rs +++ b/src/oned/cpp/one_d_reader.rs @@ -8,14 +8,14 @@ use std::any::Any; use std::collections::HashMap; -use crate::common::cpp_essentials::{PatternRow, PatternView}; +use crate::common::cpp_essentials::{GetPatternRow, PatternRow, PatternView}; use crate::Binarizer; use crate::{multi::MultipleBarcodeReader, RXingResult, Reader}; use crate::{ point, BarcodeFormat, BinaryBitmap, DecodingHintDictionary, Exceptions, PointT, ResultPoint, }; -use crate::common::Result; +use crate::common::{LineOrientation, Quadrilateral, Result}; use super::dxfilm_edge_reader::DXFilmEdgeReader; use super::row_reader::RowReader; @@ -32,14 +32,14 @@ pub struct ODReader<'a> { impl<'a> ODReader<'_> { /** - * We're going to examine rows from the middle outward, searching alternately above and below the - * middle, and farther out each time. rowStep is the number of rows between each successive - * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then - * middle + rowStep, then middle - (2 * rowStep), etc. - * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily - * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the - * image if "trying harder". - */ + * We're going to examine rows from the middle outward, searching alternately above and below the + * middle, and farther out each time. rowStep is the number of rows between each successive + * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then + * middle + rowStep, then middle - (2 * rowStep), etc. + * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily + * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the + * image if "trying harder". + */ pub fn DoDecode( reader: &DXFilmEdgeReader, image: &BinaryBitmap, @@ -50,13 +50,15 @@ impl<'a> ODReader<'_> { minLineCount: u32, returnErrors: bool, ) -> Vec { - let res: Vec = Vec::new(); + let mut res: Vec> = Vec::new(); - let decodingState = Vec::new(); + let mut decodingState: Vec<&mut Option> = Vec::new(); // std::vector> decodingState(readers.size()); - let width: i32 = image.get_width() as i32; - let height: i32 = image.get_height() as i32; + let mut minLineCount = minLineCount; + + let mut width: i32 = image.get_width() as i32; + let mut height: i32 = image.get_height() as i32; if (rotate) { std::mem::swap(&mut width, &mut height); @@ -73,36 +75,39 @@ impl<'a> ODReader<'_> { 32 }), ); - let maxLines: i32 = if tryHarder -{height} else // Look at the whole image, not just the center -{15}; // 15 rows spaced 1/32 apart is roughly the middle half of the image + let maxLines: i32 = if tryHarder { + height // Look at the whole image, not just the center + } else { + 15 // 15 rows spaced 1/32 apart is roughly the middle half of the image + }; if (isPure) { minLineCount = 1; } - let checkRows = Vec::new(); + let mut checkRows = Vec::new(); - let bars: PatternRow = PatternRow::new(vec![0; 128]); // e.g. EAN-13 has 59 bars/spaces + let mut bars: PatternRow = PatternRow::new(vec![0; 128]); // e.g. EAN-13 has 59 bars/spaces // bars.reserve(128); // e.g. EAN-13 has 59 bars/spaces // #ifdef PRINT_DEBUG // BitMatrix dbg(width, height); // #endif - 'outer: for i in 0..maxLines { + let mut i = 0; + 'outer: while i < maxLines { // for (int i = 0; i < maxLines; i++) { // Scanning from the middle out. Determine which row we're looking at next: let rowStepsAboveOrBelow: i32 = (i + 1) / 2; let isAbove: bool = (i & 0x01) == 0; // i.e. is x even? - let rowNumber: i32 = middle + let mut rowNumber: i32 = middle + rowStep * (if isAbove { rowStepsAboveOrBelow } else { -rowStepsAboveOrBelow }); - let isCheckRow: bool = false; + let mut isCheckRow: bool = false; if (rowNumber < 0 || rowNumber >= height) { // Oops, if we run off the top or bottom, stop break; @@ -110,18 +115,36 @@ impl<'a> ODReader<'_> { // See if we have additional check rows (see below) to process if (!checkRows.is_empty()) { - --i; - rowNumber = checkRows.back(); - checkRows.pop_back(); + //--i; + i -= 1; + rowNumber = *checkRows.last().unwrap_or(&0); + checkRows.pop(); isCheckRow = true; if (rowNumber < 0 || rowNumber >= height) { continue; } } - if (!image.getPatternRow(rowNumber, if rotate { 90 } else { 0 }, bars)) { + let br: Vec = if let Ok(r) = image.get_black_line( + rowNumber as usize, + if rotate { + LineOrientation::Column + } else { + LineOrientation::Row + }, + ) { + r.as_ref().into() + } else { continue; - } + }; + // let img = if rotate {let a = image.rotate_counter_clockwise(); &a} else {image}; + // let Ok(black_row ) = img.get_black_row(rowNumber as usize) else {continue;}; + // let br : Vec = black_row.as_ref().into(); + GetPatternRow(&br, &mut bars); + + // if (!image.getPatternRow(rowNumber, if rotate { 90 } else { 0 }, bars)) { + // continue; + // } // #ifdef PRINT_DEBUG // bool val = false; @@ -145,7 +168,7 @@ impl<'a> ODReader<'_> { if (upsideDown) { // reverse the row and continue // std::reverse(bars.begin(), bars.end()); - bars.reverse(); + bars.rev(); } let readers = vec![reader]; // Look for a barcode @@ -153,73 +176,75 @@ impl<'a> ODReader<'_> { // for (size_t r = 0; r < readers.size(); ++r) { // If this is a pure symbol, then checking a single non-empty line is sufficient for all but the stacked // DataBar codes. They are the only ones using the decodingState, which we can use as a flag here. - if (isPure && i && !decodingState[r]) { + if (isPure && i > 0 && decodingState[r].is_none()) { continue; } - let next = PatternView::from(bars); + let mut next = PatternView::new(&bars); loop { - let result = readers[r] - .decodePattern(rowNumber, &mut next, decodingState[r]) + let mut result_hld = readers[r] + .decodePattern(rowNumber as u32, &mut next, decodingState[r]) .ok(); - if (result.isValid() || (returnErrors && result.error())) { - IncrementLineCount(&result); + if result_hld.is_some() /*|| (returnErrors && result.is_none())*/ { + let mut result = result_hld.as_mut().unwrap(); + IncrementLineCount(&mut result); if (upsideDown) { // update position (flip horizontally). - let points = result.position(); + let points = result.getPointsMut(); for p in points { // for (auto& p : points) { - p = point(width - p.getX() - 1, p.getY()); + *p = point(width as f32 - p.getX() - 1.0, p.getY()); } - result.addPoints(points); + // result.addPoints(points); // result.setPosition(std::move(points)); } if (rotate) { - let points = result.position(); + let points = result.getPointsMut(); for p in points { // for (auto& p : points) { - p = point(p.getY(), width - p.getX() - 1); + *p = point(p.getY(), width as f32- p.getX() - 1.0); } - result.addPoints(points); + // result.addPoints(points); // result.setPosition(std::move(points)); } // check if we know this code already - for other in res { + for other_hld in res.iter_mut() { + let Some(mut other) = other_hld else{ continue;}; // for (auto& other : res) { - if (result == other) { + if (result == &other) { // merge the position information let dTop = PointT::maxAbsComponent( - other.position().topLeft() - result.position().topLeft(), + other.getPoints()[0] - result.getPoints()[0], ); let dBot = PointT::maxAbsComponent( - other.position().bottomLeft() - result.position().topLeft(), + other.getPoints()[2] - result.getPoints()[0], ); - let points = other.position(); + let mut points = other.getPoints().clone(); if (dTop < dBot || (dTop == dBot && rotate ^ (PointT::sumAbsComponent(points[0]) > PointT::sumAbsComponent( - result.position()[0], + result.getPoints()[0], )))) { - points[0] = result.position()[0]; - points[1] = result.position()[1]; + points[0] = result.getPoints()[0]; + points[1] = result.getPoints()[1]; } else { - points[2] = result.position()[2]; - points[3] = result.position()[3]; + points[2] = result.getPoints()[2]; + points[3] = result.getPoints()[3]; } - other.setPosition(points); - IncrementLineCount(&other); + other.replace_points(points); + IncrementLineCount(&mut other); // clear the result, so we don't insert it again below - result = None; //Result(); + result_hld = None; //Result(); break; } } - if (result.format() != BarcodeFormat::UNSUPORTED_FORMAT) { - res.push(result); + if (result.getBarcodeFormat() != &BarcodeFormat::UNSUPORTED_FORMAT) { + res.push(Some(result.clone())); // res.push_back(std::move(result)); // if we found a valid code we have not seen before but a minLineCount > 1, @@ -234,10 +259,14 @@ impl<'a> ODReader<'_> { } } - if (maxSymbols + if (maxSymbols > 0 && res.iter().fold(0, |acc, e| { - acc + i32::from((r.lineCount() >= minLineCount)) - }) == maxSymbols) + if let Some(itm) = &res[r] { + acc + i32::from((itm.line_count() >= minLineCount as usize)) + }else { + acc + } + }) == maxSymbols as i32) { break 'outer; } @@ -245,43 +274,53 @@ impl<'a> ODReader<'_> { // make sure we make progress and we start the next try on a bar next.shift(2 - (next.index() % 2)); next.extend(); - if !(tryHarder && next.size()) { + if !(tryHarder && next.size() > 0) { break; } } //while (tryHarder && next.size()); } } + i += 1; } // out: // remove all symbols with insufficient line count - let it = res.iter().filter(|e| e.lineCount() < minLineCount); + res.retain(|e| if let Some(itm) = e {itm.line_count() < minLineCount as usize} else {false}); // let it = std::remove_if(res.begin(), res.end(), [&](auto&& r) { return r.lineCount() < minLineCount; }); - res.erase(it, res.end()); + // res.erase(it, res.end()); // if symbols overlap, remove the one with a lower line count - for (i, a) in res.iter().enumerate() { + for (i, a_hld) in res.iter().enumerate() { + let a = a_hld.as_ref().unwrap(); // for (auto a = res.begin(); a != res.end(); ++a){ - for b in res.iter().skip(i) { + for b_hld in res.iter().skip(i) { + let b = b_hld.as_ref().unwrap(); // for (auto b = std::next(a); b != res.end(); ++b){ - if (PointT::HaveIntersectingBoundingBoxes(a.position(), b.position())) { - *(if a.lineCount() < b.lineCount() { a } else { b }) = None; + let Ok(q1) =Quadrilateral::try_from(a.getPoints()) else {continue;}; + let Ok(q2) = Quadrilateral::try_from(b.getPoints()) else {continue;}; + if (Quadrilateral::have_intersecting_bounding_boxes(&q1, &q2)) { + *(if a.line_count() < b.line_count() { + a_hld + } else { + b_hld + }) = None; } } } //TODO: C++20 res.erase_if() - it = res - .iter() - .filter(|r| r.getBarcodeFormat() == BarcodeFormat::None); + res + + .retain(|r| if let Some(itm) = r { + itm.getBarcodeFormat() == &BarcodeFormat::UNSUPORTED_FORMAT } else { false }); // it = std::remove_if(res.begin(), res.end(), [](auto&& r) { return r.format() == BarcodeFormat::None; }); - res.erase(it, res.end()); + // res.erase(it, res.end()); // #ifdef PRINT_DEBUG // SaveAsPBM(dbg, rotate ? "od-log-r.pnm" : "od-log.pnm"); // #endif - res + res.iter().cloned().filter_map(|e| e).collect() } } @@ -291,7 +330,7 @@ impl<'a> ODReader<'_> { hints: &DecodingHintDictionary, image: &BinaryBitmap, ) -> Result { - let result = Self::DoDecode( + let mut result = Self::DoDecode( &self.reader, image, self.try_harder, @@ -315,7 +354,7 @@ impl<'a> ODReader<'_> { ); } - result.first().ok_or(Exceptions::NOT_FOUND) + result.first().cloned().ok_or(Exceptions::NOT_FOUND) // return FirstOrDefault(std::move(result)); } @@ -325,7 +364,7 @@ impl<'a> ODReader<'_> { image: &BinaryBitmap, maxSymbols: u32, ) -> Result> { - let resH = Self::DoDecode( + let mut resH = Self::DoDecode( &self.reader, image, self.try_harder, @@ -335,8 +374,8 @@ impl<'a> ODReader<'_> { self.min_line_count, self.return_errors, ); - if ((!maxSymbols || (resH) < maxSymbols) && self.try_rotate) { - let resV = Self::DoDecode( + if ((!(maxSymbols != 0) || (resH.len()) < maxSymbols as usize) && self.try_rotate) { + let mut resV = Self::DoDecode( &self.reader, image, self.try_harder, @@ -397,7 +436,6 @@ impl<'a> ODReader<'_> { } } -fn IncrementLineCount(r: &RXingResult) { - unimplemented!() - // ++r._lineCount; +fn IncrementLineCount(r: &mut RXingResult) { + r.set_line_count(r.line_count() + 1) } diff --git a/src/rxing_result.rs b/src/rxing_result.rs index f888935..a5bd78e 100644 --- a/src/rxing_result.rs +++ b/src/rxing_result.rs @@ -30,7 +30,7 @@ use serde::{Deserialize, Serialize}; * @author Sean Owen */ #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct RXingResult { text: String, rawBytes: Vec, @@ -39,6 +39,7 @@ pub struct RXingResult { format: BarcodeFormat, resultMetadata: HashMap, timestamp: u128, + line_count: usize, } impl RXingResult { pub fn new( @@ -83,6 +84,7 @@ impl RXingResult { format, resultMetadata: HashMap::new(), timestamp, + line_count: 0, } } @@ -95,6 +97,7 @@ impl RXingResult { format: prev.format, resultMetadata: prev.resultMetadata, timestamp: prev.timestamp, + line_count: prev.line_count, } } @@ -229,6 +232,18 @@ impl RXingResult { pub fn getTimestamp(&self) -> u128 { self.timestamp } + + pub fn line_count(&self) -> usize { + self.line_count + } + + pub fn set_line_count(&mut self, lc: usize) { + self.line_count = lc + } + + pub fn replace_points(&mut self, points: Vec) { + self.resultPoints = points + } } impl fmt::Display for RXingResult { From dc1a1505b1564092f17b21922627bf00b6dcce5b Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Fri, 26 Jan 2024 14:39:24 -0600 Subject: [PATCH 4/6] wip: inop! builds but does not work --- src/common/bit_array.rs | 14 ++-- src/common/quad.rs | 10 +-- src/oned/cpp/dxfilm_edge_reader.rs | 35 ++++---- src/oned/cpp/one_d_reader.rs | 123 +++++++++++++++++++++-------- 4 files changed, 118 insertions(+), 64 deletions(-) diff --git a/src/common/bit_array.rs b/src/common/bit_array.rs index a6b8cf7..5b96873 100644 --- a/src/common/bit_array.rs +++ b/src/common/bit_array.rs @@ -413,6 +413,12 @@ impl Default for BitArray { impl From for Vec { fn from(value: BitArray) -> Self { + (&value).into() + } +} + +impl From<&BitArray> for Vec { + fn from(value: &BitArray) -> Self { let mut array = vec![0; value.getSizeInBytes()]; value.toBytes(0, &mut array, 0, value.getSizeInBytes()); array @@ -443,11 +449,3 @@ impl From<&BitArray> for Vec { array } } - -impl Index for BitArray { - type Output = bool; - - fn index(&self, index: usize) -> &Self::Output { - &self.get(index) - } -} diff --git a/src/common/quad.rs b/src/common/quad.rs index 27b2a2e..a52fa97 100644 --- a/src/common/quad.rs +++ b/src/common/quad.rs @@ -278,13 +278,9 @@ impl TryFrom<&Vec> for Quadrilateral { fn try_from(value: &Vec) -> Result { if value.len() == 4 { - Ok( - Self( - [value[0], value[1], value[2], value[3]] - ) - ) - }else{ + Ok(Self([value[0], value[1], value[2], value[3]])) + } else { Err(Exceptions::INDEX_OUT_OF_BOUNDS) } } -} \ No newline at end of file +} diff --git a/src/oned/cpp/dxfilm_edge_reader.rs b/src/oned/cpp/dxfilm_edge_reader.rs index 632ac09..5734149 100644 --- a/src/oned/cpp/dxfilm_edge_reader.rs +++ b/src/oned/cpp/dxfilm_edge_reader.rs @@ -6,13 +6,10 @@ use crate::{ common::{ - cpp_essentials::{ - FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToInt, ToIntPos, - }, + cpp_essentials::{FindLeftGuardBy, FixedPattern, IsRightGuard, PatternView, ToIntPos}, BitArray, }, - point, point_i, BarcodeFormat, DecodeHintValue, DecodingHintDictionary, Exceptions, PointI, - RXingResult, + point, BarcodeFormat, DecodeHintValue, DecodingHintDictionary, Exceptions, PointI, RXingResult, }; use super::row_reader::{DecodingState, RowReader}; @@ -42,6 +39,12 @@ pub struct DXFilmEdgeReader<'a> { options: &'a DecodingHintDictionary, } +impl<'a> DXFilmEdgeReader<'_> { + pub fn new(hints: &'a DecodingHintDictionary) -> DXFilmEdgeReader<'a> { + DXFilmEdgeReader { options: hints } + } +} + fn IsPattern( view: &PatternView, pattern: &FixedPattern, @@ -259,7 +262,7 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { let modules = (next[0] as f32 / clock.moduleSize() + 0.5) as u32; // even index means we are at a bar, otherwise at a space // dataBits.appendBits(if next.index() % 2 == 0 {0xFFFFFFFF} else {0x0}, modules); - for i in 0..modules { + for _i in 0..modules { dataBits.appendBits( if next.index() % 2 == 0 { 0xFFFFFFFF @@ -267,7 +270,7 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { 0x0 }, modules as usize, - ); + )?; // should it be 0xFFFFFFFF } @@ -287,19 +290,19 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { } // The following bits are always white (=false), they are separators. - if (dataBits[0] != false //0 - || dataBits[8] != false //0 + if (dataBits.get(0) != false //0 + || dataBits.get(8) != false //0 || (if clock.hasFrameNr { - (dataBits[20] != false/*0*/ || dataBits[22] != false/*0*/) + (dataBits.get(20) != false/*0*/ || dataBits.get(22) != false/*0*/) } else { - dataBits[14] != false//0 + dataBits.get(14) != false//0 })) { return Err(Exceptions::NOT_FOUND); } // Check the parity bit - let db_hld = Into::>::into(dataBits); //.iter().rev().skip(2).fold(0, |acc, e| acc + u8::from(*e)); + let db_hld = Into::>::into(&dataBits); //.iter().rev().skip(2).fold(0, |acc, e| acc + u8::from(*e)); let signalSum = db_hld .iter() .rev() @@ -311,12 +314,12 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { } // Compute the DX 1 number (product number) - let Some(productNumber) = ToIntPos(&Into::>::into(dataBits), 1, 7) else { + let Some(productNumber) = ToIntPos(&Into::>::into(&dataBits), 1, 7) else { return Err(Exceptions::NOT_FOUND); }; // Compute the DX 2 number (generation number) - let Some(generationNumber) = ToIntPos(&Into::>::into(dataBits), 9, 4) else { + let Some(generationNumber) = ToIntPos(&Into::>::into(&dataBits), 9, 4) else { return Err(Exceptions::NOT_FOUND); }; @@ -326,9 +329,9 @@ impl<'a> RowReader for DXFilmEdgeReader<'_> { // txt.reserve(10); txt = (productNumber.to_string()) + "-" + (&generationNumber.to_string()); if (clock.hasFrameNr) { - let frameNr = ToIntPos(&Into::>::into(dataBits), 13, 6).unwrap_or(0); + let frameNr = ToIntPos(&Into::>::into(&dataBits), 13, 6).unwrap_or(0); txt += &("/".to_owned() + &(frameNr.to_string())); - if (dataBits[19] != false/*0*/) { + if (dataBits.get(19) != false/*0*/) { txt += "A"; } } diff --git a/src/oned/cpp/one_d_reader.rs b/src/oned/cpp/one_d_reader.rs index 6032056..5ffc20f 100644 --- a/src/oned/cpp/one_d_reader.rs +++ b/src/oned/cpp/one_d_reader.rs @@ -5,15 +5,14 @@ */ // SPDX-License-Identifier: Apache-2.0 -use std::any::Any; use std::collections::HashMap; use crate::common::cpp_essentials::{GetPatternRow, PatternRow, PatternView}; -use crate::Binarizer; use crate::{multi::MultipleBarcodeReader, RXingResult, Reader}; use crate::{ point, BarcodeFormat, BinaryBitmap, DecodingHintDictionary, Exceptions, PointT, ResultPoint, }; +use crate::{Binarizer, DecodeHintType, DecodeHintValue}; use crate::common::{LineOrientation, Quadrilateral, Result}; @@ -87,14 +86,14 @@ impl<'a> ODReader<'_> { let mut checkRows = Vec::new(); let mut bars: PatternRow = PatternRow::new(vec![0; 128]); // e.g. EAN-13 has 59 bars/spaces - // bars.reserve(128); // e.g. EAN-13 has 59 bars/spaces + // bars.reserve(128); // e.g. EAN-13 has 59 bars/spaces // #ifdef PRINT_DEBUG // BitMatrix dbg(width, height); // #endif let mut i = 0; - 'outer: while i < maxLines { + 'outer: while i < maxLines { // for (int i = 0; i < maxLines; i++) { // Scanning from the middle out. Determine which row we're looking at next: @@ -185,7 +184,9 @@ impl<'a> ODReader<'_> { let mut result_hld = readers[r] .decodePattern(rowNumber as u32, &mut next, decodingState[r]) .ok(); - if result_hld.is_some() /*|| (returnErrors && result.is_none())*/ { + if result_hld.is_some() + /*|| (returnErrors && result.is_none())*/ + { let mut result = result_hld.as_mut().unwrap(); IncrementLineCount(&mut result); if (upsideDown) { @@ -202,7 +203,7 @@ impl<'a> ODReader<'_> { let points = result.getPointsMut(); for p in points { // for (auto& p : points) { - *p = point(p.getY(), width as f32- p.getX() - 1.0); + *p = point(p.getY(), width as f32 - p.getX() - 1.0); } // result.addPoints(points); // result.setPosition(std::move(points)); @@ -210,9 +211,11 @@ impl<'a> ODReader<'_> { // check if we know this code already for other_hld in res.iter_mut() { - let Some(mut other) = other_hld else{ continue;}; + let Some(mut other) = other_hld.as_mut() else { + continue; + }; // for (auto& other : res) { - if (result == &other) { + if (result == other) { // merge the position information let dTop = PointT::maxAbsComponent( other.getPoints()[0] - result.getPoints()[0], @@ -238,7 +241,7 @@ impl<'a> ODReader<'_> { other.replace_points(points); IncrementLineCount(&mut other); // clear the result, so we don't insert it again below - result_hld = None; //Result(); + // result_hld = None; //Result(); break; } } @@ -263,7 +266,7 @@ impl<'a> ODReader<'_> { && res.iter().fold(0, |acc, e| { if let Some(itm) = &res[r] { acc + i32::from((itm.line_count() >= minLineCount as usize)) - }else { + } else { acc } }) == maxSymbols as i32) @@ -285,34 +288,72 @@ impl<'a> ODReader<'_> { // out: // remove all symbols with insufficient line count - res.retain(|e| if let Some(itm) = e {itm.line_count() < minLineCount as usize} else {false}); + res.retain(|e| { + if let Some(itm) = e { + itm.line_count() < minLineCount as usize + } else { + false + } + }); // let it = std::remove_if(res.begin(), res.end(), [&](auto&& r) { return r.lineCount() < minLineCount; }); // res.erase(it, res.end()); // if symbols overlap, remove the one with a lower line count - for (i, a_hld) in res.iter().enumerate() { - let a = a_hld.as_ref().unwrap(); - // for (auto a = res.begin(); a != res.end(); ++a){ - for b_hld in res.iter().skip(i) { - let b = b_hld.as_ref().unwrap(); - // for (auto b = std::next(a); b != res.end(); ++b){ - let Ok(q1) =Quadrilateral::try_from(a.getPoints()) else {continue;}; - let Ok(q2) = Quadrilateral::try_from(b.getPoints()) else {continue;}; - if (Quadrilateral::have_intersecting_bounding_boxes(&q1, &q2)) { - *(if a.line_count() < b.line_count() { - a_hld - } else { - b_hld - }) = None; + for i in 0..res.len() { + for j in i..res.len() { + if res[i].is_some() && res[j].is_some() { + let Ok(q1) = Quadrilateral::try_from(res[i].as_ref().unwrap().getPoints()) + else { + continue; + }; + let Ok(q2) = Quadrilateral::try_from(res[j].as_ref().unwrap().getPoints()) + else { + continue; + }; + if (Quadrilateral::have_intersecting_bounding_boxes(&q1, &q2)) { + let a_lc = res[i].as_ref().unwrap().line_count(); + let b_lc = res[j].as_ref().unwrap().line_count(); + if a_lc < b_lc { + res[i] = None; + } else { + res[j] = None; + } + } } } } + // // if symbols overlap, remove the one with a lower line count + // for (i, a_hld) in res.iter().enumerate() { + // let a = a_hld.as_ref().unwrap(); + // // let a_lc = a_hld.as_ref().unwrap().line_count(); + // // let a_pts = a_hld.as_ref().unwrap().getPoints().clone(); + // // for (auto a = res.begin(); a != res.end(); ++a){ + // for (j, b_hld) in res.iter().skip(i).enumerate() { + // let b = b_hld.as_ref().unwrap(); + // // for (auto b = std::next(a); b != res.end(); ++b){ + // let Ok(q1) =Quadrilateral::try_from(a.getPoints()) else {continue;}; + // let Ok(q2) = Quadrilateral::try_from(b.getPoints()) else {continue;}; + // if (Quadrilateral::have_intersecting_bounding_boxes(&q1, &q2)) { + // if a.line_count() < b.line_count() { + // // delete_list.insert(i); + // // *a_hld = None; + // res.remove(i); + // } else { + // // delete_list.insert(i+j); + // res.remove(i+j); + // } + // } + // } + // } //TODO: C++20 res.erase_if() - res - - .retain(|r| if let Some(itm) = r { - itm.getBarcodeFormat() == &BarcodeFormat::UNSUPORTED_FORMAT } else { false }); + res.retain(|r| { + if let Some(itm) = r { + itm.getBarcodeFormat() == &BarcodeFormat::UNSUPORTED_FORMAT + } else { + false + } + }); // it = std::remove_if(res.begin(), res.end(), [](auto&& r) { return r.format() == BarcodeFormat::None; }); // res.erase(it, res.end()); @@ -327,7 +368,7 @@ impl<'a> ODReader<'_> { impl<'a> ODReader<'_> { pub fn decode_single( &self, - hints: &DecodingHintDictionary, + _hints: &DecodingHintDictionary, image: &BinaryBitmap, ) -> Result { let mut result = Self::DoDecode( @@ -360,7 +401,7 @@ impl<'a> ODReader<'_> { pub fn decode_with_max_symbols( &self, - hints: &DecodingHintDictionary, + _hints: &DecodingHintDictionary, image: &BinaryBitmap, maxSymbols: u32, ) -> Result> { @@ -431,8 +472,24 @@ impl<'a> MultipleBarcodeReader for ODReader<'_> { } impl<'a> ODReader<'_> { - pub fn new(hints: &DecodingHintDictionary) -> Self { - unimplemented!() + pub fn new(hints: &DecodingHintDictionary) -> ODReader { + ODReader { + reader: DXFilmEdgeReader::new(hints), + try_harder: matches!( + hints.get(&DecodeHintType::TRY_HARDER), + Some(DecodeHintValue::TryHarder(true)) + ), + is_pure: matches!( + hints.get(&DecodeHintType::PURE_BARCODE), + Some(DecodeHintValue::PureBarcode(true)) + ), + min_line_count: 2, + return_errors: false, + try_rotate: matches!( + hints.get(&DecodeHintType::TRY_HARDER), + Some(DecodeHintValue::TryHarder(true)) + ), + } } } From 8a109b1098047056585f3bbb1f5be5d5cfa78311 Mon Sep 17 00:00:00 2001 From: Henry Schimke Date: Tue, 30 Jan 2024 13:19:53 -0600 Subject: [PATCH 5/6] wip: inop dx_film_edge port and filtered reader --- src/barcode_format.rs | 2 + src/binary_bitmap.rs | 2 +- src/buffered_image_luminance_source.rs | 33 +++- src/common/cpp_essentials/pattern.rs | 7 +- src/common/global_histogram_binarizer.rs | 53 ++++- src/filtered_image_reader.rs | 216 +++++++++++++++++++++ src/lib.rs | 3 + src/luma_luma_source.rs | 32 +++ src/luminance_source.rs | 5 + src/multi_format_reader.rs | 7 + src/oned/cpp/dxfilm_edge_reader.rs | 2 +- src/oned/cpp/one_d_reader.rs | 35 +--- src/oned/cpp/row_reader.rs | 2 +- src/planar_yuv_luminance_source.rs | 8 + src/rgb_luminance_source.rs | 12 ++ src/svg_luminance_source.rs | 8 + test_resources/blackbox/dxfilmedge-1/1.jpg | Bin 0 -> 25235 bytes test_resources/blackbox/dxfilmedge-1/1.txt | 1 + test_resources/blackbox/dxfilmedge-1/2.png | Bin 0 -> 16300 bytes test_resources/blackbox/dxfilmedge-1/2.txt | 1 + test_resources/blackbox/dxfilmedge-1/3.jpg | Bin 0 -> 1652 bytes test_resources/blackbox/dxfilmedge-1/3.txt | 1 + tests/dx_film_edge.rs | 27 +++ 23 files changed, 421 insertions(+), 36 deletions(-) create mode 100644 src/filtered_image_reader.rs create mode 100644 test_resources/blackbox/dxfilmedge-1/1.jpg create mode 100644 test_resources/blackbox/dxfilmedge-1/1.txt create mode 100644 test_resources/blackbox/dxfilmedge-1/2.png create mode 100644 test_resources/blackbox/dxfilmedge-1/2.txt create mode 100644 test_resources/blackbox/dxfilmedge-1/3.jpg create mode 100644 test_resources/blackbox/dxfilmedge-1/3.txt create mode 100644 tests/dx_film_edge.rs diff --git a/src/barcode_format.rs b/src/barcode_format.rs index 5ca636c..b1211e2 100644 --- a/src/barcode_format.rs +++ b/src/barcode_format.rs @@ -119,6 +119,7 @@ impl Display for BarcodeFormat { BarcodeFormat::UPC_A => "upc a", BarcodeFormat::UPC_E => "upc e", BarcodeFormat::UPC_EAN_EXTENSION => "upc/ean extension", + BarcodeFormat::DXFilmEdge => "DXFilmEdge", _ => "unsuported", } ) @@ -166,6 +167,7 @@ impl From<&str> for BarcodeFormat { "upc e" | "upc_e" | "upce" => BarcodeFormat::UPC_E, "upc ean extension" | "upc extension" | "ean extension" | "upc/ean extension" | "upc_ean_extension" => BarcodeFormat::UPC_EAN_EXTENSION, + "DXFilmEdge" | "dxfilmedge" | "dx film edge" => BarcodeFormat::DXFilmEdge, _ => BarcodeFormat::UNSUPORTED_FORMAT, } } diff --git a/src/binary_bitmap.rs b/src/binary_bitmap.rs index 056c72b..3f4189e 100644 --- a/src/binary_bitmap.rs +++ b/src/binary_bitmap.rs @@ -32,7 +32,7 @@ use crate::{ pub struct BinaryBitmap { binarizer: B, - matrix: Option, + pub(crate) matrix: Option, } impl BinaryBitmap { diff --git a/src/buffered_image_luminance_source.rs b/src/buffered_image_luminance_source.rs index a736ec1..4d6bb8b 100644 --- a/src/buffered_image_luminance_source.rs +++ b/src/buffered_image_luminance_source.rs @@ -16,7 +16,7 @@ use std::rc::Rc; -use image::{DynamicImage, ImageBuffer, Luma}; +use image::{DynamicImage, GenericImageView, ImageBuffer, Luma, Pixel}; use imageproc::geometric_transformations::rotate_about_center; use crate::common::Result; @@ -108,6 +108,33 @@ impl LuminanceSource for BufferedImageLuminanceSource { pixels } + fn get_column(&self, x: usize) -> Vec { + let width = self.get_height(); // - self.left as usize; + + let pixels: Vec = || -> Option> { + Some( + self.image + .as_luma8()? + .rows() + .skip(self.top as usize) + .fold(Vec::default(), |mut acc, e| { + acc.push( + e.into_iter() + .nth(self.left as usize + x) + .unwrap_or(&Luma([0_u8])), + ); + acc + }) + .iter() + .map(|&p| p.0[0]) + .collect(), + ) + }() + .unwrap_or_default(); + + pixels + } + fn get_matrix(&self) -> Vec { if self.height == self.image.height() as usize && self.width == self.image.width() as usize { @@ -201,4 +228,8 @@ impl LuminanceSource for BufferedImageLuminanceSource { top: 0, }) } + + fn get_luma8_point(&self, x: usize, y: usize) -> u8 { + self.image.get_pixel(x as u32, y as u32).to_luma().0[0] + } } diff --git a/src/common/cpp_essentials/pattern.rs b/src/common/cpp_essentials/pattern.rs index 6e6b288..85f7b45 100644 --- a/src/common/cpp_essentials/pattern.rs +++ b/src/common/cpp_essentials/pattern.rs @@ -217,7 +217,7 @@ impl<'a> PatternView<'a> { pub fn isValidWithN(&self, n: usize) -> bool { !self.data.0.is_empty() && self.start <= self.current + self.start - && self.current + n <= (self.data.0.len()) + && self.current + n < (self.data.0.len()) /*return _data && _data >= _base && _data + n <= _end;*/ } pub fn isValid(&self) -> bool { @@ -271,7 +271,10 @@ impl<'a> PatternView<'a> { } pub fn extend(&mut self) { - self.count = std::cmp::max(0, self.data.len() - (self.current + self.start)) + self.count = std::cmp::max( + 0, + self.data.len() as isize - (self.current + self.start) as isize, + ) as usize } fn try_get_index(&self, index: isize) -> Option { diff --git a/src/common/global_histogram_binarizer.rs b/src/common/global_histogram_binarizer.rs index 722f4ee..dfc12da 100644 --- a/src/common/global_histogram_binarizer.rs +++ b/src/common/global_histogram_binarizer.rs @@ -27,7 +27,7 @@ use once_cell::unsync::OnceCell; use crate::common::Result; use crate::{Binarizer, Exceptions, LuminanceSource}; -use super::{BitArray, BitMatrix}; +use super::{BitArray, BitMatrix, LineOrientation}; const LUMINANCE_BITS: usize = 5; const LUMINANCE_SHIFT: usize = 8 - LUMINANCE_BITS; @@ -107,8 +107,55 @@ impl Binarizer for GlobalHistogramBinarizer { Ok(Cow::Borrowed(row)) } - fn get_black_line(&self, l: usize, lt: super::LineOrientation) -> Result> { - unimplemented!() + fn get_black_line(&self, l: usize, lt: LineOrientation) -> Result> { + if lt == LineOrientation::Row { + self.get_black_row(l) + } else { + let col = self.black_column_cache[l].get_or_try_init(|| { + let source = self.get_luminance_source(); + let width = source.get_height(); + let mut col = BitArray::with_size(width); + + // self.initArrays(width); + let localLuminances = source.get_column(l); + let mut localBuckets = [0; LUMINANCE_BUCKETS]; //self.buckets.clone(); + for x in 0..width { + // for (int x = 0; x < width; x++) { + localBuckets[((localLuminances[x]) >> LUMINANCE_SHIFT) as usize] += 1; + } + let blackPoint = Self::estimateBlackPoint(&localBuckets)?; + + if width < 3 { + // Special case for very small images + for (x, lum) in localLuminances.iter().enumerate().take(width) { + // for x in 0..width { + // for (int x = 0; x < width; x++) { + if (*lum as u32) < blackPoint { + col.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]; + // A simple -1 4 -1 box filter with a weight of 2. + if ((center as i64 * 4) - left as i64 - right as i64) / 2 + < blackPoint as i64 + { + col.set(x); + } + left = center; + center = right; + } + } + + Ok(col) + })?; + + Ok(Cow::Borrowed(col)) + } } // Does not sharpen the data, as this call is intended to only be used by 2D Readers. diff --git a/src/filtered_image_reader.rs b/src/filtered_image_reader.rs new file mode 100644 index 0000000..ff8dce1 --- /dev/null +++ b/src/filtered_image_reader.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; + +use crate::{Binarizer, BinaryBitmap, Exceptions, Luma8LuminanceSource, LuminanceSource, Reader}; + +use crate::common::{BitMatrix, Result}; + +pub const DEFAULT_DOWNSCALE_THRESHHOLD: usize = 500; +pub const DEFAULT_DOWNSCALE_FACTOR: usize = 3; + +/// Passed image data is ignored, only the image data +pub struct FilteredImageReader> { + reader: R, + source: Luma8LuminanceSource, + binarizer: B, +} + +impl> FilteredImageReader { + pub fn new>( + reader: R, + source: I, + binarizer: B, + ) -> Self { + Self { + reader, + source: source.into(), + binarizer, + } + } +} + +impl> Reader + for FilteredImageReader +{ + fn decode( + &mut self, + _image: &mut crate::BinaryBitmap, + ) -> crate::common::Result { + self.decode_with_hints(_image, &HashMap::default()) + } + + fn decode_with_hints( + &mut self, + _image: &mut crate::BinaryBitmap, + hints: &crate::DecodingHintDictionary, + ) -> crate::common::Result { + let pyramids = LumImagePyramid::new( + self.source.clone(), + DEFAULT_DOWNSCALE_THRESHHOLD, + DEFAULT_DOWNSCALE_FACTOR, + ) + .ok_or(Exceptions::ILLEGAL_ARGUMENT)?; + for layer in pyramids.layers { + let mut b = BinaryBitmap::new(self.binarizer.create_binarizer(layer)); + for close in [false, true] { + if close { + let Ok(_) = b.close() else { + continue; + }; + } + if let Ok(res) = self.reader.decode_with_hints(&mut b, hints) { + return Ok(res); + } else { + continue; + } + } + } + Err(Exceptions::NOT_FOUND) + } +} + +#[derive(Debug, Clone)] +struct LumImagePyramid { + buffers: Vec, + pub layers: Vec, +} + +impl Default for LumImagePyramid { + fn default() -> Self { + Self { + buffers: Default::default(), + layers: Default::default(), + } + } +} + +impl LumImagePyramid { + pub fn new(image: Luma8LuminanceSource, threshold: usize, factor: usize) -> Option { + let mut new_self = Self::default(); + + new_self.layers.push(image); + // TODO: if only matrix codes were considered, then using std::min would be sufficient (see #425) + while threshold > 0 + && std::cmp::max( + new_self.layers.last()?.get_width(), + new_self.layers.last()?.get_height(), + ) > threshold + && std::cmp::min( + new_self.layers.last()?.get_width(), + new_self.layers.last()?.get_height(), + ) >= factor + { + new_self.add_layer_with_factor(factor).ok()?; + } + + if false { + // Reversing the layers means we'd start with the smallest. that can make sense if we are only looking for a + // single symbol. If we start with the higher resolution, we get better (high res) position information. + // TODO: see if masking out higher res layers based on found symbols in lower res helps overall performance. + new_self.layers.reverse(); + } + + Some(new_self) + } + + fn add_layer(&mut self) -> Result<()> { + let siv = self.layers.last().ok_or(Exceptions::ILLEGAL_ARGUMENT)?; + + self.buffers.push(Luma8LuminanceSource::with_empty_image( + siv.get_width() / N, + siv.get_height() / N, + )); + + let div = self + .buffers + .last_mut() + .ok_or(Exceptions::ILLEGAL_ARGUMENT)?; + + let div_height = div.get_height(); + let div_width = div.get_width(); + + let d_vec = div.get_matrix_mut(); + + for d in d_vec.iter_mut() { + for dy in 0..div_height { + // for (int dy = 0; dy < div.height(); ++dy){ + for dx in 0..div_width { + // for (int dx = 0; dx < div.width(); ++dx) { + let mut sum = (N * N) as u8 / 2; + for ty in 0..N { + // for (int ty = 0; ty < N; ++ty){ + for tx in 0..N { + // for (int tx = 0; tx < N; ++tx) { + sum += siv.get_luma8_point(dx * N + tx, dy * N + ty); + } + } + *d = sum / (N * N) as u8; + } + } + } + + self.layers.push( + self.buffers + .last() + .ok_or(Exceptions::ILLEGAL_ARGUMENT)? + .clone(), + ); + + Ok(()) + } + + fn add_layer_with_factor(&mut self, factor: usize) -> Result<()> { + // help the compiler's auto-vectorizer by hard-coding the scale factor + match factor { + 2 => self.add_layer::<2>(), + 3 => self.add_layer::<3>(), + 4 => self.add_layer::<4>(), + _ => Err(Exceptions::illegal_argument_with( + "Invalid ReaderOptions::downscaleFactor", + )), + } + } +} + +const SET_V: u32 = 0xff; // allows playing with SIMD binarization + +impl BinaryBitmap { + pub fn close(&mut self) -> Result<()> { + if let Some(mut matrix) = self.matrix.as_mut() { + // if (_cache->matrix) { + // auto& matrix = *const_cast(_cache->matrix.get()); + let mut tmp = BitMatrix::new(matrix.width(), matrix.height())?; + + // dilate + SumFilter(matrix, &mut tmp, |sum| { + return u32::from(sum > 0 * SET_V) * SET_V; + }); + // erode + SumFilter(&tmp, &mut matrix, |sum| { + return u32::from(sum == 9 * SET_V) * SET_V; + }); + } + Ok(()) + // _closed = true; + } +} + +fn SumFilter(input: &BitMatrix, output: &mut BitMatrix, func: F) +where + F: Fn(u32) -> u32, +{ + for row in 1..output.height() { + for col in 0..output.width() - 1 { + let in0 = input.getRow(row); //.row(0).begin(); + let in1 = input.getRow(row + 1); //.row(1).begin(); + let in2 = input.getRow(row + 2); //.row(2).begin(); + + let mut sum = 0; + for j in 0..3 { + // for (int j = 0; j < 3; ++j){ + sum += in0.get(j) as u32 + in1.get(j) as u32 + in2.get(j) as u32; + } + + output.set_bool(row, col, func(sum) != 0); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d6675e2..b312d08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,9 @@ pub mod helpers; mod luma_luma_source; pub use luma_luma_source::*; +mod filtered_image_reader; +pub use filtered_image_reader::*; + #[cfg(feature = "svg_read")] mod svg_luminance_source; #[cfg(feature = "svg_read")] diff --git a/src/luma_luma_source.rs b/src/luma_luma_source.rs index 9b525ee..f81a59b 100644 --- a/src/luma_luma_source.rs +++ b/src/luma_luma_source.rs @@ -2,6 +2,7 @@ use crate::common::Result; use crate::LuminanceSource; /// A simple luma8 source for bytes, supports cropping but not rotation +#[derive(Debug, Clone)] pub struct Luma8LuminanceSource { /// image dimension in form (x,y) dimensions: (u32, u32), @@ -27,6 +28,19 @@ impl LuminanceSource for Luma8LuminanceSource { .collect() } + fn get_column(&self, x: usize) -> Vec { + self.data + .chunks_exact(self.original_dimension.0 as usize) + .skip(self.origin.1 as usize) + .fold(Vec::default(), |mut acc, e| { + acc.push(e[self.origin.0 as usize + x as usize]); + acc + }) + .iter() + .map(|byte| Self::invert_if_should(*byte, self.inverted)) + .collect() + } + fn get_matrix(&self) -> Vec { self.data .iter() @@ -93,6 +107,10 @@ impl LuminanceSource for Luma8LuminanceSource { "This luminance source does not support rotation by 45 degrees.", )) } + + fn get_luma8_point(&self, x: usize, y: usize) -> u8 { + todo!() + } } impl Luma8LuminanceSource { @@ -159,6 +177,20 @@ impl Luma8LuminanceSource { } } + pub fn with_empty_image(width: usize, height: usize) -> Self { + Self { + dimensions: (width as u32, height as u32), + origin: (0, 0), + data: vec![0u8; width * height], + inverted: false, + original_dimension: (width as u32, height as u32), + } + } + + pub fn get_matrix_mut(&mut self) -> &mut Vec { + &mut self.data + } + #[inline(always)] fn invert_if_should(byte: u8, invert: bool) -> u8 { if invert { diff --git a/src/luminance_source.rs b/src/luminance_source.rs index 9c62d2e..54ee32d 100644 --- a/src/luminance_source.rs +++ b/src/luminance_source.rs @@ -48,6 +48,9 @@ pub trait LuminanceSource { */ fn get_row(&self, y: usize) -> Vec; + /// Get a column of of the image + fn get_column(&self, x: usize) -> Vec; + /** * Fetches luminance data for the underlying bitmap. Values should be fetched using: * {@code int luminance = array[y * width + x] & 0xff} @@ -149,6 +152,8 @@ pub trait LuminanceSource { iv } + fn get_luma8_point(&self, x: usize, y: usize) -> u8; + /* @Override public final String toString() { diff --git a/src/multi_format_reader.rs b/src/multi_format_reader.rs index 9583d6c..1c017aa 100644 --- a/src/multi_format_reader.rs +++ b/src/multi_format_reader.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use crate::common::Result; +use crate::oned::cpp::ODReader; use crate::qrcode::cpp_port::QrReader; use crate::{ aztec::AztecReader, datamatrix::DataMatrixReader, maxicode::MaxiCodeReader, @@ -194,6 +195,9 @@ impl MultiFormatReader { BarcodeFormat::MAXICODE => { MaxiCodeReader::default().decode_with_hints(image, &self.hints) } + BarcodeFormat::DXFilmEdge => { + ODReader::new(&self.hints).decode_with_hints(image, &self.hints) + } _ => Err(Exceptions::UNSUPPORTED_OPERATION), }; if res.is_ok() { @@ -230,6 +234,9 @@ impl MultiFormatReader { if let Ok(res) = MaxiCodeReader::default().decode_with_hints(image, &self.hints) { return Ok(res); } + if let Ok(res) = ODReader::new(&self.hints).decode_with_hints(image, &self.hints) { + return Ok(res); + } if self.try_harder { if let Ok(res) = self.one_d_reader.decode_with_hints(image, &self.hints) { diff --git a/src/oned/cpp/dxfilm_edge_reader.rs b/src/oned/cpp/dxfilm_edge_reader.rs index 5734149..02294e7 100644 --- a/src/oned/cpp/dxfilm_edge_reader.rs +++ b/src/oned/cpp/dxfilm_edge_reader.rs @@ -71,7 +71,7 @@ fn DistIsBelowThreshold(a: PointI, b: PointI, threshold: PointI) -> bool { } // DX Film Edge clock track found on 35mm films. -#[derive(Debug)] +#[derive(Debug, Clone)] pub(super) struct Clock { hasFrameNr: bool, // = false; // Clock track (thus data track) with frame number (longer version) rowNumber: u32, // = 0, diff --git a/src/oned/cpp/one_d_reader.rs b/src/oned/cpp/one_d_reader.rs index 5ffc20f..8f3a690 100644 --- a/src/oned/cpp/one_d_reader.rs +++ b/src/oned/cpp/one_d_reader.rs @@ -17,7 +17,7 @@ use crate::{Binarizer, DecodeHintType, DecodeHintValue}; use crate::common::{LineOrientation, Quadrilateral, Result}; use super::dxfilm_edge_reader::DXFilmEdgeReader; -use super::row_reader::RowReader; +use super::row_reader::{DecodingState, RowReader}; pub struct ODReader<'a> { reader: DXFilmEdgeReader<'a>, // THIS IS WRONG, SEE BELOW ONLY DOES ONE @@ -47,11 +47,11 @@ impl<'a> ODReader<'_> { isPure: bool, maxSymbols: u32, minLineCount: u32, - returnErrors: bool, + _returnErrors: bool, ) -> Vec { let mut res: Vec> = Vec::new(); - let mut decodingState: Vec<&mut Option> = Vec::new(); + let mut decodingState: Vec> = vec![Some(DecodingState::default()); 1]; // std::vector> decodingState(readers.size()); let mut minLineCount = minLineCount; @@ -120,6 +120,7 @@ impl<'a> ODReader<'_> { checkRows.pop(); isCheckRow = true; if (rowNumber < 0 || rowNumber >= height) { + i += 1; continue; } } @@ -134,6 +135,7 @@ impl<'a> ODReader<'_> { ) { r.as_ref().into() } else { + i += 1; continue; }; // let img = if rotate {let a = image.rotate_counter_clockwise(); &a} else {image}; @@ -182,7 +184,7 @@ impl<'a> ODReader<'_> { let mut next = PatternView::new(&bars); loop { let mut result_hld = readers[r] - .decodePattern(rowNumber as u32, &mut next, decodingState[r]) + .decodePattern(rowNumber as u32, &mut next, &mut decodingState[r]) .ok(); if result_hld.is_some() /*|| (returnErrors && result.is_none())*/ @@ -277,6 +279,7 @@ impl<'a> ODReader<'_> { // make sure we make progress and we start the next try on a bar next.shift(2 - (next.index() % 2)); next.extend(); + if !(tryHarder && next.size() > 0) { break; } @@ -371,29 +374,7 @@ impl<'a> ODReader<'_> { _hints: &DecodingHintDictionary, image: &BinaryBitmap, ) -> Result { - let mut result = Self::DoDecode( - &self.reader, - image, - self.try_harder, - false, - self.is_pure, - 1, - self.min_line_count, - self.return_errors, - ); - - if (result.is_empty() && self.try_rotate) { - result = Self::DoDecode( - &self.reader, - image, - self.try_harder, - true, - self.is_pure, - 1, - self.min_line_count, - self.return_errors, - ); - } + let result = self.decode_with_max_symbols(_hints, image, u32::MAX)?; result.first().cloned().ok_or(Exceptions::NOT_FOUND) // return FirstOrDefault(std::move(result)); diff --git a/src/oned/cpp/row_reader.rs b/src/oned/cpp/row_reader.rs index 69260c3..2dc95ec 100644 --- a/src/oned/cpp/row_reader.rs +++ b/src/oned/cpp/row_reader.rs @@ -41,7 +41,7 @@ type Alphabet = Vec; // // virtual ~DecodingState() = default; // } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct DecodingState { // DXO pub centerRow: u32, diff --git a/src/planar_yuv_luminance_source.rs b/src/planar_yuv_luminance_source.rs index aff992b..7bdf857 100644 --- a/src/planar_yuv_luminance_source.rs +++ b/src/planar_yuv_luminance_source.rs @@ -256,6 +256,10 @@ impl LuminanceSource for PlanarYUVLuminanceSource { row } + fn get_column(&self, x: usize) -> Vec { + unimplemented!() + } + fn get_matrix(&self) -> Vec { let width = self.get_width(); let height = self.get_height(); @@ -332,4 +336,8 @@ impl LuminanceSource for PlanarYUVLuminanceSource { fn invert(&mut self) { self.invert = !self.invert; } + + fn get_luma8_point(&self, x: usize, y: usize) -> u8 { + unimplemented!() + } } diff --git a/src/rgb_luminance_source.rs b/src/rgb_luminance_source.rs index 144350a..aa95341 100644 --- a/src/rgb_luminance_source.rs +++ b/src/rgb_luminance_source.rs @@ -58,6 +58,10 @@ impl LuminanceSource for RGBLuminanceSource { row } + fn get_column(&self, x: usize) -> Vec { + unimplemented!() + } + fn get_matrix(&self) -> Vec { let width = self.get_width(); let height = self.get_height(); @@ -127,6 +131,14 @@ impl LuminanceSource for RGBLuminanceSource { fn invert(&mut self) { self.invert = !self.invert; } + + fn get_luma8_point(&self, x: usize, y: usize) -> u8 { + let width = self.get_width(); + let row_offset = (y + self.top) * self.dataWidth + self.left; + let col_offset = (x + self.left); + + self.luminances[row_offset + col_offset] + } } impl RGBLuminanceSource { diff --git a/src/svg_luminance_source.rs b/src/svg_luminance_source.rs index dcd7ab8..02dd03f 100644 --- a/src/svg_luminance_source.rs +++ b/src/svg_luminance_source.rs @@ -11,6 +11,10 @@ impl LuminanceSource for SVGLuminanceSource { self.0.get_row(y) } + fn get_column(&self, x: usize) -> Vec { + self.0.get_column(x) + } + fn get_matrix(&self) -> Vec { self.0.get_matrix() } @@ -46,6 +50,10 @@ impl LuminanceSource for SVGLuminanceSource { fn rotate_counter_clockwise_45(&self) -> Result { self.0.rotate_counter_clockwise_45().map(Self) } + + fn get_luma8_point(&self, x: usize, y: usize) -> u8 { + self.0.get_luma8_point(x, y) + } } impl SVGLuminanceSource { diff --git a/test_resources/blackbox/dxfilmedge-1/1.jpg b/test_resources/blackbox/dxfilmedge-1/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f342117169c4b693bff2d6744e6139835fd2c3d GIT binary patch literal 25235 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 OLf<{4@HnJ69Q{9+5I#=; literal 0 HcmV?d00001 diff --git a/test_resources/blackbox/dxfilmedge-1/1.txt b/test_resources/blackbox/dxfilmedge-1/1.txt new file mode 100644 index 0000000..49f6980 --- /dev/null +++ b/test_resources/blackbox/dxfilmedge-1/1.txt @@ -0,0 +1 @@ +36-2/21 \ No newline at end of file diff --git a/test_resources/blackbox/dxfilmedge-1/2.png b/test_resources/blackbox/dxfilmedge-1/2.png new file mode 100644 index 0000000000000000000000000000000000000000..665f080a3788f00ab91dbd03237d9b9680534fe0 GIT binary patch literal 16300 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 iFuD1RTy!_yTmFBuYL+5CfUGnC00005;zb5 literal 0 HcmV?d00001 diff --git a/test_resources/blackbox/dxfilmedge-1/2.txt b/test_resources/blackbox/dxfilmedge-1/2.txt new file mode 100644 index 0000000..a2b1434 --- /dev/null +++ b/test_resources/blackbox/dxfilmedge-1/2.txt @@ -0,0 +1 @@ +80-11/23 \ No newline at end of file diff --git a/test_resources/blackbox/dxfilmedge-1/3.jpg b/test_resources/blackbox/dxfilmedge-1/3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ce106b7804c215430fe9b9ee4e37a3b73e379eeb GIT binary patch literal 1652 zcmV-)28;Rs*#F=F5K2Z#MgRc;000310RRC1+WwA}S+3F)J%EK1e({JV;kgPFHPcZI6$@|H1$Z2mnU_ z>Hz@}0RO}Q8~^|T1pxs80000000000000011O)~M0|E&D+5iXv0RR910NMq_St5bC zx;^s48~OGfcTS+s4Grx)eC*muuU}j~hjfe20OY)#Gm+kU1AGC=qwt|+Mf$TP?k8iE zl4To@baw@BqG)z2uR@)X-h_mu1^%rpMO!Gk6&lYC(fxMiL(r&gfga!eBo$I#4?&A; z#cnm*Eos)_H-aU`Rda|g7DIf#(4Pi<1+^W0G z{<4o!V8_8qNUDR@-}<3Bjk|}zZ~@d+ApGKY{9iM?{1{JgV@rwH0V?BsqBkn%g5n)J z$oE3~NA)PCWXum?%SxW*O-}O33CI~#wa`^(lR3v)s;i2bpvZBPg$2r*cX1fw#wwA` zbP*Puw6G9$XHQ8~Z~LW#}gBX=8T7;RAYF`uM-6<``^f&GOtsu#-kHb$2R9huk9S|1NG zsNwatorTg<&4Iaak4X=&r+qyH4}>@nWO2ihIXOD5oys3dy%K8P6@z1CK4D*uV~^IY zN2W*od_3O^sLgDg7ly+P8R6lgTr3vFR2%R(*=(*OM8* zm?m)Zg3s!>`?2eu{22cLV&CfJ&yEa#v2XQq;XaYA#)^7dW1S?DG`Ez&0jk#inh7Xt zz8;9};Ss1SaOiOQLcI6MKZRoalJ5%r*~RKsuK3NxR`*$bDTnWk>4WfDxmO%jOh@i2 z%iUTP@4Pp1u=djKSJvi~AE+x>>5Nsq&Tm0c_M>=E_AK11c2&>n_-%(3k*5ApyIu73 zTzQ2ACTUq1J4sSl>Pz~uVq+n z7)BMKbhhp)cm&YBnZ`n8f>yjcly1Y67(`*JuFpJ^Nbv9jii)y3dwBT7E%Sw-u8QsZ-nPDjCOSG3KgiC8ls}n8^yBnp%f(W9!CMMY^V*K0Cn1RRP6Z zJJWiGyYc)6Z&tCI*p*Lqm(o!mfB95{6!DLzRqwqIPOCWSV|T#rrFHMGRxZk2>ijCwELV@{h&(;@>Wxdhx%3DCXMQsBBCSaoB2lVcka$H=B-WX zj8%sek{_h5x3qbvJ0@;fSFN>U(y*+&moZ307|9N>*-or%Dk$*9y+TWda Date: Tue, 30 Jan 2024 14:06:03 -0600 Subject: [PATCH 6/6] wip: fix for rows in SumFilter --- src/filtered_image_reader.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/filtered_image_reader.rs b/src/filtered_image_reader.rs index ff8dce1..421f042 100644 --- a/src/filtered_image_reader.rs +++ b/src/filtered_image_reader.rs @@ -200,9 +200,9 @@ where { for row in 1..output.height() { for col in 0..output.width() - 1 { - let in0 = input.getRow(row); //.row(0).begin(); - let in1 = input.getRow(row + 1); //.row(1).begin(); - let in2 = input.getRow(row + 2); //.row(2).begin(); + let in0 = input.getRow(row - 1); //.row(0).begin(); + let in1 = input.getRow(row); //.row(1).begin(); + let in2 = input.getRow(row + 1); //.row(2).begin(); let mut sum = 0; for j in 0..3 {