使用Zbar定位、识别二维码

之前整个项目用的是Java,结果在树莓派上运行速度很慢。现在要用C++来改写。Java平台上我是使用ZXing来识别二维码的,ZXing是一个纯Java实现,所以也就没法用在C++中。于是我找到了Zbar,它的核心是用C写的。据说Zbar的性能是ZXing的两倍以上~

要使用Zbar,首先肯定是要安装Zbar库。当然也可以使用源码安装,虽然我没有成功......

sudo apt install libzbar-dev

之后,就可以编写一个测试代码:

scan_image.c

#include <stdio.h>
#include <stdlib.h>
#include <png.h>
#include <zbar.h>

#define zbar_fourcc(a, b, c, d)                 \
        ((unsigned long)(a) |                   \
         ((unsigned long)(b) << 8) |            \
         ((unsigned long)(c) << 16) |           \
         ((unsigned long)(d) << 24))

#if !defined(PNG_LIBPNG_VER) || \
    PNG_LIBPNG_VER < 10018 ||   \
    (PNG_LIBPNG_VER > 10200 &&  \
     PNG_LIBPNG_VER < 10209)
  /* Changes to Libpng from version 1.2.42 to 1.4.0 (January 4, 2010)
   * ...
   * 2. m. The function png_set_gray_1_2_4_to_8() was removed. It has been
   *       deprecated since libpng-1.0.18 and 1.2.9, when it was replaced with
   *       png_set_expand_gray_1_2_4_to_8() because the former function also
   *       expanded palette images.
   */
#define png_set_expand_gray_1_2_4_to_8 png_set_gray_1_2_4_to_8
#endif

zbar_image_scanner_t *scanner = NULL;

/* to complete a runnable example, this abbreviated implementation of
 * get_data() will use libpng to read an image file. refer to libpng
 * documentation for details
 */
static void get_data (const char *name,
                      int *width, int *height,
                      void **raw)
{
    FILE *file = fopen(name, "rb");
    if(!file) exit(2);
    png_structp png =
        png_create_read_struct(PNG_LIBPNG_VER_STRING,
                               NULL, NULL, NULL);
    if(!png) exit(3);
    if(setjmp(png_jmpbuf(png))) exit(4);
    png_infop info = png_create_info_struct(png);
    if(!info) exit(5);
    png_init_io(png, file);
    png_read_info(png, info);
    /* configure for 8bpp grayscale input */
    int color = png_get_color_type(png, info);
    int bits = png_get_bit_depth(png, info);
    if(color & PNG_COLOR_TYPE_PALETTE)
        png_set_palette_to_rgb(png);
    if(color == PNG_COLOR_TYPE_GRAY && bits < 8)
        png_set_expand_gray_1_2_4_to_8(png);
    if(bits == 16)
        png_set_strip_16(png);
    if(color & PNG_COLOR_MASK_ALPHA)
        png_set_strip_alpha(png);
    if(color & PNG_COLOR_MASK_COLOR)
        png_set_rgb_to_gray_fixed(png, 1, -1, -1);
    /* allocate image */
    *width = png_get_image_width(png, info);
    *height = png_get_image_height(png, info);
    *raw = malloc(*width * *height);
    png_bytep rows[*height];
    int i;
    for(i = 0; i < *height; i++)
        rows[i] = *raw + (*width * i);
    png_read_image(png, rows);
}

int main (int argc, char **argv)
{
    if(argc < 2) return(1);

    /* create a reader */
    scanner = zbar_image_scanner_create();

    /* configure the reader */
    zbar_image_scanner_set_config(scanner, 0, ZBAR_CFG_ENABLE, 1);

    /* obtain image data */
    int width = 0, height = 0;
    void *raw = NULL;
    get_data(argv[1], &width, &height, &raw);

    /* wrap image data */
    zbar_image_t *image = zbar_image_create();
    zbar_image_set_format(image, zbar_fourcc('Y','8','0','0'));
    zbar_image_set_size(image, width, height);
    zbar_image_set_data(image, raw, width * height, zbar_image_free_data);

    /* scan the image for barcodes */
    int n = zbar_scan_image(scanner, image);

    /* extract results */
    const zbar_symbol_t *symbol = zbar_image_first_symbol(image);
    for(; symbol; symbol = zbar_symbol_next(symbol))
    {
        /* do something useful with results */
        zbar_symbol_type_t typ = zbar_symbol_get_type(symbol);
        const char *data = zbar_symbol_get_data(symbol);
        printf("decoded %s symbol \"%s\"\n",zbar_get_symbol_name(typ), data);
        int pointCount=zbar_symbol_get_loc_size(symbol);
        printf("point count: %d\n",pointCount);
        int i;
        for(i=0;i<pointCount;i++)
        {
            int x=zbar_symbol_get_loc_x(symbol,i);
            int y=zbar_symbol_get_loc_y(symbol,i);
            printf("point%d=(%d,%d)\n",i,x,y);
        }
    }
    
    /* clean up */
    zbar_image_destroy(image);
    zbar_image_scanner_destroy(scanner);

    return(0);
}

这段代码是我基于官方给的examples/scan_image.c略做修改得到的。官方的最新代码中,zbar_fourcc这个宏定义是zbar.h自带的,可是我通过apt安装的zbar库可能版本落后,没有这个宏定义,所以我就只能复制了一份过来。

我承认一开始看这段代码还是有点抵触的,但是研究透彻了以后觉得还是很清晰的。这段代码获取命令行的参数作为一个文件名,然后使用png格式读取图片,并转换成一张灰度图,然后使用zbar库来识别图中的码(可能是条形码也可能是二维码),依次输出每一个码的类型、内容和定位点。

gcc scan_image.c -lpng -lzbar -o scan_image

一句命令就可以编译这段代码了,然后产生一个scan_image可执行文件。接下来准备一张包含二维码的png图片,比如下面这张:

1.png

然后运行scan_image:

./scan_image 1.png

可以看到如下输出:

说明zbar成功识别了图中的二维码。当然,还可以使用这一张P过的图片:

3.png

然后运行scan_image:

./scan_image 3.png

可以看到如下输出:

如果读了get_data()的代码,那么可以发现它的作用是把一张png图像转成了raw数组,而这个raw数组是这么来的:

void *raw = NULL;

//...

*raw = malloc(*width * *height);

而*width和*height分别是图像的宽和高。因此,一个字节代表了一个像素,所以我立刻猜测这个字节很可能就是这个像素的灰度值。接下来我做了一个实验来验证我的想法。我在

get_data(argv[1], &width, &height, &raw);

之后添加了这么一段代码:

printf("size=%dx%d\n",width,height);
int x,y;
for(y=0;y<10;y++)
{
    for(x=0;x<10;x++)
    {
        int pixel=((unsigned char*)raw)[y*width+x];
        printf("%x ",pixel);
    }
    printf("\n");
}

这段代码先打印图片的宽和高,然后把最左上角一个10x10的方块内的值打印出来。接着,我把1.png中(0,0)位置的像素点涂成了#808080,(1,0)位置的像素点涂成了#ababab,(2,0)位置的像素点涂成了#ff0000。如图:

再次运行,程序输出结果为:

很明显,raw数组中保存的确实是灰度矩阵。至于RGB是用何种算法变成灰度的,还需要具体研究一下。不过至此已经能够知道,只要获取了一个灰度矩阵,就能用Zbar方便地解析其中的条码或者二维码了。