基于Java的EAN-13条形码识别 基于Java的EAN-13条形码识别 | Winnie’s 快乐星球

野生菜鸡技术员一位

桂ICP备2021005834号-1

基于Java的EAN-13条形码识别

导言:条形码技术实验,基于Java的EAN-13条码识别

实验原理

EAN-13简介

European Article Number (欧洲物品编码的缩写),其中共计13位代码的EAN-13是比较通用的一般终端产品的条形码协议和标准,主要应用于超级市场和其它零售业,因此这种是我们比较常见的,随便拿起身边的一个从超市买来的商品都可以从包装上看得到。

条码结构

EAN-13商品条码是表示EAN/UCC-13商品标识代码的条码符号,由左侧空白区起始符左侧数据符中间分隔符右侧数据符校验符终止符右侧空白区及供人识别字符组成。

EAN-13

标识符

  • 左侧空白区:位于条码符号最左侧与空的反射率相同的区域,其最小宽度为11个模块宽。
  • 起始符:位于条码符号左侧空白区的右侧,表示信息开始的特殊符号,由3个模块组成。
  • 左侧数据符:位于起始符右侧,表示6位数字信息的一组条码字符,由42个模块组成。
  • 中间分隔符:位于左侧数据符的右侧,是平分条码字符的特殊符号,由5个模块组成。
  • 右侧数据符:位于中间分隔符右侧,表示5位数字信息的一组条码字符,由35个模块组成。
  • 校验符:位于右侧数据符的右侧,表示校验码的条码字符,由7个模块组成。
  • 终止符:位于条码符号校验符的右侧,表示信息结束的特殊符号,由3个模块组成。
  • 右侧空白区:位于条码符号最右侧的与空的反射率相同的区域,其最小宽度为7个模块宽。为保护右侧空白区的宽度,可在条码符号右下角加“>”符号。
  • 供人识读字符:位于条码符号的下方,是与条码字符相对应的供人识别的13位数字,最左边一位称前置码。供人识别字符优先选用OCR-B字符集,字符顶部和条码底部的最小距离为0.5个模块宽。标准版商品条码中的前置码印制在条码符号起始符的左侧。

字符集

包括EAN-13在内的商品条码,每一条码数据字符由 2个条和 2个空 构成,每一条或空由1~4个模块组成,每一条码字符的总模块数为7

用二进制“1”表示条的模块,即黑色;用二进制“0”表示空的模块,即白色

条码的二进制表示方法有三个子集:A、B 和 C

数字字符 A 子集 B 子集 C 子集
0 0001101 0100111 1110010
1 0011001 0110011 1100110
2 0010011 0011011 1101100
3 0111101 0100001 1000010
4 0100011 0011101 1011100
5 0110001 0111001 1001110
6 0101111 0000101 1010000
7 0111011 0010001 1000100
8 0110111 0001001 1001000
9 0001011 0010111 1110100

A 子集中条码字符所包含的条的模块的个数为奇数,称为奇排列;

B、C 子集中条码字符所包含的条的模块的个数为偶数,称为偶排列。

前置码

EAN-13商品条码中的前置码不用条码字符表示,不包括在左侧数据符内

右侧数据符及校验符均用字符集中的 C 子集表示;

左侧数据符使用的子集取决于前置码的数值

下表中列出了左侧数据符的字符集的选择规则:

前置码 左1 左2 左3 左4 左5 左6
0 A A A A A A
1 A A B A B B
2 A A B B A B
3 A A B B B A
4 A B A A B B
5 A B B A A B
6 A B B B A A
7 A B A B A B
8 A B A B B A
9 A B B A B A

校验符

EAN-13商品条码中的校验符用字符集中的 C 子集表示,校验符的作用是检验前面12个数字是否正确,在条码机每次读入数据时,都会计算一次数据符的校验并与校验符进行比对。

校验符的计算方法非常简单,将12个数据符从左起将所有的奇数位相加得出一个数a,将所有的偶数位相加得出一个数b,然后将数b乘以3再与a相加得到数c,用10减去数c的个位数,如果结果不为10则检验符为结果本身,如果为10则检验符为0。

校验符的计算与本次实验无关,暂且不过多赘述。

识别原理

该识别主要应用于简单场景下的 EAN-13 条码识别,需要标准的 EAN-13 图片。

首先选取图片中间部分的一行像素点,由于起始符 “101” 是1个标准模块宽,于是先识别1个标准模块宽对应的像素点个数,随后依次识别左右数据区,获得编码数据后,对比 A、B、C 字符集得到解码后的数据(不包括前置位),再根据前6位数据所属的字符集,推出前置位,从而实现解码。

实验内容

定义全局变量

首先声明需要的全局变量

/** 自定义类 用于存储像素点的RGB值 */
class Color{
    int r;
    int g;
    int b;
}

/** 文件选择器 */
JFileChooser jfc = new JFileChooser();
/** 待识别的图片 */
BufferedImage image;
/** 图片长度 */
static int length;
/** 图片宽度 */
static int width;
/** 单位宽度 */
static int perLength = 0;

/** 字符集 */
String[] a = {"0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"};
String[] b = {"0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"};
String[] c = {"1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"};

/** 前置码集 */
String[] front = {"AAAAAA", "AABABB", "AABBAB", "AABBBA", "ABAABB", "ABBAAB", "ABBBAA","ABABAB","ABABBA","ABBABA"};

获得图片

获得需要识别的图片

/** 获取图片 */
public void loadImage(){
    jfc.setFileSelectionMode(JFileChooser.FILES_ONLY );
    jfc.setMultiSelectionEnabled(false);
    jfc.setFileFilter(new FileNameExtensionFilter("图片文件(*.png, *.jpg, *.jpeg)",
                                                  "png","jpg","jpeg"));
    File directory = new File("");
    jfc.setCurrentDirectory(new File(directory.getAbsolutePath()));
    int status = jfc.showOpenDialog(null);

    // 打开图片
    if(status==JFileChooser.APPROVE_OPTION) {
        File sourceImage = jfc.getSelectedFile();
        try {
            image = ImageIO.read(sourceImage);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

打开条码对话框

处理图片

对图片进行处理

public Color[] proc(){
    length = image.getHeight();
    width = image.getWidth();

    // 第一行条码
    Color[] color = new Color[width];

    // true 是白 false 是黑
    boolean last = true;
    boolean measure = true;

    // 获取第一行的像素值,并获取标准宽度
    for (int i = 0; i < width; i++){
        Color temp = new Color();
        int pixel = image.getRGB(i, length/2);
        temp.r = ((pixel >> 16) &  0xFF);
        temp.g = ((pixel >> 8) &  0xFF);
        temp.b = ((pixel &  0xFF));
        color[i] = temp;

        // 是否在获取标准宽度
        if (measure){
            if (temp.r == 255 || temp.g == 255 || temp.b == 255) {
                if (!last){
                    measure = false;
                }
                last = true;
            } else if (temp.r == 0 || temp.g == 0 || temp.b == 0){
                perLength++;
                last = false;
            }
        }
    }
    return color;
}

使用 BufferedImage 的 getRGB() 函数得到的值是 sRGB 色彩空间的值,想要得到我们日常生活中常见的 R、G、B 值,需要进行对位与的操作

temp.r = ((pixel >> 16) &  0xFF);
temp.g = ((pixel >> 8) &  0xFF);
temp.b = ((pixel &  0xFF));

该部分代码为获取标准模块宽

if (measure){
    if (temp.r == 255 || temp.g == 255 || temp.b == 255) {
        if (!last){
            measure = false;
        }
        last = true;
    } else if (temp.r == 0 || temp.g == 0 || temp.b == 0){
        perLength++;
        last = false;
    }
}

解码处理

EAN-13 共有 95个模块,除去起始符各 3 个模块、中间分隔符 5 个模块,剩余数据区共 84 个模块

// EN-13 共 84 块数据区
int[] data = new int[84 * perLength];
int[] pureData = new int[84];

于是我们在左右两侧数据区采集数据,范围为:

左侧数据区:count > 3 * perLength - 1 && count < 45 * perLength - 1

右侧数据区:count > 50 * perLength - 1 && count < 85 * perLength - 1

perLength 为上一步获取的标准模块宽

for (int i = 0; i < width; i++){
    if (!measure){
        if(color[i].r == 0 || color[i].g == 0 || color[i].b == 0){
            measure = true;
        }
    }

    if (measure){
        boolean range = count > 3 * perLength - 1 && count < 45 * perLength - 1
                || count > 50 * perLength - 1 && count < 85 * perLength - 1;
        if (range){
            if (color[i].r == 255 || color[i].g == 255 || color[i].b == 255) {
                data[start] = 0;
                start++;
            } else if (color[i].r == 0 || color[i].g == 0 || color[i].b == 0){
                data[start] = 1;
                start++;
            }
        }
        count++;
    }
}

在识别中,采集数据时,相同的数据会重复采集 perLength 次,所以使用下列代码来获取纯净的数据

// 获取纯净数据
for (int i = 0; i < pureData.length; i++) {
    pureData[i] = data[i * perLength];
}

接下来需要对纯净的数据 pureData 进行分析,由于每一条码数据字符由 2个条和 2个空 构成,每一条码字符的总模块数为7。所以以 7 个一组对数据进行分析,通过对比 A、B、C 字符集获得解码数据,存入code中,并用 sort 记录左侧数据区分别所属的字符集

int[] result = new int[12];
count = 1;
int size = 7;
StringBuilder code = new StringBuilder();
StringBuilder sort = new StringBuilder();

for (int i = 0; i < pureData.length; i++) {
    code.append(pureData[i]);
    if (((i+1) % size) == 0){
        for (int j = 0; j < a.length; j++){
            if (code.toString().equals(a[j])){
                if (count < 7){
                    sort.append("A");
                }
                result[count] = j;
                count++;
            } else if (code.toString().equals(b[j])){
                if (count < 7){
                    sort.append("B");
                }
                result[count] = j;
                count++;
            } else if (code.toString().equals(c[j])){
                if (count < 7){
                    sort.append("C");
                }
                result[count] = j;
                count++;
            }
        }
        code = new StringBuilder();
    }
}

通过对比前置码,获得前置码,并填充至 code 的首位

for (int i = 0; i < front.length; i++){
    if (sort.toString().equals(front[i])){
        result[0] = i;
    }
}

实验结果

原条码如下

待识别条码

解码结果

识别结果

解码成功!👍