大邑縣建設(shè)局網(wǎng)站深圳開發(fā)公司網(wǎng)站建設(shè)
目錄
- 概述
- 一、如何用Swift調(diào)用OpenCV庫
- 1.項(xiàng)目引入OpenCV庫
- 2.橋接OpenCV及Swift
- 二、運(yùn)用AVFoundation獲取實(shí)時圖像數(shù)據(jù)
- 1.建立視頻流數(shù)據(jù)捕獲框架
- 2.建立 Capture Session
- 3.取得并配置 Capture Devices
- 4.設(shè)定 Device Inputs
- 5.配置Video Data Output輸出
- 6.工程隱私權(quán)限配置
- 7.處理相機(jī)視頻回調(diào)
- 三、視頻流原始數(shù)據(jù)CMSampleBuffer處理
- 1.CMSampleBuffer數(shù)據(jù)轉(zhuǎn)換為Mat數(shù)據(jù)
- 2.回調(diào)中的數(shù)據(jù)處理
- 3.Mat數(shù)據(jù)轉(zhuǎn)換為UIImage數(shù)據(jù)用于顯示
- 四、Swift界面搭建
- 1.在UI層捕獲相機(jī)數(shù)據(jù)
- 2.直接顯示CMSampleBuffer方法
- 五、基于Object-C++的OpenCV圖像處理部分
- 1.引入頭文件
- 2.OpenCV人臉識別輸出識別框
- 總結(jié)
概述
在2020年6月9日之后,OpenCV可以直接在Objective-C和Swift中使用它,而無需自己編寫Objective-C++,可以直接在OpenCV官網(wǎng)下載iOS Package包,使用起來也是比較簡單。但由于之前對OpenCV庫的使用是使用C++編寫,所以O(shè)bjective-C++在圖像處理部分使用起來更順手,因此本文主要的技術(shù)框架是使用Objective-C++編寫圖像處理流程,Swift編寫iOS界面及AVFoundation相機(jī)等的調(diào)用以獲取實(shí)時的圖像數(shù)據(jù)。本文主要以實(shí)時框出人臉為示例,iOS移動端界面的顯示結(jié)果大致如下圖。
OpenCV官網(wǎng):https://opencv.org/releases/
一、如何用Swift調(diào)用OpenCV庫
1.項(xiàng)目引入OpenCV庫
- 使用cocoapods就非常簡單:
pod 'OpenCV'
- 自行手動添加:在官網(wǎng)下載相應(yīng)版本的iOS Pack,解壓后得到一個 opencv2.framework 庫,創(chuàng)建項(xiàng)目并右鍵添加文件到項(xiàng)目。
2.橋接OpenCV及Swift
- 前面說到OpenCV框架是用C++進(jìn)行編程的,因此要用Objective-C++代碼于Swift代碼進(jìn)行橋接。首先添加一個 Objective-C 文件到項(xiàng)目中,會彈出一個是否添加 Bridging-Header 文件,選擇添加(若此處沒彈出,則可以手動添加Bridging-Header 文件,即添加一個頭文件(Header file),重命名為“項(xiàng)目名-Bridging-Header.h”),這就實(shí)現(xiàn)了Swift和Object-C的混編。
- 將這個Object-C的文件擴(kuò)展名“.m”改為“.mm”這就將該文件變成了Objective-C++文件,文件大致如下
二、運(yùn)用AVFoundation獲取實(shí)時圖像數(shù)據(jù)
Apple預(yù)設(shè)的APIs 如UIImagePickerController能夠直接獲取攝像頭獲取的圖像并顯示在界面上,操作簡單,但無法對原數(shù)據(jù)進(jìn)行操作,因此本文中應(yīng)用AVFoundation的 Capture Sessions來采集圖像和視頻流。根據(jù)官方文檔,Capture Session 是用以【管理采集活動、并協(xié)調(diào)來自 Input Devices 到采集 Outputs 的數(shù)據(jù)流】。在 AVFoundation 內(nèi),Capture Sessions 是由AVCaptureSession來管理的。
1.建立視頻流數(shù)據(jù)捕獲框架
首先創(chuàng)建一個NSObject類型的Controller名為CameraController,處理攝像頭的事務(wù),設(shè)置prepare函數(shù)以供主程序調(diào)用,其主要負(fù)責(zé)設(shè)立一個新的 Capture Session。設(shè)定 Capture Session 分為五個步驟:
- 建立一個 Capture Session
- 取得并配置 Capture Devices
- 在 Capture Device 上建立 Inputs
- 設(shè)置一個 Video Data Output 物件
- 配置Video Data Output Queue參數(shù)
func prepare(completionHandler: @escaping (Error?) -> Void) {//建立一個 Capture Sessionfunc createCaptureSession() { }//取得并配置 Capture Devicesfunc configureCaptureDevices() throws { }//在 Capture Device 上建立 Inputsfunc configureDeviceInputs() throws { }//設(shè)置一個 Video Data Output 物件func configureVideoDataOutput() throws { }//配置Video Data Output Queue參數(shù)func configureVideoDataOutputQueue() throws{ }DispatchQueue(label: "prepare").async {do {createCaptureSession()try configureCaptureDevices()try configureDeviceInputs()try configureVideoDataOutput()try configureVideoDataOutputQueue()}catch {DispatchQueue.main.async {completionHandler(error)} return}DispatchQueue.main.async {completionHandler(nil)}}
}
2.建立 Capture Session
建立新的AVCaptureSession,并將它存儲在captureSession的屬性里,并設(shè)定一些用于拋出的錯誤類型
var captureSession: AVCaptureSession?func createCaptureSession() { self.captureSession = AVCaptureSession()
}//設(shè)定prepare過程中遇到的錯誤類型enum CameraControllerError: Swift.Error {case captureSessionAlreadyRunningcase captureSessionIsMissingcase inputsAreInvalidcase invalidOperationcase noCamerasAvailablecase unknown}//設(shè)定相機(jī)位置為前后相機(jī)public enum CameraPosition {case frontcase rear}
3.取得并配置 Capture Devices
建立了一個AVCaptureSession后,需要建立AVCaptureDevice物件來代表實(shí)際的相機(jī)
//前置鏡頭var frontCamera: AVCaptureDevice?//后置鏡頭var rearCamera: AVCaptureDevice?func configureCaptureDevices() throws {//使用了AVCaptureDeviceDiscoverySession找出設(shè)備上所有可用的內(nèi)置相機(jī) (`.builtInDualCamera`)。//若沒找到相機(jī)則拋出異常。let session = AVCaptureDevice.DiscoverySession.init(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified) let cameras = session.devices.compactMap { $0 }guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }//遍歷前面找到的可用相機(jī),分辨出前后相機(jī)。//然后,將該相機(jī)設(shè)定為自動對焦,遇到任何問題也會拋出異常。for camera in cameras {if camera.position == .front {self.frontCamera = camera}if camera.position == .back {self.rearCamera = cameratry camera.lockForConfiguration()camera.focusMode = .continuousAutoFocuscamera.unlockForConfiguration()}}}
4.設(shè)定 Device Inputs
var currentCameraPosition: CameraPosition?
var frontCameraInput: AVCaptureDeviceInput?
var rearCameraInput: AVCaptureDeviceInput?func configureDeviceInputs() throws {//確認(rèn)`captureSession`是否存在,若不存在拋出異常guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }//建立所需的 Capture Device Input 來進(jìn)行數(shù)據(jù)采集。//`AVFoundation`每一次 Capture Session 僅能允許一臺相機(jī)輸入。//由于裝置的初始設(shè)定為后相相機(jī)。先嘗試用后相機(jī) Input,再加到 Capture Session;if let rearCamera = self.rearCamera {self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }self.currentCameraPosition = .rear}//嘗試建立前相機(jī)Input else if let frontCamera = self.frontCamera {self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) }else { throw CameraControllerError.inputsAreInvalid }self.currentCameraPosition = .front}else { throw CameraControllerError.noCamerasAvailable }
}
5.配置Video Data Output輸出
var videoOutput: AVCaptureVideoDataOutput?//配置相機(jī)的視頻輸出,并開始func configureVideoDataOutput() throws {guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }self.videoOutput = AVCaptureVideoDataOutput()if captureSession.canAddOutput(self.videoOutput!) { captureSession.addOutput(self.videoOutput!) }captureSession.startRunning()}//配置視頻的輸出代理及輸出格式func configureVideoDataOutputQueue() throws{let videoDataOutputQueue = DispatchQueue(label: "videoDataOutputQueue")self.videoOutput!.setSampleBufferDelegate(self, queue: videoDataOutputQueue)self.videoOutput!.alwaysDiscardsLateVideoFrames = falselet BGRA32PixelFormat = NSNumber(value: Int32(kCVPixelFormatType_32BGRA))let rgbOutputSetting = [kCVPixelBufferPixelFormatTypeKey.string : BGRA32PixelFormat]self.videoOutput!.videoSettings = rgbOutputSetting}
6.工程隱私權(quán)限配置
根據(jù)Apple 規(guī)定的安全性要求,必須提供一個app使用相機(jī)權(quán)限的原因。在工程的Info.plist,加入下圖的設(shè)置:
7.處理相機(jī)視頻回調(diào)
能夠從下方的回調(diào)中得到相機(jī)返回的實(shí)時數(shù)據(jù),格式為CMSampleBuffer,該視頻流格式不止包含圖像信息還包含時間戳信息等,若想通過opencv進(jìn)行處理還需進(jìn)行數(shù)據(jù)轉(zhuǎn)換。
extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate{func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {}
}
參考地址:https://www.appcoda.com.tw/avfoundation-camera-app/
三、視頻流原始數(shù)據(jù)CMSampleBuffer處理
1.CMSampleBuffer數(shù)據(jù)轉(zhuǎn)換為Mat數(shù)據(jù)
OpenCV提供了UIImageToMat的函數(shù),根據(jù)這個思路,我們應(yīng)當(dāng)將CMSampleBuffer轉(zhuǎn)換為UIImage數(shù)據(jù),CMSsampleBuffer不止包含ImageBuffer,通過API自帶的CMSampleBufferGetImageBuffer(),可以得到與我們希望得到的圖像數(shù)據(jù)更為接近的cvPixelBuffer。
總的來說,下方是CMSampleBuffer轉(zhuǎn)換為UIImage的兩種方式,第一種通過CIImage第二種通過CGImage,通過CIImage轉(zhuǎn)換成的UIImage雖然能顯示在UIImageVIew上,但是在轉(zhuǎn)換成Mat格式的時候會報錯,因此選用第二種通過CGImage的轉(zhuǎn)換。最后調(diào)用opencv庫的UIImageToMat函數(shù)便能得到Mat數(shù)據(jù)了。
func image(orientation: UIImage.Orientation = .up, scale: CGFloat = 1.0) -> UIImage? {if let buffer = CMSampleBufferGetImageBuffer(self) {let ciImage = CIImage(cvPixelBuffer: buffer)return UIImage(ciImage: ciImage, scale: scale, orientation: orientation)}return nil}func imageWithCGImage(orientation: UIImage.Orientation = .up, scale: CGFloat = 1.0) -> UIImage? {if let buffer = CMSampleBufferGetImageBuffer(self) {let ciImage = CIImage(cvPixelBuffer: buffer)let context = CIContext(options: nil)guard let cg = context.createCGImage(ciImage, from: ciImage.extent) else {return nil} return UIImage(cgImage: cg, scale: scale, orientation: orientation)}return nil}
2.回調(diào)中的數(shù)據(jù)處理
這邊選用的方案是UIImageView來顯示原始圖像,并且在UIImageView上添加一個蒙層圖像來顯示識別框。此處選用蒙層的原因是,圖像處理每幀需要70ms的處理時間,若直接顯示處理后的圖片會有延遲丟幀的情況視覺效果較差,因此實(shí)時圖像采用原始圖像數(shù)據(jù),而識別框丟幀并不影響視覺效果。
//回調(diào)原始圖像var videoCpatureCompletionBlock: ((UIImage) -> Void)?//回調(diào)CMSsmapleBuffer圖像var videoCaptureCompletionBlockCMS: ((CMSampleBuffer)-> Void)?//回調(diào)蒙層圖像var videoCaptureCompletionBlockMask: ((UIImage) -> Void)?//用于記錄幀數(shù)var frameFlag : Int = 0//用于給異步線程加鎖var lockFlagBool : Bool = falsefunc captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {if let image = sampleBuffer.imageWithCGImage(orientation: .up, scale: 1.0){self.frameFlag = self.frameFlag + 1var output = imageif(self.frameFlag != -1){self.videoCaptureCompletionBlockCMS?(sampleBuffer)self.videoCpatureCompletionBlock?(output)if(self.lockFlagBool == false){//此處必須開線程處理,否則會報錯DispatchQueue.global().async {lockFlagBool = truevar output = image//addimageProcess為opencv圖像處理過程,寫在Objecj-C++文件中,本文后面記錄output = opencv_test.addimageProcess(output)self.videoCaptureCompletionBlockMask?(output)lockFlagBool = false}}}else{print("丟幀")self.frameFlag = 0}}}
3.Mat數(shù)據(jù)轉(zhuǎn)換為UIImage數(shù)據(jù)用于顯示
為了最后能用于顯示,還要轉(zhuǎn)換為UImage,該部分很簡單,直接調(diào)用OpenCV的庫函數(shù),當(dāng)然如果想轉(zhuǎn)換為CMSampleBuffer的話還需要重新添加丟失的數(shù)據(jù),比如時間戳。
MatToUIImage()
參考地址:https://stackoverflow.com/questions/15726761/make-an-uiimage-from-a-cmsamplebuffer
四、Swift界面搭建
1.在UI層捕獲相機(jī)數(shù)據(jù)
UI界面的操作比較簡單,實(shí)例化之前的CameraController類,并設(shè)定configureCameraController函數(shù)來調(diào)用類中的prepare函數(shù),以及接受回調(diào)的圖像數(shù)據(jù),這些回調(diào)對UIImageView的圖像刷新必須要在主線程中,否則會報錯。其中,selfImageView和maskImageView是兩個自己創(chuàng)建的UImageView來顯示UIImage圖像的,這兩個UIImageView要保持在同樣位置同樣大小。
let cameraController = CameraController()override func viewDidLoad() {configureCameraController() }func configureCameraController() {cameraController.prepare {(error) inif let error = error {print(error)}self.cameraController.videoCpatureCompletionBlock = { image inDispatchQueue.main.async {self.selfImageView.image = image}}self.cameraController.videoCaptureCompletionBlockMask = { image inDispatchQueue.main.async {self.maskImageView.image = image}}//直接顯示CMSampleBuffer的方法// self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in//self.displayLayer.enqueue(CMSampleBuffer)//}}}
2.直接顯示CMSampleBuffer方法
其實(shí)蘋果的API也提供了直接顯示CMSampleBuffer的簡單方法,通過AVSampleBufferDisplayLayer以及其.enqueue方法,其展示方式如下:
var displayLayer:AVSampleBufferDisplayLayer!override func viewDidLoad() {displayLayer = AVSampleBufferDisplayLayer()displayLayer.videoGravity = .resizeAspect self.imageView.layer.addSublayer(displayLayer)self.displayLayer.frame.origin.y = self.imageView.frame.origin.yself.displayLayer.frame.origin.x = self.imageView.frame.origin.x}func configureCameraController() {cameraController.prepare {(error) inif let error = error {print(error)}//直接顯示CMSampleBuffer的方法self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer inself.displayLayer.enqueue(CMSampleBuffer)}}}
五、基于Object-C++的OpenCV圖像處理部分
1.引入頭文件
這部分用C++編寫過OpenCV的都相當(dāng)熟悉了,在.mm文件中引入以下頭文件,并引入命名空間,若該部分找不到文件應(yīng)當(dāng)確認(rèn)是否已正確安裝OpenCV庫。
#import <opencv2/opencv.hpp>
#import "opencv-test.h"
#import <opencv2/imgcodecs/ios.h>//對iOS支持
#import <opencv2/imgcodecs/ios.h>
//導(dǎo)入矩陣幫助類
#import <opencv2/highgui.hpp>
#import <opencv2/core/types.hpp>
#import <iostream>using namespace std;
using namespace cv;@implementation opencv_test//各類處理函數(shù)
@end
2.OpenCV人臉識別輸出識別框
本文使用了OpenCV自帶的人臉識別框架CascadeClassifier,將得到的人臉坐標(biāo)放入vector中,最后繪制在蒙層上,最后輸出蒙層圖片。其它對于圖像的處理也可以用相同的方式處理,在參考資料中有馬賽克操作。
+(UIImage*)addimageProcess:(UIImage*)image {//用于記錄時間CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();Mat src;//將iOS圖片->OpenCV圖片(Mat矩陣)UIImageToMat(image, src);Mat src_gray;//圖像灰度化cvtColor(src, src_gray, COLOR_RGBA2GRAY, 1);std::vector<cv::Rect> faces;//初始化OpenCV的人臉識別檢測器CascadeClassifier faceDetector;//獲取權(quán)重文件,文件需要提前導(dǎo)入至工程目錄中NSString* cascadePath = [[NSBundle mainBundle]pathForResource:@"haarcascade_frontalface_alt"ofType:@"xml"];//配置檢測器faceDetector.load([cascadePath UTF8String]);faceDetector.detectMultiScale(src_gray, faces, 1.1,2, 0|CASCADE_SCALE_IMAGE, cv::Size(30, 30));//確定圖像寬高int width = src.cols;int height = src.rows;//Mat Mask = Mat::zeros(width, height, CV_8UC4);//創(chuàng)建透明蒙層圖像 Scalar(0,0,0,0) 分別是RGBA A為透明度Mat Mask = Mat(height, width, CV_8UC4, Scalar(0,0,0,0));// Draw all detected facesfor(unsigned int i = 0; i < faces.size(); i++){const cv::Rect& face = faces[i];// Get top-left and bottom-right corner pointscv::Point tl(face.x, face.y);cv::Point br = tl + cv::Point(face.width, face.height);// Draw rectangle around the faceScalar magenta = Scalar(0, 255, 0, 255);cv::rectangle(Mask, tl, br, magenta, 4, 8, 0);}//打印處理時間CFAbsoluteTime endTime = (CFAbsoluteTimeGetCurrent() - startTime);NSLog(@"normalProcess方法耗時: %f ms", endTime * 1000.0);return MatToUIImage(Mask);
}
參考資料:https://www.twblogs.net/a/5b830b452b717766a1eadb20/?lang=zh-cn
總結(jié)
遇到的困難:一是在于方案中用UIImageView來進(jìn)行顯示,必須在主線程中進(jìn)行渲染,對于線程的處理相對繁瑣,若是處理不得當(dāng)便會有延時丟幀不刷新等的問題。
存在的問題:OpenCV自帶的人臉識別算法比較老舊,處理速度也比較慢效果也一般,要引入其他神經(jīng)網(wǎng)絡(luò)框架在客戶端上的可行性有待討論,處理速度也未知。
另外,若有需要總的工程文件的可以私聊我。