在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/
pod 'OpenCV'
Apple预设的APIs 如UIImagePickerController能够直接获取摄像头获取的图像并显示在界面上,操作简单,但无法对原数据进行操作,因此本文中应用AVFoundation的 Capture Sessions来采集图像和视频流。根据官方文档,Capture Session 是用以【管理采集活动、并协调来自 Input Devices 到采集 Outputs 的数据流】。在 AVFoundation 内,Capture Sessions 是由AVCaptureSession来管理的。
首先创建一个NSObject类型的Controller名为CameraController,处理摄像头的事务,设置prepare函数以供主程序调用,其主要负责设立一个新的 Capture Session。设定 Capture Session 分为五个步骤:
func prepare(completionHandler: @escaping (Error?) -> Void) {
//建立一个 Capture Session
func createCaptureSession() { }
//取得并配置 Capture Devices
func configureCaptureDevices() throws { }
//在 Capture Device 上建立 Inputs
func configureDeviceInputs() throws { }
//设置一个 Video Data Output 物件
func configureVideoDataOutput() throws { }
//配置Video Data Output Queue参数
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)
}
}
}
建立新的AVCaptureSession,并将它存储在captureSession的属性里,并设定一些用于抛出的错误类型
var captureSession: AVCaptureSession?
func createCaptureSession() {
self.captureSession = AVCaptureSession()
}
//设定prepare过程中遇到的错误类型
enum CameraControllerError: Swift.Error {
case captureSessionAlreadyRunning
case captureSessionIsMissing
case inputsAreInvalid
case invalidOperation
case noCamerasAvailable
case unknown
}
//设定相机位置为前后相机
public enum CameraPosition {
case front
case rear
}
建立了一个AVCaptureSession后,需要建立AVCaptureDevice物件来代表实际的相机
//前置镜头
var frontCamera: AVCaptureDevice?
//后置镜头
var rearCamera: AVCaptureDevice?
func configureCaptureDevices() throws {
//使用了AVCaptureDeviceDiscoverySession找出设备上所有可用的内置相机 (`.builtInDualCamera`)。
//若没找到相机则抛出异常。
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()
}
}
}
var currentCameraPosition: CameraPosition?
var frontCameraInput: AVCaptureDeviceInput?
var rearCameraInput: AVCaptureDeviceInput?
func configureDeviceInputs() throws {
//确认`captureSession`是否存在,若不存在抛出异常
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
//建立所需的 Capture Device Input 来进行数据采集。
//`AVFoundation`每一次 Capture Session 仅能允许一台相机输入。
//由于装置的初始设定为后相相机。先尝试用后相机 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
}
//尝试建立前相机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 }
}
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
}
根据Apple 规定的安全性要求,必须提供一个app使用相机权限的原因。在工程的Info.plist,加入下图的设置:
能够从下方的回调中得到相机返回的实时数据,格式为CMSampleBuffer,该视频流格式不止包含图像信息还包含时间戳信息等,若想通过opencv进行处理还需进行数据转换。
extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate{
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
}
}
参考地址:https://www.appcoda.com.tw/avfoundation-camera-app/
OpenCV提供了UIImageToMat的函数,根据这个思路,我们应当将CMSampleBuffer转换为UIImage数据,CMSsampleBuffer不止包含ImageBuffer,通过API自带的CMSampleBufferGetImageBuffer(),可以得到与我们希望得到的图像数据更为接近的cvPixelBuffer。
总的来说,下方是CMSampleBuffer转换为UIImage的两种方式,第一种通过CIImage第二种通过CGImage,通过CIImage转换成的UIImage虽然能显示在UIImageVIew上,但是在转换成Mat格式的时候会报错,因此选用第二种通过CGImage的转换。最后调用opencv库的UIImageToMat函数便能得到Mat数据了。
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
}
这边选用的方案是UIImageView来显示原始图像,并且在UIImageView上添加一个蒙层图像来显示识别框。此处选用蒙层的原因是,图像处理每帧需要70ms的处理时间,若直接显示处理后的图片会有延迟丢帧的情况视觉效果较差,因此实时图像采用原始图像数据,而识别框丢帧并不影响视觉效果。
//回调原始图像
var videoCpatureCompletionBlock: ((UIImage) -> Void)?
//回调CMSsmapleBuffer图像
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
//addimageProcess为opencv图像处理过程,写在Objecj-C++文件中,本文后面记录
output = opencv_test.addimageProcess(output)
self.videoCaptureCompletionBlockMask?(output)
lockFlagBool = false
}
}
}else{
print("丢帧")
self.frameFlag = 0
}
}
}
为了最后能用于显示,还要转换为UImage,该部分很简单,直接调用OpenCV的库函数,当然如果想转换为CMSampleBuffer的话还需要重新添加丢失的数据,比如时间戳。
MatToUIImage()
参考地址:https://stackoverflow.com/questions/15726761/make-an-uiimage-from-a-cmsamplebuffer
UI界面的操作比较简单,实例化之前的CameraController类,并设定configureCameraController函数来调用类中的prepare函数,以及接受回调的图像数据,这些回调对UIImageView的图像刷新必须要在主线程中,否则会报错。其中,selfImageView和maskImageView是两个自己创建的UImageView来显示UIImage图像的,这两个UIImageView要保持在同样位置同样大小。
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
}
}
//直接显示CMSampleBuffer的方法
// self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in
//self.displayLayer.enqueue(CMSampleBuffer)
//}
}
}
其实苹果的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.y
self.displayLayer.frame.origin.x = self.imageView.frame.origin.x
}
func configureCameraController() {
cameraController.prepare {(error) in
if let error = error {
print(error)
}
//直接显示CMSampleBuffer的方法
self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in
self.displayLayer.enqueue(CMSampleBuffer)
}
}
}
这部分用C++编写过OpenCV的都相当熟悉了,在.mm文件中引入以下头文件,并引入命名空间,若该部分找不到文件应当确认是否已正确安装OpenCV库。
#import <opencv2/opencv.hpp>
#import "opencv-test.h"
#import <opencv2/imgcodecs/ios.h>
//对iOS支持
#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
本文使用了OpenCV自带的人脸识别框架CascadeClassifier,将得到的人脸坐标放入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;
//获取权重文件,文件需要提前导入至工程目录中
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);
//创建透明蒙层图像 Scalar(0,0,0,0) 分别是RGBA A为透明度
Mat Mask = Mat(height, width, CV_8UC4, Scalar(0,0,0,0));
// Draw all detected faces
for(unsigned int i = 0; i < faces.size(); i++)
{
const cv::Rect& face = faces[i];
// Get top-left and bottom-right corner points
cv::Point tl(face.x, face.y);
cv::Point br = tl + cv::Point(face.width, face.height);
// Draw rectangle around the face
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自带的人脸识别算法比较老旧,处理速度也比较慢效果也一般,要引入其他神经网络框架在客户端上的可行性有待讨论,处理速度也未知。
另外,若有需要总的工程文件的可以私聊我。