美化二维码

  • 二维码内容长度不作控制

      大多数互联网技术使用到二维码生成,都是使用开源的包生成,输入字符信息,即可生成一张包含此字符信息的黑白二维码图片:

美化二维码

 

美化二维码

      上面有两个二维码图片,扫码结果都是访问到同个页面,但是大多数使用二维码的地方,都是生成第一种样式,WC里的广告就是直接的例子:

美化二维码

 

      很明显,第一种二维码点阵非常复杂,在某种复杂的环境下,如扫码角度大、屏幕光线反光、光线明暗对比变化大、打印粘贴出去后污点影响,这时候扫码的图像识别会变得很困难,甚至根本扫不出来,直接原因是点阵太密,摄像头对点阵的误判超过码本身的容错率。

 

      第二种二维码点阵则相对简单很多,在同样环境复杂条件下,扫码几乎都没有难度,同样优等环境下,扫码读取的速度也会 快很多。

 

       这两种码本质上有什么区别呢?如果使用扫码解析出内容,你会发现内容不一样,扫出来结果分别是:

https://work.alibaba-inc.com/work/search?type=person&tabIndex=1&offset=0&isSingleTabSearch=1&filterParameters=%7B%22fieldindexes%22%3A%7B%22province%22%3A%22%E6%B5%99%E6%B1%9F%E7%9C%81%22%2C%22city%22%3A%22%E8%A1%A2%E5%B7%9E%E5%B8%82%22%7D%7D

 

http://ma.taobao.com/ZSk4YV

 

 

 

       现在可以很清楚看到,第二个码是使用了短地址跳转,短地址的URL字符长度特别短,使得码的占阵变得很简单,究其原因,要了解下二维码的版本,最早的版本1只有17x17的黑白点阵,到现在最大的版本40有177x177点阵,每种版本的信息容量有限,版本越高容量越大。二维码编码过程中先是对数据进行编码,然后对照数据需要的容量来选择版本,字符长度越短,当然需要版本就越低。

 

      另外,参照《QR Code Specification》里数据编码章节,你会发现数字编码(Numeric mode )和大写字母编码(Alphanumeric mode)所占的比特位特别少,在某些条件允许的情况下,可以尽量使用数字或大写字母来作来码信息内容载体,这样的二维码生成会更加简单一些。

 

 

  • 二维码大小随意设置

      由于交互设计师的需要,经常听到运营、交互设计同学让开发在产品中生成的二维码尺寸固定为多少长宽,比如171x171相素(不带边框)的二维码,这个需求是否能实现呢?事实上,如果按照标准二维码规范,这样大小的二维码无法生成,即使不按标准尺寸生产出来码能被扫码识别,也是不推荐的(只是利用了扫码的一些容错能力达到扫码结果,但是会影响扫码响应、容错等,我们希望是越优越好),具体原因如下:

       二维码最早版本是17x17大小点阵,后面每增加一版本,点阵都是以4点阵增加,某一版本的点阵计算公式是:(V-1)*4 + 21(V是版本号),按照上面170x170大小计算:

版本3 29点阵x5放大= 145相素

版本3 29点阵x6放大= 174相素

版本4 33点阵x5放大= 165相素

版本4 33点阵x6放大= 198相素

...

 

       你会发现,怎么也算不出有171相素大小的二维码,除非你加上边框拼凑。

  • 二维码logo随添加

      你会很多品牌、产品为了推广自己,往往会在二维码中间或边角加上logo:

美化二维码

 

美化二维码

 

      当然,为了突出logo,你可能会这样加:

美化二维码

 

      这时你会发现,二维码扫不出来了。二维码规范里根本没有logo这个概念,加logo是利用二维码容错率这个功能,生成二维码时,可以设置二维码的容错率(某些地方也叫容错级别),即对二维码点阵破坏纠错能力,分为:L、M、Q、H级别,级别越高,容错能力越大,当然点阵也会更加复杂,而加logo,对二维码来说,其实是对数据的一种污染,当染污破坏数据数超过当前设置的容错级别时,数据就不能再被纠错读取出来了。

 

      在设计视觉效果前提下,建议logo越小越好,这样对点阵数据破坏小,能更好的被读取。如果对二维码解码了解的话,你可以兼顾效果和纠错,设计出如下的二维码视觉(图像和点阵复用):

美化二维码

 

  • 二维码不设置边框

二维码扫码器读取二维码有一步是读取定位符(即三个角上的矩形),定位符的识别,是按照黑白点阵比例(1:1:3:1:1)来确定定位符:

美化二维码

      要确定此比例,得先找到比例起止点(即上图中的A和B),标准黑白二维码就是二维码最边上的黑白交界处,实际二维码可能不是纯黑白,但是灰阶处理后还是按黑白来判断,假如你生成的二维码没有边框,贴在浅色背景上,扫码没问题,背景本身是浅色,灰阶等处理后,还是一样找到边界,假如背景是深色的,扫码识别就很难定位到边界了:

 

美化二维码

 

对扫码器来说,图像处理二值化后,背景颜色会被认为黑色,二维码定位符起点也是黑色,没有边界了,就无支法确定定位符位置。

目前主流的一些APP扫码器,如手淘、微信,都修复增加了这种情况的容错能力,能够用其他图像定位算法识别二维码,虽然速度可能会有影响(影响下一般感觉不出来),但是不排除有些旧的扫码SDK无法读取,或读取速度影响很大。

       建议如果把二维码加入到深色背景上时,请加上浅色的边框,边框大小大于等于单位点阵大小为佳。

 

 

样式改进

  • 尺寸更改

      按照二维码规范,最小版本是17x17的点阵,最大是177x177点阵,常用的版本3和版本4大小也就分别是29和33个单位点阵,如果直接转成位图相素点的话,这么小是很难用扫码器扫出来的,存储或网络传输时可以考虑直接点阵存储,显示或打印出来,一般要按照固定比例放大,单个点单位长宽高等比放大即可。

  • 矢量图

     一般图形设计时会会用到矢量图,如果是正方形的单位点,一般来说位图直接放大图像也不会有什么失真,因为单位点都是黑白点,很多处理软件放大时进行插值算法(插入位图相素点),这种矩形失真很少。

     规范图形处理还是要使用到矢量图,一般黑白二维码矢量生成都采用直接EPS文件结构生成代码:

    /**
     * Constructs an empty EpsDevice that writes directly to a file. Bounds must
     * be set before use.
     */

    EpsDocument(String title, OutputStream outputStream, int minX, int minY, int maxX, int maxY) throws IOException {
       
this.title = title;
       
this.minX = minX;
       
this.minY = minY;
       
this.maxX = maxX;
       
this.maxY = maxY;
        bufferedWriter =
new BufferedWriter(new OutputStreamWriter(outputStream));
       
this.stream = outputStream;
       
write(bufferedWriter);
    }
   
/**
     * Outputs the contents of the EPS document to the specified Writer,
     * complete with headers and bounding box.
     */

    public synchronized void write(Writer writer) throws IOException {
       
float offsetX = -minX;
       
float offsetY = -minY;
        writer.
write("%!PS-Adobe-3.0 EPSF-3.0\n");
        writer.
write("%%Creator: EpsGraphics " + EpsGraphics.VERSION
                +
" by M.Taobao.Com, http://www.taobao.com/\n");
        writer.
write("%%Title: " + title + "\n");
        writer.
write("%%CreationDate: " + new Date() + "\n");
        writer.
write("%%BoundingBox: 0 0 " + ((int) Math.ceil(maxX + offsetX)) + " "
                + ((int) Math.ceil(maxY + offsetY)) + "\n");
        writer.
write("%%DocumentData: Clean7Bit\n");
        writer.
write("%%LanguageLevel: 2\n");
        writer.
write("%%DocumentProcessColors: Black\n");
        writer.
write("%%ColorUsage: Color\n");
        writer.
write("%%Origin: 0 0\n");
        writer.
write("%%Pages: 1\n");
        writer.
write("%%Page: 1 1\n");
        writer.
write("%%EndComments\n\n");
        writer.
write("gsave\n");
        writer.
write(offsetX + " " + (maxY + offsetY) + " translate\n");
        writer.flush();

writer.write("grestore\n");
       
if (isClipSet()) {
            writer.
write("grestore\n");
        }
        writer.
write("showpage\n");
        writer.
write("\n");
        writer.
write("%%EOF");
        writer.flush();
    }

       这个生成的二维码矢量是黑白的,在某些设计上需要其他色彩的二维码,就不能用这个生成了,可以先找到点阵的向量信息,再用画矢量的一些工具(如EpsGraphics)进行绘制,绘制时可以填充需要的颜色,这里提供一种简易找到点阵矩形向量信息的算法:

       1、把二维码点阵循环进行逐行处理,找到黑块的起止点,以线条为单位保存起来(保存起点坐标信息和终点坐标信息),直至最后一行结束;

       2、把相同横坐标的起点和终点且纵坐标相同的线条合到一起,即找到黑块的矩形坐标向量集合。

       以上绘制二维码只适合简单矩形或圆形的单位点阵二维码,如果涉及复杂的背景图,目前没有很好的矢量化方法,一般只能使用图形设计软件来人工制作了,如果您有把复杂图形矢量化的程序实现方法,欢迎提供。

  • 点阵样式美化

            点阵美化是常见的二维码美化手段,下面可以例举一些例子:

美化二维码

 

美化二维码

 

美化二维码

 

      此类样式的处理方法也不复杂, 主要是看画图的算法实现,一般是先使用开源软件得到二维码原始二维占阵数据BitMatrix[][],然后通过特定的算法,逐个绘制定位符或者把黑点转成彩色圆点。

      这里提供一个绘制定位符和黑点算法例子的部分java代码,国外网站上有些其他语言实现的算法,有兴趣的可以研究下。

        /**
         * 在指定位置绘制定位符
         *
         * @param startX 起点坐标X
         * @param startY 起点坐标Y
         * @param joinType 绘制类型 方角,圆角,斜角,
         * @param argb 绘制颜色
         * @param pointSize
         * @return 绘制是否成功
         */

        private boolean drawPartern(int startX, int startY, int joinType,int[] rgba, int pointSize, int makeMore) {
                
if (rgba == null || rgba.length != 4) {
                        
log.error("绘制颜色不对,必须为RGBA数组");
                        
return false;
                }
                
this.smooth();
                
this.strokeWeight(pointSize);
                
this.strokeJoin(joinType);
                
// this.noFill();
                this.fill(rgba[0], rgba[1], rgba[2], rgba[3]);
                
this.stroke(rgba[0], rgba[1], rgba[2], rgba[3]);
                
int xx = startX;
                
int yy = startY;
                
this.rect(xx + makeMore * pointSize + pointSize / 2, yy + makeMore
                                * pointSize + pointSize /
2, pointSize * 6, pointSize * 6);
                
this.stroke(255f);
                
this.strokeJoin(joinType);
                
this.fill(255f);
                
this.rect(xx + (makeMore + 1) * pointSize + pointSize / 2, yy
                                + (makeMore +
1) * pointSize + pointSize / 2, pointSize * 4,
                                pointSize *
4);
                
this.fill(rgba[0], rgba[1], rgba[2], rgba[3]);
                
this.stroke(rgba[0], rgba[1], rgba[2], rgba[3]);
                
this.strokeJoin(joinType);
                
this.rect(xx + (makeMore + 2) * pointSize + pointSize / 2, yy
                                + (makeMore +
2) * pointSize + pointSize / 2, pointSize * 2,
                                pointSize *
2);
                
// drawPartenInner(startX, startY, joinType, rgba, pointSize, makeMore);

return true;
        }

 

        /**
         * 绘制自定义圆角填充方形
         * @param xx 中心点X
         * @param yy 中心点y
         * @param asize 总大小
         * @param rsize 圆角半径
         * @param r 颜色R
         * @param g 颜色G
         * @param b 颜色B
         * @param arf 颜色透明度
         */

        private void drawRoundRect(float xx, float yy, float asize, float rsize,float r, float g, float b, float alpha) {
                
this.noStroke();
                
this.smooth();
                
float ax;
                
float ay;
                
// 四个90度扇形
                ax = xx - asize / 2 + rsize;
                ay = yy - asize /
2 + rsize;
                
this.arc(ax, ay, 2 * rsize, 2 * rsize, PI, PI + PI / 2);
                ax = xx + asize /
2 - rsize;
                ay = yy - asize /
2 + rsize;
                
this.arc(ax, ay, 2 * rsize, 2 * rsize, PI + PI / 2, 2 * PI);
                ax = xx - asize /
2 + rsize;
                ay = yy + asize /
2 - rsize;
                
this.arc(ax, ay, 2 * rsize, 2 * rsize, PI / 2, PI);
                ax = xx + asize /
2 - rsize;
                ay = yy + asize /
2 - rsize;
                
this.arc(ax, ay, 2 * rsize, 2 * rsize, 0, PI / 2);

ax = xx - asize / 2 + rsize;
                ay = yy - asize /
2;
                
this.rect(ax - 1, ay, asize - 2 * rsize + 2, rsize + 1);// 上沿
                ay = yy + asize / 2 - +rsize;
                
this.rect(ax - 1, ay - 1, asize - 2 * rsize + 2, rsize + 1);// 下沿
                ax = xx - asize / 2;
                ay = yy - asize /
2 + rsize;
                
this.rect(ax, ay - 1, asize, asize - 2 * rsize + 2);// 中间区块
        }

public void drawContent() {
                
boolean[][] nMatrix = getBitMatrix();
                
boolean[][] cMatrix = nMatrix;
                
if (getBitMatrix() == null) {
                        
// 矩阵没有生成
                        log.error("生成错误");
                        
return;
                }
                
int 拓展 = super.getDilatation();
                
int 码宽 = this.getBitMatrix().length;
                
float 雕刻宽度 = super.getUnitSize();

CImage img = this.loadImageJarIO(this.getBackGroundImage());
                
this.size(img.width, img.height);
                
this.image(img, 0, 0, img.width, img.height);

int xx = (img.width - this.getArctualSize()) / 2;
                
int yy = xx;
                
// 定位框
                float[] rgba = { 151, 56, 124, 255 };
                
// 151, 56, 124
                float[] rgbb = { 255, 126, 141, 255 };
                
float[] rgbc = getAvP(rgba, rgbb, 6f / (码宽 - 2 * this.getDilatation()));

this.noStroke();
                drawParternRound(xx, yy,
this.ROUND, rgbc, 雕刻宽度, 拓展);
                drawParternRound(xx + 雕刻宽度 * (码宽 -
7 - 2 * 拓展), yy, this.ROUND, rgbc,雕刻宽度, 拓展);
                rgbc = getAvP(rgba, rgbb,
0.9f);
                drawParternRound(xx, yy + 雕刻宽度 * (码宽 -
7 - 2 * 拓展), this.ROUND, rgbc,雕刻宽度, 拓展);

// 去掉定位符涂黑
                for (int i = 0; i < 7; i++) {
                        
for (int j = 0; j < 7; j++) {
                                nMatrix[i + 拓展][j + 拓展] =
false;
                                nMatrix[码宽 -
7 - 拓展 + i][j + 拓展] = false;
                                nMatrix[i + 拓展][码宽 -
7 - 拓展 + j] = false;
                        }
                }
                
this.noStroke();
                
float 半径 = 雕刻宽度 / 2;
                
for (int i = 0; i < 码宽; i++) {

for (int j = 0; j < 码宽; j++) {
                                
if (nMatrix[i][j]) {
                                        rgbc = getAvP(rgba,rgbb,(
float) (j - this.getDilatation())/ (码宽 - 2 * this.getDilatation()));
                                        
this.fill(rgbc[0], rgbc[1], rgbc[2], rgbc[3]);
                                        
this.ellipse(xx + i * 雕刻宽度 + 半径, yy + j * 雕刻宽度 + 半径, 雕刻宽度,雕刻宽度);
                                }
                        }
                }
        }
 

  • 背景图

      二维码加背景也是常见的样式改进,和二维码增加logo大同小异,说白了,就是二维码图片和背景图片合成,关键是设计背景图的色彩和二维码色彩配当,二维码生成注意前面提到的内容增加版本增加时边框会有变化,需要保证有深色背景时的边框大小,常见的洋葱头二维码就是一个简单的例子:

美化二维码

使用突破

  • 视觉效果

      黑白二维码到处可见,彩色二维码你一定也见过,但是下面这样的二维码,你不一定见过下面这样的二维码:

美化二维码

 

美化二维码

       这样的二维码是不是看起来美观多了?除了点阵外,还可以通过图片信息来给看到二维码的人传递更多的信息,

      上面这种二维码是在二维码规范基础上作了一定的改进,因为没有违反二维码规范,所以通用的二维码扫码器都能扫码识别出来。这两种图片其实处理的关键技部分都一样的,第二个会动的GIF图,只是多了GIF图拆帧、单帧图片处理、再合成,GIF拆帧都是有现成成熟的处理方法的。所以关键还是单帧处理,业界上有类似的处理方法,但是效果不是特别好:

 

美化二维码

 

      以上处理合成,不是整个图一次处理的,还是按照单位点来进行处理,因为要保证单位点处理完后的图片,在扫码器二值化后,识别出来和原始的二维码黑白点对应,这里二值化时,涉及到一个黑/白点阀值,所以得保证二维码和图像合成后的单位点处在阀值的左边还是右边。

      淘宝最新的处理技术和业务有点不同,采用视觉码最新技术:

美化二维码

处理的过程大致如下:

    1.背景图片与二维码重合部分处理成简单的bit流V0;

    2. V0 Xor Qrcode bit流(V1),异或得到中间变量V2;

    3. V1进行里德所罗门算法循环变换某些参数,得到变量结果V2’, V2’’, V2’’’…;

    4. 中间变量V2再和V2’, V2’’, V2’’’…一系列值参考,得到最优化的mask图像;

    5. mask图像和二维码bit流V1合并,得到视觉效果优化后的图像。

上面主要是使用到了德所罗门算法对数据的编码特性,二维码标准文档里生成纠错码写也是这样处理的,只是一般生成码实现,只是实现了一种纠结码生成方式,如zxing里的数据编码,这里处理要生成很多参选的V2’, V2’’, V2’’’…,所以效率上会有点影响。关于德所罗门算法有兴趣的同学可以深入了解下,这个技术使用还是很广泛的,比如碟片数据读取。

  • 动态视频

      和GIF一样,只是多了些视频处理技术,对视频单帧处理,增加二维码,然后再压制成视频,看起来就是一个视频了,由于压制视频比较耗资源,一般都是离线处理。

  • 隐藏信息

美化二维码

      

美化二维码

      以上两个二维码,用通码的扫码器或二维码解析出来的内容都是:http://s.tb.cn/3/H8R8Q/ZhpkPe ,但是第一个是正版商品二维码,第二个是盗版商从第一个解析出来内容,自己再生成赝品二维码,用手淘扫码,可以识别出是否是正品,原理是什么呢?

      二信码除了正常的信息编码到点阵外,还可以把一些隐藏信息加入到二维码上面。前面提到过,不同版本的二维码信息容量不同,如果指定使用某个版本的二维码时,需要编码的信息不需要此版本的容量这么大,这时候多余的信息容量可以存放特殊的一些信息--即隐藏信息。当然这个不是二维码的标准规范,生成的二维码标准信息部分可以被通用的扫码器识别,你加上去的隐藏信息,就得你自己来解码了。

      关于隐藏信息存放位置,想了解的同学,可以参考下《QR Code Specification》里面8.4.8章节和8.4.9章节关于数据编码的补齐码和终结符。

      隐藏信息可以使用到很多地方,因为信息编码是你生成的,所以别处无法得知和防造,可以保证二维码生成的唯一性:

      

美化二维码

     

      扫码得到商品信息判断真假就是一个典型的使用。

      如果盗版者用拍照或扫描原始二维码图,进行拷贝,也可以利用二维码的扫码次数进行次要验证,同一商品码是否被大量扫码,验证是否正品,一般正品卖出来后,扫码次数都是只有买家扫的少数几次。

      当然这种隐藏信息使用也有缺点,就是扫码需要你的专用SDK,不然无法读取,对于手淘扫码用户量这么大情况下,可以很好的利用起来。

其他二维码

       除了Qrcode之外,还有不少其他类型的二维码,如PDF417、QRCCode、Data Matrix、Maxi Code、Code 49、Code 16K、Code One等二维码,这些互联网上都有不少介绍,这里不再赘述。这里举几个比较形象的例子:

  • 彩链码

      彩链码是一种利用二维点阵再加上其中基本色彩(Blue、Red、Green、Black)作为信息载体,所以包含的信息长度有限,但是排列组合可以很多:

美化二维码

      上面的第一张图是最简单的彩链码,只有方块和颜色,再加上边框。第二三张是在第一张基础上的变形美化,扫码识别过程和第一种也是一样的,按照码的版本,对点阵图进行切割固定的块,然后对块主要色彩识别,这时块不是简单的0或1两进制了,而是4进制数据。

     这种二维码可以在符合点阵规律的基础上,适当发挥把各种图形融合进来,以达到码信息传递和视觉效果的并存。

     由于点阵不能太复杂,包含信息不多,所以不能直接表达复杂的信息,一般扫码后,需要通过这个特定排列的组合去服务端拿到真正的复杂数据信息。

      除了需在服务端访问外,还有的缺点是新型的码很难被用户认知和使用起来,一来图形和码融合太紧密,需要让用户知道可以当作码扫,另外这个需要专用的SDK才能扫,如果拥有海量用户的APP可以很好推广起来,如手淘、微信。

  • 4G

     4G码严格来说,不属于二维码范畴,属于图像特征信息扫描,不过利用了二维码定位符,扫码时将定位符内的图像裁剪然后进行图像特征处理,相对于整个图像处理地,这种方法提取的图像的特征会比较稳定,各种方式扫码得到的特征值都比较容易确定为某一值,容错能力比较强,生成码时,训练下扫码图像特征值,确定后与确定目标信息绑定,如商品连接,目前手淘扫码很快就支持4G码扫码:

 

美化二维码

       这种码对图像的破坏很小,生成的码非常美观,不过用户的认知性会比较差,在引导用户扫码上需要成本。