關于二維碼(或者條形碼,以下歸類簡稱二維碼)掃描和生成的,我相信網絡上相關的文章層數不窮,但是,大部分都是直接粘貼上代碼,不去解釋,這樣導致每次遇到諸如此類的功能行的問題,簡單方便的CV工程師程序,久而久之,對于程序開發更局限于表面,開發這條道路也會越來越局限了.
好了,言歸正傳,接下來我就分享一下,自己在二維碼開發的過程中遇到的問題和一些經驗吧.
注:這里的掃描僅限于相機掃描,所以建議各位開發者,需要在真機上進行測試
一. 二維碼的掃描
0.準備工作
- 1).宏定義
定義當前頁面的寬和高,通過delegate.window獲取frame
#define SCREEN_WIDTH [UIApplication sharedApplication].delegate.window.frame.size.width
#define SCREEN_HEIGHT [UIApplication sharedApplication].delegate.window.frame.size.height - 2).協議
-
AVCaptureMetadataOutputObjectsDelegate
-
這是有關攝像設備輸出的相關的代理,這里我們需要用到掃描后的結果,后面會做出詳細的解釋
-
UIAlertViewDelegate
主要是顯示出來掃描的結果,可以看做相對的輔助
1.依賴庫
因為二維碼的掃描是基于真機上的相機,我們需要引入<AVFoundation/AVFoundation.h>
#import <AVFoundation/AVFoundation.h>
關于這個庫的介紹,相信很多做過視頻和音頻播放的童鞋們并不陌生,這個也是基于cocoa下比較常用的庫
2.定義對應變量屬性
關于屬性的創建,我們需要了解到每個屬性的作用和相關操作
1).創建相機AVCaptureDevice
AVCaptureDevice的每個實例對應一個設備,如攝像頭或麥克風。集體的信息可以參考蘋果相關API.
@property (strong,nonatomic)AVCaptureDevice * device;
2).創建輸入設備AVCaptureDeviceInput
AVCaptureDeviceInput是AVCaptureInput子類提供一個接口,用于捕獲從一個AVCaptureDevice媒體。AVCaptureDeviceInput是AVCaptureSession實例的輸入源,提供媒體數據從設備連接到系統。
@property (strong,nonatomic)AVCaptureDeviceInput * input;
3).創建輸出設備AVCaptureMetadataOutput
AVCaptureMetadataOutput對象攔截元數據對象發出的相關捕獲連接,并將它們轉發給委托對象進行處理。您可以使用這個類的實例來處理特定類型的元數據中包含的輸入數據。你使用這個類你做其他的輸出對象的方式,通常是通過添加一個AVCaptureSession對象作為輸出。簡單而言就是,AVCaptureMetadataOutput將獲取到的元數據交給AVCaptureSession進行處理的途徑.
@property (strong,nonatomic)AVCaptureMetadataOutput * output;
4).創建AVFoundation中央樞紐捕獲類AVCaptureSession。
下面的是關于AVCaptureSession的原生API
To perform a real-time capture, a client may instantiate AVCaptureSession and add appropriate AVCaptureInputs, such as AVCaptureDeviceInput, and outputs, such as AVCaptureMovieFileOutput. [AVCaptureSession startRunning] starts the flow of data from the inputs to the outputs, and [AVCaptureSession stopRunning] stops the flow. A client may set the sessionPreset property to customize the quality level or bitrate of the output.
在蘋果的API中大致是這樣重點解釋的:
- 執行實時捕獲,一個客戶可以實例化AVCaptureSession并添加適當AVCaptureInputs,AVCaptureDeviceInput和相關的輸出,如AVCaptureMovieFileOutput。
- [AVCaptureSession startRunning]開始的數據流從輸入到輸出
- [AVCaptureSession stopRunning]停止流動。
- 客戶端可以設置sessionPreset屬性定制質量水平或輸出的比特率
@property (strong,nonatomic)AVCaptureSession * session;
5).創建AVCaptureSession預覽視覺輸出AVCaptureVideoPreviewLayer,在API介紹中,我們不難發現,他是繼承自CoreAnimation的CALayer的子類,,這里我們可以看做是將圖片輸出的一個平臺(搭載), 因此適合插入在一層的層次結構作為一個圖形界面的一部分。
- 在蘋果原生API介紹中,我們可以了解到,我們可以通過創建+ layerWithSession:或-initWithSession:對AVCaptureVideoPreviewLayer進行實例與捕獲會話預覽。
- 使用"videoGravity”屬性,可以影響內容是如何看待相對于層界限。
- 在某些硬件配置,層可以使用"orientation"(操縱的方向) 和 "mirrored"(鏡像)等進行操作.
@property (strong,nonatomic)AVCaptureVideoPreviewLayer * preview;
3.初始化變量
確定了相關屬性,接下來,我們對相關變量進行初始化,就好比原料我們有了,接下來我們對這些材料進行粗略的加工.
至于初始化的位置,一般情況下我們將一個頁面作為二維碼操作的,這邊算作是一個模塊處理,所以,建議在ViewDidLoad方法(生命周期)里面進行創建.如果需要特殊處理,具體情況具體分析吧,因為需求不一樣,所以,下面的栗子采用在ViewDidLoad中進行.
1).初始化基礎"引擎"Device
// Device,這里需要注意的是AVCaptureDevice不能直接創建的實例
self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
2).初始化輸入流 Input,并添加Device
self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];
3).初始化輸出流Output
self.output = [[AVCaptureMetadataOutput alloc] init];
下面,敲黑板了,
這里需要注意的是:在輸出流的設置中,如果不對AVCaptureMetadataOutput的屬性rectOfInterest進行設置,掃描的區域默認是展示的AVCaptureVideoPreviewLayer全部區域.這里我們采用區域掃描,也就是所謂的條框掃描,提高用戶體驗度.
// 創建view,通過layer層進行設置邊框寬度和顏色,用來輔助展示掃描的區域
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
redView.layer.borderWidth = 2;
redView.layer.borderColor = [UIColor cyanColor].CGColor;
[self.view addSubview:redView];
//設置輸出流的相關屬性
// 確定輸出流的代理和所在的線程,這里代理遵循的就是上面我們在準備工作中提到的第一個代理,至于線程的選擇,建議選在主線程,這樣方便當前頁面對數據的捕獲.
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
3+). 設置掃描區域的大小(這個也是我在開發中,遇到的最*最^n坑爹的問題,標注一下)
為什么是"3+"呢,主要是本來想將這部分放在后面單獨講,但是考慮到連貫性,就單獨做一個補充小節來講吧,而且屬于設置session層的部分.
self.output.rectOfInterest = CGRectMake((100)/(SCREEN_HEIGHT),(SCREEN_WIDTH - 100 - 200)/SCREEN_WIDTH,100/SCREEN_HEIGHT,200/SCREEN_WIDTH);
其實呢,我是想在當前view創建一塊CGRectMake(100, 100, 200, 100)的掃描區域,如下圖的掃描區域框:
但是呢,這里需要說明的一點就是,我們如果按照常規的CGRect創建方式去設置,是肯定不對的,他會出現掃描區域不是預設的,為什么呢?
原因很蛋疼,因為我們平常的設置CGRect是以左上角為原點,橫向增加為+x,縱向增加為+y,橫向為寬度width,縱向為高度height,沒毛病吧,但是,坑爹的就是output的rectOfInterest是以左上角為原點,x與y數值對調,width和height數值對調,并且,x,y,width和height的數值為0 ~ 1.如下對比圖:
說明一下:這里的計算對比,針對的是,知道掃描框相對于父視圖的位置,我們根據掃描框的CGRect可以計算出需要設置的output的rectOfInterest的CGRect.至于為什么需要對調原理,最近有時間研究研究(蘋果這個設計讓很多人不解),目前僅供參考.大家要是有相關的方案或者明確為何這樣,希望在下面留言,我們一起深究一下.
這里的概念區別于我們所認知的CGRect的設置,建議童鞋們還是手動算一下,之后進行邊緣化測試,就是測試二維碼從邊緣完全進入掃描區域并且存在掃描任務的位置.
4).初始化捕獲數據類AVCaptureSession
// 初始化session
self.session = [[AVCaptureSession alloc]init];
// 設置session類型,AVCaptureSessionPresetHigh 是 sessionPreset 的默認值。
[_session setSessionPreset:AVCaptureSessionPresetHigh];
補充:這里簡單對sessionPreset的屬性值進行以下說明:
蘋果API中提供了如下的四種方式:
// AVCaptureSession 預設適用于高分辨率照片質量的輸出
AVF_EXPORT NSString *const AVCaptureSessionPresetPhoto NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預設適用于高分辨率照片質量的輸出
AVF_EXPORT NSString *const AVCaptureSessionPresetHigh NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預設適用于中等質量的輸出。 實現的輸出適合于在無線網絡共享的視頻和音頻比特率。
AVF_EXPORT NSString *const AVCaptureSessionPresetMedium NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預設適用于低質量的輸出。為了實現的輸出視頻和音頻比特率適合共享 3G。
AVF_EXPORT NSString *const AVCaptureSessionPresetLow NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
PS:在API的介紹中,除了以上的跡象,我們還會看到好幾種類型,不過不是針對 ipad iphone 的。針對 MAC_OS,不便介紹,感興趣的可以查看相關API
5).將輸入流和輸出流添加到session中
這里可以看做是集成,就好比是,我們現在正在建造一輛汽車,我們的原件已經做好了,現在要放到汽車的骨架上.
// 添加輸入流
if ([_session canAddInput:self.input]) {
[_session addInput:self.input];
}
// 添加輸出流
if ([_session canAddOutput:self.output]) {
[_session addOutput:self.output];
}
// 下面的是比較重要的,也是最容易出現崩潰的原因,就是我們的輸出流的類型
// 1.這里可以設置多種輸出類型,這里必須要保證session層包括輸出流
// 2.必須要當前項目訪問相機權限必須通過,所以最好在程序進入當前頁面的時候進行一次權限訪問的判斷(在文章的最后,我會貼出相關的代買)
self.output.metadataObjectTypes =@[AVMetadataObjectTypeQRCode];
6).設置輸出展示平臺AVCaptureVideoPreviewLayer
// 初始化
self.preview =[AVCaptureVideoPreviewLayer layerWithSession:_session];
// 設置Video Gravity,顧名思義就是視頻播放時的拉伸方式,默認是AVLayerVideoGravityResizeAspect
// AVLayerVideoGravityResizeAspect 保持視頻的寬高比并使播放內容自動適應播放窗口的大小。
// AVLayerVideoGravityResizeAspectFill 和前者類似,但它是以播放內容填充而不是適應播放窗口的大小。最后一個值會拉伸播放內容以適應播放窗口.
// 因為考慮到全屏顯示以及設備自適應,這里我們采用fill填充
self.preview.videoGravity =AVLayerVideoGravityResizeAspectFill;
// 設置展示平臺的frame
self.preview.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// 因為 AVCaptureVideoPreviewLayer是繼承CALayer,所以添加到當前view的layer層
[self.view.layer insertSublayer:self.preview atIndex:0];
7).一切準備就去,開始運行
[self.session startRunning];
4.掃描結果處理
這里就需要用到我們之前設置的兩個代理AVCaptureMetadataOutputObjectsDelegate和UIAlertViewDelegate
在AVCaptureMetadataOutputObjectsDelegate的代理方法中,有didOutputMetadataObjects這個方法,表示輸出的結果,我們掃描二維碼的結果將要在這里進行處理
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
// 判斷掃描結果的數據是否存在
if ([metadataObjects count] >0){
// 如果存在數據,停止掃描
[self.session stopRunning];
// AVMetadataMachineReadableCodeObject是AVMetadataObject的具體子類定義的特性檢測一維或二維條形碼。
// AVMetadataMachineReadableCodeObject代表一個單一的照片中發現機器可讀的代碼。這是一個不可變對象描述條碼的特性和載荷。
// 在支持的平臺上,AVCaptureMetadataOutput輸出檢測機器可讀的代碼對象的數組
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex:0];
// 獲取掃描到的信息
NSString *stringValue = metadataObject.stringValue;
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"掃描結果"
message:stringValue
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"確定", nil];
[self.view addSubview:alert];
[alert show];
}
}
在UIAlertViewDelegate代理方法中,我們確認信息后,可以對信息有相應的操作,這里我只是簡單的進行了繼續進行數據捕捉(掃描)
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
[self.session startRunning];
}
5.運行展示
下面我們看看運行的結果,這里測試過程包括區域掃描的邊緣化測試:
二.二維碼的生成
講完了二維碼的掃描,接下來我們接著講講二維碼的生成.
二維碼的生成的核心在于圖形的繪制,我們通過濾鏡CIFilter和圖形繪制的上下文方式生成二維碼.
0.準備工作
1.依賴庫
二維碼的生成區別于二維碼的掃描,因為他的核心是基于圖形的繪制完成的,所以需要導入CoreImage框架
#import <CoreImage/CoreImage.h>
2.創建濾鏡CIFilte
1.創建濾鏡CIFilter實例對象,并通過類方法,將filter的名稱指定為CIQRCodeGenerator
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
2.由于filter的強大,我們目前僅是實現簡單的二維碼的生成,所以,我們將filter的各項屬性均設置成默認
[filter setDefaults];
3.給過濾器CIFilter添加數據
這里需要說明的是,二維碼的主要內容可以是如下幾種類型(傳統的條形碼只能放數字):
- 純文本
- URL
- 名片(這個有待考證,表示我并沒有試驗過)
// 基于多種類型,我們簡單的生成字符串的二維碼
// 創建字符串
NSString *dataString = @"鋒繪動漫";
// 將字符串轉換成date類型,并通過KVO的形式保存至濾鏡CIFilter(目前指定為二維碼)的inputMessage中
NSData *data = [dataString dataUsingEncoding:NSUTF8StringEncoding];
[filter setValue:data forKeyPath:@"inputMessage"];
4.獲取輸出的二維碼
CIImage *outputImage = [filter outputImage];
5.獲取高清的二維碼,并展示
因為CIFilter生成的二維碼相對而言模糊,達不到設備快速識別的需求,同時用戶體驗差.
所以通過圖像繪制的上下文來獲得高清的二維碼圖片.
PS:由于獲取高清圖片不是該章節的重點,相關的代碼部分來自網絡,放到文章的最后,僅供看考
// 獲取二維碼
self.imageView.image = [self createErWeiMaImageFormCIImage:outputImage withSize:200];
6.運行結果
掃描的內容請參考第一節"二維碼生成"的運行結果
我是調皮的分割線
小結
這就是我理解的二維碼的生成和二維碼的掃描,其中主要的還是針對兩個框架的研究,讓我學到了很多東西.
在學習的過程中,比較建議大家多去查看蘋果原生的API,這個對自我理解是比較重要的,網絡上的總結出來的,只能作為自己的參考,切不可取而代之,最大的禁忌就是CV工程師的道路,再簡單的代碼也要自己敲出來.
有什么問題歡迎大家留言多多留言,多多交流
下面附上demo地址(本人的github上):
二維碼掃描(可區域)和生成的Demo地址
PS:相關代碼
1.權限訪問
NSString *mediaType =AVMediaTypeVideo;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
if(authStatus ==AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
UIAlertView *alert =[[UIAlertView alloc] initWithTitle:@“項目名稱”
message:@"請在iPhone的“設置”-“隱私”-“相機”功能中,找到“項目名稱”打開相機訪問權限"
delegate:nil
cancelButtonTitle:@"確定"
otherButtonTitles: nil];
[alert show];
return;
}
2.獲取高清圖片
- (UIImage *)getErWeiMaImageFormCIImage:(CIImage *)image withSize:(CGFloat) size {
CGRect extent = CGRectIntegral(image.extent);
CGFloat scale = MIN(size/CGRectGetWidth(extent), size/CGRectGetHeight(extent));
// 1.創建bitmap;
size_t width = CGRectGetWidth(extent) * scale;
size_t height = CGRectGetHeight(extent) * scale;
CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray();
CGContextRef bitmapRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone);
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef bitmapImage = [context createCGImage:image fromRect:extent];
CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone);
CGContextScaleCTM(bitmapRef, scale, scale);
CGContextDrawImage(bitmapRef, extent, bitmapImage);
// 2.保存bitmap到圖片
CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapRef);
CGContextRelease(bitmapRef);
CGImageRelease(bitmapImage);
return [UIImage imageWithCGImage:scaledImage];
}