2.4 控制相机
iOS SDK和OpenCV都为摄像头的控制提供了一些接口.通常情况下,录制和播放音视频(audiovisual,AV)内容,都可以使用iOS SDK提供的AVFoundation框架.AVFoundation框架提供了对相机参数的完整设置,包括图片的格式,焦点,曝光,闪光,帧率以及数字焦距.然而AVFoundation框架没有任何的用户界面相关的内容.应用程序开发者可以自定义一个相机的界面,或者使用一个更加高级的提供GUI的库,或者让相机可以自动设置不需要界面操作.AVFoundation足以灵活来满足这些需求,但是代价是其复杂性.
[官方的AVFouncation编程指南位于 https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG]
iOS SDK在UIImagePickerController类中实现了标准相机GUI,该类构建在AVFoundation之上。该GUI使用户能够配置相机并捕获照片或视频。应用程序开发人员可以在捕获照片或视频之后处理它,但是定制控件和视频预览的选项是有限的。因此,UIImagePickerController不适合我们的LightWork应用程序,因为我们的应用程序将提供已处理图像的自定义预览。
[请在iOS相机编程主题集中参考UIImagePickerController的官方指南
https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/CameraAndPhotoLib_TopicsForIOS]
OpenCV提供了一个类CvVideoCamera,它实现了高级相机控制功能和预览GUI,并且支持高度定制。CvVideoCamera建立在AVFoundation之上,并提供了对一些底层类的访问。因此,应用程序开发人员可以选择使用高级CvVideoCamera功能和低级AVFoundation功能的组合。应用程序开发人员实现大多数GUI,并且可以不使用视频预览,或者指定一个父视图来让CvVideoCamera将呈现预览。此外,应用程序可以在捕获每个视频帧时对其进行处理,并且如果应用程序就地编辑捕获的帧,则CvVideoCamera将在预览中显示结果。因此,CvVideoCamera是LightWork的合适的起点。
[OpenCV也提供了一个CvPhotoCarmera的类,该类是用来捕获高质量的静态图片,而不是连续的视频数据流.不想CvVideoCamera,CvPhoto不允许我们在实时预览中进行自定义的图像处理]
2.4.1 子类化CvVideoCamera
CvVideoCamera是一个Objective-C类,Objective-C允许我们覆盖子类中的任何实例方法或属性。此外,由于OpenCV是开源的,我们可以研究CvVideoCamera的整个实现。因此,我们有能力和知识来创建一个子类,通过修改部分代码来重新实现CvVideoCamera。这是在不修改和重建库源代码的情况下调整或修补开源库的代码的一种方便的方法。
[你可以到OpenCV的GitHub仓库https://github.com/opencv/opencv/blob/master/modules/videoio/src/cap_ios_video_camera.mm 去浏览CvVideoCamera的最新版本的实现代码]
当前,在OpenCV3.1中,(作者使用的是OpenCV3.1),CvVideoCamera有以下几个特别要注意的bug:
- 根据所设置的图片质量,分辨率可能被默认设置为一个不正确的值.
- 根据设备的方向,预览和捕获的图片可能会不正确的旋转,预览会被拉伸至不正确的宽高比.
我们将创建一个名为VideoCamera的子类,以便修补这些问题并提供额外的功能。添加一个名为VideoCamera.h的新头文件。首先,我们生命这个子类的公开接口,包括一个新的属性和方法,如下面代码所示:
#import <opencv2/videoio/cap_ios.h>
@interface VideoCamera : CvVideoCamera
@property BOOL letterboxPreview;
- (void)setPointOfInterestInParentViewSpace:(CGPoint)point;
@end
当letterboxPreview属性被设为YES,VideoCamera将用信箱模式显示视频预览.否则,预览将和父类一样用剪切模式显示.
setPointOfInterestInParentViewSpace:
方法将为相机的自动对焦和自动曝光算法设置一个关注点。在简单地搜索最佳解决方案之后,相机会重新调整,让其焦距和亮度匹配该给定点的邻域,该邻域在预览的父视图中用像素坐标表示。换言之,调整之后,该点及其邻近区域会被聚焦,并且亮度约为50%的灰色。然而,好的自动曝光算法允许亮度根据颜色和场景的其他区域进行变化.
[自动聚焦和自动曝光算法没有在iOS SDK文档中给出,他们是特定于设备的.]
现在让我们创建类的实现文件,VideoCamera.m.我们将增加一个私有的接口,customPreviewLayer,如下面代码所示:
@interface VideoCamera ()
@property (nonatomic, retain) CALayer *customPreviewLayer;
@end
我们让customPreviewLayer访问在父类的私有接口中定义的实例变量_customPreviewLayer.该变量是视频预览的层,我们将在ViewController中自定义他的位置和大小.下面的代码是VideoCamera实现的开始,并且建立了属性和实例变量之间的关联.
@implementation VideoCamera
@synthesize customPreviewLayer = _customPreviewLayer;
为了自定义视频预览层的布局,需要重写CvVedioCamera的以下方法:
(int)imageWidth
和(int)imageHeight
:这两个getter方法,返回的是相机当前使用的水平和垂直分辨率.父类实现中存在着bug,因为它使用的是各种质量模式下的默认分辨率,而不是直接查询当前分辨率。(void)updateSize
:父类使用该方法来尝试调整分辨率.但是这实际上是无效的.(void)layoutPreviewLayer
:该方法用来根据当前设备的方向对预览层进行布局.父类实现中存在bug,预览层会被拉伸,或者某些情况下图片的方向会不正确.
为了得到正确的分辨率,我们通过AVFoundation的AVCaptureVideoDataOutput类来查询相机当前的捕获参数.根据以下代码重写imageWidth方法:
- (int)imageWidth {
AVCaptureVideoDataOutput *output = [self.captureSession.outputs lastObject];
NSDictionary *videoSettings = [output videoSettings];
int videoWidth = [[videoSettings objectForKey:@"Width"] intValue];
return videoWidth;
}
类似的,让我们重写imageHeight方法:
[如果相机没有运行,imageWidth和imageHeight方法将返回0.对于LightWork的应用场景,这样并不会产生任何问题]
现在,我们用恰当的方式解决了获取相机分辨率的问题.因此我们可以用空的实现来重写updateSize
方法:
- (void)updateSize {
//Do nothing.
}
在布局视频预览层时,我们先将他放在父视图的中心.然后,我们得到视频的宽高比,并根据这个宽高比得到一个合适的预览层的尺寸.如果letterboxPreview是“YES”,则预览层在一个维度中可能小于其父视图。否则,在一个维度上它可能比它的父视图大,在这种情况下,它的两端可能在屏幕外因此被裁剪。(类似于imageView的图片显示的两个模式:AspectFit和AspectFill)下面的代码演示了如何定位和设置预览层的大小:
- (void)layoutPreviewLayer {
if (self.parentView != nil) {
// Center the video preview.
self.customPreviewLayer.position = CGPointMake(0.5 * self.parentView.frame.size.width,0.5 * self.parentView.frame.size.height);
// Find the video's aspect ratio.
CGFloat videoAspectRatio = self.imageWidth / (CGFloat)self.imageHeight;
// Scale the video preview while maintaining its aspect ratio.
CGFloat boundsW;
CGFloat boundsH;
if (self.imageHeight > self.imageWidth){
if (self.letterboxPreview){
boundsH = self.parentView.frame.size.height;
boundsW = boundsH * videoAspectRatio;
}
else {
boundsW = self.parentView.frame.size.width;
boundsH = boundsW / videoAspectRatio;
}
}
else {
if (self.letterboxPreview) {
boundsW = self.parentView.frame.size.width;
boundsH = boundsW / videoAspectRatio;
}
else {
boundsH = self.parentView.frame.size.height;
boundsW = boundsH * videoAspectRatio;
}
} self.customPreviewLayer.bounds = CGRectMake( 0.0, 0.0, boundsW, boundsH);
}
}
现在,让我们考虑setPointOfInterestInParentViewSpace:
方法.他的实现包括几个方面,但概念相当简单.该方法接收一个在预览层的父视图坐标系中的一点的坐标作为参数.如果视图控制器是该函数的调用者的话,这是一个很方便的坐标系.AVFoundation让我们为聚焦和曝光指定一点,但是使用的是Landscape-right坐标系.这意味着左上角是(0.0,0.0),右下角是(1.0,1.0),并且轴方向基于横向-右方向,而不管设备的实际方向。Landscape-right方向意味着设备的Home按钮在用户的右手边。因此,X正方向指向home按钮,Y正方向指向远离音量按钮的方向。下面是我们的方法的实现,它检查相机的自动曝光和自动对焦能力,执行坐标转换,验证坐标,并通过AVFoundation功能设置关注点:
- (void)setPointOfInterestInParentViewSpace:(CGPoint)parentViewPoint {
if (!self.running) {
return;
}
// Find the current capture device.
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;
}
// Find the preview's offset relative to the parent view.
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);
// Find the focus coordinates, proportional to the preview size.
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) {
// The point is outside the preview.
return;
}
// Adjust the focus coordinates based on the orientation.
// They should be in the landscape-right coordinate system.
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: {
// Do nothing.
break;
}
default: {
// Portrait
CGFloat oldFocusX = focusX;
focusX = focusY;
focusY = 1.0 - oldFocusX;
break;
}
}
if (self.defaultAVCaptureDevicePosition == AVCaptureDevicePositionFront) {
// De-mirror the X coordinate.
focusX = 1.0 - focusX;
}
CGPoint focusPoint = CGPointMake(focusX, focusY);
if (canSetFocus) {
// Auto-focus on the selected point.
captureDevice.focusMode = AVCaptureFocusModeAutoFocus;
captureDevice.focusPointOfInterest = focusPoint; }
if (canSetExposure) {
// Auto-expose for the selected point.
captureDevice.exposureMode = AVCaptureExposureModeAutoExpose; captureDevice.exposurePointOfInterest = focusPoint;
}
[captureDevice unlockForConfiguration];
}
此时,我们已经实现了一个能够配置相机和捕获帧的类。但是,我们仍然需要实现另一个类来进行配置和接收帧数据。
2.4.2 在视图控制器中使用CvVideoCamera的子类
打开ViewController.m查看我们的ViewController的私有接口申明.我们类实现了CvVideoCameraDelegate协议,并且有一个VideoCamera属性.同时,还有一个当相机处于不活动状态时作为站位图的静态图像的拷贝.通常,我们在ViewController的viewDidLoad方法中进行初始化操作.首先,我们将从文件中加载静态图像,然后将他转化为合适的格式.然后,我们创建VideoCamera实例,并将imageView作为预览视图的父视图.我们控制相机将每一帧的数据传送到代理ViewController,使用高分辨率的模式以及30FPS的帧率,使用信箱模式预览.下面是viewDidLoad的实现:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[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;
}
[对于iPhone 6和6Plus,后置摄像头使用1280x720分辨率时最大的帧率为240FPS。参见苹果技术说明TN2409中的规范https://developer.apple.com/library/ios/technotes/tn2409/.高帧速率将使预览看起来更平滑和更有响应性,并有助于捕获快速移动的物体。然而,它也会给设备的处理器带来更大的负担,并更快地耗尽电池。]
我们还将覆盖另一个名为viewLayoutSubviews
的UIViewController方法。该方法在viewDidLoad
之后,以及在视图控制器完成了所有的布局(包括方向)之后运行。注意,每当方向改变时,都会再次调用该方法。这里,我们将配置相机的方向以匹配设备方向,如下面的代码所示:
- (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。我们将在本节之后实现refresh
方法。它将确保相机按最新的配置重新启动或者静态图像以最新配置进行了处理。
当用户点击了预览视图的父视图时,我们将找到该点击的坐标,并将其传递给setPointOfInterestInParentViewSpace:方法,我们之前在VideoCamera中实现了该方法。以下是tap事件的相关回调:
- (IBAction)onTapToSetPointOfInterest:(UITapGestureRecognizer *)tapGesture{
if (tapGesture.state == UIGestureRecognizerStateEnded) {
if (self.videoCamera.running) {
CGPoint tapPoint =[tapGesture locationInView:self.imageView];
[self.videoCamera setPointOfInterestInParentViewSpace:tapPoint];
}
}
}
当用户在分段控件中选择了Gray或者Color按钮之后,我们将设置VideoCamera的grayscaleMode属性设为YES或者NO.这个属性,继承自CvVideoCamera类.在设置grayscaleMode之后,我们将调用ViewController的refresh帮助方法来让相机以合适设置重启.下面是分段控件的状态改变的回调函数:
- (IBAction)onColorModeSelected:(UISegmentedControl *)segmentedControl{
switch (segmentedControl.selectedSegmentIndex) {
case 0:{
self.videoCamera.grayscaleMode = NO;
break;
}
default:{
self.videoCamera.grayscaleMode = YES;
break;
}
}
[self refresh];
}
当用户点击了切换相机的按钮,我们将激活下一个相机,或者循环回归到显示女士和园丁的静态图像.在每个转换期间,我们必须确保前一个相机已经停止或前一个静态图像被隐藏,并且启动下一个相机已经启动或下一个静态图像已经处理和显示。同样,我们的refresh
帮助方法是有用的。下面是该按钮的回调的实现:
- (IBAction)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 {
// Hide the still image.
self.imageView.image = nil;
self.videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionFront;
[self.videoCamera start];
}
}
refresh
帮助方法会检查相机是否在运行,如果是,我们将确保静态图像被隐藏,停止并重启相机.否则,我们将处理静态图像并显示结果.处理过程包括,以合适的颜色格式显示图像,并且传递给processImage:
方法.请记住,CvVideoCamera和我们的VideoCamera子类一样,也会将视频帧数据传递给CvVideoCameraDelegate中的processImage:
方法.下面,在refresh
方法中,我们将对静态图像使用同样的图像处理方法.refresh
的实现方法如下所示:
- (void)refresh {
if (self.videoCamera.running) {
// Hide the still image.
self.imageView.image = nil;
// Restart the video.
[self.videoCamera stop];
[self.videoCamera start];
}
else {
// Refresh the still image.
UIImage *image;
if (self.videoCamera.grayscaleMode) {
cv::cvtColor(originalStillMat, updatedStillMatGray,cv::COLOR_RGBA2GRAY);
[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;
}
}
processImage:
方法具有好几种责任.首先,他将修正库中的另一个bug.CvVideoCamera(在OpenCV3.1)在手机方向是landscape模式时,捕获到的图像却是upside down模式.这个bug在processImage:
中修改比在ViewCamera子类中修改要方便.在确保了图像的方向是正确的之后,我们将他传递给另一个方法processImageHelper:
,这个方法是我们实现图像处理主要功能的方便的地方.最后,如果用户最近点击了Save按钮,我们将把图片转换成合适的格式,并将他传递给savaImage:
帮助方法.下面是相关的代码:
- (void)processImage:(cv::Mat &)mat {
if (self.videoCamera.running) {
switch (self.videoCamera.defaultAVCaptureVideoOrientation) {
case AVCaptureVideoOrientationLandscapeLeft:
case AVCaptureVideoOrientationLandscapeRight: {
// The landscape video is captured upside-down.
// Rotate it by 180 degrees.
cv::flip(mat, mat, -1);
break;
}
default: {
break;
}
}
}
[self processImageHelper:mat];
if (self.saveNextFrame) {
// The video frame, 'mat', is not safe for long-running // operations such as saving to file. Thus, we copy its // data to another cv::Mat first.
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;
}
}
目前,我们除了颜色转换和旋转,还没有进行更的多图像处理.我们增加如下方法,在第3节图像混合中我们将在该方法中增加附加的图像处理:
- (void)processImageHelper:(cv::Mat &)mat {
// TODO: Implement in Chapter 3.
}
我们的ViewController类,仍然需要实现saveImage:
帮助方法,以及和保存和分享图片有关的GUI功能.然而,我们已经实现了相机控制功能,并提供了图像处理功能的骨架,该骨架以一些必要的颜色转换开始和结束.