二维码生成与检测

生成二维码

生成二维码主要用到 CIFilter,主要的函数如下

    CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
    [filter setDefaults];
    NSData *data = [@"微信 Yasic" dataUsingEncoding:NSUTF8StringEncoding];
    [filter setValue:data forKey:@"inputMessage"];
    [filter setValue:@"H" forKey:@"inputCorrectionLevel"];
    CIImage *outPutImage = [filter outputImage];

但是要注意的是,接下来如果直接用 CIImage 来生成 UIImage,得到的效果可能如下图所示

UIImage *result1 = [UIImage imageWithCIImage:outPutImage];
UncorrectQRImage.png

可以看到图片很模糊,此时网上大部分博客给出的解决方案如下

    /// 将 CGRect 取整到最近的完整点是非常重要的。小数值会让边框画在像素边界处。因为像素已经是最小单元(不能再细分),小数值会使绘制时取周围几个像素的平均值,这样看起来就模糊了。CGRectIntegral 将表示原点的值向下取整,表示大小的值向上取整,这样就保证了你的绘制代码平整地对齐到像素边界。作为一个经验性的原则,如果你在执行任何一个可能产生小数值的操作(例如除法,CGGetMid[X|Y],或是 CGRectDivide),在把一矩形作为视图的边框之前应该用CGRectIntegral正则化它。
    CGRect extent = CGRectIntegral(image.extent);
    CGFloat size = 300;
    CGFloat scale = MIN(size/CGRectGetWidth(extent), size/CGRectGetHeight(extent)); // 计算需要缩放的比例
    
    size_t width = CGRectGetWidth(extent) * scale; // 计算缩放后的尺寸
    size_t height = CGRectGetHeight(extent) * scale;
    CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray();
    /// CIContext 属于Core Image框架(文档中提到主要的功能就是,用内置或自定义的过滤器处理图片和视频 以及在视频图片中检测面部和眼睛子类的特征和跟踪面部。和 Core Graphics 的主要区别 就是更注重于视频图片的加工处理)的,是一个 OC 对象
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef bitmapImage = [context createCGImage:image fromRect:extent]; // 转化为位图
    
    /// CGContextRef 属于Core Graphics 框架(使用 Quartz 进行 2D 渲染,处理基于路径的绘图、抗锯齿渲染、渐变、图像、颜色管理、pdf文档等 2D 绘图渲染功能)
    /// data 是一个指针,指向存储绘制的bitmap context的实际数据的地址,最少大小为bytesPerRow* height.可以传入null,让quartz自动分配计算
    /// width/height bitmap的宽度,高度,以像素为单位
    /// bitsPerComponent 一个component占据多少字节。对于32bit的RGBA空间,则是8(8*4=32)。
    /// bytesPerRow 每一行的byte数目。如果data传入null,这里传入0,则会自动计算
    /// space 颜色空间,一般就是DeviceRGB
    /// bitmapInfo,一个常量,指定了是否具有alpha通道,alpha通道的位置,像素点存储的数据类型是float还是Integer等信息
    CGContextRef bitmapContextRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone);
    CGContextSetInterpolationQuality(bitmapContextRef, kCGInterpolationNone);//不允许上下文在各个保真度等级插入像素
    
    /// 该方法控制坐标系统水平方向上缩放 sx,垂直方向上缩放 sy。在缩放后的坐标系统上绘制图形时,所有点的 X 坐标都相当于乘以 sx 因子,所有点的 Y 坐标都相当于乘以 sy 因子
    CGContextScaleCTM(bitmapContextRef, scale, scale); //对位图上下文进行缩放
    
    CGContextDrawImage(bitmapContextRef, extent, bitmapImage);//绘制位图到位图上下文
    CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapContextRef);//生成缩放后的位图
    UIImage *outputImage = [UIImage imageWithCGImage:scaledImage];
    CGContextRelease(bitmapContextRef);
    CGImageRelease(bitmapImage);

它的原理是,将 CIImage 绘制到一个灰度图上下文中(CGColorSpaceCreateDeviceGray),根据给定的 size,对画布和图像进行缩放。

这样生成的图片如下所示

CorrectQRImage.png

可以看出二维码的确清晰了很多,但是没有博客具体说明了真正影响清晰度的原因,实际上影响清晰度的原因是这一句代码

CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone);//允许上下文在各个保真度等级插入像素

这里对图像插值质量做了设置,图像插值的概念如下

插值是对原图像的像素重新分布,从而来改变像素数量的一种方法。在图像放大过程中,像素也相应地增加,增加的过程就是“插值”发生作用的过程,“插值”程序自动选择信息较好的像素作为增加、弥补空白像素的空间,而并非只使用临近的像素,所以在放大图像时,图像看上去会比较平滑、干净。不过需要说明的是插值并不能增加图像信息,尽管图像尺寸变大,但效果也相对要模糊些,过程可以理解为白酒掺水。

而 CGContextSetInterpolationQuality 可能默认采用了高品质的插值函数,导致生成的图片由于进行了冗余的像素插值而变得模糊,因此将插值品质设置为 NONE,禁止进行插值操作,就可以保证像素按照原本的分布来缩放了。

猜测三种插值品质枚举值对应的插值算法:

  • Low: Nearest neighbor interpolation

  • Medium: Linear or bilinear interpolation

  • High: Quadratic or bicubic interpolation

  • 给二维码加水印

主要原理就是在生成的二维码图片上 drawImage 自己的水印图片

    UIGraphicsBeginImageContextWithOptions(outputImage.size, NO, [[UIScreen mainScreen] scale]);
    [outputImage drawInRect:CGRectMake(0,0 , size, size)];
    UIImage *waterimage = [UIImage imageNamed:@"DogLogo"];
    [waterimage drawInRect:CGRectMake((size - waterImagesize)/2.0, (size - waterImagesize)/2.0, waterImagesize, waterImagesize)];
    UIImage *newPic = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
WaterQRImage.png
  • 生成彩色二维码

彩色二维码原理是遍历图片的每一个像素点,进行 rgb 值的改变。


void ProviderReleaseData (void *info, const void *data, size_t size){
    free((void*)data);
}

{
    UIColor *targetColor = [UIColor colorWithRed:0.3 green:0.7 blue:0.7 alpha:1.0];
    const CGFloat *components = CGColorGetComponents(targetColor.CGColor);
    CGFloat red = components[0] * 255;
    CGFloat green = components[1] * 255;
    CGFloat blue = components[2] * 255;//分别获取 RGB 值
    
    int imageWidth = outputImage.size.width;
    int imageHeight = outputImage.size.height;
    
    size_t bytesPerRow = imageWidth * 4;
    uint32_t *rgbImageBuf = (uint32_t*)malloc(bytesPerRow * imageHeight);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef rgbContext = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
    CGContextDrawImage(rgbContext, CGRectMake(0, 0, imageWidth, imageHeight), outputImage.CGImage); //绘制图像到rgb上下文中
    
    int pixelNum = imageWidth * imageHeight; //像素总数,每一个像素包含rgba四个通道
    uint32_t *pCurPtr = rgbImageBuf;
    for (int i = 0; i < pixelNum; i++, pCurPtr++){ //遍历像素
        if ((*pCurPtr & 0xFFFFFF00) < 0x99999900){ // 按rgb区分
            uint8_t *ptr = (uint8_t*)pCurPtr;
            ptr[3] = red;
            ptr[2] = green;
            ptr[1] = blue;
        }else{
            uint8_t* ptr = (uint8_t*)pCurPtr;
            ptr[0] = 255;
        }
    }
    
    // rgbImageBuf 里包含了 image 的像素信息
    CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight, ProviderReleaseData);
    CGImageRef imageRef = CGImageCreate(imageWidth, // 图片的宽度
                                        imageHeight, // 图片的高度
                                        8, // 一个component占据多少字节
                                        32, // 每一个像素占用的bits
                                        bytesPerRow, // 每一行占用多少bytes,宽度乘以 4
                                        colorSpace,
                                        kCGImageAlphaLast | kCGBitmapByteOrder32Little,
                                        dataProvider, // 数据源提供者
                                        NULL, // 一个解码数组
                                        true, // 抗锯齿参数
                                        kCGRenderingIntentDefault); //图片渲染相关参数
    outputImage = [UIImage imageWithCGImage:imageRef];
    CGDataProviderRelease(dataProvider);
}
ColorfulQRImage.png

相机识别二维码

这里不赘述关于 iOS 相机的配置过程,只关注与二维码相关的步骤。

iOS 有识别二维码的 API,只需要将 AVCaptureMetadataOutput 配置到 session 的输出中,然后设置其 AVCaptureMetadataOutputObjectsDelegate 代理对象,系统检测到二维码后会调用相应的委托方法。

  • 配置 AVCaptureMetadataOutput
- (AVCaptureMetadataOutput *)metaDataOutput
{
    if (!_metaDataOutput) {
        _metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
        [_metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        _metaDataOutput.rectOfInterest = CGRectMake(0, 0, 1, 1);
    }
    return _metaDataOutput;
}

{
    // 配置QROutput
    if ([self.captureSession canAddOutput:self.metaDataOutput]){
        // 顺序相反会崩溃,因为未配置到 session 时 availableMetadataObjectTypes 为空
        [self.captureSession addOutput:self.metaDataOutput];
        self.metaDataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
    }
}
  • 回调方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray<AVMetadataMachineReadableCodeObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count == 0) {
        return;
    }
    NSString *result = [metadataObjects.firstObject stringValue];
    NSLog(@"%@", result);
}

这里要注意一点,在设置 metaDatOutput 时设置了一个 rectOfInterest 属性,它能限制扫描二维码的区域,它定义了对相机采集的每一帧图像里代码真正感兴趣的检测区域,它有两个特点

  • rectOfInterest 结构体的四个值取值范围在 0~1,即指明对于每一帧,其关心的范围的相对比例
  • rectOfInterest 默认是横屏模式,其 x y 值与通常坐标系相反,也就是说它的真正含义是 CGRectMake(y坐标, x坐标, 高度, 宽度)

当然默认情况下 rectOfInterest 的值是 CGRectMake(0, 0, 1, 1),即整个帧范围。

识别内存中二维码图片

对于网络或者相册选取后读到内存中的图片,以及项目中自带的图片,要检测二维码信息就比较简单,直接使用 CIDetector 就可以检测。

    CIContext *context = [CIContext contextWithOptions:nil];
    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
    CIImage *targetImage = [CIImage imageWithCGImage:[UIImage imageNamed:@"CorrectQRImage.png"].CGImage];
    NSArray *features = [detector featuresInImage:targetImage];
    
    if (features.count == 0) {
        
    } else {
        CIQRCodeFeature *feature = features.firstObject;
        NSLog(@"%@", feature.messageString);
    }

可以看到,CIDetector 会返回一个数组,其中包含多个 CIQRCodeFeature,对应此图片包含的多个二维码对象,CIQRCodeFeature 对象则包含了具体的编码信息。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容