關(guān)于二維碼(或者條形碼,以下歸類(lèi)簡(jiǎn)稱(chēng)二維碼)掃描和生成的,我相信網(wǎng)絡(luò)上相關(guān)的文章層數(shù)不窮,但是,大部分都是直接粘貼上代碼,不去解釋,這樣導(dǎo)致每次遇到諸如此類(lèi)的功能行的問(wèn)題,簡(jiǎn)單方便的CV工程師程序,久而久之,對(duì)于程序開(kāi)發(fā)更局限于表面,開(kāi)發(fā)這條道路也會(huì)越來(lái)越局限了.
好了,言歸正傳,接下來(lái)我就分享一下,自己在二維碼開(kāi)發(fā)的過(guò)程中遇到的問(wèn)題和一些經(jīng)驗(yàn)吧.
注:這里的掃描僅限于相機(jī)掃描,所以建議各位開(kāi)發(fā)者,需要在真機(jī)上進(jìn)行測(cè)試
一. 二維碼的掃描
0.準(zhǔn)備工作
- 1).宏定義
定義當(dāng)前頁(yè)面的寬和高,通過(guò)delegate.window獲取frame
#define SCREEN_WIDTH [UIApplication sharedApplication].delegate.window.frame.size.width
#define SCREEN_HEIGHT [UIApplication sharedApplication].delegate.window.frame.size.height - 2).協(xié)議
-
AVCaptureMetadataOutputObjectsDelegate
-
這是有關(guān)攝像設(shè)備輸出的相關(guān)的代理,這里我們需要用到掃描后的結(jié)果,后面會(huì)做出詳細(xì)的解釋
-
UIAlertViewDelegate
主要是顯示出來(lái)掃描的結(jié)果,可以看做相對(duì)的輔助
1.依賴(lài)庫(kù)
因?yàn)槎S碼的掃描是基于真機(jī)上的相機(jī),我們需要引入<AVFoundation/AVFoundation.h>
#import <AVFoundation/AVFoundation.h>
關(guān)于這個(gè)庫(kù)的介紹,相信很多做過(guò)視頻和音頻播放的童鞋們并不陌生,這個(gè)也是基于cocoa下比較常用的庫(kù)
2.定義對(duì)應(yīng)變量屬性
關(guān)于屬性的創(chuàng)建,我們需要了解到每個(gè)屬性的作用和相關(guān)操作
1).創(chuàng)建相機(jī)AVCaptureDevice
AVCaptureDevice的每個(gè)實(shí)例對(duì)應(yīng)一個(gè)設(shè)備,如攝像頭或麥克風(fēng)。集體的信息可以參考蘋(píng)果相關(guān)API.
@property (strong,nonatomic)AVCaptureDevice * device;
2).創(chuàng)建輸入設(shè)備AVCaptureDeviceInput
AVCaptureDeviceInput是AVCaptureInput子類(lèi)提供一個(gè)接口,用于捕獲從一個(gè)AVCaptureDevice媒體甩卓。AVCaptureDeviceInput是AVCaptureSession實(shí)例的輸入源,提供媒體數(shù)據(jù)從設(shè)備連接到系統(tǒng)狸演。
@property (strong,nonatomic)AVCaptureDeviceInput * input;
3).創(chuàng)建輸出設(shè)備AVCaptureMetadataOutput
AVCaptureMetadataOutput對(duì)象攔截元數(shù)據(jù)對(duì)象發(fā)出的相關(guān)捕獲連接,并將它們轉(zhuǎn)發(fā)給委托對(duì)象進(jìn)行處理责语。您可以使用這個(gè)類(lèi)的實(shí)例來(lái)處理特定類(lèi)型的元數(shù)據(jù)中包含的輸入數(shù)據(jù)。你使用這個(gè)類(lèi)你做其他的輸出對(duì)象的方式,通常是通過(guò)添加一個(gè)AVCaptureSession對(duì)象作為輸出槐壳。簡(jiǎn)單而言就是,AVCaptureMetadataOutput將獲取到的元數(shù)據(jù)交給AVCaptureSession進(jìn)行處理的途徑.
@property (strong,nonatomic)AVCaptureMetadataOutput * output;
4).創(chuàng)建AVFoundation中央樞紐捕獲類(lèi)AVCaptureSession。
下面的是關(guān)于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.
在蘋(píng)果的API中大致是這樣重點(diǎn)解釋的:
- 執(zhí)行實(shí)時(shí)捕獲,一個(gè)客戶可以實(shí)例化AVCaptureSession并添加適當(dāng)AVCaptureInputs,AVCaptureDeviceInput和相關(guān)的輸出,如AVCaptureMovieFileOutput喜每。
- [AVCaptureSession startRunning]開(kāi)始的數(shù)據(jù)流從輸入到輸出
- [AVCaptureSession stopRunning]停止流動(dòng)务唐。
- 客戶端可以設(shè)置sessionPreset屬性定制質(zhì)量水平或輸出的比特率
@property (strong,nonatomic)AVCaptureSession * session;
5).創(chuàng)建AVCaptureSession預(yù)覽視覺(jué)輸出AVCaptureVideoPreviewLayer,在API介紹中,我們不難發(fā)現(xiàn),他是繼承自CoreAnimation的CALayer的子類(lèi),,這里我們可以看做是將圖片輸出的一個(gè)平臺(tái)(搭載), 因此適合插入在一層的層次結(jié)構(gòu)作為一個(gè)圖形界面的一部分。
- 在蘋(píng)果原生API介紹中,我們可以了解到,我們可以通過(guò)創(chuàng)建+ layerWithSession:或-initWithSession:對(duì)AVCaptureVideoPreviewLayer進(jìn)行實(shí)例與捕獲會(huì)話預(yù)覽带兜。
- 使用"videoGravity”屬性,可以影響內(nèi)容是如何看待相對(duì)于層界限枫笛。
- 在某些硬件配置,層可以使用"orientation"(操縱的方向) 和 "mirrored"(鏡像)等進(jìn)行操作.
@property (strong,nonatomic)AVCaptureVideoPreviewLayer * preview;
3.初始化變量
確定了相關(guān)屬性,接下來(lái),我們對(duì)相關(guān)變量進(jìn)行初始化,就好比原料我們有了,接下來(lái)我們對(duì)這些材料進(jìn)行粗略的加工.
至于初始化的位置,一般情況下我們將一個(gè)頁(yè)面作為二維碼操作的,這邊算作是一個(gè)模塊處理,所以,建議在ViewDidLoad方法(生命周期)里面進(jìn)行創(chuàng)建.如果需要特殊處理,具體情況具體分析吧,因?yàn)樾枨蟛灰粯?所以,下面的栗子采用在ViewDidLoad中進(jìn)行.
1).初始化基礎(chǔ)"引擎"Device
// Device,這里需要注意的是AVCaptureDevice不能直接創(chuàng)建的實(shí)例
self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
2).初始化輸入流 Input,并添加Device
self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];
3).初始化輸出流Output
self.output = [[AVCaptureMetadataOutput alloc] init];
下面,敲黑板了,
這里需要注意的是:在輸出流的設(shè)置中,如果不對(duì)AVCaptureMetadataOutput的屬性rectOfInterest進(jìn)行設(shè)置,掃描的區(qū)域默認(rèn)是展示的AVCaptureVideoPreviewLayer全部區(qū)域.這里我們采用區(qū)域掃描,也就是所謂的條框掃描,提高用戶體驗(yàn)度.
// 創(chuàng)建view,通過(guò)layer層進(jìn)行設(shè)置邊框?qū)挾群皖伾?用來(lái)輔助展示掃描的區(qū)域
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
redView.layer.borderWidth = 2;
redView.layer.borderColor = [UIColor cyanColor].CGColor;
[self.view addSubview:redView];
//設(shè)置輸出流的相關(guān)屬性
// 確定輸出流的代理和所在的線程,這里代理遵循的就是上面我們?cè)跍?zhǔn)備工作中提到的第一個(gè)代理,至于線程的選擇,建議選在主線程,這樣方便當(dāng)前頁(yè)面對(duì)數(shù)據(jù)的捕獲.
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
3+). 設(shè)置掃描區(qū)域的大小(這個(gè)也是我在開(kāi)發(fā)中,遇到的最*最^n坑爹的問(wèn)題,標(biāo)注一下)
為什么是"3+"呢,主要是本來(lái)想將這部分放在后面單獨(dú)講,但是考慮到連貫性,就單獨(dú)做一個(gè)補(bǔ)充小節(jié)來(lái)講吧,而且屬于設(shè)置session層的部分.
self.output.rectOfInterest = CGRectMake((100)/(SCREEN_HEIGHT),(SCREEN_WIDTH - 100 - 200)/SCREEN_WIDTH,100/SCREEN_HEIGHT,200/SCREEN_WIDTH);
其實(shí)呢,我是想在當(dāng)前view創(chuàng)建一塊CGRectMake(100, 100, 200, 100)的掃描區(qū)域,如下圖的掃描區(qū)域框:
但是呢,這里需要說(shuō)明的一點(diǎn)就是,我們?nèi)绻凑粘R?guī)的CGRect創(chuàng)建方式去設(shè)置,是肯定不對(duì)的,他會(huì)出現(xiàn)掃描區(qū)域不是預(yù)設(shè)的,為什么呢?
原因很蛋疼,因?yàn)槲覀兤匠5脑O(shè)置CGRect是以左上角為原點(diǎn),橫向增加為+x,縱向增加為+y,橫向?yàn)閷挾葁idth,縱向?yàn)楦叨萮eight,沒(méi)毛病吧,但是,坑爹的就是output的rectOfInterest是以左上角為原點(diǎn),x與y數(shù)值對(duì)調(diào),width和height數(shù)值對(duì)調(diào),并且,x,y,width和height的數(shù)值為0 ~ 1.如下對(duì)比圖:
說(shuō)明一下:這里的計(jì)算對(duì)比,針對(duì)的是,知道掃描框相對(duì)于父視圖的位置,我們根據(jù)掃描框的CGRect可以計(jì)算出需要設(shè)置的output的rectOfInterest的CGRect.至于為什么需要對(duì)調(diào)原理,最近有時(shí)間研究研究(蘋(píng)果這個(gè)設(shè)計(jì)讓很多人不解),目前僅供參考.大家要是有相關(guān)的方案或者明確為何這樣,希望在下面留言,我們一起深究一下.
這里的概念區(qū)別于我們所認(rèn)知的CGRect的設(shè)置,建議童鞋們還是手動(dòng)算一下,之后進(jìn)行邊緣化測(cè)試,就是測(cè)試二維碼從邊緣完全進(jìn)入掃描區(qū)域并且存在掃描任務(wù)的位置.
4).初始化捕獲數(shù)據(jù)類(lèi)AVCaptureSession
// 初始化session
self.session = [[AVCaptureSession alloc]init];
// 設(shè)置session類(lèi)型,AVCaptureSessionPresetHigh 是 sessionPreset 的默認(rèn)值。
[_session setSessionPreset:AVCaptureSessionPresetHigh];
補(bǔ)充:這里簡(jiǎn)單對(duì)sessionPreset的屬性值進(jìn)行以下說(shuō)明:
蘋(píng)果API中提供了如下的四種方式:
// AVCaptureSession 預(yù)設(shè)適用于高分辨率照片質(zhì)量的輸出
AVF_EXPORT NSString *const AVCaptureSessionPresetPhoto NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預(yù)設(shè)適用于高分辨率照片質(zhì)量的輸出
AVF_EXPORT NSString *const AVCaptureSessionPresetHigh NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預(yù)設(shè)適用于中等質(zhì)量的輸出刚照。 實(shí)現(xiàn)的輸出適合于在無(wú)線網(wǎng)絡(luò)共享的視頻和音頻比特率崇堰。
AVF_EXPORT NSString *const AVCaptureSessionPresetMedium NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
// AVCaptureSession 預(yù)設(shè)適用于低質(zhì)量的輸出。為了實(shí)現(xiàn)的輸出視頻和音頻比特率適合共享 3G涩咖。
AVF_EXPORT NSString *const AVCaptureSessionPresetLow NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;
PS:在API的介紹中,除了以上的跡象,我們還會(huì)看到好幾種類(lèi)型,不過(guò)不是針對(duì) ipad iphone 的海诲。針對(duì) MAC_OS,不便介紹,感興趣的可以查看相關(guān)API
5).將輸入流和輸出流添加到session中
這里可以看做是集成,就好比是,我們現(xiàn)在正在建造一輛汽車(chē),我們的原件已經(jīng)做好了,現(xiàn)在要放到汽車(chē)的骨架上.
// 添加輸入流
if ([_session canAddInput:self.input]) {
[_session addInput:self.input];
}
// 添加輸出流
if ([_session canAddOutput:self.output]) {
[_session addOutput:self.output];
}
// 下面的是比較重要的,也是最容易出現(xiàn)崩潰的原因,就是我們的輸出流的類(lèi)型
// 1.這里可以設(shè)置多種輸出類(lèi)型,這里必須要保證session層包括輸出流
// 2.必須要當(dāng)前項(xiàng)目訪問(wèn)相機(jī)權(quán)限必須通過(guò),所以最好在程序進(jìn)入當(dāng)前頁(yè)面的時(shí)候進(jìn)行一次權(quán)限訪問(wèn)的判斷(在文章的最后,我會(huì)貼出相關(guān)的代買(mǎi))
self.output.metadataObjectTypes =@[AVMetadataObjectTypeQRCode];
6).設(shè)置輸出展示平臺(tái)AVCaptureVideoPreviewLayer
// 初始化
self.preview =[AVCaptureVideoPreviewLayer layerWithSession:_session];
// 設(shè)置Video Gravity,顧名思義就是視頻播放時(shí)的拉伸方式,默認(rèn)是AVLayerVideoGravityResizeAspect
// AVLayerVideoGravityResizeAspect 保持視頻的寬高比并使播放內(nèi)容自動(dòng)適應(yīng)播放窗口的大小。
// AVLayerVideoGravityResizeAspectFill 和前者類(lèi)似檩互,但它是以播放內(nèi)容填充而不是適應(yīng)播放窗口的大小特幔。最后一個(gè)值會(huì)拉伸播放內(nèi)容以適應(yīng)播放窗口.
// 因?yàn)榭紤]到全屏顯示以及設(shè)備自適應(yīng),這里我們采用fill填充
self.preview.videoGravity =AVLayerVideoGravityResizeAspectFill;
// 設(shè)置展示平臺(tái)的frame
self.preview.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// 因?yàn)?AVCaptureVideoPreviewLayer是繼承CALayer,所以添加到當(dāng)前view的layer層
[self.view.layer insertSublayer:self.preview atIndex:0];
7).一切準(zhǔn)備就去,開(kāi)始運(yùn)行
[self.session startRunning];
4.掃描結(jié)果處理
這里就需要用到我們之前設(shè)置的兩個(gè)代理AVCaptureMetadataOutputObjectsDelegate和UIAlertViewDelegate
在AVCaptureMetadataOutputObjectsDelegate的代理方法中,有didOutputMetadataObjects這個(gè)方法,表示輸出的結(jié)果,我們掃描二維碼的結(jié)果將要在這里進(jìn)行處理
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
// 判斷掃描結(jié)果的數(shù)據(jù)是否存在
if ([metadataObjects count] >0){
// 如果存在數(shù)據(jù),停止掃描
[self.session stopRunning];
// AVMetadataMachineReadableCodeObject是AVMetadataObject的具體子類(lèi)定義的特性檢測(cè)一維或二維條形碼。
// AVMetadataMachineReadableCodeObject代表一個(gè)單一的照片中發(fā)現(xiàn)機(jī)器可讀的代碼闸昨。這是一個(gè)不可變對(duì)象描述條碼的特性和載荷蚯斯。
// 在支持的平臺(tái)上,AVCaptureMetadataOutput輸出檢測(cè)機(jī)器可讀的代碼對(duì)象的數(shù)組
AVMetadataMachineReadableCodeObject * metadataObject = [metadataObjects objectAtIndex:0];
// 獲取掃描到的信息
NSString *stringValue = metadataObject.stringValue;
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"掃描結(jié)果"
message:stringValue
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"確定", nil];
[self.view addSubview:alert];
[alert show];
}
}
在UIAlertViewDelegate代理方法中,我們確認(rèn)信息后,可以對(duì)信息有相應(yīng)的操作,這里我只是簡(jiǎn)單的進(jìn)行了繼續(xù)進(jìn)行數(shù)據(jù)捕捉(掃描)
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
[self.session startRunning];
}
5.運(yùn)行展示
下面我們看看運(yùn)行的結(jié)果,這里測(cè)試過(guò)程包括區(qū)域掃描的邊緣化測(cè)試:
二.二維碼的生成
講完了二維碼的掃描,接下來(lái)我們接著講講二維碼的生成.
二維碼的生成的核心在于圖形的繪制,我們通過(guò)濾鏡CIFilter和圖形繪制的上下文方式生成二維碼.
0.準(zhǔn)備工作
1.依賴(lài)庫(kù)
二維碼的生成區(qū)別于二維碼的掃描,因?yàn)樗暮诵氖腔趫D形的繪制完成的,所以需要導(dǎo)入CoreImage框架
#import <CoreImage/CoreImage.h>
2.創(chuàng)建濾鏡CIFilte
1.創(chuàng)建濾鏡CIFilter實(shí)例對(duì)象,并通過(guò)類(lèi)方法,將filter的名稱(chēng)指定為CIQRCodeGenerator
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
2.由于filter的強(qiáng)大,我們目前僅是實(shí)現(xiàn)簡(jiǎn)單的二維碼的生成,所以,我們將filter的各項(xiàng)屬性均設(shè)置成默認(rèn)
[filter setDefaults];
3.給過(guò)濾器CIFilter添加數(shù)據(jù)
這里需要說(shuō)明的是,二維碼的主要內(nèi)容可以是如下幾種類(lèi)型(傳統(tǒng)的條形碼只能放數(shù)字):
- 純文本
- URL
- 名片(這個(gè)有待考證,表示我并沒(méi)有試驗(yàn)過(guò))
// 基于多種類(lèi)型,我們簡(jiǎn)單的生成字符串的二維碼
// 創(chuàng)建字符串
NSString *dataString = @"鋒繪動(dòng)漫";
// 將字符串轉(zhuǎn)換成date類(lèi)型,并通過(guò)KVO的形式保存至濾鏡CIFilter(目前指定為二維碼)的inputMessage中
NSData *data = [dataString dataUsingEncoding:NSUTF8StringEncoding];
[filter setValue:data forKeyPath:@"inputMessage"];
4.獲取輸出的二維碼
CIImage *outputImage = [filter outputImage];
5.獲取高清的二維碼,并展示
因?yàn)镃IFilter生成的二維碼相對(duì)而言模糊,達(dá)不到設(shè)備快速識(shí)別的需求,同時(shí)用戶體驗(yàn)差.
所以通過(guò)圖像繪制的上下文來(lái)獲得高清的二維碼圖片.
PS:由于獲取高清圖片不是該章節(jié)的重點(diǎn),相關(guān)的代碼部分來(lái)自網(wǎng)絡(luò),放到文章的最后,僅供看考
// 獲取二維碼
self.imageView.image = [self createErWeiMaImageFormCIImage:outputImage withSize:200];
6.運(yùn)行結(jié)果
掃描的內(nèi)容請(qǐng)參考第一節(jié)"二維碼生成"的運(yùn)行結(jié)果
我是調(diào)皮的分割線
小結(jié)
這就是我理解的二維碼的生成和二維碼的掃描,其中主要的還是針對(duì)兩個(gè)框架的研究,讓我學(xué)到了很多東西.
在學(xué)習(xí)的過(guò)程中,比較建議大家多去查看蘋(píng)果原生的API,這個(gè)對(duì)自我理解是比較重要的,網(wǎng)絡(luò)上的總結(jié)出來(lái)的,只能作為自己的參考,切不可取而代之,最大的禁忌就是CV工程師的道路,再簡(jiǎn)單的代碼也要自己敲出來(lái).
有什么問(wèn)題歡迎大家留言多多留言,多多交流
下面附上demo地址(本人的github上):
二維碼掃描(可區(qū)域)和生成的Demo地址
PS:相關(guān)代碼
1.權(quán)限訪問(wèn)
NSString *mediaType =AVMediaTypeVideo;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
if(authStatus ==AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
UIAlertView *alert =[[UIAlertView alloc] initWithTitle:@“項(xiàng)目名稱(chēng)”
message:@"請(qǐng)?jiān)趇Phone的“設(shè)置”-“隱私”-“相機(jī)”功能中薄风,找到“項(xiàng)目名稱(chēng)”打開(kāi)相機(jī)訪問(wèn)權(quán)限"
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.創(chuàng)建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];
}