iOS下使用OpenCV来控制相机
本文翻译自iOS Application Development with OpenCV 3
iOS SDK和OpenCV提供了几个用于摄像头控制的编程接口。 在iOS SDK中,AVFoundation是用于视听(AV)内容的所有录制和回放的通用框架。 AVFoundation提供对iOS相机参数的完全访问,包括图像格式,焦距,曝光,闪光,帧速率和数字变焦(裁剪因子)。 但是,AVFoundation无法解决任何GUI问题。 应用程序开发人员可以创建自定义相机GUI,使用提供GUI的更高级别框架,或自动化相机,使其在没有GUI输入的情况下运行。 AVFoundation足够灵活,可以支持任何这些设计,但由于AVFoundation很复杂,这种灵活性是有代价的。
iOS SDK在UIImagePickerController类中实现了标准的相机GUI,该类在AVFoundation上构建。 该GUI使用户能够配置相机并捕获照片或视频。 应用程序开发人员可以在捕获后处理照片或视频,但可以选择自定义控件和视频预览有限。
OpenCV提供了一个CvVideoCamera类,它实现了高级摄像机控制功能和预览GUI,但支持高度自定义。 CvVideoCamera在AVFoundation之上构建,并提供对某些底层类的访问。 因此,应用程序开发人员可以选择使用高级CvVideoCamera功能和较低级别AVFoundation功能的组合。 应用程序开发人员实现了大部分GUI,可能会禁用视频预览或指定其中CvVideoCamera将呈现它的父视图。 此外,应用程序可以在捕获时处理每个视频帧,并且如果应用程序就地编辑捕获的帧,则CvVideoCamera将在预览中显示结果。
子类化CvVideoCamera
CvVideoCamera是一个Objective-C类,Objective-C允许我们覆盖子类中的任何实例方法或属性。 而且,由于OpenCV是开源的,我们可以研究CvVideoCamera的整个实现。 因此,我们有能力和知识来创建一个子类,通过修改重新实现CvVideoCamera的各个部分。 这是一种调整或修补开源类实现的便捷方式,无需修改和重建库的源代码。
我们将创建一个名为VideoCamera的子类。 添加一个名为VideoCamera.h的新头文件。 在这里,我们将声明子类的公共接口,包括一个新的属性和方法,如下面的代码所示:
#import <opencv2/videoio/cap_ios.h>
@interface VideoCamera : CvVideoCamera
@property BOOL letterboxPreview;
- (void)setPointOfInterestInParentViewSpace:(CGPoint)point;
@end
setPointOfInterestInParentViewSpace:方法将为相机的自动对焦和自动曝光算法设置一个感兴趣的点。
现在,让我们创建类的实现文件VideoCamera.m。 我们将添加一个带有属性customPreviewLayer的私有接口,如以下代码所示:
#import "VideoCamera.h"
@interface VideoCamera ()
@property (nonatomic, retain) CALayer *customPreviewLayer;
@end
我们将实现customPreviewLayer,以便它访问一个变量_customPreviewLayer,它在父类的私有接口中定义。此变量是视频预览图层,我们将在VideoCamera中自定义其位置和大小。 以下是开始实现VideoCamera并设置属性和变量之间关系的代码:
@implementation VideoCamera
@synthesize customPreviewLayer = _customPreviewLayer;
接下来,让我们考虑设置PointOfInterestInParentViewSpace:方法.这个方法实现将涉及几个案例,但概念相当简单。作为参数,我们接受预览的父视图的坐标系中的一个点。如果我们假设调用者是视图控制器,那么这是一个方便的坐标系。 AVFoundation允许我们指定焦点和曝光的兴趣点,但它使用比例横向坐标系。这意味着左上角为(0.0,0.0),右下角为(1.0,1.0),轴方向基于横向右方向,与设备的实际方向无关。横向右方向意味着设备的主页按钮位于用户的右侧。因此,正X指向主页按钮,正Y指向远离音量按钮。这是我们方法的实现,它检查相机的自动曝光和自动对焦功能,执行坐标转换,验证坐标,并通过AVFoundation功能设置感兴趣的点:
- (void)setPointOfInterestInParentViewSpace:(CGPoint)parentViewPoint {
if (!self.running) {
return;
}
NSArray *captureDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *captureDevice;
for (captureDevice in captureDevices) {
if (captureDevice.position == self.defaultAVCaptureDevicePosition) {
break;
}
}
BOOL canSetFocus = [captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus] && captureDevice.isFocusPointOfInterestSupported;
BOOL canSetExposure = [captureDevice isExposureModeSupported:AVCaptureExposureModeAutoExpose] && captureDevice.isExposurePointOfInterestSupported;
if (!canSetFocus && !canSetExposure) {
return;
}
if (![captureDevice lockForConfiguration:nil]) {
return;
}
CGFloat offsetX = 0.5 * (self.parentView.bounds.size.width - self.customPreviewLayer.bounds.size.width);
CGFloat offsetY = 0.5 * (self.parentView.bounds.size.height - self.customPreviewLayer.bounds.size.height);
CGFloat focusX = (parentViewPoint.x - offsetX) / self.customPreviewLayer.bounds.size.width;
CGFloat focusY = (parentViewPoint.y - offsetY) / self.customPreviewLayer.bounds.size.height;
if (focusX < 0.0 || focusX > 1.0 || focusY < 0.0 || focusY > 1.0) {
return;
}
switch (self.defaultAVCaptureVideoOrientation) {
case AVCaptureVideoOrientationPortraitUpsideDown:{
CGFloat oldFocusX = focusX;
focusX = 1.0 - focusY;
focusY = oldFocusX;
break;
}
case AVCaptureVideoOrientationLandscapeLeft:{
focusX = 1.0 - focusX;
focusY = 1.0 - focusY;
break;
}
case AVCaptureVideoOrientationLandscapeRight:{
break;
}
default:{
CGFloat oldFocuX = focusX;
focusX = focusY;
focusY = 1.0 - oldFocuX;
break;
}
}
if (self.defaultAVCaptureDevicePosition == AVCaptureDevicePositionFront) {
focusX = 1.0 - focusX;
}
CGPoint focusPoint = CGPointMake(focusX, focusY);
if (canSetFocus) {
captureDevice.focusMode = AVCaptureFocusModeAutoFocus;
captureDevice.focusPointOfInterest = focusPoint;
}
if (canSetExposure) {
captureDevice.exposureMode = AVCaptureExposureModeAutoExpose;
captureDevice.exposurePointOfInterest = focusPoint;
}
[captureDevice unlockForConfiguration];
}
此时,我们已经实现了一个类,该类能够配置摄像机并捕获帧。但是,我们仍然需要实现另一个类来选择一个配置并接收帧。
在视图控制器中使用CvVideoCamera子类
以下是viewDidLoad的实现:
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *originalStillImage = [UIImage imageNamed:@"Fleur.jpg"];
UIImageToMat(originalStillImage, originalStillMat);
self.videoCamera = [[VideoCamera alloc] initWithParentView:self.imageView];
self.videoCamera.delegate = self;
self.videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPresetHigh;
self.videoCamera.defaultFPS = 30;
self.videoCamera.letterboxPreview = YES;
}
我们还将覆盖另一个名为viewDidLayoutSubviews的UIViewController方法。
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
switch ([UIDevice currentDevice].orientation) {
case UIDeviceOrientationPortraitUpsideDown:
self.videoCamera.defaultAVCaptureVideoOrientation =
AVCaptureVideoOrientationPortraitUpsideDown;
break;
case UIDeviceOrientationLandscapeLeft:
self.videoCamera.defaultAVCaptureVideoOrientation =
AVCaptureVideoOrientationLandscapeLeft;
break;
case UIDeviceOrientationLandscapeRight:
self.videoCamera.defaultAVCaptureVideoOrientation =
AVCaptureVideoOrientationLandscapeRight;
break;
default:
self.videoCamera.defaultAVCaptureVideoOrientation =
AVCaptureVideoOrientationPortrait;
break;
}
[self refresh];
}
请注意,我们在重新配置相机后调用辅助方法refresh。 它将确保重新启动摄像机或重新处理静态图像以反映最新配置。
当用户点击预览的父视图时,我们将找到坐标点击并将它们传递给我们之前在VideoCamera中实现的setPointOfInterestInParentViewSpace:方法。 以下是点击事件的相关回调:
- (void)onTapToSetPointOfInterest:(UITapGestureRecognizer *)tapGesture {
if (tapGesture.state == UIGestureRecognizerStateEnded) {
if (self.videoCamera.running) {
CGPoint tapPoint = [tapGesture locationInView:self.imageView];
[self.videoCamera setPointOfInterestInParentViewSpace:tapPoint];
}
}
}
切换灰度图和彩色图
- (IBAction)onColorModeSelected:
(UISegmentedControl *)segmentedControl {
switch (segmentedControl.selectedSegmentIndex) {
case 0:
self.videoCamera.grayscaleMode = NO;
break;
default:
self.videoCamera.grayscaleMode = YES;
break;
}
[self refresh];
}
切换摄像头
- (void)onSwitchCameraButtonPressed {
if (self.videoCamera.running) {
switch (self.videoCamera.defaultAVCaptureDevicePosition) {
case AVCaptureDevicePositionFront:
self.videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionBack;
[self refresh];
break;
default:
[self.videoCamera stop];
[self refresh];
break;
}
}else {
self.imageView.image = nil;
self.videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionFront;
[self.videoCamera start];
}
}
- (void)refresh {
if (self.videoCamera.running) {
self.imageView.image = nil;
[self.videoCamera stop];
[self.videoCamera start];
} else {
UIImage *image;
if (self.videoCamera.grayscaleMode) {
cv::cvtColor(originalStillMat, updatedStillMatGray, cv::COLOR_RGB2GRAY);
[self processImage:updatedStillMatGray];
image = MatToUIImage(updatedStillMatGray);
} else {
cv::cvtColor(originalStillMat, updatedStillMatRGBA, cv::COLOR_RGBA2BGRA);
[self processImage:updatedStillMatRGBA];
cv::cvtColor(updatedStillMatRGBA, updatedStillMatRGBA, cv::COLOR_BGRA2RGBA);
image = MatToUIImage(updatedStillMatRGBA);
}
self.imageView.image = image;
}
}
- (void)processImage:(cv::Mat &)mat {
if (self.videoCamera.running) {
switch (self.videoCamera.defaultAVCaptureVideoOrientation) {
case AVCaptureVideoOrientationLandscapeLeft:
case AVCaptureVideoOrientationLandscapeRight:
cv::flip(mat, mat, -1);
break;
default:
break;
}
}
[self processImageHelper:mat];
if (self.saveNextFrame) {
UIImage *image;
if (self.videoCamera.grayscaleMode) {
mat.copyTo(updatedVideoMatGray);
image = MatToUIImage(updatedVideoMatGray);
} else {
cv::cvtColor(mat, updatedVideoMatRGBA, cv::COLOR_BGRA2RGBA);
image = MatToUIImage(updatedVideoMatRGBA);
}
[self saveImage:image];
self.saveNextFrame = NO;
}
}