IOS音視頻:相機(jī)識(shí)別

原創(chuàng):知識(shí)探索型文章
創(chuàng)作不易,請(qǐng)珍惜鬼悠,之后會(huì)持續(xù)更新删性,不斷完善
個(gè)人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈焕窝,這些文章記錄了我的IOS成長(zhǎng)歷程蹬挺,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書不支持目錄跳轉(zhuǎn),大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容

目錄

  • 一它掂、AR
    • 1巴帮、概念
    • 2、設(shè)備追蹤
    • 3虐秋、ARFrame
    • 4晰韵、場(chǎng)景解析
    • 5、SceneKit
  • 二熟妓、二維碼識(shí)別
    • 1、設(shè)備的配置流程
    • 2栏尚、CameraController 中的二維碼識(shí)別方法
    • 3起愈、PreviewView中的二維碼識(shí)別方法
  • 三、人臉識(shí)別
    • 1、人臉識(shí)別簡(jiǎn)介
    • 2抬虽、使用 CoreImage 實(shí)現(xiàn)靜態(tài)人臉識(shí)別
    • 3官觅、使用 Vision 實(shí)現(xiàn)靜態(tài)人臉識(shí)別
    • 4、使用 OpenCV 實(shí)現(xiàn)靜態(tài)人臉識(shí)別
    • 5阐污、使用 AVFoundation 實(shí)現(xiàn)動(dòng)態(tài)人臉識(shí)別
  • Demo
  • 參考文獻(xiàn)

一休涤、AR

1、概念

AR 全稱 Augmented Reality(增強(qiáng)現(xiàn)實(shí))是一種在視覺(jué)上呈現(xiàn)虛擬物體與現(xiàn)實(shí)場(chǎng)景結(jié)合的技術(shù)笛辟。Apple 公司在 2017 年正式推出了ARKit功氨,iOS 開發(fā)者可以在這個(gè)平臺(tái)上使用簡(jiǎn)單便捷的 API 來(lái)開發(fā) AR 應(yīng)用程序。為了獲得 ARKit的完整功能手幢,需要A9 及以上芯片捷凄。

AR 的全稱是增強(qiáng)現(xiàn)實(shí),通過(guò)顯示屏與各類傳感器围来,能讓虛擬世界中創(chuàng)作出的物品與現(xiàn)實(shí)世界產(chǎn)生交互跺涤。

技術(shù)方面,現(xiàn)有 AR 解決方案已經(jīng)可以檢測(cè)并記錄虛擬物品在現(xiàn)實(shí)環(huán)境中的準(zhǔn)確位置监透、分辨虛擬物品與現(xiàn)實(shí)中物品的遮擋關(guān)系桶错、為虛擬物體增加在現(xiàn)實(shí)中的光源與陰影、讓虛擬物品與現(xiàn)實(shí)中的物品產(chǎn)生交互等胀蛮。就像其它新技術(shù)一樣院刁,AR 的具體使用場(chǎng)景仍處于探索階段,下面是我在關(guān)注的幾個(gè)使用角度醇滥。

游戲 - 沉浸與代入感:說(shuō)到虛擬現(xiàn)實(shí)的應(yīng)用黎比,許多人可能都會(huì)想到游戲。起先我并沒(méi)太注意這些所謂 AR 游戲與直接在手機(jī)里玩有什么區(qū)別鸳玩,直到前一陣子阅虫,精靈寶可夢(mèng)支持了 AR 模式,允許你把寶可夢(mèng)放在世界中

在真實(shí)世界中與寶可夢(mèng)互動(dòng)的體驗(yàn)讓我意識(shí)到不跟,在手機(jī)中與模型交互颓帝,給人直觀的感覺(jué)始終是在和屏幕互動(dòng),無(wú)論模型和場(chǎng)景搭建的有多精致窝革,用戶感知到的始終只是屏幕购城。將模型放在現(xiàn)實(shí)世界中,無(wú)論你怎么移動(dòng)手機(jī)虐译,它還在那里瘪板。雖然仍舊摸不到,但能看到它出現(xiàn)在日常生活中漆诽,熟悉的現(xiàn)實(shí)里侮攀,帶來(lái)的沉浸與代入感完全不同锣枝。

當(dāng)我打開 AR 應(yīng)用時(shí),恍然意識(shí)到三維模型本就應(yīng)該空間概念兰英。建筑學(xué)用二維的三視圖來(lái)避免圖紙與模型潛在的不確定因素撇叁,呈現(xiàn)在空間中的三維模型則可以徹底避免這個(gè)問(wèn)題,人可以自然地走動(dòng)來(lái)確定所有細(xì)節(jié)畦贸。

若你將 AR 的技術(shù)開發(fā)進(jìn)度想象成建造一輛車陨闹,ARKit 的定位便是配件,之后的所有功能都建立在這個(gè)基礎(chǔ)之上薄坏。ARKit 中提供一些與 AR 相關(guān)的核心能力趋厉,如:提供景深信息來(lái)判斷物體在空間中的位置、用空間錨點(diǎn)來(lái)記錄虛擬物品在真實(shí)世界中的經(jīng)緯度及海拔坐標(biāo)颤殴、用攝像頭對(duì)面部信息進(jìn)行追蹤等觅廓。下圖中,圖標(biāo)下方的陰影及切換角度后物品位置不變的特性涵但,便是 ARKit 提供的基礎(chǔ)能力杈绸。

原深感鏡頭組

許多人了解這個(gè)鏡頭組件是因?yàn)?iPhone X 發(fā)布會(huì)上的面容 ID,替換了傳統(tǒng)指紋識(shí)別來(lái)用于安全的驗(yàn)證身份矮瘟。實(shí)際上原深感鏡頭組用途不僅于身份驗(yàn)證瞳脓,它會(huì)投射 30,000 多個(gè)不可見(jiàn)的點(diǎn)來(lái)來(lái)獲取準(zhǔn)確的深度數(shù)據(jù),這些數(shù)據(jù)對(duì) AR 很有幫助澈侠。前置攝像頭所做的攝像時(shí)背景替換劫侧,臉上試戴墨鏡這類 AR 應(yīng)用,許多都是通過(guò) ARKit 調(diào)用原深感攝像頭組件的硬件來(lái)實(shí)現(xiàn)的哨啃。

針對(duì) AR 模型的技術(shù)準(zhǔn)備 USDZ

市面上 3D 建模軟件眾多烧栋,各家選用的導(dǎo)出格式也差異非常大。為解決素材格式不統(tǒng)一的問(wèn)題拳球,Apple 與皮克斯合作推出了 USDZ 模型封裝格式审姓。此后 AR 場(chǎng)景所采用的素材均需為 USDZ ,避免了項(xiàng)目中各類文件混雜的情況祝峻。USDZ 自身也是個(gè)實(shí)力派魔吐,支持將模型的動(dòng)畫,紋理材質(zhì)等眾多與模型相關(guān)的內(nèi)容莱找,全部封裝在一個(gè)文件中酬姆。

空間感知芯片 U 系列

U 系列是在 iPhone 設(shè)備中新增的超寬帶 UWB 芯片,它能提供在較大空間內(nèi)極度精準(zhǔn)定位的能力奥溺。舉個(gè)例子辞色,若你的 AirPods Pro 找不到了,配備 U 系列芯片的手機(jī)可以準(zhǔn)確指向該耳機(jī)的準(zhǔn)確方向與距離「《ǎ現(xiàn)有 AR 技術(shù)主要基于攝像頭淫僻,當(dāng)用戶脫離攝像頭區(qū)域后诱篷,便會(huì)丟失目標(biāo),若用戶攜帶 Air Tag 定位器雳灵,則可能用 U 芯片進(jìn)行定位,來(lái)實(shí)現(xiàn)許多之前無(wú)法做到的場(chǎng)景闸盔。雖 Apple 暫時(shí)還沒(méi)開放它在 AR 場(chǎng)景的應(yīng)用悯辙,但 U 系列芯片給開發(fā)者提供了許多想象力。

iOS 平臺(tái)的 AR 應(yīng)用通常由 ARKit 和渲染引擎兩部分構(gòu)成:

架構(gòu)

ARKitARSession 負(fù)責(zé)管理每一幀的信息迎吵。ARSession 做了兩件事躲撰,拍攝圖像并獲取傳感器數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行分析處理后逐幀輸出击费。如下圖:

ARSession

2拢蛋、設(shè)備追蹤

a、啟動(dòng) ARSession

設(shè)備追蹤確保了虛擬物體的位置不受設(shè)備移動(dòng)的影響蔫巩。在啟動(dòng) ARSession時(shí)需要傳入一個(gè) ARSessionConfiguration的子類對(duì)象谆棱,以區(qū)別三種追蹤模式:ARFaceTrackingConfigurationARWorldTrackingConfiguration圆仔、AROrientationTrackingConfiguration贡翘。其中 ARFaceTrackingConfiguration 可以識(shí)別人臉的位置雪隧、方向以及獲取拓?fù)浣Y(jié)構(gòu)。此外,還可以探測(cè)到預(yù)設(shè)的 52 種豐富的面部動(dòng)作再芋,如眨眼、微笑竣蹦、皺眉等等橄霉。ARFaceTrackingConfiguration 需要調(diào)用支持 TrueDepth 的前置攝像頭進(jìn)行追蹤。以ARWorldTrackingConfiguration為例進(jìn)行追蹤沪曙,獲取特征點(diǎn)奕污。

// 創(chuàng)建一個(gè) ARSessionConfiguration
let configuration = ARWorldTrackingSessionConfiguration()

// Create a session
let session = ARSession()

// Run
session.run(configuration)
b、ARSession 底層如何進(jìn)行世界追蹤
  1. ARSession 底層使用了 AVCaputreSession 來(lái)獲取攝像機(jī)拍攝的視頻(一幀一幀的圖像序列)珊蟀。
  2. ARSession 底層使用了CMMotionManager 來(lái)獲取設(shè)備的運(yùn)動(dòng)信息(比如旋轉(zhuǎn)角度菊值、移動(dòng)距離等)
  3. ARSession 根據(jù)獲取的圖像序列以及設(shè)備的運(yùn)動(dòng)信息進(jìn)行分析,最后輸出 ARFrame育灸,ARFrame 中就包含有渲染虛擬世界所需的所有信息腻窒。
ARSession 底層如何進(jìn)行世界追蹤
ARSession 底層如何進(jìn)行世界追蹤
c、追蹤信息點(diǎn)

AR-World 的坐標(biāo)系如下磅崭,當(dāng)我們運(yùn)行 ARSession 時(shí)設(shè)備所在的位置就是 AR-World 的坐標(biāo)系原點(diǎn)儿子。

AR-World的坐標(biāo)系

在這個(gè) AR-World 坐標(biāo)系中,ARKit 會(huì)追蹤以下幾個(gè)信息:

  • 追蹤設(shè)備的位置以及旋轉(zhuǎn)砸喻,這里的兩個(gè)信息均是相對(duì)于設(shè)備起始時(shí)的信息柔逼。
  • 追蹤物理距離(以“米”為單位)蒋譬,例如 ARKit 檢測(cè)到一個(gè)平面,我們希望知道這個(gè)平面有多大愉适。
  • 追蹤我們手動(dòng)添加的希望追蹤的點(diǎn)犯助,例如我們手動(dòng)添加的一個(gè)虛擬物體。
d维咸、追蹤如何工作

ARKit使用視覺(jué)慣性測(cè)距技術(shù)剂买,對(duì)攝像頭采集到的圖像序列進(jìn)行計(jì)算機(jī)視覺(jué)分析,并且與設(shè)備的運(yùn)動(dòng)傳感器信息相結(jié)合癌蓖。ARKit 會(huì)識(shí)別出每一幀圖像中的特征點(diǎn)瞬哼,并且根據(jù)特征點(diǎn)在連續(xù)的圖像幀之間的位置變化,然后與運(yùn)動(dòng)傳感器提供的信息進(jìn)行比較租副,最終得到高精度的設(shè)備位置和偏轉(zhuǎn)信息坐慰。

追蹤如何工作

上圖中劃出曲線的運(yùn)動(dòng)的點(diǎn)代表設(shè)備,可以看到以設(shè)備為中心有一個(gè)坐標(biāo)系也在移動(dòng)和旋轉(zhuǎn)用僧,這代表著設(shè)備在不斷的移動(dòng)和旋轉(zhuǎn)结胀。這個(gè)信息是通過(guò)設(shè)備的運(yùn)動(dòng)傳感器獲取的。

動(dòng)圖中右側(cè)的黃色點(diǎn)是 3D 特征點(diǎn)永毅。3D特征點(diǎn)就是處理捕捉到的圖像得到的把跨,能代表物體特征的點(diǎn)。例如地板的紋理沼死、物體的邊邊角角都可以成為特征點(diǎn)着逐。上圖中我們看到當(dāng)設(shè)備移動(dòng)時(shí),ARKit 在不斷的追蹤捕捉到的畫面中的特征點(diǎn)意蛀。

ARKit 將上面兩個(gè)信息進(jìn)行結(jié)合耸别,最終得到了高精度的設(shè)備位置和偏轉(zhuǎn)信息。

e县钥、ARWorldTrackingConfiguration

ARWorldTrackingConfiguration 提供 6DoFSix Degree of Freedom)的設(shè)備追蹤秀姐。包括三個(gè)姿態(tài)角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滾角)若贮,以及沿笛卡爾坐標(biāo)系中 X省有、YZ 三軸的偏移量。

ARWorldTrackingConfiguration

不僅如此谴麦,ARKit 還使用了 VIOVisual-Inertial Odometry)來(lái)提高設(shè)備運(yùn)動(dòng)追蹤的精度蠢沿。在使用慣性測(cè)量單元(IMU)檢測(cè)運(yùn)動(dòng)軌跡的同時(shí),對(duì)運(yùn)動(dòng)過(guò)程中攝像頭拍攝到的圖片進(jìn)行圖像處理匾效。將圖像中的一些特征點(diǎn)的變化軌跡與傳感器的結(jié)果進(jìn)行比對(duì)后舷蟀,輸出最終的高精度結(jié)果。

從追蹤的維度和準(zhǔn)確度來(lái)看,ARWorldTrackingConfiguration非常強(qiáng)悍野宜。但它也有兩個(gè)致命的缺點(diǎn):受環(huán)境光線質(zhì)量影響和受劇烈運(yùn)動(dòng)影響扫步。由于在追蹤過(guò)程中要通過(guò)采集圖像來(lái)提取特征點(diǎn),所以圖像的質(zhì)量會(huì)影響追蹤的結(jié)果匈子。在光線較差的環(huán)境下(比如夜晚或者強(qiáng)光)河胎,拍攝的圖像無(wú)法提供正確的參考,追蹤的質(zhì)量也會(huì)隨之下降旬牲。追蹤過(guò)程中會(huì)逐幀比對(duì)圖像與傳感器結(jié)果仿粹,如果設(shè)備在短時(shí)間內(nèi)劇烈的移動(dòng),會(huì)很大程度上干擾追蹤結(jié)果原茅。

e、追蹤狀態(tài)

世界追蹤有三種狀態(tài)堕仔,我們可以通過(guò) camera.trackingState 獲取當(dāng)前的追蹤狀態(tài)擂橘。

追蹤狀態(tài)

從上圖我們看到有三種追蹤狀態(tài):

Not Available// 世界追蹤正在初始化,還未開始工作
Normal// 正常工作狀態(tài)
Limited// 限制狀態(tài)摩骨,當(dāng)追蹤質(zhì)量受到影響時(shí)通贞,追蹤狀態(tài)可能會(huì)變?yōu)?Limited 狀態(tài)

TrackingState 關(guān)聯(lián)的一個(gè)信息是ARCamera.TrackingState.Reason,這是一個(gè)枚舉類型:

case excessiveMotion// 設(shè)備移動(dòng)過(guò)快恼五,無(wú)法正常追蹤
case initializing// 正在初始化
case insufficientFeatures// 特征過(guò)少昌罩,無(wú)法正常追蹤
case none// 正常工作

我們可以通過(guò) ARSessionObserver 協(xié)議去獲取追蹤狀態(tài)的變化。


3灾馒、ARFrame

a茎用、ARFrame

ARFrame 中包含有世界追蹤過(guò)程獲取的所有信息,ARFrame 中與世界追蹤有關(guān)的信息主要是:anchorscamera睬罗。

//camera: 含有攝像機(jī)的位置轨功、旋轉(zhuǎn)以及拍照參數(shù)等信息
var camera: [ARCamera]

//ahchors: 代表了追蹤的點(diǎn)或面
var anchors: [ARAnchor]
b、ARAnchor

ARAnchor 是空間中相對(duì)真實(shí)世界的位置和角度容达。ARAnchor 可以添加到場(chǎng)景中古涧,或是從場(chǎng)景中移除』ㄑ危基本上來(lái)說(shuō)羡滑,它們用于表示虛擬內(nèi)容在物理環(huán)境中的錨定,所以如果要添加自定義 anchor算芯,添加到 session 里就可以了柒昏,它會(huì)在 session 生命周期中一直存在。但如果你在運(yùn)行諸如平面檢測(cè)功能也祠,ARAnchor 則會(huì)被自動(dòng)添加到 session 中昙楚。

要響應(yīng)被添加的 anchor,可以從 current ARFrame 中獲得完整列表诈嘿,此列表包含 session 正在追蹤的所有 anchor堪旧∠鞔校或者也可以響應(yīng) delegate方法,例如 add淳梦、update 以及 remove析砸,session 中的 anchor 被添加、更新或移除時(shí)會(huì)通知爆袍。

c首繁、ARCamera
ARCamera

每個(gè) ARFrame 都會(huì)包含一個(gè) ARCameraARCamera 對(duì)象表示虛擬攝像頭陨囊。虛擬攝像頭就代表了設(shè)備的角度和位置弦疮。

  • ARCamera提供了一個(gè)transformtransform 是一個(gè)4x4矩陣蜘醋。提供了物理設(shè)備相對(duì)于初始位置的變換胁塞。
  • ARCamera 提供了追蹤狀態(tài)(tracking state),通知你如何使用 transform压语。
  • ARCamera 提供了相機(jī)內(nèi)部功能(camera intrinsics)啸罢。包括焦距和主焦點(diǎn),用于尋找投影矩陣胎食。投影矩陣是 ARCamera 上的一個(gè) convenience 方法扰才,可用于渲染虛擬你的幾何體。

4厕怜、場(chǎng)景解析

場(chǎng)景解析主要功能是對(duì)現(xiàn)實(shí)世界的場(chǎng)景進(jìn)行分析衩匣,解析出比如現(xiàn)實(shí)世界的平面等信息,可以讓我們把一些虛擬物體放在某些實(shí)物處酣倾。ARKit 提供的場(chǎng)景解析主要有平面檢測(cè)舵揭、場(chǎng)景交互以及光照估計(jì)三種,下面逐個(gè)分析躁锡。

場(chǎng)景解析
a午绳、平面檢測(cè)(Plane detection)

ARKit 的平面檢測(cè)用于檢測(cè)出現(xiàn)實(shí)世界的水平面。

平面檢測(cè)

? 上圖中可以看出映之,ARkit 檢測(cè)出了兩個(gè)平面拦焚,圖中的兩個(gè)三維坐標(biāo)系是檢測(cè)出的平面的本地坐標(biāo)系,此外杠输,檢測(cè)出的平面是有一個(gè)大小范圍的赎败。

? 平面檢測(cè)是一個(gè)動(dòng)態(tài)的過(guò)程,當(dāng)攝像機(jī)不斷移動(dòng)時(shí)蠢甲,檢測(cè)到的平面也會(huì)不斷的變化僵刮。下圖中可以看到當(dāng)移動(dòng)攝像機(jī)時(shí),已經(jīng)檢測(cè)到的平面的坐標(biāo)原點(diǎn)以及平面范圍都在不斷的變化。

平面檢測(cè)

? 開啟平面檢測(cè)很簡(jiǎn)單搞糕,只需要在 run ARSession 之前勇吊,將 ARSessionConfigurationplaneDetection 屬性設(shè)為 true 即可。

// Create a world tracking session configuration.
let configuration = ARWorldTrackingSessionConfiguration()
configuration.planeDetection = .horizontal

// Create a session.
let session = ARSession()

// Run.
session.run(configuration)

? 平面的表示方式:當(dāng)ARKit檢測(cè)到一個(gè)平面時(shí)窍仰,ARKit會(huì)為該平面自動(dòng)添加一個(gè) ARPlaneAnchor汉规,這個(gè) ARPlaneAnchor 就表示了一個(gè)平面。

? 當(dāng) ARKit 系統(tǒng)檢測(cè)到新平面時(shí)驹吮,ARKit 會(huì)自動(dòng)添加一個(gè) ARPlaneAnchorARSession 中针史。我們可以通過(guò) ARSessionDelegate 獲取當(dāng)前 ARSessionARAnchor 改變的通知,主要有以下三種情況:

新加入了 ARAnchor:對(duì)于平面檢測(cè)來(lái)說(shuō)碟狞,當(dāng)新檢測(cè)到某平面時(shí)啄枕,我們會(huì)收到該通知,通知中的 ARAnchor 數(shù)組會(huì)包含新添加的平面族沃,其類型是 ARPlaneAnchor

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
    for anchor in anchors {
        if let anchor = anchor as? ARPlaneAnchor {
            print(anchor.center)
            print(anchor.extent)
        }
    }
}

ARAnchor更新:從上面我們知道當(dāng)設(shè)備移動(dòng)時(shí)射亏,檢測(cè)到的平面是不斷更新的,當(dāng)平面更新時(shí)竭业,會(huì)回調(diào)這個(gè)接口。

func session(_ session: ARSession, didUpdate anchors: [ARAnchor])

刪除 ARAnchor:當(dāng)手動(dòng)刪除某個(gè) Anchor 時(shí)就會(huì)回調(diào)此方法及舍。此外未辆,對(duì)于檢測(cè)到的平面來(lái)說(shuō),如果兩個(gè)平面進(jìn)行了合并锯玛,則會(huì)刪除其中一個(gè)咐柜,此時(shí)也會(huì)回調(diào)此方法。

func session(_ session: ARSession, didRemove anchors: [ARAnchor])
b攘残、場(chǎng)景交互(Hit-testing)

Hit-testing 是為了獲取當(dāng)前捕捉到的圖像中某點(diǎn)擊位置有關(guān)的信息(包括平面拙友、特征點(diǎn)、ARAnchor 等)歼郭。原理圖如下:

場(chǎng)景交互

當(dāng)點(diǎn)擊屏幕時(shí)遗契,ARKit會(huì)發(fā)射一個(gè)射線,假設(shè)屏幕平面是三維坐標(biāo)系中的 xy 平面病曾,那么該射線會(huì)沿著 z 軸方向射向屏幕里面牍蜂,這就是一次 Hit-testing 過(guò)程。此次過(guò)程會(huì)將射線遇到的所有有用信息返回泰涂,返回結(jié)果以離屏幕距離進(jìn)行排序鲫竞,離屏幕最近的排在最前面。

ARFrame 提供了Hit-testing的接口:

func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]

上述接口中有一個(gè) types 參數(shù)逼蒙,該參數(shù)表示此次 Hit-testing 過(guò)程需要獲取的信息類型从绘,ResultType 有以下四種。

featurePoint:表示此次 Hit-testing 過(guò)程希望返回當(dāng)前圖像中Hit-testing 射線經(jīng)過(guò)的 3D 特征點(diǎn)。

featurePoint

estimatedHorizontalPlane:表示此次 Hit-testing 過(guò)程希望返回當(dāng)前圖像中 Hit-testing 射線經(jīng)過(guò)的預(yù)估平面僵井。預(yù)估平面表示 ARKit 當(dāng)前檢測(cè)到一個(gè)可能是平面的信息陕截,但當(dāng)前尚未確定是平面,所以 ARKit 還沒(méi)有為此預(yù)估平面添加 ARPlaneAnchor驹沿。

estimatedHorizontalPlane

existingPlaneUsingExtent:表示此次Hit-testing過(guò)程希望返回當(dāng)前圖像中Hit-testing射線經(jīng)過(guò)的有大小范圍的平面艘策。

existingPlaneUsingExtent

existingPlane:表示此次Hit-testing過(guò)程希望返回當(dāng)前圖像中Hit-testing射線經(jīng)過(guò)的無(wú)限大小的平面。

existingPlane

上圖中渊季,平面大小是綠色平面所展示的大小朋蔫,但exsitingPlane選項(xiàng)表示即使Hit-testing射線落在了綠色平面外面,也會(huì)將此平面返回却汉。換句話說(shuō)驯妄,將所有平面無(wú)限延展,只要 Hit-testing 射線經(jīng)過(guò)了無(wú)限延展后的平面合砂,就會(huì)返回該平面青扔。

Demo演示:Hit-testingpoint(0.5, 0.5)代表屏幕的中心,屏幕左上角為(0, 0)翩伪,右下角為(1, 1)微猖。 對(duì)于 featurePointestimatedHorizontalPlane 的結(jié)果,ARKit沒(méi)有為其添加ARAnchor,我們可以使用Hit-testing獲取信息后自己為ARSession 添加 ARAnchor,下面代碼就顯示了此過(guò)程熔恢。

// Adding an ARAnchor based on hit-test
let point = CGPoint(x: 0.5, y: 0.5)  // Image center

// Perform hit-test on frame.
let results = frame. hitTest(point, types: [.featurePoint, .estimatedHorizontalPlane])

// Use the first result.
if let closestResult = results.first {
    // Create an anchor for it.
    anchor = ARAnchor(transform: closestResult.worldTransform)
    // Add it to the session.
    session.add(anchor: anchor)
}
c、光照估計(jì)(Light estimation)

下圖中犁珠,一個(gè)虛擬物體茶杯被放在了現(xiàn)實(shí)世界的桌子上。

光照估計(jì)

當(dāng)周圍環(huán)境光線較好時(shí)互亮,攝像機(jī)捕捉到的圖像光照強(qiáng)度也較好犁享,此時(shí),我們放在桌子上的茶杯看起來(lái)就比較貼近于現(xiàn)實(shí)效果豹休,如上圖最左邊的圖炊昆。但是當(dāng)周圍光線較暗時(shí),攝像機(jī)捕捉到的圖像也較暗慕爬,如上圖中間的圖窑眯,此時(shí)茶杯的亮度就顯得跟現(xiàn)實(shí)世界格格不入。

針對(duì)這種情況医窿,ARKit 提供了光照估計(jì)磅甩,開啟光照估計(jì)后,我們可以拿到當(dāng)前圖像的光照強(qiáng)度姥卢,從而能夠以更自然的光照強(qiáng)度去渲染虛擬物體卷要,如上圖最右邊的圖渣聚。

光照估計(jì)基于當(dāng)前捕捉到的圖像的曝光等信息,給出一個(gè)估計(jì)的光照強(qiáng)度值(單位為lumen僧叉,光強(qiáng)單位)奕枝。默認(rèn)的光照強(qiáng)度為 1000lumen,當(dāng)現(xiàn)實(shí)世界較亮?xí)r瓶堕,我們可以拿到一個(gè)高于 1000lumen 的值隘道,相反,當(dāng)現(xiàn)實(shí)世界光照較暗時(shí)郎笆,我們會(huì)拿到一個(gè)低于 1000lumen的值谭梗。

ARKit 的光照估計(jì)默認(rèn)是開啟的,當(dāng)然也可以通過(guò)下述方式手動(dòng)配置:

configuration.isLightEstimationEnabled = true

獲取光照估計(jì)的光照強(qiáng)度也很簡(jiǎn)單宛蚓,只需要拿到當(dāng)前的 ARFrame激捏,通過(guò)以下代碼即可獲取估計(jì)的光照強(qiáng)度:

let intensity = frame.lightEstimate?.ambientIntensity

5、SceneKit

a凄吏、簡(jiǎn)介

渲染是呈現(xiàn)AR world的最后一個(gè)過(guò)程远舅。此過(guò)程將創(chuàng)建的虛擬世界、捕捉的真實(shí)世界痕钢、ARKit 追蹤的信息以及 ARKit場(chǎng)景解析的的信息結(jié)合在一起图柏,渲染出一個(gè) AR world。渲染過(guò)程需要實(shí)現(xiàn)以下幾點(diǎn)才能渲染出正確的 AR world

  • 將攝像機(jī)捕捉到的真實(shí)世界的視頻作為背景任连。
  • 將世界追蹤到的相機(jī)狀態(tài)信息實(shí)時(shí)更新到 AR world 中的相機(jī)爆办。
  • 處理光照估計(jì)的光照強(qiáng)度。
  • 實(shí)時(shí)渲染虛擬世界物體在屏幕中的位置课梳。
SceneKit

如果我們自己處理這個(gè)過(guò)程,可以看到還是比較復(fù)雜的余佃,ARKit為簡(jiǎn)化開發(fā)者的渲染過(guò)程暮刃,為開發(fā)者提供了簡(jiǎn)單易用的使用SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的視圖ARSCNView以及ARSKView。當(dāng)然開發(fā)者也可以使用其他引擎進(jìn)行渲染爆土,只需要將以上幾個(gè)信息進(jìn)行處理融合即可椭懊。

b、SceneKit 的坐標(biāo)系

我們知道 UIKit使用一個(gè)包含有 xy信息的 CGPoint 來(lái)表示一個(gè)點(diǎn)的位置步势,但是在 3D 系統(tǒng)中氧猬,需要一個(gè)z參數(shù)來(lái)描述物體在空間中的深度,SceneKit 的坐標(biāo)系可以參考下圖:

SceneKit 的坐標(biāo)系

這個(gè)三維坐標(biāo)系中坏瘩,表示一個(gè)點(diǎn)的位置需要使用(x,y,z)坐標(biāo)表示盅抚。紅色方塊位于x軸,綠色方塊位于 y軸倔矾,藍(lán)色方塊位于z軸妄均,灰色方塊位于原點(diǎn)柱锹。在SceneKit中我們可以這樣創(chuàng)建一個(gè)三維坐標(biāo):

let position = SCNVector3(x: 0, y: 5, z: 10)
c、SceneKit 中的場(chǎng)景和節(jié)點(diǎn)

我們可以將 SceneKit 中的場(chǎng)景(SCNScene)想象為一個(gè)虛擬的 3D 空間丰包,然后可以將一個(gè)個(gè)的節(jié)點(diǎn)(SCNNode)添加到場(chǎng)景中禁熏。SCNScene 中有唯一一個(gè)根節(jié)點(diǎn)(坐標(biāo)是(x:0, y:0, z:0)),除了根節(jié)點(diǎn)外邑彪,所有添加到 SCNScene 中的節(jié)點(diǎn)都需要一個(gè)父節(jié)點(diǎn)瞧毙。

下圖中位于坐標(biāo)系中心的就是根節(jié)點(diǎn),此外還有添加的兩個(gè)節(jié)點(diǎn) NodeANodeB寄症,其中 NodeA的父節(jié)點(diǎn)是根節(jié)點(diǎn)宙彪,NodeB 的父節(jié)點(diǎn)是 NodeA

SceneKit 中的場(chǎng)景和節(jié)點(diǎn)

SCNScene 中的節(jié)點(diǎn)加入時(shí)可以指定一個(gè)三維坐標(biāo)(默認(rèn)為(x:0, y:0, z:0))瘸爽,這個(gè)坐標(biāo)是相對(duì)于其父節(jié)點(diǎn)的位置您访。這里說(shuō)明兩個(gè)概念:

  • 本地坐標(biāo)系:以場(chǎng)景中的某節(jié)點(diǎn)(非根節(jié)點(diǎn))為原點(diǎn)建立的三維坐標(biāo)系
  • 世界坐標(biāo)系:以根節(jié)點(diǎn)為原點(diǎn)創(chuàng)建的三維坐標(biāo)系稱為世界坐標(biāo)系。

上圖中我們可以看到 NodeA 的坐標(biāo)是相對(duì)于世界坐標(biāo)系(由于NodeA的父節(jié)點(diǎn)是根節(jié)點(diǎn))的位置剪决,而NodeB 的坐標(biāo)代表了 NodeBNodeA 的本地坐標(biāo)系位置(NodeB 的父節(jié)點(diǎn)是NodeA)灵汪。

d、SceneKit 中的攝像機(jī)

有了 SCNSceneSCNNode 后柑潦,我們還需要一個(gè)攝像機(jī)(SCNCamera)來(lái)決定我們可以看到場(chǎng)景中的哪一塊區(qū)域(就好比現(xiàn)實(shí)世界中有了各種物體享言,但還需要人的眼睛才能看到物體)。攝像機(jī)在 SCNScene 的工作模式如下圖:

SceneKit 中的攝像機(jī)
  • SceneKitSCNCamera 拍攝的方向始終為 z 軸負(fù)方向渗鬼。
  • 視野(Field of View)是攝像機(jī)的可視區(qū)域的極限角度览露。角度越小,視野越窄譬胎,反之差牛,角度越大,視野越寬堰乔。
  • 視錐體(Viewing Frustum)決定著攝像頭可視區(qū)域的深度(z 軸表示深度)偏化。任何不在這個(gè)區(qū)域內(nèi)的物體將被剪裁掉(離攝像頭太近或者太遠(yuǎn)),不會(huì)顯示在最終的畫面中镐侯。

SceneKit中我們可以使用如下方式創(chuàng)建一個(gè)攝像機(jī):

let scene = SCNScene()
let cameraNode = SCNNode()
let camera = SCNCamera()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(cameraNode)
e侦讨、SCNView

最后,我們需要一個(gè) View 來(lái)將 SCNScene 中的內(nèi)容渲染到顯示屏幕上苟翻,這個(gè)工作由 SCNView 完成韵卤。這一步其實(shí)很簡(jiǎn)單,只需要?jiǎng)?chuàng)建一個(gè)SCNView 實(shí)例崇猫,然后將 SCNViewscene屬性設(shè)置為剛剛創(chuàng)建的 SCNScene沈条,然后將 SCNView 添加到UIKitviewwindow 上即可。示例代碼如下:

let scnView = SCNView()
scnView.scene = scene
vc.view.addSubview(scnView)
scnView.frame = vc.view.bounds

二诅炉、二維碼識(shí)別

運(yùn)行效果
2020-12-21 16:11:05.525933+0800 CodeKamera[18444:3128121] <AVMetadataMachineReadableCodeObject: 二維碼信息:0x281251580, type="org.iso.QRCode", bounds={ 0.1,0.0 0.2x0.3 }>corners { 0.1,0.3 0.4,0.3 0.4,0.0 0.2,0.0 }, time 441939661045708, stringValue "謝佳培"

1拍鲤、設(shè)備的配置流程

默認(rèn)使用后置攝像頭進(jìn)行掃描贴谎,使用AVMediaTypeVideo表示視頻

self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

設(shè)備輸入初始化

self.input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:nil];

設(shè)備輸出初始化,并設(shè)置代理和回調(diào)季稳。當(dāng)設(shè)備掃描到數(shù)據(jù)時(shí)通過(guò)該代理輸出隊(duì)列,一般輸出隊(duì)列都設(shè)置為主隊(duì)列景鼠,也是設(shè)置了回調(diào)方法執(zhí)行所在的隊(duì)列環(huán)境仲翎。

self.output = [[AVCaptureMetadataOutput alloc] init];
[self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

會(huì)話初始化,并設(shè)置采樣質(zhì)量為高

self.session = [[AVCaptureSession alloc] init];
[self.session setSessionPreset:AVCaptureSessionPresetHigh];

通過(guò)會(huì)話連接設(shè)備的輸入輸出

if ([self.session canAddInput:_input])
{
    [self.session addInput:_input];
}

if ([self.session canAddOutput:_output])
{
    [self.session addOutput:_output];
}

指定設(shè)備的識(shí)別類型铛漓,這里只指定二維碼識(shí)別這一種類型 AVMetadataObjectTypeQRCode溯香。指定識(shí)別類型這一步一定要在輸出添加到會(huì)話之后,否則設(shè)備的可識(shí)別類型會(huì)為空浓恶,程序會(huì)出現(xiàn)崩潰玫坛。

[self.output setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];

設(shè)置掃描信息的識(shí)別區(qū)域,以下代碼設(shè)置為正中央的一塊正方形區(qū)域包晰,識(shí)別區(qū)域越小識(shí)別效率越高湿镀,所以不設(shè)置整個(gè)屏幕。

[self.output setRectOfInterest:CGRectMake(x, y, width, height)];

預(yù)覽層初始化伐憾,self.session負(fù)責(zé)驅(qū)動(dòng)input進(jìn)行信息的采集勉痴,layer負(fù)責(zé)把圖像渲染顯示。預(yù)覽層的區(qū)域設(shè)置為整個(gè)屏幕树肃,這樣可以方便我們進(jìn)行移動(dòng)二維碼到掃描區(qū)域蒸矛。

self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
self.previewLayer.frame = self.view.bounds;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer addSublayer:self.previewLayer];

重寫代理的回調(diào)方法,獲取后置攝像頭掃描到二維碼的信息胸嘴,實(shí)現(xiàn)我們?cè)诔晒ψR(shí)別二維碼之后要實(shí)現(xiàn)的功能雏掠。

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    // 停止掃描
    [self.session stopRunning];
    
    if (metadataObjects.count >= 1)
    {
        // 數(shù)組中包含的都是AVMetadataMachineReadableCodeObject類型的對(duì)象,該對(duì)象中包含解碼后的數(shù)據(jù)
        AVMetadataMachineReadableCodeObject *QRObject = [metadataObjects lastObject];
        // 拿到掃描內(nèi)容在這里進(jìn)行個(gè)性化處理
        NSString *result = QRObject.stringValue;
        // 解析數(shù)據(jù)進(jìn)行處理并實(shí)現(xiàn)相應(yīng)的邏輯...
        NSLog(@"掃描到的二維碼的信息:%@",result);
    }
}

2劣像、CameraController 中的二維碼識(shí)別方法

a磁玉、設(shè)置會(huì)話的輸入設(shè)備

設(shè)置相機(jī)自動(dòng)對(duì)焦,這樣可以在任何距離都可以進(jìn)行掃描驾讲。 因?yàn)閽呙钘l碼,距離都比較近席赂,所以AVCaptureAutoFocusRangeRestrictionNear通過(guò)縮小距離吮铭,來(lái)提高識(shí)別成功率。

- (BOOL)setupSessionInputs:(NSError *__autoreleasing *)error
{
    //設(shè)置相機(jī)自動(dòng)對(duì)焦颅停,這樣可以在任何距離都可以進(jìn)行掃描
    BOOL success = [super setupSessionInputs:error];
    if(success)
    {
        //判斷是否能自動(dòng)聚焦
        if (self.activeCamera.autoFocusRangeRestrictionSupported)
        {
            //鎖定設(shè)備
            if ([self.activeCamera lockForConfiguration:error])
            {
                self.activeCamera.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;
                
                //釋放排他鎖
                [self.activeCamera  unlockForConfiguration];
            }
        }
    }
    
    return YES;
}
b谓晌、設(shè)置會(huì)話輸出設(shè)備
- (BOOL)setupSessionOutputs:(NSError **)error
{

    //獲取輸出設(shè)備
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    
    //判斷是否能添加輸出設(shè)備
    if ([self.captureSession canAddOutput:self.metadataOutput])
    {
        //添加輸出設(shè)備
        [self.captureSession addOutput:self.metadataOutput];
        
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        
        //設(shè)置委托代理
        [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
        
        //指定掃描對(duì)是OR碼(移動(dòng)營(yíng)銷) & Aztec 碼(登機(jī)牌)
        NSArray *types = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeAztecCode,AVMetadataObjectTypeDataMatrixCode,AVMetadataObjectTypePDF417Code];
        
        self.metadataOutput.metadataObjectTypes = types;
        
    }
    else
    {
        //錯(cuò)誤時(shí),存儲(chǔ)錯(cuò)誤信息
        NSDictionary *userInfo = @{NSLocalizedDescriptionKey:@"Faild to add metadata output."};
        *error = [NSError errorWithDomain:THCameraErrorDomain code:THCameraErrorFailedToAddOutput userInfo:userInfo];
    
        return NO;
    }
    
    return YES;
}
c癞揉、處理二維碼的委托方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    if (metadataObjects.count > 0)
    {
        NSLog(@"二維碼信息:%@",metadataObjects[0]);
    }

    //通過(guò)委托將二維碼傳遞到preview
    [self.codeDetectionDelegate didDetectCodes:metadataObjects];
}

3纸肉、PreviewView中的二維碼識(shí)別方法

- (void)didDetectCodes:(NSArray *)codes
{
    //保存轉(zhuǎn)換完成的元數(shù)據(jù)對(duì)象
    NSArray *transformedCodes = [self transformedCodesFromCodes:codes];
    
    //從codeLayers字典中獲得key溺欧,用來(lái)判斷那個(gè)圖層應(yīng)該在方法尾部移除
    NSMutableArray *lostCodes = [self.codeLayers.allKeys mutableCopy];
    
    //遍歷數(shù)組
    for (AVMetadataMachineReadableCodeObject *code in transformedCodes)
    {
        //獲得code.stringValue
        NSString *stringValue = code.stringValue;
        
        if (stringValue)
        {
            [lostCodes removeObject:stringValue];
        }
        else
        {
            continue;
        }
        ......
    }
    
    //遍歷lostCodes
    for (NSString *stringValue in lostCodes)
    {
        //將里面的條目圖層從 previewLayer 中移除
        for (CALayer *layer in self.codeLayers[stringValue])
        {
            [layer removeFromSuperlayer];
        }
        
        //數(shù)組條目中也要移除
        [self.codeLayers removeObjectForKey:stringValue];
    }
    
}

根據(jù)當(dāng)前的 stringValue 查找圖層。如果沒(méi)有對(duì)應(yīng)的類目柏肪,則進(jìn)行新建圖層姐刁。

//根據(jù)當(dāng)前的 stringValue 查找圖層
NSArray *layers = self.codeLayers[stringValue];

//如果沒(méi)有對(duì)應(yīng)的類目
if (!layers)
{
    //新建圖層 方、圓
    layers = @[[self makeBoundsLayer],[self makeCornersLayer]];
    
    //將圖層以stringValue 為key 存入字典中
    self.codeLayers[stringValue] = layers;
    
    //在預(yù)覽圖層上添加 圖層0烦味、圖層1
    [self.previewLayer addSublayer:layers[0]];
    [self.previewLayer addSublayer:layers[1]];
}

//創(chuàng)建一個(gè)和對(duì)象的bounds關(guān)聯(lián)的UIBezierPath
//畫方框
CAShapeLayer *boundsLayer = layers[0];
boundsLayer.path = [self bezierPathForBounds:code.bounds].CGPath;

//對(duì)于cornersLayer 構(gòu)建一個(gè)CGPath
CAShapeLayer *cornersLayer = layers[1];
cornersLayer.path = [self bezierPathForCorners:code.corners].CGPath;

三聂使、人臉識(shí)別

1、人臉識(shí)別簡(jiǎn)介

計(jì)算機(jī)視覺(jué)

計(jì)算機(jī)視覺(jué)指用攝影機(jī)和計(jì)算機(jī)代替人眼對(duì)目標(biāo)進(jìn)行識(shí)別谬俄、跟蹤和測(cè)量等機(jī)器視覺(jué)柏靶,并進(jìn)一步做圖像處理。計(jì)算機(jī)視覺(jué)系統(tǒng)的具體實(shí)現(xiàn)方法由其功能決定——是預(yù)先固定的抑或是在運(yùn)行過(guò)程中自動(dòng)學(xué)習(xí)調(diào)整溃论。盡管如此屎蜓,計(jì)算機(jī)視覺(jué)的都需要具備以下處理步驟:

處理步驟
人臉識(shí)別

人臉識(shí)別是計(jì)算機(jī)視覺(jué)的一種應(yīng)用,指利用分析比較人臉視覺(jué)特征信息進(jìn)行身份鑒別的計(jì)算機(jī)技術(shù)钥勋,包括人臉圖像采集炬转、人臉定位、人臉識(shí)別預(yù)處理笔诵、身份確認(rèn)以及身份查找等返吻。iOS中常用的有四種實(shí)現(xiàn)方式:CoreImageVision乎婿、OpenCV测僵、AVFoundation


2谢翎、使用 CoreImage 實(shí)現(xiàn)靜態(tài)人臉識(shí)別

CoreImage 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的關(guān)鍵類
CoreImage
操作部分
  • 濾鏡(CIFliter):CIFilter產(chǎn)生一個(gè)CIImage捍靠,接受一到多的圖片作為輸入,經(jīng)過(guò)一些過(guò)濾操作森逮,產(chǎn)生指定輸出的圖片榨婆。
  • 檢測(cè)(CIDetector):CIDetector檢測(cè)處理圖片的特性,如使用來(lái)檢測(cè)圖片中人臉的眼睛褒侧、嘴巴等等良风。
  • 特征(CIFeature):CIFeature 代表由 detector處理后產(chǎn)生的特征。
圖像部分
  • 畫布(CIContext):畫布類可被用來(lái)處理Quartz 2D 或者OpenGL闷供,也可以用它來(lái)關(guān)聯(lián)CoreImage類烟央,進(jìn)行濾鏡、顏色等渲染處理歪脏。
  • 顏色(CIColor): 圖片的關(guān)聯(lián)與畫布疑俭、圖片像素顏色的處理。
  • 向量(CIVector): 圖片的坐標(biāo)向量等幾何方法處理婿失。
  • 圖片(CIImage):代表一個(gè)圖像钞艇,可代表關(guān)聯(lián)后輸出的圖像啄寡。

使用CoreImage 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的Demo演示
a、運(yùn)行效果
2020-08-27 10:02:46.840694+0800 FaceRecognitionDemo[16278:796341] Metal API Validation Enabled
人臉區(qū)域?yàn)椋篎ound bounds are (533.2980717893497, 906.6676903116021, 636.9456902844558, 625.2230840751323)
左眼位置: (747.0441848951896, 1391.4379881573832)
右眼位置: (1054.0997744993274, 1418.2982372168135)

b哩照、檢測(cè)人臉進(jìn)行識(shí)別的方法
func detect()
{
    .....
}

獲取人像圖片

guard let personciImage = CIImage(image: personPic.image!) else { return }

設(shè)置人臉檢測(cè)的精確度

let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh]

進(jìn)行人臉檢測(cè)

let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy)

在人像圖片中鎖定人臉挺物,可能有多個(gè)人

let faces = faceDetector?.features(in: personciImage)

轉(zhuǎn)換坐標(biāo)系

let ciImageSize = personciImage.extent.size
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -ciImageSize.height)

c、針對(duì)每張人臉進(jìn)行處理
for face in faces as! [CIFaceFeature]
{
    print("人臉區(qū)域?yàn)椋篎ound bounds are \(face.bounds)")

    ....

    if face.hasLeftEyePosition
    {
        print("左眼位置: \(face.leftEyePosition)")
    }

    if face.hasRightEyePosition
    {
        print("右眼位置: \(face.rightEyePosition)")
    }
}

在圖像視圖中計(jì)算矩形的實(shí)際位置和大小

// 應(yīng)用變換轉(zhuǎn)換坐標(biāo)
var faceViewBounds = face.bounds.applying(transform)

// 在圖像視圖中計(jì)算矩形的實(shí)際位置和大小
let viewSize = personPic.bounds.size
let scale = min(viewSize.width / ciImageSize.width, viewSize.height / ciImageSize.height)
let offsetX = (viewSize.width - ciImageSize.width * scale) / 2
let offsetY = (viewSize.height - ciImageSize.height * scale) / 2

faceViewBounds = faceViewBounds.applying(CGAffineTransform(scaleX: scale, y: scale))
faceViewBounds.origin.x += offsetX
faceViewBounds.origin.y += offsetY

添加人臉邊框

let faceBox = UIView(frame: faceViewBounds)
faceBox.layer.borderWidth = 3
faceBox.layer.borderColor = UIColor.red.cgColor
faceBox.backgroundColor = UIColor.clear
personPic.addSubview(faceBox)

3葡秒、使用 Vision 實(shí)現(xiàn)靜態(tài)人臉識(shí)別

Vision 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的關(guān)鍵類

Vision:是 Apple 在 WWDC 2017 推出的圖像識(shí)別框架姻乓,它基于 Core ML
Vision支持多種圖片類型:CIImage眯牧、NSURL蹋岩、NSDataCGImageRef学少、CVPixelBufferRef剪个。
Vision使用的角色有:RequestRequestHandler版确,resultsresults中的Observation數(shù)組扣囊。
Request類型:比如圖中列出的 人臉識(shí)別、特征識(shí)別绒疗、文本識(shí)別侵歇、二維碼識(shí)別等。

Vision結(jié)構(gòu)

我們?cè)谑褂眠^(guò)程中是給各種功能的 Request 提供給一個(gè) RequestHandler吓蘑,Handler 持有需要識(shí)別的圖片信息惕虑,并將處理結(jié)果分發(fā)給每個(gè) Requestcompletion Block 中,可以從 results 屬性中得到 Observation 數(shù)組磨镶。

observations數(shù)組中的內(nèi)容根據(jù)不同的request請(qǐng)求返回了不同的observation溃蔫,如:VNFaceObservationVNTextObservation琳猫、VNBarcodeObservation伟叛、VNHorizonObservation,不同的Observation都繼承于VNDetectedObjectObservation脐嫂,而VNDetectedObjectObservation則是繼承于VNObservation统刮。每種ObservationboundingBoxlandmarks等屬性账千,存儲(chǔ)的是識(shí)別后物體的坐標(biāo)侥蒙,點(diǎn)位等,我們拿到坐標(biāo)后蕊爵,就可以進(jìn)行一些UI繪制。


使用 Vision 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的Demo演示
a桦山、運(yùn)行效果
人臉識(shí)別
b攒射、檢測(cè)人臉
func detect()
{
    let handler = VNImageRequestHandler.init(cgImage: (imageView.image?.cgImage!)!, orientation: CGImagePropertyOrientation.up)
    
    // 創(chuàng)建檢測(cè)人臉邊框的請(qǐng)求
    let request = detectRequest()
    
    DispatchQueue.global(qos: .userInteractive).async {
        do
        {
            try handler.perform([request])
        }
        catch
        {
            print("出錯(cuò)了")
        }
    }
}
c醋旦、創(chuàng)建檢測(cè)人臉邊框的請(qǐng)求
func detectRequest() -> VNDetectFaceRectanglesRequest
{
    let request = VNDetectFaceRectanglesRequest { (request, error) in
        
        DispatchQueue.main.async {
            if let result = request.results
            {
                .......
            }
        }
    }
    return request
}

對(duì)結(jié)果進(jìn)行旋轉(zhuǎn)變換

let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -self.imageView!.frame.size.height)

對(duì)結(jié)果進(jìn)行平移變換

let translate = CGAffineTransform.identity.scaledBy(x: self.imageView!.frame.size.width, y: self.imageView!.frame.size.height)

遍歷所有識(shí)別結(jié)果,創(chuàng)建人臉邊框会放,調(diào)整人臉邊框位置饲齐,再將移動(dòng)和旋轉(zhuǎn)的仿射變化運(yùn)用到邊框上,即是人臉邊框的最終位置咧最。

//遍歷所有識(shí)別結(jié)果
for item in result
{
    // 創(chuàng)建人臉邊框
    let faceRect = UIView(frame: CGRect.zero)
    faceRect.layer.borderWidth = 3
    faceRect.layer.borderColor = UIColor.red.cgColor
    faceRect.backgroundColor = UIColor.clear
    self.imageView!.addSubview(faceRect)
    
    // 調(diào)整人臉邊框位置
    if let faceObservation = item as? VNFaceObservation
    {
        // 將移動(dòng)和旋轉(zhuǎn)的仿射變化運(yùn)用到邊框上
        let finalRect = faceObservation.boundingBox.applying(translate).applying(transform)
        
        // 人臉邊框的最終位置
        faceRect.frame = finalRect
    }
}

4捂人、使用 OpenCV 實(shí)現(xiàn)靜態(tài)人臉識(shí)別

OpenCV 的概念

OpenCV (Open Source Computer Vision Library)是一個(gè)在BSD許可下發(fā)布的開源庫(kù)卸留,因此它免費(fèi)提供給學(xué)術(shù)和商業(yè)用途垛玻。有C++C兔毒、PythonJava接口捣鲸,支持Windows瑟匆、LinuxMacOS栽惶、iOSAndroid等系統(tǒng)愁溜。OpenCV是為計(jì)算效率而設(shè)計(jì)的,而且密切關(guān)注實(shí)時(shí)應(yīng)用程序的發(fā)展和支持外厂。該庫(kù)用優(yōu)化的C/C++編寫冕象,可以應(yīng)用于多核處理≈基于OpenCV渐扮,iOS應(yīng)用程序可以實(shí)現(xiàn)很多有趣的功能,也可以把很多復(fù)雜的工作簡(jiǎn)單化穿仪。

  • 對(duì)圖片進(jìn)行灰度處理
  • 人臉識(shí)別席爽,即特征跟蹤
  • 訓(xùn)練圖片特征庫(kù)(可用于模式識(shí)別)
  • 提取特定圖像內(nèi)容(根據(jù)需求還原有用圖像信息)

使用 OpenCV 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的關(guān)鍵類
  • core:簡(jiǎn)潔的核心模塊,定義了基本的數(shù)據(jù)結(jié)構(gòu)啊片,包括稠密多維數(shù)組 Mat 和其他模塊需要的基本函數(shù)只锻。
  • imgproc:圖像處理模塊,包括線性和非線性圖像濾波紫谷、幾何圖像轉(zhuǎn)換 (縮放齐饮、仿射與透視變換、一般性基于表的重映射)笤昨、顏色空間轉(zhuǎn)換祖驱、直方圖等等。
  • video:視頻分析模塊瞒窒,包括運(yùn)動(dòng)估計(jì)捺僻、背景消除、物體跟蹤算法。
  • calib3d:包括基本的多視角幾何算法匕坯、單體和立體相機(jī)的標(biāo)定束昵、對(duì)象姿態(tài)估計(jì)、雙目立體匹配算法和元素的三維重建葛峻。
  • features2d:包含了顯著特征檢測(cè)算法锹雏、描述算子和算子匹配算法。
  • objdetect:物體檢測(cè)和一些預(yù)定義的物體的檢測(cè) (如人臉术奖、眼睛礁遵、杯子、人采记、汽車等)佣耐。
  • ml:多種機(jī)器學(xué)習(xí)算法,如 K 均值挺庞、支持向量機(jī)和神經(jīng)網(wǎng)絡(luò)晰赞。
  • highgui:一個(gè)簡(jiǎn)單易用的接口,提供視頻捕捉选侨、圖像和視頻編碼等功能掖鱼,還有簡(jiǎn)單的 UI 接口 (iOS 上可用的僅是其一個(gè)子集)。
  • gpuOpenCV中不同模塊的 GPU 加速算法 (iOS 上不可用)援制。
  • ocl:使用 OpenCL 實(shí)現(xiàn)的通用算法 (iOS 上不可用)戏挡。
  • 一些其它輔助模塊,如 Python 綁定和用戶貢獻(xiàn)的算法晨仑。

cv::MatOpenCV 的核心數(shù)據(jù)結(jié)構(gòu)褐墅,用來(lái)表示任意 N 維矩陣。因?yàn)閳D像只是 2 維矩陣的一個(gè)特殊場(chǎng)景洪己,所以也是使用 cv::Mat 來(lái)表示的妥凳。也就是說(shuō),cv::Mat 將是你在 OpenCV 中用到最多的類答捕。

如前面所說(shuō)逝钥,OpenCV 是一個(gè) C++API,因此不能直接在 SwiftObjective-C 代碼中使用拱镐,但能在 Objective-C++ 文件中使用艘款。

Objective-C++Objective-CC++ 的混合物,讓你可以在 Objective-C 類中使用 C++ 對(duì)象沃琅。clang 編譯器會(huì)把所有后綴名為.mm 的文件都當(dāng)做是 Objective-C++哗咆。一般來(lái)說(shuō),它會(huì)如你所期望的那樣運(yùn)行益眉,但還是有一些使用 Objective-C++ 的注意事項(xiàng)晌柬。內(nèi)存管理是你最應(yīng)該格外注意的點(diǎn)姥份,因?yàn)?ARC 只對(duì) Objective-C 對(duì)象有效。當(dāng)你使用一個(gè) C++ 對(duì)象作為類屬性的時(shí)候年碘,其唯一有效的屬性就是 assign殿衰。因此,你的 dealloc 函數(shù)應(yīng)確保 C++ 對(duì)象被正確地釋放了盛泡。

第二重要的點(diǎn)就是,如果你在 Objective-C++ 頭文件中引入了 C++ 頭文件娱颊,當(dāng)你在工程中使用該 Objective-C++ 文件的時(shí)候就泄露了 C++ 的依賴傲诵。任何引入你的 Objective-C++ 類的 Objective-C 類也會(huì)引入該 C++ 類,因此該 Objective-C 文件也要被聲明為 Objective-C++ 的文件箱硕。這會(huì)像森林大火一樣在工程中迅速蔓延拴竹。所以,應(yīng)該把你引入C++ 文件的地方都用 #ifdef __cplusplus 包起來(lái)剧罩,并且只要可能栓拜,就盡量只在.mm實(shí)現(xiàn)文件中引入 C++ 頭文件。


使用 OpenCV 實(shí)現(xiàn)靜態(tài)人臉識(shí)別的Demo演示
a惠昔、運(yùn)行效果
人臉識(shí)別
b幕与、導(dǎo)入頭文件和擴(kuò)展里面的屬性
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif

#import "ViewController.h"
#import <opencv2/imgcodecs/ios.h>

using namespace cv;
using namespace std;

@interface ViewController ()
{
    CascadeClassifier _faceDetector;
    
    vector<cv::Rect> _faceRects;
    vector<cv::Mat> _faceImgs;
}

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end
b、viewDidLoad
- (void)viewDidLoad 
{
    [super viewDidLoad];
    
    //預(yù)設(shè)置face探測(cè)的參數(shù)
    [self preSetFace];
    
    //image轉(zhuǎn)mat
    cv::Mat mat;
    UIImageToMat(self.imageView.image, mat);
    
    //執(zhí)行face
    [self processImage:mat];
}
c镇防、preSetFace
- (void)preSetFace 
{
    NSString *faceCascadePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_frontalface_alt2"
                                                                ofType:@"xml"];
    
    const CFIndex CASCADE_NAME_LEN = 2048;
    char *CASCADE_NAME = (char *) malloc(CASCADE_NAME_LEN);
    CFStringGetFileSystemRepresentation( (CFStringRef)faceCascadePath, CASCADE_NAME, CASCADE_NAME_LEN);
    
    _faceDetector.load(CASCADE_NAME);
    
    free(CASCADE_NAME);
}
d啦鸣、processImage
- (void)processImage:(cv::Mat&)inputImage 
{
    // Do some OpenCV stuff with the image
    cv::Mat frame_gray;

    //轉(zhuǎn)換為灰度圖像
    cv::cvtColor(inputImage, frame_gray, CV_BGR2GRAY);
    
    //圖像均衡化
    cv::equalizeHist(frame_gray, frame_gray);

    //分類器識(shí)別
    _faceDetector.detectMultiScale(frame_gray, _faceRects,1.1,2,0,cv::Size(30,30));

    vector<cv::Rect> faces;
    faces = _faceRects;
    
    // 在每個(gè)人臉上畫一個(gè)紅色四方形
    for(unsigned int i= 0;i < faces.size();I++)
    {
        const cv::Rect& face = faces[I];
        cv::Point tl(face.x,face.y);
        cv::Point br = tl + cv::Point(face.width,face.height);
        // 四方形的畫法
        cv::Scalar magenta = cv::Scalar(255, 0, 0, 255);
        cv::rectangle(inputImage, tl, br, magenta, 3, 8, 0);
    }
    UIImage *outputImage = MatToUIImage(inputImage);
    self.imageView.image = outputImage;
}

使用 OpenCV 遇到的問(wèn)題

無(wú)法將項(xiàng)目上傳到Github上,因?yàn)?code>OpenCL框架太大了来氧,超出了上傳的最大限制100MB诫给,我嘗試了兩種方案來(lái)解決都無(wú)法順利搞定,最后只能上傳了一份不帶該框架的源碼Demo啦扬。

xiejiapei@xiejiapeis-iMac dafsfs % git push
Enumerating objects: 286, done.
Counting objects: 100% (286/286), done.
Delta compression using up to 6 threads
Compressing objects: 100% (267/267), done.
Writing objects: 100% (283/283), 120.12 MiB | 331.00 KiB/s, done.
Total 283 (delta 59), reused 0 (delta 0)
remote: Resolving deltas: 100% (59/59), completed with 2 local objects.
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 7470123636eba7f03883923fcf95870e
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File OpenCVTest/Pods/OpenCV/opencv2.framework/Versions/A/opencv2 is 261.37 MB; this exceeds GitHub's file size limit of 100.00 MB
To https://github.com/xiejiapei-creator/dafsfs.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://github.com/xiejiapei-creator/dafsfs.git'

解決方案一:通過(guò).gitignore不上傳pod庫(kù)中狂,結(jié)果報(bào)同樣的錯(cuò)。
解決方案二:當(dāng)我擁有像opencv2這樣的大型框架文件時(shí)扑毡,如何將更改推送到GitHub胃榕。谷歌上這類資料比較少且零散,在一篇日文博客上看到了類似情況僚楞,總結(jié)解決步驟如下:安裝lfsgit install lfs)勤晚,將大文件復(fù)制到倉(cāng)庫(kù)目錄中,讓lfs跟蹤文件泉褐,然后赐写,提交并推送。實(shí)踐證明膜赃,該方案不可挺邀,真不可。勸你別瞎折騰了,很無(wú)用功很耗時(shí)端铛。

xiejiapei@xiejiapeis-iMac DesignPatternsDemo % cd /Users/xiejiapei/Desktop/Github/Multi-MediaDemo
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git lfs install
Updated git hooks.
Git LFS initialized.
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git lfs track "Pods/OpenCV/opencv2.framework/Versions/A/opencv2"
Tracking "Pods/OpenCV/opencv2.framework/Versions/A/opencv2"
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git add .gitattributes
xiejiapei@xiejiapeis-iMac Multi-MediaDemo % git push

5泣矛、使用 AVFoundation 實(shí)現(xiàn)動(dòng)態(tài)人臉識(shí)別

實(shí)現(xiàn)動(dòng)態(tài)人臉識(shí)別的簡(jiǎn)介
a、運(yùn)行效果

AVFoundation 支持最多同時(shí)識(shí)別10張人臉禾蚕。


b您朽、動(dòng)態(tài)人臉識(shí)別的簡(jiǎn)介

AVFoundation框架搭建之初照片和視頻捕捉功能就是它的強(qiáng)項(xiàng)。 通過(guò)一個(gè)特定的AVCaptureOutput類型的AVCaptureMetadataOutput可以實(shí)現(xiàn)人臉檢測(cè)功能换淆。支持硬件加速以及同時(shí)對(duì)10個(gè)人臉進(jìn)行實(shí)時(shí)檢測(cè).

當(dāng)使用人臉檢測(cè)時(shí)哗总,會(huì)輸出一個(gè)具體的子類類型AVMetadataFaceObject,該類型定義了多個(gè)用于描述被檢測(cè)到的人臉的屬性倍试,包括人臉的邊界(設(shè)備坐標(biāo)系)讯屈、斜傾角(roll angle:表示人頭部向肩膀方向的側(cè)傾角度)、偏轉(zhuǎn)角(yaw angle:表示人臉繞Y軸旋轉(zhuǎn)的角度)县习。


c涮母、動(dòng)態(tài)人臉識(shí)別的步驟
  1. 視頻采集
  2. session添加一個(gè)元數(shù)據(jù)的輸出AVCaptureMetadataOutput
  3. 設(shè)置元數(shù)據(jù)的范圍(人臉數(shù)據(jù)、二維碼數(shù)據(jù)....)
  4. 開始捕捉(設(shè)置捕捉完成代理)
  5. 在代理方法didoutputMetadataObjects中可以獲取到捕捉人臉的相關(guān)信息躁愿,進(jìn)行對(duì)人臉數(shù)據(jù)的處理

d叛本、 AVFoundation實(shí)現(xiàn)視頻捕捉的步驟

捕捉設(shè)備:AVCaptureDevice為攝像頭、麥克風(fēng)等物理設(shè)備提供接口彤钟。大部分我們使用的設(shè)備都是內(nèi)置于MAC或者iPhone炮赦、iPad上的,當(dāng)然也可能出現(xiàn)外部設(shè)備样勃。AVCaptureDevice 針對(duì)物理設(shè)備提供了大量的控制方法吠勘,比如控制攝像頭聚焦、曝光峡眶、白平衡剧防、閃光燈等。

捕捉設(shè)備的輸入:為捕捉設(shè)備添加輸入辫樱,不能添加到AVCaptureSession 中峭拘,必須通過(guò)將它封裝到一個(gè)AVCaptureDeviceInputs實(shí)例中,這個(gè)對(duì)象在設(shè)備輸出數(shù)據(jù)和捕捉會(huì)話間扮演接線板的作用狮暑。

捕捉設(shè)備的輸出:AVCaptureOutput 是一個(gè)抽象類鸡挠,用于為捕捉會(huì)話得到的數(shù)據(jù)尋找輸出的目的地“崮校框架定義了一些抽象類的高級(jí)擴(kuò)展類拣展,例如使用 AVCaptureStillImageOutputAVCaptureMovieFileOutput類來(lái)捕捉靜態(tài)照片、視頻缔逛,使用 AVCaptureAudioDataOutputAVCaptureVideoDataOutput 來(lái)直接訪問(wèn)硬件捕捉到的數(shù)字樣本备埃。

捕捉連接:AVCaptureConnection類捕捉會(huì)話會(huì)先確定由給定捕捉設(shè)備輸入渲染的媒體類型姓惑,并自動(dòng)建立其到能夠接收該媒體類型的捕捉輸出端的連接。

捕捉預(yù)覽:如果不能在影像捕捉中看到正在捕捉的場(chǎng)景按脚,那么應(yīng)用程序用戶體驗(yàn)就會(huì)很差于毙。幸運(yùn)的是框架定義了AVCaptureVideoPreviewLayer類來(lái)滿足該需求,這樣就可以對(duì)捕捉的數(shù)據(jù)進(jìn)行實(shí)時(shí)預(yù)覽辅搬。


e唯沮、擴(kuò)展里面的屬性和委托
@interface THCameraController ()<AVCaptureMetadataOutputObjectsDelegate>

@property(nonatomic,strong)AVCaptureMetadataOutput  *metadataOutput;

@end

使用 AVFoundation 實(shí)現(xiàn)動(dòng)態(tài)人臉識(shí)別簡(jiǎn)易版的Demo演示
a、導(dǎo)入框架和擴(kuò)展中的屬性
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()<AVCaptureMetadataOutputObjectsDelegate>

@property (nonatomic,strong) AVCaptureSession *session;
// 對(duì)捕捉的數(shù)據(jù)進(jìn)行實(shí)時(shí)預(yù)覽
@property (nonatomic,strong) AVCaptureVideoPreviewLayer *previewLayer;

// 所有臉龐
@property (nonatomic,copy) NSMutableArray *facesViewArray;

@end
b堪遂、viewDidLoad
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _facesViewArray = [NSMutableArray arrayWithCapacity:0];
    
    //1.獲取輸入設(shè)備(攝像頭)
    NSArray *devices = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack].devices;
    AVCaptureDevice *deviceFace = devices[0];
    
    //2.根據(jù)輸入設(shè)備創(chuàng)建輸入對(duì)象
    AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:deviceFace error:nil];
    
    //3.創(chuàng)建原數(shù)據(jù)的輸出對(duì)象
    AVCaptureMetadataOutput *metaout = [[AVCaptureMetadataOutput alloc] init];
    
    //4.設(shè)置代理監(jiān)聽(tīng)輸出對(duì)象輸出的數(shù)據(jù)烂翰,在主線程中刷新
    [metaout setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    
    //5.設(shè)置輸出質(zhì)量(高像素輸出)
    self.session = [[AVCaptureSession alloc] init];
    if ([self.session canSetSessionPreset:AVCaptureSessionPreset640x480])
    {
        [self.session setSessionPreset:AVCaptureSessionPreset640x480];
    }
    
    //6.添加輸入和輸出到會(huì)話
    [self.session beginConfiguration];
    if ([self.session canAddInput:input])
    {
        [self.session addInput:input];
    }
    if ([self.session canAddOutput:metaout])
    {
        [self.session addOutput:metaout];
    }
    [self.session commitConfiguration];
    
    //7.告訴輸出對(duì)象要輸出什么樣的數(shù)據(jù),識(shí)別人臉蚤氏,最多可識(shí)別10張人臉
    [metaout setMetadataObjectTypes:@[AVMetadataObjectTypeFace]];
    
    //8.創(chuàng)建預(yù)覽圖層
    AVCaptureSession *session = (AVCaptureSession *)self.session;
    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    _previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:_previewLayer above:0];
    
    //9.設(shè)置有效掃描區(qū)域(默認(rèn)整個(gè)屏幕區(qū)域)(每個(gè)取值0~1, 以屏幕右上角為坐標(biāo)原點(diǎn))
    metaout.rectOfInterest = self.view.bounds;
    
    //10.前置攝像頭一定要設(shè)置一下,要不然畫面是鏡像
    for (AVCaptureVideoDataOutput *output in session.outputs)
    {
        for (AVCaptureConnection *connection in output.connections)
        {
            //判斷是否是前置攝像頭狀態(tài)
            if (connection.supportsVideoMirroring)
            {
                //鏡像設(shè)置
                connection.videoOrientation = AVCaptureVideoOrientationPortrait;
            }
        }
    }
    
    //11.開始掃描
    [self.session startRunning];
}
c踊兜、AVCaptureMetadataOutputObjectsDelegate
//當(dāng)檢測(cè)到了人臉會(huì)走這個(gè)回調(diào)
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    //移除舊畫框
    for (UIView *faceView in self.facesViewArray)
    {
        [faceView removeFromSuperview];
    }
    [self.facesViewArray removeAllObjects];
    
    //將臉龐數(shù)據(jù)轉(zhuǎn)換為臉圖像竿滨,并創(chuàng)建臉?lè)娇?    for (AVMetadataFaceObject *faceobject in metadataObjects)
    {
        //轉(zhuǎn)換為臉圖像
        AVMetadataObject *face = [self.previewLayer transformedMetadataObjectForMetadataObject:faceobject];
        
        //創(chuàng)建臉?lè)娇?        UIView *faceBox = [[UIView alloc] initWithFrame:face.bounds];
        faceBox.layer.borderWidth = 3;
        faceBox.layer.borderColor = [UIColor redColor].CGColor;
        faceBox.backgroundColor = [UIColor clearColor];
        [self.view addSubview:faceBox];
        [self.facesViewArray addObject:faceBox];
    }
}

為捕捉會(huì)話添加輸出設(shè)備

為捕捉會(huì)話添加輸出設(shè)備,報(bào)錯(cuò)則打印錯(cuò)誤信息捏境。

- (BOOL)setupSessionOutputs:(NSError **)error
{
    self.metadataOutput = [[AVCaptureMetadataOutput alloc]init];
    if ([self.captureSession canAddOutput:self.metadataOutput])
    {
        [self.captureSession addOutput:self.metadataOutput];
        ......
        return YES;
    }
    else
    {
        //報(bào)錯(cuò)
        if (error)
        {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey:@"Failed to still image output"};
            
            *error = [NSError errorWithDomain:THCameraErrorDomain code:THCameraErrorFailedToAddOutput userInfo:userInfo];
            
        }
        return NO;
    }
}

設(shè)置 metadataObjectTypes 指定對(duì)象輸出的元數(shù)據(jù)類型于游,支持多種元數(shù)據(jù)。限制檢查到的元數(shù)據(jù)類型集合可以屏蔽我們不感興趣的對(duì)象數(shù)量垫言。這里只保留人臉元數(shù)據(jù)贰剥。

NSArray *metadatObjectTypes = @[AVMetadataObjectTypeFace];
self.metadataOutput.metadataObjectTypes = metadatObjectTypes;

創(chuàng)建主隊(duì)列。因?yàn)槿四槞z測(cè)用到了硬件加速筷频,而且許多重要的任務(wù)都在主線程中執(zhí)行蚌成,所以需要為這次參數(shù)指定主隊(duì)列。

dispatch_queue_t mainQueue = dispatch_get_main_queue();

通過(guò)設(shè)置AVCaptureVideoDataOutput的代理凛捏,就能獲取捕獲到一幀一幀數(shù)據(jù)担忧,檢測(cè)每一幀中是否包含人臉數(shù)據(jù),如果包含則調(diào)用回調(diào)方法坯癣。

[self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];

捕捉到人臉數(shù)據(jù)調(diào)用代理方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    .......
}

MetadataObject包含了捕獲到的人臉數(shù)據(jù)瓶盛。人臉數(shù)據(jù)可能存在重復(fù),因?yàn)槿四樔绻麤](méi)有移動(dòng)的話示罗,下一秒還是會(huì)捕獲到上一秒的信息惩猫。使用循環(huán),打印人臉數(shù)據(jù)蚜点。

for (AVMetadataFaceObject *face in metadataObjects)
{
    NSLog(@"Face detected with ID:%li",(long)face.faceID);
    NSLog(@"Face bounds:%@",NSStringFromCGRect(face.bounds));
}

已經(jīng)獲取到視頻中人臉的位置和個(gè)數(shù)轧房,現(xiàn)在進(jìn)行對(duì)人臉數(shù)據(jù)的處理。在預(yù)覽圖層 THPreviewView 上進(jìn)行人臉的處理绍绘,所以需要通過(guò)代理將捕捉到的人臉元數(shù)據(jù)傳遞給 THPreviewView锯厢。

@protocol THFaceDetectionDelegate <NSObject>

- (void)didDetectFaces:(NSArray *)faces;

@end

@property (weak, nonatomic) id <THFaceDetectionDelegate> faceDetectionDelegate;

[self.faceDetectionDelegate didDetectFaces:metadataObjects];

@interface THPreviewView : UIView <THFaceDetectionDelegate>

創(chuàng)建人臉圖層
- (void)setupView
{
    .....
}

初始化faceLayers屬性皮官,其為一個(gè)字典,用來(lái)存儲(chǔ)每張人臉的圖層实辑,faceID 和人臉圖層一一對(duì)應(yīng)捺氢。

self.faceLayers = [NSMutableDictionary dictionary];

設(shè)置 videoGravity。使用 AVLayerVideoGravityResizeAspectFill 鋪滿整個(gè)預(yù)覽層的邊界范圍剪撬。

self.overlayLayer = [CALayer layer];

一般在 previewLayer 上添加一個(gè)透明的圖層摄乒。設(shè)置它的framepreviewLayerbounds相同。

self.overlayLayer = [CALayer layer];
self.overlayLayer.frame = self.bounds;

假設(shè)你的圖層上的圖像會(huì)發(fā)生3D變換残黑,則需要設(shè)置其投影方式馍佑。

self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);

將子圖層添加到預(yù)覽圖層來(lái)。

[self.previewLayer addSublayer:self.overlayLayer];

將檢測(cè)到的人臉進(jìn)行可視化
- (void)didDetectFaces:(NSArray *)faces
{
    .......
}

獲取到的人臉數(shù)據(jù)位置信息是攝像頭坐標(biāo)系的梨水,需要將其轉(zhuǎn)化為屏幕坐標(biāo)系拭荤。創(chuàng)建一個(gè)本地?cái)?shù)組,保存轉(zhuǎn)換后的人臉數(shù)據(jù)疫诽。

NSArray *transformedFaces = [self transformedFacesFromFaces:faces];

如果這個(gè)人臉從攝像頭中消失掉了舅世,則需要根據(jù)faceID刪除掉它的圖層。因?yàn)橹С滞瑫r(shí)識(shí)別多張人臉奇徒,所以可以獲取faceLayersallKey雏亚,假定剛開始所有的人臉都需要?jiǎng)h除,然后再一一從刪除列表中進(jìn)行移除摩钙。

NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];

遍歷每個(gè)轉(zhuǎn)換的人臉對(duì)象罢低,獲取其關(guān)聯(lián)的faceID。這個(gè)屬性唯一標(biāo)識(shí)一個(gè)檢測(cè)到的人臉胖笛。

for (AVMetadataFaceObject *face in transformedFaces)
{
    NSNumber *faceID = @(face.faceID);
    .....
}

如果faceID存在网持,則表示人臉沒(méi)有從攝像頭中移除,不需要進(jìn)行刪除长踊,將其從待移除人臉列表中移出翎碑。

[lostFaces removeObject:faceID];

如果是之前舊的人臉,則從人臉圖層字典中獲取到舊的人臉圖層之斯。拿到當(dāng)前faceID對(duì)應(yīng)的layer日杈。

CALayer *layer = self.faceLayers[faceID];

如果給定的faceID沒(méi)有找到對(duì)應(yīng)的圖層,說(shuō)明是新的人臉佑刷,需要?jiǎng)?chuàng)建人臉圖層莉擒,再將新的人臉圖層添加到 overlayLayer上,最后將layer加入到字典中瘫絮,更新字典涨冀。

if (!layer)
{
    //調(diào)用makeFaceLayer 創(chuàng)建一個(gè)新的人臉圖層
    layer = [self makeFaceLayer];
    
    //將新的人臉圖層添加到 overlayLayer上
    [self.overlayLayer addSublayer:layer];
    
    //將layer加入到字典中
    self.faceLayers[faceID] = layer;
}

圖層的大小 = 人臉的大小,所以根據(jù)人臉的bounds設(shè)置layerframe麦萤。

layer.frame = face.bounds;

人臉識(shí)別是3D動(dòng)態(tài)識(shí)別鹿鳖,屬于立體圖像扁眯,涉及到歐拉角。歐拉角的Yaw表示繞Y軸旋轉(zhuǎn)的角度(人臉左右旋轉(zhuǎn))翅帜,Pitch表示繞X軸旋轉(zhuǎn)角度(人臉左右搖擺)姻檀,Roll表示繞Z軸旋轉(zhuǎn)的角度(人臉上下?lián)u擺)。設(shè)置圖層的transform屬性為 CATransform3DIdentity涝滴,使用Core Animation對(duì)圖層進(jìn)行變化绣版。

layer.transform = CATransform3DIdentity;

判斷人臉對(duì)象是否具有有效的斜傾交(人的頭部向肩部方向傾斜)。如果為YES歼疮,則獲取相應(yīng)的CATransform3D值杂抽,將它與標(biāo)識(shí)變化關(guān)聯(lián)在一起,并設(shè)置transform屬性韩脏。實(shí)現(xiàn)當(dāng)人臉傾斜的時(shí)候缩麸,捕獲人臉的紅框也發(fā)生同樣的傾斜。

if (face.hasRollAngle)
{
    //如果為YES赡矢,則獲取相應(yīng)的CATransform3D 值
    CATransform3D t = [self transformForRollAngle:face.rollAngle];
    
    //將它與標(biāo)識(shí)變化關(guān)聯(lián)在一起杭朱,并設(shè)置transform屬性
    layer.transform = CATransform3DConcat(layer.transform, t);
}

判斷人臉對(duì)象是否具有有效的偏轉(zhuǎn)角。如果為YES济竹,則獲取相應(yīng)的 CATransform3D 值,再將它與標(biāo)識(shí)變化關(guān)聯(lián)在一起霎槐,并設(shè)置transform屬性送浊。

if (face.hasYawAngle)
{
    //如果為YES,則獲取相應(yīng)的CATransform3D 值
    CATransform3D  t = [self transformForYawAngle:face.yawAngle];
    
    //將它與標(biāo)識(shí)變化關(guān)聯(lián)在一起,并設(shè)置transform屬性
    layer.transform = CATransform3DConcat(layer.transform, t);
}

處理那些已經(jīng)從鏡頭中消失的人臉圖層丘跌。遍歷數(shù)組袭景,將剩下的人臉I(yè)D集合從上一個(gè)圖層和faceLayers字典中移除。

for (NSNumber *faceID in lostFaces)
{
    CALayer *layer = self.faceLayers[faceID];
    [layer removeFromSuperlayer];
    [self.faceLayers  removeObjectForKey:faceID];
}

輔助方法
? 攝像頭坐標(biāo)系轉(zhuǎn)化為屏幕坐標(biāo)系

將攝像頭的人臉數(shù)據(jù)轉(zhuǎn)換為視圖上的可展示的數(shù)據(jù)闭树。因?yàn)?code>UIKit的坐標(biāo)與攝像頭坐標(biāo)系統(tǒng)(0耸棒,0)-(1,1)不一樣报辱,所以需要轉(zhuǎn)換与殃,轉(zhuǎn)換成功后证逻,加入到數(shù)組中俱诸。

- (NSArray *)transformedFacesFromFaces:(NSArray *)faces
{
    NSMutableArray *transformeFaces = [NSMutableArray array];
    
    for (AVMetadataObject *face in faces)
    {
        AVMetadataObject *transformedFace = [self.previewLayer transformedMetadataObjectForMetadataObject:face];
        [transformeFaces addObject:transformedFace];
    }
    
    return transformeFaces;
}
? 調(diào)用makeFaceLayer 創(chuàng)建一個(gè)新的人臉圖層
- (CALayer *)makeFaceLayer
{
    //創(chuàng)建一個(gè)layer
    CALayer *layer = [CALayer layer];
    
    //邊框?qū)挾葹?.0f
    layer.borderWidth = 5.0f;
    
    //邊框顏色為紅色
    layer.borderColor = [UIColor redColor].CGColor;
    
    // 為view添加背景圖片
    layer.contents = (id)[UIImage imageNamed:@"551.png"].CGImage;
    
    //返回layer
    return layer;
}
? 將投影方式修改為透視投影
self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);

static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition)
{
    //CATransform3D 圖層的旋轉(zhuǎn)槽袄,縮放缀皱,偏移辆影,歪斜
    //CATransform3DIdentity是單位矩陣洞斯,該矩陣沒(méi)有縮放周蹭,旋轉(zhuǎn)绰筛,歪斜慢睡,透視逐工。該矩陣應(yīng)用到圖層上铡溪,就是設(shè)置默認(rèn)值
    CATransform3D transform = CATransform3DIdentity;
    
    
    //透視效果(就是近大遠(yuǎn)小)泪喊,是通過(guò)設(shè)置m34 m34 = -1.0/D 默認(rèn)是0.D越小透視效果越明顯
    //D:eyePosition 觀察者到投射面的距離
    transform.m34 = -1.0/eyePosition;
    
    return transform;
}
? 為設(shè)備方向計(jì)算一個(gè)相應(yīng)的旋轉(zhuǎn)變換
- (CATransform3D)orientationTransform
{

    CGFloat angle = 0.0;
    //拿到設(shè)備方向
    switch ([UIDevice currentDevice].orientation)
    {
            //方向:下
        case UIDeviceOrientationPortraitUpsideDown:
            angle = M_PI;
            break;
            
            //方向:右
        case UIDeviceOrientationLandscapeRight:
            angle = -M_PI / 2.0f;
            break;
        
            //方向:左
        case UIDeviceOrientationLandscapeLeft:
            angle = M_PI /2.0f;
            break;

            //其他
        default:
            angle = 0.0f;
            break;
    }
    
    return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
}
? 將 RollAngle 的 rollAngleInDegrees 值轉(zhuǎn)換為 CATransform3D
static CGFloat THDegreesToRadians(CGFloat degrees)
{
    return degrees * M_PI / 180;
}

- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees
{
    //將人臉對(duì)象得到的RollAngle 單位“度” 轉(zhuǎn)為Core Animation需要的弧度值
    CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);

    //rollAngle圍繞Z軸旋轉(zhuǎn)
    //將結(jié)果賦給CATransform3DMakeRotation x,y,z軸為0棕硫,0,1 得到繞Z軸傾斜角旋轉(zhuǎn)轉(zhuǎn)換
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}
? 將 YawAngle 的 yawAngleInDegrees 值轉(zhuǎn)換為 CATransform3D

YawAngleyawAngleInDegrees 值轉(zhuǎn)換為 CATransform3D窘俺。將角度轉(zhuǎn)換為弧度值饲帅,再將弧度值結(jié)果進(jìn)行CATransform3DMakeRotationx,y,z軸為0瘤泪,-1灶泵,0 得到繞Y軸旋轉(zhuǎn)結(jié)果。因?yàn)閼?yīng)用程序的界面固定為垂直方向对途,所以需要為設(shè)備方向計(jì)算一個(gè)相應(yīng)的旋轉(zhuǎn)變換赦邻,最后將設(shè)備的旋轉(zhuǎn)變換和人臉的旋轉(zhuǎn)變化進(jìn)行矩陣相乘得到最后結(jié)果,如果不這樣实檀,會(huì)造成人臉圖層的偏轉(zhuǎn)效果不正確惶洲。

- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees
{
    CGFloat yawAngleInRaians = THDegreesToRadians(yawAngleInDegrees);
    CATransform3D yawTransform = CATransform3DMakeRotation(yawAngleInRaians, 0.0f, -1.0f, 0.0f);
    return CATransform3DConcat(yawTransform, [self orientationTransform]);
}

Demo

Demo在我的Github上,歡迎下載膳犹。
Multi-MediaDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載恬吕,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末须床,一起剝皮案震驚了整個(gè)濱河市铐料,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌豺旬,老刑警劉巖钠惩,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異族阅,居然都是意外死亡篓跛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門坦刀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)愧沟,“玉大人,你說(shuō)我怎么就攤上這事鲤遥⊙朐” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵渴频,是天一觀的道長(zhǎng)芽丹。 經(jīng)常有香客問(wèn)我,道長(zhǎng)卜朗,這世上最難降的妖魔是什么拔第? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任咕村,我火速辦了婚禮,結(jié)果婚禮上蚊俺,老公的妹妹穿的比我還像新娘懈涛。我一直安慰自己,他們只是感情好泳猬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布批钠。 她就那樣靜靜地躺著,像睡著了一般得封。 火紅的嫁衣襯著肌膚如雪埋心。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天忙上,我揣著相機(jī)與錄音拷呆,去河邊找鬼。 笑死疫粥,一個(gè)胖子當(dāng)著我的面吹牛茬斧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梗逮,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼项秉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了慷彤?” 一聲冷哼從身側(cè)響起娄蔼,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瞬欧,沒(méi)想到半個(gè)月后贷屎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罢防,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡艘虎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咒吐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片野建。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恬叹,靈堂內(nèi)的尸體忽然破棺而出候生,到底是詐尸還是另有隱情,我是刑警寧澤绽昼,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布唯鸭,位于F島的核電站,受9級(jí)特大地震影響硅确,放射性物質(zhì)發(fā)生泄漏目溉。R本人自食惡果不足惜明肮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缭付。 院中可真熱鬧柿估,春花似錦、人聲如沸陷猫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绣檬。三九已至足陨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間河咽,已是汗流浹背钠右。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留忘蟹,地道東北人飒房。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像媚值,于是被迫代替她去往敵國(guó)和親狠毯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容