iOS開發--二維碼/條形碼(掃描(可區域)和生成)

注:文章來自我的簡書的文章http://www.jianshu.com/p/08a5f6966cb8

關於二維碼(或者條形碼,以下歸類簡稱二維碼)掃描和生成的,我相信網絡上相關的文章層數不窮,但是,大部分都是直接粘貼上代碼,不去解釋,這樣導致每次遇到諸如此類的功能行的問題,簡單方便的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.依賴庫

因爲二維碼的掃描是基於真機上的相機,我們需要引入

#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)的掃描區域,如下圖的掃描區域框:

掃描區域.PNG
但是呢,這裏需要說明的一點就是,我們如果按照常規的CGRect創建方式去設置,是肯定不對的,他會出現掃描區域不是預設的,爲什麼呢?
原因很蛋疼,因爲我們平常的設置CGRect是以右上角爲原點,橫向增加爲+x,縱向增加爲+y,橫向爲寬度width,縱向爲高度height,沒毛病吧,但是,坑爹的就是output的rectOfInterest是以左上角爲原點,x與y數值對調,width和height數值對調.
常規座標CGRect座標計算體系.png
output的rectOfInterest座標計算體系.png

並且,x,y,width和height的數值爲0 ~ 1.如下對比圖:

對比.png
這裏的概念區別於我們所認知的CGRect的設置,建議童鞋們還是手動算一下,之後進行邊緣化測試,就是測試二維碼從邊緣完全進入掃描區域並且存在掃描任務的位置.

4).初始化捕獲數據類AVCaptureSession

// 初始化session
self.session = [[AVCaptureSession alloc]init];
// 設置session類型,AVCaptureSessionPresetHigh 是 sessionPreset 的默認值。
[self.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 ([self.session canAddInput:self.input]) {
    [self.session addInput:self.input];
}
// 添加輸出流
if ([self.session canAddOutput:self.output]) {
    [self.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);
// 因爲 AVCaptureeoPreviewLayer是繼承CALayer,所以添加到當前view的layer層

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.運行展示

下面我們看看運行的結果,這裏測試過程包括區域掃描的邊緣化測試:
區域掃描.gif


二.二維碼的生成

講完了二維碼的掃描,接下來我們接着講講二維碼的生成.
二維碼的生成的核心在於圖形的繪製,我們通過濾鏡CIFilter和圖形繪製的上下文方式生成二維碼.

0.準備工作

1.依賴庫

二維碼的生成區別於二維碼的掃描,因爲他的核心是基於圖形的繪製完成的,所以需要導入CoreImage框架
#import

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.運行結果

掃描的內容請參考第一節”二維碼生成”的運行結果
二維碼生成.gif


我是調皮的分割線


小結

這就是我理解的二維碼的生成和二維碼的掃描,其中主要的還是針對兩個框架的研究,讓我學到了很多東西.
在學習的過程中,比較建議大家多去查看蘋果原生的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];
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章