概述 在2020年6月9日之后,OpenCV可以直接在Objective-C和Swift中使用它,而无需自己编写Objective-C++,可以直接在OpenCV官网下载iOS Package包,使用起来也是比较简单。但由于之前对OpenCV库的使用是使用C++编写,所以Objective-C++在图像处理部分使用起来更顺手,因此本文主要的技术框架是使用Objective-C++编写图像处理流程,Swift编写iOS界面及AVFoundation相机等的调用以获取实时的图像数据。本文主要以实时框出人脸为示例 ,iOS移动端界面的显示结果大致如下图。
OpenCV官网:https://opencv.org/releases/
一、如何用Swift调用OpenCV库 1.项目引入OpenCV库
使用cocoapods就非常简单:
自行手动添加:在官网下载相应版本的iOS Pack,解压后得到一个 opencv2.framework 库,创建项目并右键添加文件到项目。
2.桥接OpenCV及Swift
前面说到OpenCV框架是用C++进行编程的,因此要用Objective-C++代码于Swift代码进行桥接。首先添加一个 Objective-C 文件到项目中,会弹出一个是否添加 Bridging-Header 文件,选择添加(若此处没弹出,则可以手动添加Bridging-Header 文件,即添加一个头文件(Header file),重命名为“项目名-Bridging-Header.h”),这就实现了Swift和Object-C的混编。
将这个Object-C的文件扩展名“.m”改为“.mm”这就将该文件变成了Objective-C++文件,文件大致如下
二、运用AVFoundation获取实时图像数据 Apple预设的APIs 如UIImagePickerController能够直接获取摄像头获取的图像并显示在界面上,操作简单,但无法对原数据进行操作,因此本文中应用AVFoundation的 Capture Sessions来采集图像和视频流。根据官方文档,Capture Session 是用以【管理采集活动、并协调来自 Input Devices 到采集 Outputs 的数据流】。在 AVFoundation 内,Capture Sessions 是由AVCaptureSession来管理的。
1.建立视频流数据捕获框架 首先创建一个NSObject类型的Controller名为CameraController,处理摄像头的事务,设置prepare函数以供主程序调用,其主要负责设立一个新的 Capture Session。设定 Capture Session 分为五个步骤:
建立一个 Capture Session
取得并配置 Capture Devices
在 Capture Device 上建立 Inputs
设置一个 Video Data Output 物件
配置Video Data Output Queue参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func prepare (completionHandler : @escaping (Error ?) -> Void ) { func createCaptureSession () { } func configureCaptureDevices () throws { } func configureDeviceInputs () throws { } func configureVideoDataOutput () throws { } 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的属性里,并设定一些用于抛出的错误类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var captureSession: AVCaptureSession ?func createCaptureSession () { self .captureSession = AVCaptureSession () } enum CameraControllerError : Swift .Error { case captureSessionAlreadyRunning case captureSessionIsMissing case inputsAreInvalid case invalidOperation case noCamerasAvailable case unknown } public enum CameraPosition { case front case rear }
3.取得并配置 Capture Devices 建立了一个AVCaptureSession后,需要建立AVCaptureDevice物件来代表实际的相机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 var frontCamera: AVCaptureDevice ?var rearCamera: AVCaptureDevice ? func configureCaptureDevices () throws { 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 } for camera in cameras { if camera.position == .front { self .frontCamera = camera } if camera.position == .back { self .rearCamera = camera try camera.lockForConfiguration() camera.focusMode = .continuousAutoFocus camera.unlockForConfiguration() } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 var currentCameraPosition: CameraPosition ?var frontCameraInput: AVCaptureDeviceInput ?var rearCameraInput: AVCaptureDeviceInput ?func configureDeviceInputs () throws { guard let captureSession = self .captureSession else { throw CameraControllerError .captureSessionIsMissing } if let rearCamera = self .rearCamera { self .rearCameraInput = try AVCaptureDeviceInput (device: rearCamera) if captureSession.canAddInput(self .rearCameraInput! ) { captureSession.addInput(self .rearCameraInput! ) } self .currentCameraPosition = .rear } 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输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var videoOutput: AVCaptureVideoDataOutput ?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 = false let BGRA32PixelFormat = NSNumber (value: Int32 (kCVPixelFormatType_32BGRA)) let rgbOutputSetting = [kCVPixelBufferPixelFormatTypeKey.string : BGRA32PixelFormat ] self .videoOutput! .videoSettings = rgbOutputSetting }
6.工程隐私权限配置 根据Apple 规定的安全性要求,必须提供一个app使用相机权限的原因。在工程的Info.plist,加入下图的设置:
7.处理相机视频回调 能够从下方的回调中得到相机返回的实时数据,格式为CMSampleBuffer,该视频流格式不止包含图像信息还包含时间戳信息等,若想通过opencv进行处理还需进行数据转换。
1 2 3 4 extension CameraController : AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput (_ output : AVCaptureOutput , didOutput sampleBuffer : CMSampleBuffer , from connection : AVCaptureConnection ) { } }
参考地址:https://www.appcoda.com.tw/avfoundation-camera-app/
三、视频流原始数据CMSampleBuffer处理 1.CMSampleBuffer数据转换为Mat数据 OpenCV提供了UIImageToMat 的函数,根据这个思路,我们应当将CMSampleBuffer转换为UIImage数据,CMSsampleBuffer不止包含ImageBuffer,通过API自带的CMSampleBufferGetImageBuffer(),可以得到与我们希望得到的图像数据更为接近的cvPixelBuffer。
总的来说,下方是CMSampleBuffer转换为UIImage的两种方式,第一种通过CIImage第二种通过CGImage,通过CIImage转换成的UIImage虽然能显示在UIImageVIew上,但是在转换成Mat格式的时候会报错,因此选用第二种通过CGImage的转换。最后调用opencv库的UIImageToMat 函数便能得到Mat数据了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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.回调中的数据处理 这边选用的方案是UIImageView来显示原始图像,并且在UIImageView上添加一个蒙层图像来显示识别框。此处选用蒙层的原因是,图像处理每帧需要70ms的处理时间,若直接显示处理后的图片会有延迟丢帧的情况视觉效果较差,因此实时图像采用原始图像数据,而识别框丢帧并不影响视觉效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 var videoCpatureCompletionBlock: ((UIImage ) -> Void )? var videoCaptureCompletionBlockCMS: ((CMSampleBuffer )-> Void )? var videoCaptureCompletionBlockMask: ((UIImage ) -> Void )? var frameFlag : Int = 0 var lockFlagBool : Bool = false func captureOutput (_ output : AVCaptureOutput , didOutput sampleBuffer : CMSampleBuffer , from connection : AVCaptureConnection ) { if let image = sampleBuffer.imageWithCGImage(orientation: .up, scale: 1.0 ){ self .frameFlag = self .frameFlag + 1 var output = image if (self .frameFlag != - 1 ){ self .videoCaptureCompletionBlockCMS? (sampleBuffer) self .videoCpatureCompletionBlock? (output) if (self .lockFlagBool == false ){ DispatchQueue .global().async { lockFlagBool = true var output = image output = opencv_test.addimageProcess(output) self .videoCaptureCompletionBlockMask? (output) lockFlagBool = false } } }else { print ("丢帧" ) self .frameFlag = 0 } } }
3.Mat数据转换为UIImage数据用于显示 为了最后能用于显示,还要转换为UImage,该部分很简单,直接调用OpenCV的库函数,当然如果想转换为CMSampleBuffer的话还需要重新添加丢失的数据,比如时间戳。
参考地址:https://stackoverflow.com/questions/15726761/make-an-uiimage-from-a-cmsamplebuffer
四、Swift界面搭建 1.在UI层捕获相机数据 UI界面的操作比较简单,实例化之前的CameraController类,并设定configureCameraController函数来调用类中的prepare函数,以及接受回调的图像数据,这些回调对UIImageView的图像刷新必须要在主线程中,否则会报错。其中,selfImageView和maskImageView是两个自己创建的UImageView来显示UIImage图像的,这两个UIImageView要保持在同样位置同样大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 let cameraController = CameraController ()override func viewDidLoad () { configureCameraController() } func configureCameraController () { cameraController.prepare {(error) in if let error = error { print (error) } self .cameraController.videoCpatureCompletionBlock = { image in DispatchQueue .main.async { self .selfImageView.image = image } } self .cameraController.videoCaptureCompletionBlockMask = { image in DispatchQueue .main.async { self .maskImageView.image = image } } } }
2.直接显示CMSampleBuffer方法 其实苹果的API也提供了直接显示CMSampleBuffer的简单方法,通过AVSampleBufferDisplayLayer以及其.enqueue方法,其展示方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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.y self .displayLayer.frame.origin.x = self .imageView.frame.origin.x } func configureCameraController () { cameraController.prepare {(error) in if let error = error { print (error) } self .cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in self .displayLayer.enqueue(CMSampleBuffer ) } } }
五、基于Object-C++的OpenCV图像处理部分 1.引入头文件 这部分用C++编写过OpenCV的都相当熟悉了,在.mm文件中引入以下头文件,并引入命名空间,若该部分找不到文件应当确认是否已正确安装OpenCV库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #import <opencv2/opencv.hpp> #import "opencv-test.h" #import <opencv2/imgcodecs/ios.h> #import <opencv2/imgcodecs/ios.h> #import <opencv2/highgui.hpp> #import <opencv2/core/types.hpp> #import <iostream> using namespace std; using namespace cv; @implementation opencv_test @end
2.OpenCV人脸识别输出识别框 本文使用了OpenCV自带的人脸识别框架CascadeClassifier,将得到的人脸坐标放入vector中,最后绘制在蒙层上,最后输出蒙层图片。其它对于图像的处理也可以用相同的方式处理,在参考资料中有马赛克操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 + (UIImage * )addimageProcess:(UIImage * )image { CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent (); Mat src; UIImageToMat (image, src); Mat src_gray; cvtColor(src, src_gray, COLOR_RGBA2GRAY , 1 ); std::vector< cv::Rect > faces; CascadeClassifier faceDetector; 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 (height, width, CV_8UC4 , Scalar (0 ,0 ,0 ,0 )); 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); Scalar 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
总结 遇到的困难:一是在于方案中用UIImageView来进行显示,必须在主线程中进行渲染,对于线程的处理相对繁琐,若是处理不得当便会有延时丢帧不刷新等的问题。 存在的问题:OpenCV自带的人脸识别算法比较老旧,处理速度也比较慢效果也一般,要引入其他神经网络框架在客户端上的可行性有待讨论,处理速度也未知。
另外,若有需要总的工程文件的可以私聊我。