IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 2021SC@SDUSC-Zxing(十三):一维码解码分析 -> 正文阅读

[C++知识库]2021SC@SDUSC-Zxing(十三):一维码解码分析

2021SC@SDUSC

一、Zxing中的一维码

在Zxing的目录结构中,所有的一维码有关代码被放到了同一个文件夹中oned
在这里插入图片描述
这是由于不同的一维码的编码解码逻辑比较简单并且关联性较大。这篇博客主要以CodaBar为例分析解码过程。

二、OneDReader

所有一维码的解码器都要继承OneDReader类。OneDReader是Reader的子类。在二维码中,解码方法都是decode,但是在一维码中,解码方法都命名为decodeRow。
各方法介绍如下:

方法作用
decode(BinaryBitmap image):Result重写方法。
decode(BinaryBitmap image,Map<DecodeHintType,?> hints) :Result重写方法,外部调用的解码方法
reset():void重写方法,方法里面是空的,也就是OneDreader不具体描述这个方法
doDecode(BinaryBitmap image, Map<DecodeHintType,?> hints):Result内部实现的解码方法
recordPattern(BitArray row, int start,int[] counters):void记录从给定点开始的一行中连续运行的白色和黑色像素的数量
recordPatternInReverse(BitArray row, int start, int[] counters):void上一个方法是从前向后记录,这个方法是从后向前记录,只在rss中用到
patternMatchVariance(int[] counters,int[] pattern,float maxIndividualVariance) :float确定一组观察到的黑白值运行计数与给定目标模式的匹配程度。是所有模式中,预期模式比例的总方差与模式长度的比率。
decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints): Result抽象方法,尝试对给定一行图像的一维条形码格式进行解码。需要每个码根据自己的特点实现。

docode
在这里插入图片描述

 //注:即使图片支持旋转,在没有TRY_HARDER标志的情况下也不会尝试旋转
  @Override
  public Result decode(BinaryBitmap image,
                       Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {
    try {
      // 直接调用解码算法
      return doDecode(image, hints);
    } catch (NotFoundException nfe) {
      // 传输的条形码可能发生形变
      boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
      //如果解码的目标是花更多的时间去寻找条形码;优化精度,而不是速度,并且此子类支持逆时针旋转。
      if (tryHarder && image.isRotateSupported()) {
        // 返回图像数据逆时针旋转90度的新对象
        BinaryBitmap rotatedImage = image.rotateCounterClockwise();
        // 调用解码算法
        Result result = doDecode(rotatedImage, hints);
        //记录我们发现它逆时针旋转90度/270度
        Map<ResultMetadataType,?> metadata = result.getResultMetadata();
        int orientation = 270;
        if (metadata != null && metadata.containsKey(ResultMetadataType.ORIENTATION)) {
          //但如果我们在doDecode()中发现了相反的结果,在此处添加该结果
          //ORIENTATION表示图像中条形码的可能近似方向。该值以从正常垂直方向顺时针旋转的角度给出,其值在[0360]范围内。例如,通过自上而下读取找到的1D条形码将被称为方向为“90”。
          orientation = (orientation +
              (Integer) metadata.get(ResultMetadataType.ORIENTATION)) % 360;
        }
        result.putMetadata(ResultMetadataType.ORIENTATION, orientation);
        //更新结果点
        ResultPoint[] points = result.getResultPoints();
        if (points != null) {
          int height = rotatedImage.getHeight();
          for (int i = 0; i < points.length; i++) {
            points[i] = new ResultPoint(height - points[i].getY() - 1, points[i].getX());
          }
        }
        return result;
      } else {
        // 抛出NotFoundException没有找到异常
        throw nfe;
      }
    }
  }

doDecode
decode交付给doDecode进行后续解码操作,而doDecode只是负责扫描定位,具体的解码方法在每个码的decodeRow中实现。

private Result doDecode(BinaryBitmap image,
                          Map<DecodeHintType,?> hints) throws NotFoundException {
    int width = image.getWidth();//宽
    int height = image.getHeight();//高
    BitArray row = new BitArray(width);//定义一个大小为宽度的BitArray来存放结果

    boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
    // 如果不知道扫码类型并且希望更加重视扫码的精度,扫描行的步长为(height>>8和1)中最大的一个,反之为(height>>5和1)中最大的一个
    int rowStep = Math.max(1, height >> (tryHarder ? 8 : 5));
    int maxLines;
    // 如果不知道扫码类型并且希望更加重视扫码的精度
    if (tryHarder) {
      maxLines = height; //看看整个图像,而不仅仅是中心
    } else {
      // 如果不知道扫码类型并且希望更加重视扫码的速度
      maxLines = 15; // 间隔1/32的15行大致是图像的中间部分
    }

    int middle = height / 2;
    for (int x = 0; x < maxLines; x++) {

      // 从中间向外扫描。确定我们接下来要查看的行
      int rowStepsAboveOrBelow = (x + 1) / 2;
      boolean isAbove = (x & 0x01) == 0; // 判断x是否为奇数
      int rowNumber = middle + rowStep * (isAbove ? rowStepsAboveOrBelow : -rowStepsAboveOrBelow);
      if (rowNumber < 0 || rowNumber >= height) {
        // 如果超出顶部或底部得范围,stop
        break;
      }
      //估计此行的黑点并加载它
      try {
        row = image.getBlackRow(rowNumber, row);
      } catch (NotFoundException ignored) {
        continue;
      }
      // 虽然我们将图像数据放在一个位数组中,但将其反转以处理颠倒条形码的解码是相当方便的。
      for (int attempt = 0; attempt < 2; attempt++) {
        if (attempt == 1) {
          row.reverse(); // 将行反转并继续
          //这意味着在该方法的生命周期中,我们将只绘制一次结果点,因为我们希望避免在翻转行后绘制错误的点,并且不希望在每一行扫描中都出现噪音,只绘制从中心线开始的扫描。
          if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) {
            Map<DecodeHintType,Object> newHints = new EnumMap<>(DecodeHintType.class);
            newHints.putAll(hints);
            newHints.remove(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
            hints = newHints;
          }
        }
        try {
          // 寻找条形码,decodeRow是解码的关键方法
          Result result = decodeRow(rowNumber, row, hints);
          // 找到了条形码
          if (attempt == 1) {
            // 但它是颠倒的,所以请注意
            result.putMetadata(ResultMetadataType.ORIENTATION, 180);
            // 并记住水平翻转结果点
            ResultPoint[] points = result.getResultPoints();
            if (points != null) {
              points[0] = new ResultPoint(width - points[0].getX() - 1, points[0].getY());
              points[1] = new ResultPoint(width - points[1].getX() - 1, points[1].getY());
            }
          }
          return result;
        } catch (ReaderException re) {
          // 继续,直到无法解码这一行
        }
      }
    }

三、一维码万能解码类——MultiFormatOneDReader

还记得在Reader中我们介绍的MultiFormatReader吗?我们称它是万能解码类,MultiFormatOneDReader和Reader、OneDReader、MultiFormatReader有着密切的关系,如下:所有一/二维码解码器都要实现Reader,所有一维码解码器都要继承OneDReader。为了调用方便,Zxing写了两个工厂类:MultiFormatOneDReade、MultiFormatReader;MultiFormatReader是面向一/二维码的,MultiFormatOneDReade是面向一维码的,在MultiFormatReader中对一维码的解析调用的就是MultiFormatOneDReader类。

  if (addOneDReader && !tryHarder) {
    readers.add(new MultiFormatOneDReader(hints));
  }

四者关系如图:
在这里插入图片描述
作为工厂类,MultiFormatOneDReader的逻辑和MultiFormatReader基本一致:

  • 判断hints是否为空?
    是->尝试所有一维码解码器
    否->尝试所有可能类型匹配的一维码解码器。
  public MultiFormatOneDReader(Map<DecodeHintType,?> hints) {
    @SuppressWarnings("unchecked")
    Collection<BarcodeFormat> possibleFormats = hints == null ? null :
        (Collection<BarcodeFormat>) hints.get(DecodeHintType.POSSIBLE_FORMATS);//POSSIBLE_FORMATS表示图像是几种可能的格式之一。
        //ASSUME_CODE_39_CHECK_DIGIT表示不管它实际是什么,都假设使用Code39的校验位。
    boolean useCode39CheckDigit = hints != null &&
        hints.get(DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT) != null;
    Collection<OneDReader> readers = new ArrayList<>();
    if (possibleFormats != null) {
      if (possibleFormats.contains(BarcodeFormat.EAN_13) ||
          possibleFormats.contains(BarcodeFormat.UPC_A) ||
          possibleFormats.contains(BarcodeFormat.EAN_8) ||
          possibleFormats.contains(BarcodeFormat.UPC_E)) {
        // 可以推测MultiFormatUPCEANReader也是一个工厂类,针对的是EAN_13、UPC_A、EAN_8、UPC_E四种条形码
        readers.add(new MultiFormatUPCEANReader(hints));
      }
      if (possibleFormats.contains(BarcodeFormat.CODE_39)) {
        readers.add(new Code39Reader(useCode39CheckDigit));
      }
      if (possibleFormats.contains(BarcodeFormat.CODE_93)) {
        readers.add(new Code93Reader());
      }
      if (possibleFormats.contains(BarcodeFormat.CODE_128)) {
        readers.add(new Code128Reader());
      }
      if (possibleFormats.contains(BarcodeFormat.ITF)) {
         readers.add(new ITFReader());
      }
      if (possibleFormats.contains(BarcodeFormat.CODABAR)) {
         readers.add(new CodaBarReader());
      }
      if (possibleFormats.contains(BarcodeFormat.RSS_14)) {
         readers.add(new RSS14Reader());
      }
      if (possibleFormats.contains(BarcodeFormat.RSS_EXPANDED)) {
        readers.add(new RSSExpandedReader());
      }
    }
    if (readers.isEmpty()) {
    // 可以推测MultiFormatUPCEANReader也是一个工厂类,针对的是EAN_13、UPC_A、EAN_8、UPC_E四种条形码
      readers.add(new MultiFormatUPCEANReader(hints));
      readers.add(new Code39Reader());
      readers.add(new CodaBarReader());
      readers.add(new Code93Reader());
      readers.add(new Code128Reader());
      readers.add(new ITFReader());
      readers.add(new RSS14Reader());
      readers.add(new RSSExpandedReader());
    }
    this.readers = readers.toArray(EMPTY_ONED_ARRAY);
  }

四、CodaBarReader

一维码和二维码不同,Reader不需要将定位和解码的任务交给其他类别完成,所有的算法都在各自的Reader中(rss除外)。

认识CodaBar

  • Codabar构成:Codabar具有4个条和3个空(共7个单元),每个窄或宽的宽度代表一个字符(字母)。
    Codabar的基本构成如下:
    在这里插入图片描述
    Zxing对起始终止字符的定义如下:
  private static final char[] STARTEND_ENCODING = {'A', 'B', 'C', 'D'};
  • Codabar字符的构成:Codabar可以用数字(0至9)、字母(A、B、C、D)以及符号(-、$、/、. 、+)来表示字符。
    在这里插入图片描述
    在Zxing中,定义了字符字母串,并用一个数组存储他们
  private static final String ALPHABET_STRING = "0123456789-$:/.+ABCD";
  static final char[] ALPHABET = ALPHABET_STRING.toCharArray();

规定这些表示字符的编码如下,表示宽条和窄条的模式。每个int的7个最低有效位对应于宽和窄的模式,1表示“宽”,0表示“窄”。

  static final int[] CHARACTER_ENCODINGS = {
      0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-9
      0x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD
  };

以“0”为例,0的编码为0x003,低7位为0000011,即,窄窄窄窄窄宽宽,与上图中0的条式图案一致。

  • Codabar的特征:Codabar的遗漏读取比ITF的要少。同CODE 39相比,条码尺寸也较小。但这并不总意味着Codabar就不存在遗漏读取。如果条码的打印质量不好,往往在以下情形中会出现遗漏读取。在这里插入图片描述
    为了避免遗漏读取,推荐采用和ITF一样的办法,把条码读取仪设置在"数位指定"功能上,只读取规定位数的数字。例如,A––––A用于罗列价格,A––––C用于特别折扣价格而C––––C为大减价。
  • Codabar的应用:应用于验血(标本)的试管上,以确定各个身份。

CodaBar解码方法——decodeRow

总的来说,所有一维码的解码思路都基本一致:
第一步:定位。条形码Reader扫描图像的整个宽度,试图识别是否有条形码候选对象——黑/白图。
第二步:解码。定位出条形码后开始解码。在这一步中,Reader它对图像像素进行计数和比较,以匹配开始和结束标识符。然后,它根据该代码类型的规范来解析开始标识符和结束标识符之间的模式,以解开编码的数据。

  @Override
  public Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) throws NotFoundException {

    Arrays.fill(counters, 0);
    // 记录所有白色和黑色像素的大小,从白色开始。这与recordPattern类似,只是它记录所有计数器,并使用内置的“counters”成员进行存储。在这个方法中初始化counterLength大小。
    setCounters(row);
    // 设置起始偏移量。在开始模式之前查找空白(>=开始模式宽度的50%,如上面的图所示,ABCD的起始位置都是窄框)。
    int startOffset = findStartPattern();
    int nextStart = startOffset;

    decodeRowResult.setLength(0);
    do {
      int charOffset = toNarrowWidePattern(nextStart);
      if (charOffset == -1) {
        throw NotFoundException.getNotFoundInstance();
      }
      //我们将字母表中的位置存储到StringBuilder中,以便在validatePattern中访问解码的模式。我们稍后将转换为实际字符。
      decodeRowResult.append((char) charOffset);
      nextStart += 8;
      // 我们一看到结束字符就停止。arrayContains方法判断元素是否在数组中
      if (decodeRowResult.length() > 1 &&
          arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) {
        break;
      }
    } while (nextStart < counterLength); // 直到超出长度范围停止

    // 查找空格
    int trailingWhitespace = counters[nextStart - 1];
    int lastPatternSize = 0;
    for (int i = -8; i < -1; i++) {
      lastPatternSize += counters[nextStart + i];
    }

    // 我们需要看到空白大于等于最后一个图案大小的50%,否则这可能是误报。
    if (nextStart < counterLength && trailingWhitespace < lastPatternSize / 2) {
      throw NotFoundException.getNotFoundInstance();
    }
    // 验证startOffset
    validatePattern(startOffset);

    // 将字符表偏移量转换为当前字符。
    for (int i = 0; i < decodeRowResult.length(); i++) {
      decodeRowResult.setCharAt(i, ALPHABET[decodeRowResult.charAt(i)]);
    }
    // Ensure a valid start and end character
    // 确保起始字符和结束字符有效
    char startchar = decodeRowResult.charAt(0);
    if (!arrayContains(STARTEND_ENCODING, startchar)) {
      throw NotFoundException.getNotFoundInstance();
    }
    char endchar = decodeRowResult.charAt(decodeRowResult.length() - 1);
    if (!arrayContains(STARTEND_ENCODING, endchar)) {
      throw NotFoundException.getNotFoundInstance();
    }
    // 删除停止/开始字符并检查是否包含足够长的字符串
    if (decodeRowResult.length() <= MIN_CHARACTER_LENGTH) {
      // Almost surely a false positive ( start + stop + at least 1 character)
      // 几乎肯定是误报(开始+停止+至少1个字符)
      throw NotFoundException.getNotFoundInstance();
    }
//RETURN_CODABAR_START_END表示如果为true,则返回Codabar条形码中的起始和结束数字,而不是将其剥离。它们是字母,而其余的是数字。
    if (hints == null || !hints.containsKey(DecodeHintType.RETURN_CODABAR_START_END)) {
      decodeRowResult.deleteCharAt(decodeRowResult.length() - 1);
      decodeRowResult.deleteCharAt(0);
    }

    int runningCount = 0;
    for (int i = 0; i < startOffset; i++) {
      runningCount += counters[i];
    }
    float left = runningCount;
    for (int i = startOffset; i < nextStart - 1; i++) {
      runningCount += counters[i];
    }
    float right = runningCount;

    Result result = new Result(
        decodeRowResult.toString(),
        null,
        new ResultPoint[]{
            new ResultPoint(left, rowNumber),
            new ResultPoint(right, rowNumber)},
        BarcodeFormat.CODABAR);
    //SYMBOLOGY_IDENTIFIER条形码符号标识符。注:根据GS1规范,在条形码内容前加前缀时,标识符可能必须替换前导FNC1/GS字符。
    result.putMetadata(ResultMetadataType.SYMBOLOGY_IDENTIFIER, "]F0");
    return result;
  }

五、一维码和二维码区别

  • 可以看到一维码没有纠错机制,如果一维码有破损,就不能被读取;对于二维码来说,即使有破损,也有可能可以正常读取。
  • 一维码只能在水平方向单向的表达商品信息,而在垂直方向则不表达任何信息,它的一定高度通常是为了便于条码设备的对准,读取。而二维码在水平和垂直方向都可表达信息,也就是说它在二维空间内存储信息。
  • 由于只有水平方向有信息,一维码扫码逻辑很简单。

欢迎提出宝贵意见,感谢观看!
参考:
ZxingAPI
Codabar介绍

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-12-08 13:38:17  更:2021-12-08 13:40:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:35:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码