iOS 二維碼有效區(qū)域rectOfInterest詳解

demo

前言

  • 關(guān)于二維碼的有效區(qū)域,在開(kāi)發(fā)中遇到的人可能并不是很多,大多數(shù)情況都是直接用第三方,但是當(dāng)你真正自己去嘗試寫(xiě)的時(shí)候,你會(huì)發(fā)現(xiàn)二維碼的有效區(qū)域是一個(gè)很令人捉摸不定的問(wèn)題,其實(shí)很多基于系統(tǒng)的第三方并沒(méi)有解決這個(gè)問(wèn)題,它們都是全屏掃描。
  • 網(wǎng)上有一些關(guān)于rectOfInterest屬性的解釋?zhuān)墙?jīng)過(guò)我自己的核對(duì),發(fā)現(xiàn)他們說(shuō)的并不是很精確,甚至可以說(shuō)是錯(cuò)誤的。我覺(jué)得我還是有必要跟大家分享一下,在網(wǎng)上你真的再也找不到這么詳細(xì)的有關(guān)rectOfInterest的解釋。

影響rectOfInterest的因素

rectOfInterest跟2個(gè)屬性息息相關(guān):一個(gè)是AVCaptureSession(會(huì)話(huà)對(duì)象)的sessionPreset屬性,另一個(gè)是AVCaptureVideoPreviewLayer(預(yù)覽圖層)的videoGravity屬性。在這里,我簡(jiǎn)單講一下這2個(gè)屬性的意義:

sessionPreset屬性

該屬性是設(shè)置圖像音頻等輸出分辨率,大約一共有11個(gè):

// 完整的圖像分辨率輸出,不支持音頻
NSString *const AVCaptureSessionPresetPhoto; 
// 最高分辨率,根據(jù)設(shè)備系統(tǒng)自動(dòng)選擇最高分辨率
NSString *const AVCaptureSessionPresetHigh;
// 中等分辨率,根據(jù)設(shè)備系統(tǒng)自動(dòng)選擇中等分辨率
NSString *const AVCaptureSessionPresetMedium;
// 最低分辨率,根據(jù)設(shè)備系統(tǒng)自動(dòng)選擇最低分辨率
NSString *const AVCaptureSessionPresetLow;
// 以352x288分辨率輸出
NSString *const AVCaptureSessionPreset352x288;
// 以640x480分辨率輸出
NSString *const AVCaptureSessionPreset640x480;
// 以1280x720分辨率輸出
NSString *const AVCaptureSessionPreset1280x720;
// 以1920x1080分辨率輸出
NSString *const AVCaptureSessionPreset1920x1080;
// 以960x540分辨率輸出
NSString *const AVCaptureSessionPresetiFrame960x540;
// 以1280x720分辨率輸出
NSString *const AVCaptureSessionPresetiFrame1280x720;
// 不去控制音頻與視頻輸出設(shè)置,而是通過(guò)已連接的捕獲設(shè)備的 activeFormat 來(lái)反過(guò)來(lái)控制 capture session 的輸出質(zhì)量等級(jí)
NSString *const AVCaptureSessionPresetInputPriority;
videoGravity屬性

該屬性共有3個(gè)值:如果你不了解,我建議你先去熟悉一下UIView的contentMode屬性,光了解沒(méi)有用,必須知道它的原理以及計(jì)算方式

// 保持原始比例,自適應(yīng)最小的bounds,不足的會(huì)有留白;類(lèi)似于UIView的contentMode屬性的UIViewContentModeScaleAspectFit.
AVLayerVideoGravityResizeAspect;

// 保持原始比例,填充整個(gè)bounds,多余的會(huì)被剪掉,類(lèi)似于UIView的contentMode屬性的UIViewContentModeScaleAspectFill.
AVLayerVideoGravityResizeAspectFill;

// 拉伸直到填充整個(gè)bounds,類(lèi)似于UIView的contentMode屬性的UIViewContentModeScaleToFill.
AVLayerVideoGravityResize

正題

一般的,掃描區(qū)域就是預(yù)覽視圖previewLayer的frame對(duì)應(yīng)的矩形框,一般是設(shè)置全屏。如果我們想要設(shè)置一個(gè)有效區(qū)域怎么辦,如同支付寶、微信等將掃碼區(qū)域限制在一個(gè)小正方形內(nèi)。這就要用到輸出流AVCaptureMetadataOutput的一個(gè)rectOfInterest屬性。

rectOfInterest默認(rèn)為(0,0,1,1);

大家應(yīng)該提出質(zhì)疑:為什么寬高才為1?這也太小了吧,然而這個(gè)區(qū)域卻是全屏。這是腫么肥四呢? 聰明的你應(yīng)該猜到了,rectOfInterest肯定是經(jīng)過(guò)某種轉(zhuǎn)化而來(lái),而且x,y, w, h的范圍均在0~1之間。究竟是如何轉(zhuǎn)化的,且聽(tīng)我慢慢說(shuō)給你聽(tīng):
假如在手機(jī)屏幕中,我想限制有效掃描區(qū)域在矩形框(10,10,100,100)內(nèi),是不是這樣設(shè)置:

metadataOutput.rectOfInterest = CGRectMake(10, 10, 100, 100);

這樣對(duì)嗎?肯定不對(duì)咯,因?yàn)檫€沒(méi)有轉(zhuǎn)化為0~1的范圍內(nèi)呢。
好的,我們一起來(lái)轉(zhuǎn)化一下,由于圖像都是顯示在預(yù)覽視圖previewLayer中,所以自然是通過(guò)previewLayer的frame來(lái)轉(zhuǎn)化.

假設(shè)previewLayer的frame為全屏,記為:
preViewRect = CGRectMake(0,0,kScreenW,kScreenH);
有效掃描區(qū)域?yàn)?validRect =  CGRectMake(x, y, w, h);
轉(zhuǎn)化后:
rectOfInterest = (x / kScreenW, y / kScreenH, w / kScreenW, h / kScreenH)

到此,轉(zhuǎn)化結(jié)束!就這樣完了嗎?還早著呢!

我問(wèn)大家一個(gè)問(wèn)題:矩形框rectOfInterest=(0,0,1,1)應(yīng)該在屏幕的哪個(gè)位置?
大家應(yīng)該會(huì)回答在屏幕的左上方,沒(méi)錯(cuò),不僅是你,就連官方文檔的解釋都是這樣說(shuō)的,官方文檔說(shuō):

rectOfInterest中的origin如果為(0,0),表示在圖像的左上方;如果為(1,1),表示 在未經(jīng)過(guò)旋轉(zhuǎn)的圖像的右下方。這很符合我們的想象。

好。如果按照我們的想象或者官方文檔所說(shuō),我們?cè)O(shè)置的有效區(qū)域:(10 ,10 , 100 ,100)應(yīng)該會(huì)偏左上方。然而,結(jié)果并非如此,顯示結(jié)果是這樣的:

紅色矩形框代表掃碼區(qū)域
17298B562720FB2280DC530FD12022EA.jpg

有沒(méi)有發(fā)現(xiàn),顯示結(jié)果和我們想的完全相反,偏右上角,也就是說(shuō):

核心句子:

實(shí)際顯示在我們?nèi)庋劭吹降钠聊恢械淖鴺?biāo)原點(diǎn),應(yīng)該是在右上角,這就好比是小明在照鏡子,假如小明真人的左臉頰有一顆痣,那么在鏡子中,痣應(yīng)該是在右臉頰。我們所想的rectOfInterest,都是鏡中的rectOfInterest。

既然我們已經(jīng)知道了坐標(biāo)原點(diǎn),那么我們想讓掃描區(qū)域(10 ,10 , 100 ,100)顯示在左上方,就不是難事了。


17298B562720FB2280DC530FD12022EA.jpg

如上圖:左邊紅色矩形框就是我們實(shí)際要的掃描區(qū)域所在位置,最關(guān)鍵是要求出圖中藍(lán)點(diǎn)相對(duì)原點(diǎn)(右上角)的坐標(biāo)。

藍(lán)點(diǎn)的坐標(biāo)(相對(duì)右上角)為:
x = (kScreenW-(100+10)) / kScreenW;
y = 10 / kScreenH; 

// 除以kScreenW和kScreenH是轉(zhuǎn)化比例

// 由此,我們可以推導(dǎo)出一個(gè)轉(zhuǎn)化公式:

設(shè) 有效區(qū)域?yàn)?validRect = CGRectMake(x, y, w, h);
預(yù)覽圖層的frame為
preViewRect = CGRectMake(0, 0, kScreenW, kScreenH);
那么
rectOfInterest = CGRectMake((kScreenW-(w+x)) / kScreenW, y / kScreenH, w / kScreenW, h / kScreenH);

到了這里, 離成功似乎很近了,但是很遺憾, 漫長(zhǎng)的路才剛起步!用此公式代入計(jì)算,發(fā)現(xiàn)掃碼區(qū)域完全不對(duì),好桑心,為什么會(huì)這樣?于是猜想: AVCapture輸出的圖片大小都是橫著的,而iPhone的屏幕是豎著的,那么我把它旋轉(zhuǎn)90°呢:
旋轉(zhuǎn)90°也就意味著x與y互換,w和h互換,即:rectOfInterest的x, y, w , h 應(yīng)該對(duì)應(yīng)y, x , h, w;轉(zhuǎn)換如下:

有一定正確性的轉(zhuǎn)化公式:

設(shè) 有效區(qū)域?yàn)?validRect = CGRectMake(x, y, w, h);
預(yù)覽圖層的frame為
preViewRect = CGRectMake(0, 0, kScreenW, kScreenH);
那么
rectOfInterest = CGRectMake(y / kScreenH, (kScreenW-(w+x)) / kScreenW, h / kScreenH, w / kScreenW);

// 這個(gè)公式上升了一個(gè)級(jí)別,有了一定的正確性,但是它太“死”了,不夠靈活,也就是說(shuō),假如我隨意更換設(shè)備,隨意修改sessionPreset和videoGravity屬性的話(huà),此公式計(jì)算出來(lái)的掃描區(qū)域是不準(zhǔn)確的。這下該怎么辦,我差點(diǎn)就要放棄了,到這里就結(jié)束算了,但是心里總感覺(jué)有點(diǎn)希望,于是徹夜都在想這個(gè)問(wèn)題。
大家還記得我開(kāi)篇講的sessionPreset和videoGravity屬性吧,在這里,這倆屬性就要閃亮登場(chǎng)了。

核心句子:

rectOfInterest是相對(duì)圖像大小的比例,而不是相對(duì)設(shè)備或者預(yù)覽圖層AVCaptureVideoPreviewLayer的比例

既然是相對(duì)圖像,由于圖像的輸出有多種模式,這些模式通過(guò)AVCaptureVideoPreviewLayer的videoGravity屬性設(shè)置,如AVLayerVideoGravityResizeAspectFill;由于這些模式的設(shè)置,導(dǎo)致圖像會(huì)被裁減、留白或者拉伸,所以我們計(jì)算出來(lái)的結(jié)果是相對(duì)圖像而言的,我們需要將其轉(zhuǎn)化到預(yù)覽圖層AVCaptureVideoPreviewLayer上來(lái)。所以我開(kāi)始要求大家去熟悉一下UIView的contentMode模式。
我不廢話(huà)了,我直接上轉(zhuǎn)化過(guò)程,我將其封裝成了一個(gè)方法.

最終的萬(wàn)能轉(zhuǎn)化公式:(本文核心)

// 該方法中,_preViewLayer指的是AVCaptureVideoPreviewLayer的實(shí)例對(duì)象,_session是會(huì)話(huà)對(duì)象,_metadataOutput是掃碼輸出流
- (void)coverToMetadataOutputRectOfInterestForRect:(CGRect)cropRect {
    CGSize size = _previewLayer.bounds.size;
    CGFloat p1 = size.height/size.width;
    CGFloat p2 = 0.0;

    if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset1920x1080]) {
        p2 = 1920./1080.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset352x288]) {
        p2 = 352./288.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset1280x720]) {
        p2 = 1280./720.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetiFrame960x540]) {
        p2 = 960./540.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetiFrame1280x720]) {
        p2 = 1280./720.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetHigh]) {
        p2 = 1920./1080.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetMedium]) {
        p2 = 480./360.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetLow]) {
        p2 = 192./144.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetPhoto]) { // 暫時(shí)未查到具體分辨率,但是可以推導(dǎo)出分辨率的比例為4/3
         p2 = 4./3.;
    }
    else if ([_session.sessionPreset isEqualToString:AVCaptureSessionPresetInputPriority]) {
        p2 = 1920./1080.;
    }
    else if (@available(iOS 9.0, *)) {
        if ([_session.sessionPreset isEqualToString:AVCaptureSessionPreset3840x2160]) {
            p2 = 3840./2160.;
        }
    } else {
        
    }
    if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResize]) {
        _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y)/size.height,(size.width-(cropRect.size.width+cropRect.origin.x))/size.width, cropRect.size.height/size.height,cropRect.size.width/size.width);
    } else if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) {
        if (p1 < p2) {
            CGFloat fixHeight = size.width * p2;
            CGFloat fixPadding = (fixHeight - size.height)/2;
            _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
                                                        (size.width-(cropRect.size.width+cropRect.origin.x))/size.width,
                                                        cropRect.size.height/fixHeight,
                                                        cropRect.size.width/size.width);
        } else {
            CGFloat fixWidth = size.height * (1/p2);
            CGFloat fixPadding = (fixWidth - size.width)/2;
            _metadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
                                                        (size.width-(cropRect.size.width+cropRect.origin.x)+fixPadding)/fixWidth,
                                                        cropRect.size.height/size.height,
                                                        cropRect.size.width/fixWidth);
        }
    } else if ([_previewLayer.videoGravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
        if (p1 > p2) {
            CGFloat fixHeight = size.width * p2;
            CGFloat fixPadding = (fixHeight - size.height)/2;
            _metadataOutput.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,
                                                        (size.width-(cropRect.size.width+cropRect.origin.x))/size.width,
                                                        cropRect.size.height/fixHeight,
                                                        cropRect.size.width/size.width);
        } else {
            CGFloat fixWidth = size.height * (1/p2);
            CGFloat fixPadding = (fixWidth - size.width)/2;
            _metadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,
                                                        (size.width-(cropRect.size.width+cropRect.origin.x)+fixPadding)/fixWidth,
                                                        cropRect.size.height/size.height,
                                                        cropRect.size.width/fixWidth);
        }
    }
}

上面那個(gè)公式就是最終的轉(zhuǎn)化公式,有一點(diǎn)要聲明一下,當(dāng)開(kāi)發(fā)者設(shè)置輸出分辨率為AVCaptureSessionPresetHigh、AVCaptureSessionPresetMedium、AVCaptureSessionPresetLow、AVCaptureSessionPresetPhoto等不確定性分辨率時(shí),我都是默認(rèn)給了一個(gè)對(duì)應(yīng)的明確的分辨率,例如AVCaptureSessionPresetHigh我計(jì)算時(shí)采用的是1920x1080,因?yàn)槲覝y(cè)試時(shí)是采用的iPhone6s,其他機(jī)型未必是這個(gè)分辨率,所以當(dāng)分辨率取決于設(shè)備時(shí),你自己需要根據(jù)設(shè)備的不同去修改一下。本人沒(méi)有那么多真機(jī),所以我無(wú)法給出通用的答案。

metadataOutputRectOfInterestForRect方法

我想有人肯定一直在懷疑,為什么不用系統(tǒng)自帶的metadataOutputRectOfInterestForRect方法,這個(gè)方法就是我上面那個(gè)公式的功能啊,甚至更權(quán)威。但是,試了就知道,metadataOutputRectOfInterestForRect在輸入流格式發(fā)生變化之前設(shè)置是無(wú)效的,你需要監(jiān)聽(tīng)一個(gè)通知:AVCaptureInputPortFormatDescriptionDidChangeNotification,在通知方法中調(diào)用metadataOutputRectOfInterestForRect才起作用,或者你開(kāi)啟掃碼startRunning之后再設(shè)置也行,這些做法確實(shí)也能計(jì)算出掃碼有效區(qū)域,但是會(huì)卡頓,開(kāi)啟掃描之后,總是會(huì)卡一下,才開(kāi)始掃描,這非常影響用戶(hù)體驗(yàn),所以不建議使用。

作者寄語(yǔ)

你可以用我的公式和采用系統(tǒng)的metadataOutputRectOfInterestForRect方法的轉(zhuǎn)化結(jié)果對(duì)比一下,你會(huì)發(fā)現(xiàn)結(jié)果的差距非常微妙,只有零點(diǎn)零零幾的誤差

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容