iOS中关于二维码的识别与生成

提起二维码 QR Code 想必大家都不再陌生,“扫一扫加好友”、“扫一扫付款”等已是我们日常生活中司空见惯的情形。

以这篇博客来讲述在iOS中关于二维码的识别与生成,会尽可能周全的将二维码识别与生成相关的操作讲述清楚。

关于二维码生成的原理,感兴趣的话各位可以移驾 二维码的生成细节和原理 以做参考。

注:文中识别与生成二维码的方法同样适用于条形码。

先上几张二维码压压惊……

我的二维码

识别二维码

摄像头扫描二维码

iOS7之后苹果推出系统原生API来支持通过扫描获取二维码的功能,较其它 ZBarZXing 等第三方库有明显的性能优势。

首先,你需要弄清楚要用到的以下对象分别起到什么作用:

1、 AVCaptureDevice
捕获数据的物理设备,如:摄像机、麦克风。 开关灯属性torchMode就是由它管理的。

2、 AVCaptureSession
会话,管理输入流、输出流之间的数据传递。

3、 AVCaptureDeviceInput
输入流,从物理设备获取数据。

4、 AVCaptureMetadataOutput
输出流,需要设置输出流代理及所在线程,由代理对象处理输出流数据。

需要说明的是:

  • 需要将输出流添加到会话后,才能指定元数据类型,否则会报错。
  • 将输出流设置在主线程中,其代理方法会执行一次。设置在其他线程的话,代理会执行多次且次数不可控制。
  • 可以通过设置AVFoundationrectOfInterest属性来设定扫描区域,该属性默认取值是CGRectMake(0, 0, 1, 1)即全屏扫描
    xywidthheight的取值范围都是0~1,且原点在屏幕右上角,所以和我们正常理解的CGRect相比xy对调,widthheight对调。

如图:

5、 AVCaptureVideoPreviewLayerCALayer
预览图层,显示相机拍摄到的画面。正因为它是CALayer的子类,为了将它添加到屏幕上,我们需要额外添加一个UIView,在这个UIViewlayer上添加AVCaptureVideoPreviewLayerCALayer,否则它会覆盖住所有的控件(除非你把所有的控件都在添加这个视图之后添加)。

AVCaptureSession 设置开始与结束扫描

1
2
3
4
5
// 开始会话
[self.captureSession startRunning];
// 停止会话
[self.captureSession stopRunning];
self.captureSession = nil;
  1. 获取AVCaptureDevice实例
  2. 初始化输入流
  3. 初始化输出流
    3.1.设置代理及所在线程
    3.2设置扫描区域
  4. 创建会话
    4.1添加输入流
    4.2添加输出流
    *3.3指定元数据类型
  5. 创建预览图层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 1.获取AVCaptureDevice实例
self.captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

// 2.初始化输入流
NSError * error;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:&error];
if (!input) {
NSLog(@"%@", [error localizedDescription]);
return;
}

// 3.初始化输出流
AVCaptureMetadataOutput *captureMetadataOutput = [AVCaptureMetadataOutput new];
// 3.1设置代理及所在线程
[captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// 3.2设置扫描区域
[captureMetadataOutput setRectOfInterest:[self scanPlace]];

// 4.创建会话
self.captureSession = [AVCaptureSession new];
// 4.1添加输入流
[self.captureSession addInput:input];
if ([self.captureSession canAddInput:input]) {
[self.captureSession addInput:input];
}
// 4.2添加输出流
if ([self.captureSession canAddOutput:captureMetadataOutput]) {
[self.captureSession addOutput:captureMetadataOutput];
}

// 3.3指定元数据类型
captureMetadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeUPCECode,
AVMetadataObjectTypeCode39Code,
AVMetadataObjectTypeCode39Mod43Code,
AVMetadataObjectTypeEAN13Code,
AVMetadataObjectTypeEAN8Code,
AVMetadataObjectTypeCode93Code,
AVMetadataObjectTypeCode128Code,
AVMetadataObjectTypePDF417Code,
AVMetadataObjectTypeQRCode, // 二维码
AVMetadataObjectTypeAztecCode];

// 5创建预览图层
self.captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
[self.captureVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.captureVideoPreviewLayer setFrame:self.scanView.layer.bounds];
[self.scanView.layer addSublayer:self.captureVideoPreviewLayer];

AVCaptureMetadataOutput 的代理方法处理输出流,返回扫描结果

1
2
3
4
5
6
7
8
9
10
11
12
#pragma mark - 代理方法处理输出流
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
if (metadataObjects != nil && [metadataObjects count] > 0) {
AVMetadataMachineReadableCodeObject *metadataObj = [metadataObjects objectAtIndex:0];
NSString *result = metadataObj.stringValue;
NSLog(@"result:%@", result);
if ([[metadataObj type] isEqualToString:AVMetadataObjectTypeQRCode]) {
NSLog(@"二维码");
} // ...
[self closeScanQRCode];
}
}

开灯

给开关灯按钮调用以下 AVCaptureDevice 方法,轻松实现开灯效果

1
2
3
4
5
6
7
8
9
10
11
12
- (void)turnLight {
// 判断允许设置
if ([self.device hasTorch]) {
[self.device lockForConfiguration:nil];
if (self.device.torchMode == AVCaptureTorchModeOff) {
[self.device setTorchMode:AVCaptureTorchModeOn]; // 开灯
} else if (self.device.torchMode == AVCaptureTorchModeOn) {
[self.device setTorchMode:AVCaptureTorchModeOff]; // 关灯
}
[self.device unlockForConfiguration];
}
}

总结下来就是:
AVCaptureSession管理从物理设备AVCaptureDevice那里获取的输入流AVCaptureDeviceInput数据
通过输出流AVCaptureMetadataOutput显示到预览图层AVCaptureVideoPreviewLayerCALayer
并且由代理方法-(void)captureOutput:(AVCaptureOutput*)captureOutput didOutputMetadataObjects:(NSArray*)metadataObjects fromConnection:(AVCaptureConnection*)connection;处理捕获到的数据。

相册识别二维码

iOS8之后系统提供的识别二维码图片的方法相当简单

1
2
3
4
5
6
7
8
9
// 读取二维码
UIImage *sourceImage = ...;
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *image = [CIImage imageWithCGImage:sourceImage.CGImage];
NSArray *array = [detector featuresInImage:image];
CIQRCodeFeature *feature = [array firstObject];
NSString *result = feature.messageString;
NSLog(@"result:%@", result);

UIImagePickerController 打开系统相册,选择图片识别:

1
2
3
4
5
6
7
- (void)openPhotoLibrary {
UIImagePickerController *photoPicker = [UIImagePickerController new];
photoPicker.delegate = self;
// 打开的相册类型
photoPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:photoPicker animated:YES completion:NULL];
}

UIImagePickerController 的代理方法识别相册中选中的二维码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:^{
// 读取二维码
UIImage *sourceImage = [info objectForKey:UIImagePickerControllerOriginalImage];
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *image = [CIImage imageWithCGImage:sourceImage.CGImage];
NSArray *array = [detector featuresInImage:image];
CIQRCodeFeature *feature = [array firstObject];
NSString *result = feature.messageString;
NSLog(@"result:%@", result);
}];
}

生成二维码

使用系统提供的CIFilter可以方便简单的生成二维码

生成二维码方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIImage *)generateQRCode:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
CIImage *qrcodeImage;
NSData *data = [code dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];

[filter setValue:data forKey:@"inputMessage"];
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];
qrcodeImage = [filter outputImage];

// 消除模糊
CGFloat scaleX = width / qrcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / qrcodeImage.extent.size.height;
CIImage *transformedImage = [qrcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];

return [UIImage imageWithCIImage:transformedImage];
}

使用 CIFilter 生成带背景色的二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (UIImage *)gaveColor:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
NSData *data = [code dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setValue:data forKey:@"inputMessage"];
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];

//上色
CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"
keysAndValues:
@"inputImage",filter.outputImage,
@"inputColor0",[CIColor colorWithCGColor:[UIColor purpleColor].CGColor], // 前景色
@"inputColor1",[CIColor colorWithCGColor:[UIColor cyanColor].CGColor], // 背景色
nil];
CIImage *qrcodeImage = colorFilter.outputImage;

// 消除模糊
CGFloat scaleX = width / qrcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / qrcodeImage.extent.size.height;
CIImage *transformedImage = [qrcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];

return [UIImage imageWithCIImage:transformedImage];
}

通过遍历图片的像素给二维码个性化上色

这里需要指出的是,如果你在下边方法中传入的 image 是通过 CIFilter 方法直接生成的,那么该方法是没法工作的。同理,下一节中保存图片时仍然不能用 CIFilter 方法直接生成的 image,而采用 CGContextRef 获取图片。(参见下一节:保存二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 颜色变化
void ProviderReleaseData (void *info, const void *data, size_t size) {
free((void *)data);
}
- (UIImage *)imageBlackToTransparent:(UIImage *)image {
// 分配内存
const int imageWidth = image.size.width;
const int imageHeight = image.size.height;
size_t bytesPerRow = imageWidth * 4;
uint32_t *rgbImageBuf = (uint32_t *)malloc(bytesPerRow * imageHeight);

// 创建context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image.CGImage);

// 遍历像素
int pixelNum = imageWidth * imageHeight;
uint32_t *pCurPtr = rgbImageBuf;
for (int i = 0; i < pixelNum; i++, pCurPtr++) {
if ((*pCurPtr & 0xFFFFFF00) < 0x99999900) {
// 改变下面的代码,将图片转成想要的颜色
uint8_t *ptr = (uint8_t *)pCurPtr;
if (i<pixelNum/2) {
ptr[3] = 1; //0~255
ptr[2] = 200;
ptr[1] = 200;
} else {
ptr[3] = 200; //0~255
ptr[2] = 1;
ptr[1] = 200;
}
} else { // 白色 255,255,255
uint8_t *ptr = (uint8_t *)pCurPtr;
ptr[0] = 0;
}
}

// 将内存转成image
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight,ProviderReleaseData);
CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider, NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
UIImage *resultUIImage = [UIImage imageWithCGImage:imageRef];

// 释放
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);

return resultUIImage;
}

生成条码图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UIImage *)generateBarCode:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
CIImage *barcodeImage;
NSData *data = [code dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CICode128BarcodeGenerator"];

[filter setValue:data forKey:@"inputMessage"];
barcodeImage = [filter outputImage];

// 消除模糊
CGFloat scaleX = width / barcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / barcodeImage.extent.size.height;
CIImage *transformedImage = [barcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];

return [UIImage imageWithCIImage:transformedImage];
}

保存二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 开启位图上下文
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0);
// 获取绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 将图片渲染的上下文中
[self.imageView.layer renderInContext:context];
// 获取上下文中的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭位图上下文
UIGraphicsEndImageContext();

// 保存图片
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

UIImageWriteToSavedPhotosAlbum 方法的指定回调,监测保存是否成功

1
2
3
4
5
6
7
8
9
#pragma mark -  指定回调方法
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
NSString *string;
if(!error){
NSLog(@"save success");
}else{
NSLog(@"save failed");
}
}

看一哈效果图
二维码的识别与生成

ok,就这些吧。
Demo地址:https://github.com/gonghonglou/QRCodeDemo

写到这里估计涉及了二维码操作的大部分内容,期望对诸君有所帮助。

祝大家敲码愉快!

后记