找回密码
 会员注册
查看: 12|回复: 0

有赞零售小票打印图片二值化方案

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
71625
发表于 2024-10-8 12:33:19 | 显示全部楼层 |阅读模式
文 | 王前 on 有赞零售一、背景小票打印是零售商家的基础功能,在小票信息中,必然会存在一些相关店铺的信息。比如,logo 、店铺二维码等。对于商家来说,上传 logo 及店铺二维码时,基本都是彩图,但是小票打印机基本都是只支持黑白二值图打印。为了商家的服务体验,我们没有对商家上传的图片进行要求,商家可以根据实际情况上传自己的个性化图片,因此就需要我们对商家的图片进行二值图处理后进行打印。这次文章是对《有赞零售小票打印跨平台解决方案》中的图片的二值图处理部分的解决方案的说明。二、图像二值化处理流程图像二值化就是将图像上的像素点的灰度值(如果是 RGB 彩图,则需要先将像素点的 RGB 转成灰度值)设置为 0 或 255 ,也就是将整个图像呈现出明显的黑白效果的过程。其中划分 0 和 255 的中间阈值 T 是二值化的核心,一个准确的阈值可以得到一个较好的二值图。二值化整体流程图:从上面的流程图中可以看出,获取灰度图和计算阈值 T 是二值化的核心步骤。三、以前的解决方案以前使用的方案是,首先将图像处理成灰度图,然后再基于 OTSU(大津法、最大类间方差法)算法求出分割 0 和 255 的阈值 T ,然后根据 T 对灰度值进行二值化处理,得到二值图像。我们的所有算法都有使用 C 语言实现,目的为了跨平台通用性。流程图:灰度算法:对于 RGB 彩色转灰度,有一个很著名的公式:Gray = R * 0.299 + G * 0.587 + B * 0.114这种算法叫做 Luminosity,也就是亮度算法。目前这种算法是最常用的,里面的三个数据都是经验值或者说是实验值。由来请参见 wiki。然而实际应用时,大家都希望避免低速的浮点运算,为了提高效率将上述公式变形成整数运算和移位运算。这里将采用移位运算公式:Gray = (R * 38 + G * 75 + B * 15) >> 7如果想了解具体由来,可以自行了解,这里不做过多解释。具体实现算法如下:/** 获取灰度图 @param bit_map 图像像素数组地址( ARGB 格式) @param width 图像宽 @param height 图像高 @return 灰度图像素数组地址 */int * gray_image(int *bit_map, int width, int height) { double pixel_total = width * height; // 像素总数 if (pixel_total == 0) return NULL; // 灰度像素点存储 int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i > 24); u_char red = ((*p & 0xFF0000) >> 16); u_char green = ((*p & 0x00FF00) >> 8); u_char blue = (*p & 0x0000FF); u_char gray = (red*38 + green*75 + blue*15) >> 7; if (alpha == 0 & gray == 0) { gray = 0xFF; } gray_pixels[i] = gray; } return gray_pixels;}该算法中,主要是为了统一各平台的兼容性,入参要求传入 ARGB 格式的 bitmap 。为什么使用 int 而不是用 unsigned int,是因为在 java 中没有无符号数据类型,使用 int 具有通用性。OTSU 算法:OTSU 算法也称最大类间差法,有时也称之为大津算法,由大津于 1979 年提出,被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。原理:对于图像 I ( x , y ) ,前景(即目标)和背景的分割阈值记作 T ,属于前景的像素点数占整幅图像的比例记为 ω0 ,其平均灰度 μ0 ;背景像素点数占整幅图像的比例为 ω1 ,其平均灰度为 μ1 。图像的总平均灰度记为 μ ,类间方差记为 g 。假设图像的背景较暗,并且图像的大小为 M × N ,图像中像素的灰度值小于阈值 T 的像素个数记作 N0 ,像素灰度大于等于阈值 T 的像素个数记作 N1 ,则有:ω0 = N0 / M × N (1)ω1 = N1 / M × N (2)N0 + N1 = M × N (3)ω0 + ω1 = 1 (4)μ = ω0 * μ0 + ω1 * μ1 (5)g = ω0 * (μ0 - μ)^2 + ω1 * (μ1 - μ)^2 (6)将式 (5) 代入式 (6) ,得到等价公式:g = ω0 * ω1 * (μ0 - μ1)^2    (7)公式 (7) 就是类间方差计算公式,采用遍历的方法得到使类间方差 g 最大的阈值 T ,即为所求。因为 OTSU 算法求阈值的基础是灰度直方图数据,所以使用 OTSU 算法的前两步:获取原图像的灰度图灰度直方统计这里需要多次对图像进行遍历处理,如果每一步都单独处理,会增加不少遍历次数,所以这里做了步骤整合处理,减少不必要的遍历,提高性能。具体实现算法如下:/** OTSU 算法获取二值图 @param bit_map 图像像素数组地址( ARGB 格式) @param width 图像宽 @param height 图像高 @param T 存储计算得出的阈值 @return 二值图像素数组地址 */int * binary_image_with_otsu_threshold_alg(int *bit_map, int width, int height, int *T) { double pixel_total = width * height; // 像素总数 if (pixel_total == 0) return NULL; unsigned long sum1 = 0; // 总灰度值 unsigned long sumB = 0; // 背景总灰度值 double wB = 0.0; // 背景像素点比例 double wF = 0.0; // 前景像素点比例 double mB = 0.0; // 背景平均灰度值 double mF = 0.0; // 前景平均灰度值 double max_g = 0.0; // 最大类间方差 double g = 0.0; // 类间方差 u_char threshold = 0; // 阈值 double histogram[256] = {0}; // 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数 // 获取灰度直方图和总灰度 int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i > 24); u_char red = ((*p & 0xFF0000) >> 16); u_char green = ((*p & 0x00FF00) >> 8); u_char blue = (*p & 0x0000FF); u_char gray = (red*38 + green*75 + blue*15) >> 7; if (alpha == 0 & gray == 0) { gray = 0xFF; } gray_pixels[i] = gray; // 计算灰度直方图分布,Histogram 数组下标是灰度值,保存内容是灰度值对应像素点数 histogram[gray]++; sum1 += gray; } // OTSU 算法 for (u_int i = 0; i = max_g) { threshold = i; max_g = g; } } for (u_int i = 0; i > 24); u_char red = ((*p & 0xFF0000) >> 16); u_char green = ((*p & 0x00FF00) >> 8); u_char blue = (*p & 0x0000FF); u_char gray = (red*38 + green*75 + blue*15) >> 7; if (alpha == 0 & gray == 0) { gray = 0xFF; } gray_pixels[i] = gray; sum += gray; } // 计算平均灰度 threshold = sum / pixel_total; for (u_int i = 0; i 2) return 0; } } return peak_count == 2;}/** 双峰平均值算法获取二值图 @param bit_map 图像像素数组地址( ARGB 格式) @param width 图像宽 @param height 图像高 @param T 存储计算得出的阈值 @return 二值图像素数组地址 */int * binary_image_with_average_peak_threshold_alg(int *bit_map, int width, int height, int *T) { double pixel_total = width * height; // 像素总数 if (pixel_total == 0) return NULL; // 灰度直方图,下标是灰度值,保存内容是灰度值对应的像素点总数 double histogram1[256] = {0}; double histogram2[256] = {0}; // 求均值的过程会破坏前面的数据,因此需要两份数据 u_char threshold = 0; // 阈值 // 获取灰度直方图 int *gray_pixels = (int *)malloc(pixel_total * sizeof(int)); memset(gray_pixels, 0, pixel_total * sizeof(int)); int *p = bit_map; for (u_int i = 0; i > 24); u_char red = ((*p & 0xFF0000) >> 16); u_char green = ((*p & 0x00FF00) >> 8); u_char blue = (*p & 0x0000FF); u_char gray = (red*38 + green*75 + blue*15) >> 7; if (alpha == 0 & gray == 0) { gray = 0xFF; } gray_pixels[i] = gray; // 计算灰度直方图分布,Histogram数组下标是灰度值,保存内容是灰度值对应像素点数 histogram1[gray]++; histogram2[gray]++; } // 如果不是双峰,则通过三点求均值来平滑直方图 int times = 0; while (!is_double_peak(histogram2)) { times++; if (times > 1000) { // 这里使用 1000 次,考虑到过多次循环可能会存在性能问题 return NULL; // 似乎直方图无法平滑为双峰的,返回错误代码 } histogram2[0] = (histogram1[0] + histogram1[0] + histogram1[1]) / 3; // 第一点 for (int i = 1; i = 64 。建议为 64 的倍数,为了减少误差。5.哈希值不是一个 64 位的整数,而是一个存储 64 位整数的数组,数组的长度就是像素点数量对 64 的倍数(取最大的整数倍)。这样每生成一个 64 位的 hashCode 就加入到数组中,该数组就是图片指纹。6.对比不同指纹时,遍历数组,对每一个 64 为整数进行对比不同位数,最终结果为,每一个 64 位整数的不同位数总和。在我们对商家 logo 测试实践中发现,采用 128 * 128 的压缩,可以得到比较满意的结果。最优算法为 OTSU 算法例子:最优算法为平均灰度算法例子:最优算法为双峰均值算法例子:实际实验中,发现真是中选择双峰均值的概率比较低,也就是绝大多数的 logo 都是在 OTSU 和平均灰度两个算法之间选择的。所以,后续可以考虑加入选择统计,如果双峰均值概率确实特别低且结果与其他两种差不多大,那就可以去掉该方法。4.2 问题二(每次打印都对图片进行处理,没有缓存机制)加入缓存机制,一般店铺的 logo 和店铺二维码都是固定的,很少会更换,所以,在进入店铺和修改店铺二维码时可以对其进行预处理,并缓存处理后的图片打印指令,后续打印时直接拿缓存使用即可。由于缓存的内容是处理后的打印指令字符串,所以使用 NSUserDefaults 进行存储。缓存策略流程图:这里面为什么只有修改店铺二维码,而没有店铺 logo ?因为在我们 app 中,logo 是不可修改的,只能在 pc 后台修改,而登录店铺后,本地就可以直接拿到店铺信息;店铺二维码是在小票模板设置里自行上传的图片,所以商家在 app 中是可以自行修改店铺二维码的。打印时图片处理流程图:在新流程中,如果缓存中没有查到,则会走老方案去处理图片。原因是考虑到,这时候是商家实时打印小票,如果选用新方案处理,恐怕时间会加长,使用户体验降低。老方案已经在线上跑了很久,所以使用老的方案处理也问题不大。五、未来期望与规划在后续规划中加入几点优化:添加新流程处理统计,对商家 logo 和店铺二维码处理后的最优算法进行统计,为后续优化做数据准备。处理后的结果如果商家不满意,商家可以自主选择处理二值图的阈值 T ,达到满意为止。图片更新不及时问题,PC 后台修改了图片无法及时更新本地缓存。图片精细化处理,针对二维码可以采用分块处理算法。其中第二点,商家自主选择阈值 T ,预览效果如下:参考链接灰度计算公式(https://www.cnblogs.com/zyl910/archive/2006/05/22/2186658.html), by zyl910大津算法(https://zh.wikipedia.org/wiki/%E5%A4%A7%E6%B4%A5%E7%AE%97%E6%B3%95), by wiki图像处理算法(https://www.cnblogs.com/Imageshop/p/3307308.html), by laviewpbt扩展阅读?WEB-UI 自动化实践有赞 Android 崩溃保护的探索及实践Under the Hood: NaN of JS有赞零售小票打印跨平台解决方案响应式架构与 RxJava 在有赞零售的实践-The End-Vol.173有赞技术团队为 300 万商家,150 个行业,200 亿电商交易额提供技术支持微商城|零售|美业微信公众号:有赞coder? ? 微博:@有赞技术技术博客:tech.youzan.comThe bigger the dream,?the more important the team.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-10 05:54 , Processed in 0.476474 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表