轉載:http://blog.cnbluebox.com/blog/2014/08/26/ioser-wei-ma-sao-miao/
IOS二維碼掃描,你需要注意的兩件事
在 IOS7 以前,在IOS中實作二維碼和條形碼掃描,我們所知的有,兩大開源元件 ZBar 與 ZXing. 這兩大元件我們都有用過,這裡總結下各自的缺點:
ZBar
ZBar在掃描的靈敏度上,和記憶體的使用上相對于ZXing上都是較優的,但是對于 “圓角二維碼” 的掃描确很困難。如:
ZXing
ZXing 是 Google Code上的一個開源的條形碼掃描庫,是用java設計的,連Google Glass 都在使用的。但有人為了追求更高效率以及可移植性,出現了c++ port. Github上的Objectivc-C port,其實就是用OC代碼封裝了一下而已,而且已經停止維護。這樣效率非常低,在instrument下面可以看到CPU和記憶體瘋漲,在記憶體小的機器上很容易崩潰
AVFoundation
AVFoundation無論在掃描靈敏度和性能上來說都是最優的,是以毫無疑問我們應該切換到AVFoundation,需要相容IOS6或之前的版本可以用zbar或zxing代替。
下面介紹本文的重點,無論你是用以上哪一種或其他的解決方案,都需要知道下面兩點。
1. 圖檔很小的二維碼
以前測試提了一個bug,說有二維碼掃不了,拿到二維碼一看,是個很小的二維碼,邊長不到1cm,于是就修改了 sessionPreset 為 1080p 的,當時用的是ZXing, 當把圖檔品質改清楚時,也造成了性能的下降,基本打開掃描界面就會報memoryWarning,但是也确實解決了小二維碼掃描的問題。
AVCaptureSession 可以設定 sessionPreset 屬性,這個決定了視訊輸入每一幀圖像品質的大小。
AVCaptureSessionPreset320x240
AVCaptureSessionPreset352x288
AVCaptureSessionPreset640x480
AVCaptureSessionPreset960x540
AVCaptureSessionPreset1280x720
AVCaptureSessionPreset1920x1080
以上列舉了部分的屬性值,分别代表輸入圖檔品質大小,一般來說AVCaptureSessionPreset640x480就夠使用,但是如果要保證較小的二維碼圖檔能快速掃描,最好設定高些,如AVCaptureSessionPreset1920x1080(就是我們常說的1080p).
2. scanCrop
另一個提升掃描速度和性能的就是設定解析的範圍,在zbar和zxing中就是scanCrop, AVFoundation中設定 AVCaptureMetadataOutput 的 rectOfInterest 屬性來配置解析範圍。
最開始我按照文檔說的按照比例值來設定這個屬性,如下:
CGSize size = self.view.bounds.size;
CGRect cropRect = CGRectMake(40, 100, 240, 240);
captureOutput.rectOfInterest = CGRectMake(cropRect.origin.x/size.width,
cropRect.origin.y/size.height,
cropRect.size.width/size.width,
cropRect.size.height/size.height);
但是發現, Oops, 好像不對啊,掃不到了,明顯不正确呢,于是猜想: AVCapture輸出的圖檔大小都是橫着的,而iPhone的螢幕是豎着的,那麼我把它旋轉90°呢:
CGSize size = self.view.bounds.size;
CGRect cropRect = CGRectMake(40, 100, 240, 240);
captureOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
cropRect.origin.x/size.width,
cropRect.size.height/size.height,
cropRect.size.width/size.width);
OK,貌似對了,在iPhone5上一切工作良好,但是在4s上,或者換了sessionPreset的大小之後,這個框貌似就不那麼準确了, 可能發現超出框上下一些也是可以掃描出來的。 再次猜想: 圖檔的長寬比和手機螢幕不是一樣的,這個rectOfInterest是相對于圖檔大小的比例。比如iPhone4s螢幕大小是 640x960, 而圖檔輸出大小是 1920x1080. 實際的情況可能就是下圖中的效果:
上圖中下面的代表iPhone4s螢幕,大小640x960, 上面代表AVCaptureVideoPreviewLayer中預覽到的圖檔位置,在圖檔輸入為1920x1080大小時,實際大小上下會被截取一點的,因為我們AVCaptureVideoPreviewLayer設定的videoGravity是AVLayerVideoGravityResizeAspectFill, 類似于UIView的UIViewContentModeScaleAspectFill效果。
于是我對大小做了一下修正:
CGSize size = self.view.bounds.size;
CGRect cropRect = CGRectMake(40, 100, 240, 240);
CGFloat p1 = size.height/size.width;
CGFloat p2 = 1920./1080.; //使用了1080p的圖像輸出
if (p1 < p2) {
CGFloat fixHeight = bounds.size.width * 1920. / 1080.;
CGFloat fixPadding = (fixHeight - size.height)/2;
captureOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
cropRect.origin.x/size.width,
cropRect.size.height/fixHeight,
cropRect.size.width/size.width);
} else {
CGFloat fixWidth = bounds.size.height * 1080. / 1920.;
CGFloat fixPadding = (fixWidth - size.width)/2;
captureOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
(cropRect.origin.x + fixPadding)/fixWidth,
cropRect.size.height/size.height,
cropRect.size.width/fixWidth);
}
經過上面的驗證,證明了猜想rectOfInterest是基于圖像的大小裁剪的。
3. 小結
scanCrop對于掃描來說是比較重要的,試想圖檔截小點來解析是不是理論上就會更快了呢。網絡上貌似很難搜到關于scanCrop的詳解,希望對看到的人有幫助。
---------------------------------------------------------------------------------------------------------------------
繼續補充:
http://blog.txx.im/blog/2014/05/29/qrcode-detect-with-avfoundation/
關於iOS原生條碼掃描,你需要注意的兩三事
前言
這篇文章是我們在新發佈的禮物說的iOS端開發過程中遇到的一些關於條碼的問題總結而來。
本文記錄的問題是:當AVFoundation使用多解碼器掃描的時候。二維碼是秒殺,但是條形碼卻經常掃不上。如果去掉二維碼的話,條形碼掃描又秒殺的問題。
為什麼我們沒有選用ZXing而是用AVfoundation呢,是因為我說服了老闆,iOS7開發,而不再去相容iOS5/6。是以我們終於可以拋棄效率低下的ZXing,而選擇AVFoundation。為什麼說ZXing效率低下,我們這裡可以說上幾句。
ZXing
ZXing 是 Google Code上的一個開源的條碼掃描庫,是用java設計的,連Google Glass 都在使用的。但有人為了追求更高效率以及可移植性,出現了c++ port. Github上的Objectivc-C port,其實就是用OC代碼封裝了一下而已,而且已經停止維護。
ZXing掃描,是拿到攝像頭的每一幀,然後對其根據如下公式做灰階化
1
f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j))
之後做全局直方圖二值化的方法,最後按照 ISO/IEC 18004 規範進行解析。
這樣效率非常低,在instrument下面可以看到CPU佔用遠遠高於 AVFoundation。而且全局直方圖二值化導緻精準度並不高。這個庫還會帶來一大堆C++的東西,在純iOS7的工程下,不推薦使用。
AVFoundation 掃碼的簡單使用
這裡說一下,我們禮物說是和passbook一樣,同時可以掃描二維碼和條形碼,真是因為這個特性,導緻了我寫這篇總結。 先粘一下掃碼實現部份,如下。
- (BOOL)startReading {
_isReading = YES;
NSError *error;
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
if (!input) {
NSLog(@"%@", [error localizedDescription]);
return NO;
}
_captureSession = [[AVCaptureSession alloc] init];
// Set the input device on the capture session.
[_captureSession addInput:input];
AVCaptureMetadataOutput *captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
[_captureSession addOutput:captureMetadataOutput];
// Create a new serial dispatch queue.
dispatch_queue_t dispatchQueue;
dispatchQueue = dispatch_queue_create("myQueue", NULL);
[captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatchQueue];
if (self.qrcodeFlag)
[captureMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
else
[captureMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObjects:AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode, nil]];
_videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
[_videoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[_videoPreviewLayer setFrame:self.view.layer.bounds];
[self.view.layer addSublayer:_videoPreviewLayer];
[_captureSession startRunning];
return YES;
}
-(void)stopReading{
[_captureSession stopRunning];
_captureSession = nil;
[_videoPreviewLayer removeFromSuperlayer];
}
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects
fromConnection:(AVCaptureConnection *)connection
{
if (!_isReading) return;
if (metadataObjects != nil && [metadataObjects count] > 0) {
AVMetadataMachineReadableCodeObject *metadataObj = [metadataObjects objectAtIndex:0];
Do Something....
}
}
這個代碼也不需要加什麼註釋,挺簡單易懂的。
闡述問題
我們上面說過了:當AVFoundation使用多解碼器掃描的時候。二維碼是秒殺,但是條形碼卻經常掃不上。如果去掉二維碼的話,條形碼掃描又秒殺的問題。
但有趣的事情是,如果我寫了個demo,用上述代碼的話。卻又可以秒殺掃描。這個問題困擾了我一下午,仔細對比了項目中的每一行代碼和我demo中的全部。除了demo沒有畫一個提示框在螢幕上以外,其他地方全都一模一樣。
那麼為什麼導緻項目中掃描效率如此之慢呢?
猜想1: UI以及後臺線程佔用大量CPU時間,
結果在 instrument下,不攻自破,cpu佔用,內存佔用非常非常低。
猜想2:系統架構問題
因為添加了QRCode才導緻掃描變慢的,那麼就應該是和算法效率有關。多引入了一個每一幀都要工作的解碼器,導緻條形碼掃描效率下降。我的Demo是arm64 v7s v7 系統全支援,而項目是ArmV7。
這個想法挺異想天開的。覺得可能是Arm64的指令集效率比armv7快得多導緻的。我還去問巧哥,armv7和arm64在密集運算的時候效率差多少,會不會比較明顯。
但重新配置了一下,還是錯誤的。
插曲
我發現把螢幕橫過來掃描效率比豎過來高多了。於是懷疑是不是 Capture 的方向問題。
猜想3: 攝像頭方向問題導緻解碼效率低
這個猜想,我沒有去證實,因為太麻煩了。要給Session 添加一個新的output 來輸出每一幀,而且還是個CMBuffer,還要手動轉碼。不過後面證實這個也是錯的。
猜想4:攝像頭參數問題
當初看AVCam 寫拍照模塊的時候,記得攝像頭有很多參數,ZXing 也有一個檔位叫做精確解碼,犧牲效率換精確度。於是就在想會不會蘋果家的也要設置參數。
於是就壞懷這個問題去看文檔去了,結果歪打正著的發現了正確原因。 這是記錄在蘋果的FAQ中的,並沒在AVFoundation 的 Reference 中。具體編號為:Technical Note TN2325
正確原因
就是描述問題裡面說到的,demo和工程裡面的唯一區別,多了個surfaceLayer。如下圖:
為了正確解釋這個有趣的問題,我們要解釋一下條形碼掃描原理。
上面有提過二維碼是通過全局直方圖二值化後,按照ISO標準解碼,實際上是,按照1:1:3:1:1去尋找那三個尋像圖形,就是標誌性的大方塊。然後圈出二維碼大小再去解碼的。也就是說,再沒設定邊界的情況下全屏都可以。
而條形碼完全不同,他是在Detect Center那個點,畫一個無限延伸的米字型,然後去判斷每一條線上能否解析出條形碼所需要的0101010序列。而iOS默認的Center是 Layer 的 Center。
我們再回過頭來看工程中的 SurfaceLayer,其實他提示給用戶的那個框,已經遠離了Center。是以我們豎著掃描的時候,那條水準的掃描線是沒有貫穿條形碼的,是以掃不上他。
於是乎要根據設備,iPhone4 iPhone5 通過AVCaptureDeviceFormat和AVCaptureSessionPreset 重新設置一下AVCaptureMetadataOutput rectOfInterest,結果問題就解決了。
為什麼去掉二維碼就沒事了呢
還在那篇FAQ中,有那麼一個表格。
Supported Device Scan lines (only 1D codes enabled) Scan lines (when 2D codes are also enabled)
iPhone 4 Center Center
iPhone 4S and later Center + additional Center
iPad (all) Center + additional Center
iPod Touch Center + additional Center
可見,當我們沒有二維碼的時候,他會有個additional存在。用更加優秀且稍微耗時的算法去優化掃描精準度。
總結
當我們遇到問題的時候,不光要記得看 蘋果的 guide 和 reference,還要記得看以下 sample code,tech note, FAQ。說不好有意外收穫
為什麼條形碼掃描器上往往會有一條紅線,這並不是為了擬物化,而是告訴用戶一定要用這條線對準條形碼,否則會有掃不上的可能性。
正如福爾摩斯所說:抛開所有不可能的,剩下的,不管多麼令人匪夷所思,那都是事實。兩套代碼僅有UI不一樣,效果不同,其實就是UI引導用戶錯誤的使用了掃描器。