Xamarin.iOS 中的手动相机控件
借助 iOS 8 中的 AVFoundation Framework
提供的手动相机控件,移动应用程序可以完全控制 iOS 设备的相机。 这种精细的控制级别可用于创建专业级别的相机应用程序,并通过在拍摄静止图像或视频时调整相机的参数来提供艺术家构图。
在开发科学或工业应用程序时,这些控件也很有用;在这些应用程序中,结果不太关注图像的正确性或美观,而更倾向于突出显示正在拍摄的图像的某些特征或元素。
AVFoundation 捕获对象
无论是用 iOS 设备上的相机拍摄视频还是静止图像,用于捕获这些图像的过程基本上都是相同的。 对于使用默认自动相机控件或使用新的手动相机控件的应用程序来说也是如此:
输入内容通过 AVCaptureConnection
从 AVCaptureDeviceInput
获取到 AVCaptureSession
中。 这样,输出要么是静止图像,要么是视频流。 整个过程均由 AVCaptureDevice
控制。
提供的手动控件
使用 iOS 8 提供的新 API,应用程序可控制以下相机功能:
- 手动对焦 - 通过允许最终用户直接控制对焦,应用程序可以更好地控制拍摄的图像。
- 手动曝光 - 通过提供对曝光的手动控制,应用程序可以为用户提供更多自由,并允许他们实现风格化的外观。
- 手动白平衡 - 白平衡用于调整图像中的颜色,通常是使其看起来逼真。 不同的光源有不同的色温,会调整用于捕获图像的相机设置来补偿这些差异。 同样,通过允许用户控制白平衡,用户可以进行无法自动执行的调整。
iOS 8 为现有 iOS API 提供扩展和增强功能,以提供对图像捕获过程的这种精细控制。
要求
要完成本文所述的步骤,需要满足以下条件:
- Xcode 7+ 和 iOS 8 或更高版本 - 需要在开发人员的计算机上安装和配置 Apple 的 Xcode 7 和 iOS 8 或更高版本的 API。
- Visual Studio for Mac - 应在用户设备上安装和配置最新版本的 Visual Studio for Mac。
- iOS 8 设备 - 运行最新版本的 iOS 8 设备。 无法在 iOS 模拟器中测试相机功能。
常规 AV 捕获设置
在 iOS 设备上录制视频时,始终需要一些常规设置代码。 本部分将介绍使用 iOS 设备的相机录制视频并在 UIImageView
中实时显示该视频所需的最小设置。
输出示例缓冲区委托
首先需要做的事情之一是使用委托来监视示例输出缓冲区,并在应用程序 UI 中将从缓冲区抓取的图像显示为 UIImageView
。
以下例程将监视示例缓冲区并更新 UI:
using System;
using Foundation;
using UIKit;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using AVFoundation;
using CoreVideo;
using CoreMedia;
using CoreGraphics;
namespace ManualCameraControls
{
public class OutputRecorder : AVCaptureVideoDataOutputSampleBufferDelegate
{
#region Computed Properties
public UIImageView DisplayView { get; set; }
#endregion
#region Constructors
public OutputRecorder ()
{
}
#endregion
#region Private Methods
private UIImage GetImageFromSampleBuffer(CMSampleBuffer sampleBuffer) {
// Get a pixel buffer from the sample buffer
using (var pixelBuffer = sampleBuffer.GetImageBuffer () as CVPixelBuffer) {
// Lock the base address
pixelBuffer.Lock (0);
// Prepare to decode buffer
var flags = CGBitmapFlags.PremultipliedFirst | CGBitmapFlags.ByteOrder32Little;
// Decode buffer - Create a new colorspace
using (var cs = CGColorSpace.CreateDeviceRGB ()) {
// Create new context from buffer
using (var context = new CGBitmapContext (pixelBuffer.BaseAddress,
pixelBuffer.Width,
pixelBuffer.Height,
8,
pixelBuffer.BytesPerRow,
cs,
(CGImageAlphaInfo)flags)) {
// Get the image from the context
using (var cgImage = context.ToImage ()) {
// Unlock and return image
pixelBuffer.Unlock (0);
return UIImage.FromImage (cgImage);
}
}
}
}
}
#endregion
#region Override Methods
public override void DidOutputSampleBuffer (AVCaptureOutput captureOutput, CMSampleBuffer sampleBuffer, AVCaptureConnection connection)
{
// Trap all errors
try {
// Grab an image from the buffer
var image = GetImageFromSampleBuffer(sampleBuffer);
// Display the image
if (DisplayView !=null) {
DisplayView.BeginInvokeOnMainThread(() => {
// Set the image
if (DisplayView.Image != null) DisplayView.Image.Dispose();
DisplayView.Image = image;
// Rotate image to the correct display orientation
DisplayView.Transform = CGAffineTransform.MakeRotation((float)Math.PI/2);
});
}
// IMPORTANT: You must release the buffer because AVFoundation has a fixed number
// of buffers and will stop delivering frames if it runs out.
sampleBuffer.Dispose();
}
catch(Exception e) {
// Report error
Console.WriteLine ("Error sampling buffer: {0}", e.Message);
}
}
#endregion
}
}
有了这个例程,AppDelegate
可修改为打开 AV 捕获会话来录制实时视频源。
创建 AV 捕获会话
AV 捕获会话用于控制使用 iOS 设备的相机进行的实时视频录制,并且需要它来将视频输入 iOS 应用程序。 由于 ManualCameraControl
示例应用程序在几个不同的地方使用捕获会话,因此将在 AppDelegate
中配置它,并使其对整个应用程序可用。
执行以下操作来修改应用程序的 AppDelegate
并添加所需的代码:
在解决方案资源管理器中双击
AppDelegate.cs
文件,将其打开进行编辑。将下列 using 语句添加到 文件的顶部:
using System; using Foundation; using UIKit; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using AVFoundation; using CoreVideo; using CoreMedia; using CoreGraphics; using CoreFoundation;
将以下专用变量和计算属性添加到
AppDelegate
类:#region Private Variables private NSError Error; #endregion #region Computed Properties public override UIWindow Window {get;set;} public bool CameraAvailable { get; set; } public AVCaptureSession Session { get; set; } public AVCaptureDevice CaptureDevice { get; set; } public OutputRecorder Recorder { get; set; } public DispatchQueue Queue { get; set; } public AVCaptureDeviceInput Input { get; set; } #endregion
重写已完成的方法并将其更改为:
public override void FinishedLaunching (UIApplication application) { // Create a new capture session Session = new AVCaptureSession (); Session.SessionPreset = AVCaptureSession.PresetMedium; // Create a device input CaptureDevice = AVCaptureDevice.DefaultDeviceWithMediaType (AVMediaType.Video); if (CaptureDevice == null) { // Video capture not supported, abort Console.WriteLine ("Video recording not supported on this device"); CameraAvailable = false; return; } // Prepare device for configuration CaptureDevice.LockForConfiguration (out Error); if (Error != null) { // There has been an issue, abort Console.WriteLine ("Error: {0}", Error.LocalizedDescription); CaptureDevice.UnlockForConfiguration (); return; } // Configure stream for 15 frames per second (fps) CaptureDevice.ActiveVideoMinFrameDuration = new CMTime (1, 15); // Unlock configuration CaptureDevice.UnlockForConfiguration (); // Get input from capture device Input = AVCaptureDeviceInput.FromDevice (CaptureDevice); if (Input == null) { // Error, report and abort Console.WriteLine ("Unable to gain input from capture device."); CameraAvailable = false; return; } // Attach input to session Session.AddInput (Input); // Create a new output var output = new AVCaptureVideoDataOutput (); var settings = new AVVideoSettingsUncompressed (); settings.PixelFormatType = CVPixelFormatType.CV32BGRA; output.WeakVideoSettings = settings.Dictionary; // Configure and attach to the output to the session Queue = new DispatchQueue ("ManCamQueue"); Recorder = new OutputRecorder (); output.SetSampleBufferDelegate (Recorder, Queue); Session.AddOutput (output); // Let tabs know that a camera is available CameraAvailable = true; }
保存对文件所做的更改。
有了此代码,可以轻松实现手动相机控件来进行试验和测试。
手动对焦
通过允许最终用户直接控制对焦,应用程序可以对所拍摄的图像提供更多的艺术控制。
例如,专业摄影师可柔化图像的焦点来实现背景虚化效果。 或者,创建焦点拉动效果。
对于科学家或医学应用程序的编写者,应用程序可能希望以编程方式移动镜头进行实验。 无论哪种方式,新的 API 都允许最终用户或应用程序在拍摄图像时控制对焦。
对焦的工作原理
在讨论 iOS 8 应用程序中控制对焦的细节之前, 让我们快速了解对焦在 iOS 设备中的工作原理:
线进入 iOS 设备的相机镜头,聚焦在图像传感器上。 镜头与传感器之间的距离控制焦点(图像最清晰的区域)与传感器的关系。 镜头离传感器越远,远处的对象看起来越清晰,而镜头离传感器越近,近处的对象看起来越清晰。
在 iOS 设备中,镜头通过磁铁和弹簧靠近或远离传感器。 因此,镜头的精确定位是不可能的,因为它因设备而异,并且可能受到设备方向或设备和弹簧的使用年限等参数影响。
重要对焦术语
处理对焦时,开发人员应熟悉下面几个术语:
- 景深 - 最近和最远对焦对象之间的距离。
- 镜头 - 这是焦距光谱的近端,是镜头可对焦的最近距离。
- 无穷远 - 这是焦距光谱的远端,是镜头可对焦的最远距离。
- 超焦距离 - 这是焦距光谱中对焦框中最远的对象正好位于焦距远端的点。 换句话说,这是最大化景深的焦点位置。
- 镜头位置 - 它控制着上面所有内容。 这是镜头到传感器的距离,因此是对焦控制器。
有了这些术语和知识,可以在 iOS 8 应用程序中成功实现新的手动对焦控件。
现有对焦控件
iOS 7 及更低版本通过 FocusMode
属性提供现有的对焦控件,如下所示:
AVCaptureFocusModeLocked
- 焦点锁定在单个焦点处。AVCaptureFocusModeAutoFocus
- 相机将镜头扫过所有焦点,直到它找到清晰的焦点,然后停留在那里。AVCaptureFocusModeContinuousAutoFocus
- 每当相机检测到失焦情况时,就会重新对焦。
现有控件还通过 FocusPointOfInterest
属性提供了一个可设置的兴趣点,这样用户可点击来对焦到特定区域。 应用程序还可以通过监视 IsAdjustingFocus
属性来跟踪镜头的运动。
此外,AutoFocusRangeRestriction
属性提供的范围限制如下:
AVCaptureAutoFocusRangeRestrictionNear
- 将自动对焦限制到近处的深度。 在扫描 QR 码或条形码等情况下非常有用。AVCaptureAutoFocusRangeRestrictionFar
- 将自动对焦限制到远处的深度。 在已知不相关的对象位于视野范围(例如窗框)的情况下非常有用。
最后,有一个 SmoothAutoFocus
属性,它会减慢自动对焦算法的速度,并以较小的增量进行步进,避免在录制视频时出现移动伪影。
iOS 8 中新的对焦控件
除了 iOS 7 及更高版本已提供的功能外,iOS 8 现在还提供以下功能来控制对焦:
- 锁定焦点时,完全手动控制镜头位置。
- 在任何对焦模式下对镜头位置的键值观察。
为了实现上述功能,AVCaptureDevice
类已被修改为包含一个只读 LensPosition
属性,该属性用于获取相机镜头的当前位置。
若要手动控制镜头位置,捕获设备必须处于锁定对焦模式。 示例:
CaptureDevice.FocusMode = AVCaptureFocusMode.Locked;
捕获设备的 SetFocusModeLocked
方法用于调整相机镜头的位置。 可以提供一个可选的回调例程,以在更改生效时获取通知。 示例:
ThisApp.CaptureDevice.LockForConfiguration(out Error);
ThisApp.CaptureDevice.SetFocusModeLocked(Position.Value,null);
ThisApp.CaptureDevice.UnlockForConfiguration();
如上面的代码所示,必须锁定捕获设备来进行配置,然后才能更改镜头位置。 有效的镜头位置值在 0.0 和 1.0 之间。
手动对焦示例
准备好常规 AV 捕获设置代码后,可将 UIViewController
添加到应用程序的情节提要中,并按如下所示进行配置:
视图包含以下主要元素:
- 一个
UIImageView
,它将显示视频源。 - 一个
UISegmentedControl
,它将对焦模式从“自动”更改为“锁定”。 - 一个
UISlider
,它将显示并更新当前镜头位置。
执行以下操作来连接手动对焦控制的视图控制器:
添加以下 using 语句:
using System; using Foundation; using UIKit; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using AVFoundation; using CoreVideo; using CoreMedia; using CoreGraphics; using CoreFoundation; using System.Timers;
添加以下专用变量:
#region Private Variables private NSError Error; private bool Automatic = true; #endregion
添加以下计算属性:
#region Computed Properties public AppDelegate ThisApp { get { return (AppDelegate)UIApplication.SharedApplication.Delegate; } } public Timer SampleTimer { get; set; } #endregion
重写
ViewDidLoad
方法并添加以下代码:public override void ViewDidLoad () { base.ViewDidLoad (); // Hide no camera label NoCamera.Hidden = ThisApp.CameraAvailable; // Attach to camera view ThisApp.Recorder.DisplayView = CameraView; // Create a timer to monitor and update the UI SampleTimer = new Timer (5000); SampleTimer.Elapsed += (sender, e) => { // Update position slider Position.BeginInvokeOnMainThread(() =>{ Position.Value = ThisApp.Input.Device.LensPosition; }); }; // Watch for value changes Segments.ValueChanged += (object sender, EventArgs e) => { // Lock device for change ThisApp.CaptureDevice.LockForConfiguration(out Error); // Take action based on the segment selected switch(Segments.SelectedSegment) { case 0: // Activate auto focus and start monitoring position Position.Enabled = false; ThisApp.CaptureDevice.FocusMode = AVCaptureFocusMode.ContinuousAutoFocus; SampleTimer.Start(); Automatic = true; break; case 1: // Stop auto focus and allow the user to control the camera SampleTimer.Stop(); ThisApp.CaptureDevice.FocusMode = AVCaptureFocusMode.Locked; Automatic = false; Position.Enabled = true; break; } // Unlock device ThisApp.CaptureDevice.UnlockForConfiguration(); }; // Monitor position changes Position.ValueChanged += (object sender, EventArgs e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Update Focus position ThisApp.CaptureDevice.LockForConfiguration(out Error); ThisApp.CaptureDevice.SetFocusModeLocked(Position.Value,null); ThisApp.CaptureDevice.UnlockForConfiguration(); }; }
重写
ViewDidAppear
方法并添加以下内容,以在视图加载时开始录制:public override void ViewDidAppear (bool animated) { base.ViewDidAppear (animated); // Start udating the display if (ThisApp.CameraAvailable) { // Remap to this camera view ThisApp.Recorder.DisplayView = CameraView; ThisApp.Session.StartRunning (); SampleTimer.Start (); } }
当相机处于自动模式时,随着相机调整焦点,滑块将自动移动:
点击“锁定”段并拖动位置滑块可手动调整镜头位置:
停止应用程序。
上面的代码演示了当相机处于自动模式时如何监视镜头位置,或者在相机处于锁定模式时如何使用滑块控制镜头位置。
手动曝光
曝光是指图像相对于光源亮度的亮度,它取决于有多少光照射到传感器上、持续多长时间,以及传感器的增益水平(ISO 映射)。 通过提供对曝光的手动控制,应用程序可以为最终用户提供更多自由,并允许他们实现风格化的外观。
使用手动曝光控件,从不真实的亮度到黑暗阴郁的环境,用户都可以拍摄图像:
同样,可以使用科学应用程序的编程控件或通过应用程序用户界面提供的手动控件自动完成此操作。 无论哪种方式,新的 iOS 8 曝光 API 都提供对相机曝光设置的精细控制。
曝光的工作原理
在讨论 iOS 8 应用程序中控制曝光的细节之前, 让我们快速了解曝光的工作原理:
共同控制曝光的三个基本元素包括:
- 快门速度 - 这是快门打开来让光线进入相机传感器的时间长度。 快门打开的时间越短,进入的光线越少,图像就越清晰(运动模糊越少)。 快门打开的时间越长,光进入的光线就越多,发生的运动模糊就越多。
- ISO 映射 - 这是从胶片摄影借用的术语,是指胶片中的化学物质对光的敏感度。 如果胶片中的 ISO 值较低,则具有更少的颗粒和更精细的色彩再现;如果数字传感器上的 ISO 值较低,传感器噪声更少,但亮度更低。 ISO 值越高,图像越亮,但传感器噪声越多。 数字传感器上的“ISO”是电子增益的度量值,而不是物理特征。
- 镜头光圈 - 这是镜头开口的大小。 在所有 iOS 设备上,镜头光圈都是固定的,因此只有两个值可用来调整曝光:快门速度和 ISO。
连续自动曝光的工作原理
在了解手动曝光的工作原理之前,最好先了解一下 iOS 设备上的连续自动曝光的工作原理。
首先是自动曝光块,它的工作是计算理想的曝光,并不断被馈送测光统计信息。它使用这些信息来计算 ISO 和快门速度的最佳混合,以获得良好的照明场景。 此循环称为 AE 循环。
锁定曝光的工作原理
接下来,让我们看看锁定曝光在 iOS 设备上的工作原理。
同样,你有自动曝光块,它尝试计算最佳的 iOS 和持续时间值。 但是,在此模式下,AE 块与测光统计信息引擎断开连接。
现有曝光控件
iOS 7 及更高版本,通过 ExposureMode
属性提供以下现有曝光控件:
AVCaptureExposureModeLocked
- 对场景采样一次,并在整个场景中使用这些值。AVCaptureExposureModeContinuousAutoExposure
- 持续对场景进行采样,以确保其光线充足。
ExposurePointOfInterest
可用来点击,通过选择要曝光的目标对象来曝光场景,应用程序可以监视 AdjustingExposure
属性,以查看何时调整曝光。
iOS 8 中新的曝光控件
除了 iOS 7 及更高版本已提供的功能外,iOS 8 现在还提供以下功能来控制曝光:
- 完全手动自定义曝光。
- 获取、设置和键值观察 IOS 和快门速度(持续时间)。
为了实现上述功能,添加了一个新的 AVCaptureExposureModeCustom
模式。 当相机处于自定义模式时,可使用以下代码来调整曝光持续时间和 ISO:
CaptureDevice.LockForConfiguration(out Error);
CaptureDevice.LockExposure(DurationValue,ISOValue,null);
CaptureDevice.UnlockForConfiguration();
在自动和锁定模式下,应用程序可使用以下代码调整自动曝光例程的偏差:
CaptureDevice.LockForConfiguration(out Error);
CaptureDevice.SetExposureTargetBias(Value,null);
CaptureDevice.UnlockForConfiguration();
最小和最大设置范围取决于应用程序正在运行的设备,因此不应硬编码它们。 请改用以下属性来获取最小值和最大值范围:
CaptureDevice.MinExposureTargetBias
CaptureDevice.MaxExposureTargetBias
CaptureDevice.ActiveFormat.MinISO
CaptureDevice.ActiveFormat.MaxISO
CaptureDevice.ActiveFormat.MinExposureDuration
CaptureDevice.ActiveFormat.MaxExposureDuration
如上面的代码所示,必须锁定捕获设备来进行配置,然后才能更改曝光。
手动曝光示例
准备好常规 AV 捕获设置代码后,可将 UIViewController
添加到应用程序的情节提要中,并按如下所示进行配置:
视图包含以下主要元素:
- 一个
UIImageView
,它将显示视频源。 - 一个
UISegmentedControl
,它将对焦模式从“自动”更改为“锁定”。 - 4 个
UISlider
控件,它们显示和更新偏移量、持续时间、ISO 和偏差。
执行以下操作来连接手动对焦控制的曝光控制器:
添加以下 using 语句:
using System; using Foundation; using UIKit; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using AVFoundation; using CoreVideo; using CoreMedia; using CoreGraphics; using CoreFoundation; using System.Timers;
添加以下专用变量:
#region Private Variables private NSError Error; private bool Automatic = true; private nfloat ExposureDurationPower = 5; private nfloat ExposureMinimumDuration = 1.0f/1000.0f; #endregion
添加以下计算属性:
#region Computed Properties public AppDelegate ThisApp { get { return (AppDelegate)UIApplication.SharedApplication.Delegate; } } public Timer SampleTimer { get; set; } #endregion
重写
ViewDidLoad
方法并添加以下代码:public override void ViewDidLoad () { base.ViewDidLoad (); // Hide no camera label NoCamera.Hidden = ThisApp.CameraAvailable; // Attach to camera view ThisApp.Recorder.DisplayView = CameraView; // Set min and max values Offset.MinValue = ThisApp.CaptureDevice.MinExposureTargetBias; Offset.MaxValue = ThisApp.CaptureDevice.MaxExposureTargetBias; Duration.MinValue = 0.0f; Duration.MaxValue = 1.0f; ISO.MinValue = ThisApp.CaptureDevice.ActiveFormat.MinISO; ISO.MaxValue = ThisApp.CaptureDevice.ActiveFormat.MaxISO; Bias.MinValue = ThisApp.CaptureDevice.MinExposureTargetBias; Bias.MaxValue = ThisApp.CaptureDevice.MaxExposureTargetBias; // Create a timer to monitor and update the UI SampleTimer = new Timer (5000); SampleTimer.Elapsed += (sender, e) => { // Update position slider Offset.BeginInvokeOnMainThread(() =>{ Offset.Value = ThisApp.Input.Device.ExposureTargetOffset; }); Duration.BeginInvokeOnMainThread(() =>{ var newDurationSeconds = CMTimeGetSeconds(ThisApp.Input.Device.ExposureDuration); var minDurationSeconds = Math.Max(CMTimeGetSeconds(ThisApp.CaptureDevice.ActiveFormat.MinExposureDuration), ExposureMinimumDuration); var maxDurationSeconds = CMTimeGetSeconds(ThisApp.CaptureDevice.ActiveFormat.MaxExposureDuration); var p = (newDurationSeconds - minDurationSeconds) / (maxDurationSeconds - minDurationSeconds); Duration.Value = (float)Math.Pow(p, 1.0f/ExposureDurationPower); }); ISO.BeginInvokeOnMainThread(() => { ISO.Value = ThisApp.Input.Device.ISO; }); Bias.BeginInvokeOnMainThread(() => { Bias.Value = ThisApp.Input.Device.ExposureTargetBias; }); }; // Watch for value changes Segments.ValueChanged += (object sender, EventArgs e) => { // Lock device for change ThisApp.CaptureDevice.LockForConfiguration(out Error); // Take action based on the segment selected switch(Segments.SelectedSegment) { case 0: // Activate auto exposure and start monitoring position Duration.Enabled = false; ISO.Enabled = false; ThisApp.CaptureDevice.ExposureMode = AVCaptureExposureMode.ContinuousAutoExposure; SampleTimer.Start(); Automatic = true; break; case 1: // Lock exposure and allow the user to control the camera SampleTimer.Stop(); ThisApp.CaptureDevice.ExposureMode = AVCaptureExposureMode.Locked; Automatic = false; Duration.Enabled = false; ISO.Enabled = false; break; case 2: // Custom exposure and allow the user to control the camera SampleTimer.Stop(); ThisApp.CaptureDevice.ExposureMode = AVCaptureExposureMode.Custom; Automatic = false; Duration.Enabled = true; ISO.Enabled = true; break; } // Unlock device ThisApp.CaptureDevice.UnlockForConfiguration(); }; // Monitor position changes Duration.ValueChanged += (object sender, EventArgs e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Calculate value var p = Math.Pow(Duration.Value,ExposureDurationPower); var minDurationSeconds = Math.Max(CMTimeGetSeconds(ThisApp.CaptureDevice.ActiveFormat.MinExposureDuration),ExposureMinimumDuration); var maxDurationSeconds = CMTimeGetSeconds(ThisApp.CaptureDevice.ActiveFormat.MaxExposureDuration); var newDurationSeconds = p * (maxDurationSeconds - minDurationSeconds) +minDurationSeconds; // Update Focus position ThisApp.CaptureDevice.LockForConfiguration(out Error); ThisApp.CaptureDevice.LockExposure(CMTime.FromSeconds(p,1000*1000*1000),ThisApp.CaptureDevice.ISO,null); ThisApp.CaptureDevice.UnlockForConfiguration(); }; ISO.ValueChanged += (object sender, EventArgs e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Update Focus position ThisApp.CaptureDevice.LockForConfiguration(out Error); ThisApp.CaptureDevice.LockExposure(ThisApp.CaptureDevice.ExposureDuration,ISO.Value,null); ThisApp.CaptureDevice.UnlockForConfiguration(); }; Bias.ValueChanged += (object sender, EventArgs e) => { // If we are in the automatic mode, ignore changes // if (Automatic) return; // Update Focus position ThisApp.CaptureDevice.LockForConfiguration(out Error); ThisApp.CaptureDevice.SetExposureTargetBias(Bias.Value,null); ThisApp.CaptureDevice.UnlockForConfiguration(); }; }
重写
ViewDidAppear
方法并添加以下内容,以在视图加载时开始录制:public override void ViewDidAppear (bool animated) { base.ViewDidAppear (animated); // Start udating the display if (ThisApp.CameraAvailable) { // Remap to this camera view ThisApp.Recorder.DisplayView = CameraView; ThisApp.Session.StartRunning (); SampleTimer.Start (); } }
当相机处于自动模式时,随着相机调整曝光,滑块将自动移动:
点击“锁定”段并拖动“偏差”滑块来手动调整自动曝光的偏差:
点击“自定义”段并拖动“持续时间”和“ISO”滑块来手动控制曝光:
停止应用程序。
上面的代码演示了当相机处于自动模式时如何监视曝光设置,以及当相机处于锁定或自定义模式时如何使用滑块控制曝光。
手动白平衡
通过白平衡控制,用户可以调整图像中的色彩平衡,使它们看起来更真实。 不同的光源有不同的色温,必须调整用于捕获图像的相机设置来补偿这些差异。 同样,通过允许用户控制白平衡,他们可以进行自动例程无法做出的专业调整来实现艺术效果。
例如,日光偏蓝,而钨丝白炽灯则偏暖,呈黄橙色。 (令人困惑的是,“冷色”比“暖色”有更高的色温。色温是一种物理测量,而不是感知。)
人类的大脑非常擅长补偿色温的差异,但相机没法做到这一点。 相机的工作原理是增强相反光谱上的颜色来调节色差。
新的 iOS 8 曝光 API 让应用程序能够控制该过程,并提供对相机白平衡设置的精细控制。
白平衡的工作原理
在讨论 iOS 8 应用程序中控制白平衡的细节之前, 让我们快速了解白平衡的工作原理:
在颜色感知的研究中,CIE 1931 RGB 颜色空间和 CIE 1931 XYZ 颜色空间是第一批用数学方法定义的颜色空间。 它们是 1931 年由国际照明委员会 (CIE) 创建的。
上图显示了人眼可见的所有颜色,从深蓝色到亮绿色再到亮红色。 如上图所示,图上的任何点都可以用 X 和 Y 值来绘制。
如图所示,可以在图形上绘制出超出人类视觉范围的 X 和 Y 值,因此这些颜色无法由相机再现。
上图中较小的曲线称为普朗克轨迹,它表示色温(单位是开尔文度),蓝色一侧的数字越大(色温越高),红色一侧的数字越小(色温越低)。 这些适用于典型的照明情况。
在混合照明条件下,白平衡调整需要偏离普朗克轨迹才能做出所需的更改。 在这些情况下,调整需要转移到 CIE 刻度的绿色或红色/品红色一侧。
iOS 设备通过提高相反的颜色增益来补偿偏色。 例如,如果一个场景有太多的蓝色,那么红色增益将增强以补偿。 这些增益值针对特定设备进行校准,因此它们依赖于设备。
现有白平衡控件
iOS 7 及更高版本通过 WhiteBalanceMode
属性提供了以下现有的白平衡控件:
AVCapture WhiteBalance ModeLocked
- 对场景采样一次,在整个场景中使用这些值。AVCapture WhiteBalance ModeContinuousAutoExposure
- 持续对场景进行采样,以确保很好的平衡。
应用程序可监视 AdjustingWhiteBalance
属性,以查看何时调整曝光。
iOS 8 中新的白平衡控件
除了 iOS 7 及更高版本已提供的功能外,iOS 8 现在还提供以下功能来控制白平衡:
- 完全手动控制设备 RGB 增益。
- 获取、设置和键值观察设备 RGB 增益。
- 支持使用灰色卡进行白平衡。
- 与设备无关的颜色空间的转换例程。
为了实现上述功能,向 AVCaptureWhiteBalanceGain
结构添加了以下成员:
RedGain
GreenGain
BlueGain
最大白平衡增益目前是四 (4),可以从 MaxWhiteBalanceGain
属性准备好。 因此,目前合法范围是一 (1) 到 MaxWhiteBalanceGain
(4)。
DeviceWhiteBalanceGains
属性可用于观察当前值。 在相机处于锁定白平衡模式时,使用 SetWhiteBalanceModeLockedWithDeviceWhiteBalanceGains
来调整平衡增益。
转换例程
iOS 8 中增加了转换例程,可帮助转换到与设备无关的颜色空间和从中进行转换。 为了实现转换例程,向 AVCaptureWhiteBalanceChromaticityValues
结构添加了以下成员:
X
- 是 0 到 1 之间的值。Y
- 是 0 到 1 之间的值。
此外,还向 AVCaptureWhiteBalanceTemperatureAndTintValues
结构添加了以下成员:
Temperature
- 一个浮点值(单位是开尔文度)。Tint
- 是绿色或品红从 0 到 150 的偏移量,绿色方向为正值,品红方向为负值。
使用 CaptureDevice.GetTemperatureAndTintValues
和 CaptureDevice.GetDeviceWhiteBalanceGains
方法在色温和色调、色度和 RGB 增益颜色空间之间进行转换。
注意
要转换的值越接近普朗克轨迹,转换例程越准确。
灰卡支持
Apple 使用“灰度世界”(Gray World) 这个词引用 iOS 8 中内置的灰卡支持。 它让用户能够将焦点放在覆盖画面中心至少 50% 的物理灰卡上,并使用它来调整白平衡。 灰卡的目的是达到白色,显得中立。
这可以在应用程序中实现,方法是提示用户在相机前面放置一张物理灰卡,监视 GrayWorldDeviceWhiteBalanceGains
属性,并等待值稳定下来。
然后,应用程序将使用 GrayWorldDeviceWhiteBalanceGains
属性中的值来应用更改,从而锁定 SetWhiteBalanceModeLockedWithDeviceWhiteBalanceGains
方法的白平衡增益。
必须先锁定捕获设备进行配置,然后才能更改白平衡。
手动白平衡示例
准备好常规 AV 捕获设置代码后,可将 UIViewController
添加到应用程序的情节提要中,并按如下所示进行配置:
视图包含以下主要元素:
- 一个
UIImageView
,它将显示视频源。 - 一个
UISegmentedControl
,它将对焦模式从“自动”更改为“锁定”。 - 两个
UISlider
控件,它们将显示和更新色温和色调。 - 一个
UIButton
,用于采样灰卡(灰度世界)空间并使用这些值设置白平衡。
执行以下操作来连接手动对焦控制的白平衡控制器:
添加以下 using 语句:
using System; using Foundation; using UIKit; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using AVFoundation; using CoreVideo; using CoreMedia; using CoreGraphics; using CoreFoundation; using System.Timers;
添加以下专用变量:
#region Private Variables private NSError Error; private bool Automatic = true; #endregion
添加以下计算属性:
#region Computed Properties public AppDelegate ThisApp { get { return (AppDelegate)UIApplication.SharedApplication.Delegate; } } public Timer SampleTimer { get; set; } #endregion
添加以下专用方法来设置新的白平衡温度和色调:
#region Private Methods void SetTemperatureAndTint() { // Grab current temp and tint var TempAndTint = new AVCaptureWhiteBalanceTemperatureAndTintValues (Temperature.Value, Tint.Value); // Convert Color space var gains = ThisApp.CaptureDevice.GetDeviceWhiteBalanceGains (TempAndTint); // Set the new values if (ThisApp.CaptureDevice.LockForConfiguration (out Error)) { gains = NomralizeGains (gains); ThisApp.CaptureDevice.SetWhiteBalanceModeLockedWithDeviceWhiteBalanceGains (gains, null); ThisApp.CaptureDevice.UnlockForConfiguration (); } } AVCaptureWhiteBalanceGains NomralizeGains (AVCaptureWhiteBalanceGains gains) { gains.RedGain = Math.Max (1, gains.RedGain); gains.BlueGain = Math.Max (1, gains.BlueGain); gains.GreenGain = Math.Max (1, gains.GreenGain); float maxGain = ThisApp.CaptureDevice.MaxWhiteBalanceGain; gains.RedGain = Math.Min (maxGain, gains.RedGain); gains.BlueGain = Math.Min (maxGain, gains.BlueGain); gains.GreenGain = Math.Min (maxGain, gains.GreenGain); return gains; } #endregion
重写
ViewDidLoad
方法并添加以下代码:public override void ViewDidLoad () { base.ViewDidLoad (); // Hide no camera label NoCamera.Hidden = ThisApp.CameraAvailable; // Attach to camera view ThisApp.Recorder.DisplayView = CameraView; // Set min and max values Temperature.MinValue = 1000f; Temperature.MaxValue = 10000f; Tint.MinValue = -150f; Tint.MaxValue = 150f; // Create a timer to monitor and update the UI SampleTimer = new Timer (5000); SampleTimer.Elapsed += (sender, e) => { // Convert color space var TempAndTint = ThisApp.CaptureDevice.GetTemperatureAndTintValues (ThisApp.CaptureDevice.DeviceWhiteBalanceGains); // Update slider positions Temperature.BeginInvokeOnMainThread (() => { Temperature.Value = TempAndTint.Temperature; }); Tint.BeginInvokeOnMainThread (() => { Tint.Value = TempAndTint.Tint; }); }; // Watch for value changes Segments.ValueChanged += (sender, e) => { // Lock device for change if (ThisApp.CaptureDevice.LockForConfiguration (out Error)) { // Take action based on the segment selected switch (Segments.SelectedSegment) { case 0: // Activate auto focus and start monitoring position Temperature.Enabled = false; Tint.Enabled = false; ThisApp.CaptureDevice.WhiteBalanceMode = AVCaptureWhiteBalanceMode.ContinuousAutoWhiteBalance; SampleTimer.Start (); Automatic = true; break; case 1: // Stop auto focus and allow the user to control the camera SampleTimer.Stop (); ThisApp.CaptureDevice.WhiteBalanceMode = AVCaptureWhiteBalanceMode.Locked; Automatic = false; Temperature.Enabled = true; Tint.Enabled = true; break; } // Unlock device ThisApp.CaptureDevice.UnlockForConfiguration (); } }; // Monitor position changes Temperature.TouchUpInside += (sender, e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Update white balance SetTemperatureAndTint (); }; Tint.TouchUpInside += (sender, e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Update white balance SetTemperatureAndTint (); }; GrayCardButton.TouchUpInside += (sender, e) => { // If we are in the automatic mode, ignore changes if (Automatic) return; // Get gray card values var gains = ThisApp.CaptureDevice.GrayWorldDeviceWhiteBalanceGains; // Set the new values if (ThisApp.CaptureDevice.LockForConfiguration (out Error)) { ThisApp.CaptureDevice.SetWhiteBalanceModeLockedWithDeviceWhiteBalanceGains (gains, null); ThisApp.CaptureDevice.UnlockForConfiguration (); } }; }
重写
ViewDidAppear
方法并添加以下内容,以在视图加载时开始录制:public override void ViewDidAppear (bool animated) { base.ViewDidAppear (animated); // Start udating the display if (ThisApp.CameraAvailable) { // Remap to this camera view ThisApp.Recorder.DisplayView = CameraView; ThisApp.Session.StartRunning (); SampleTimer.Start (); } }
将更改保存到保存中并运行应用程序。
当相机处于自动模式时,随着相机调整白平衡,滑块将自动移动:
点击“锁定”段并拖动“色温”和“色调”滑块来手动调整白平衡:
在“锁定”段仍处于选中状态时,在相机前面放置一张物理灰卡,然后点击“灰卡”按钮,将白平衡调整为灰度世界:
停止应用程序。
上面的代码演示了当相机处于自动模式时如何监视白平衡设置,在相机处于锁定模式时如何使用滑块控制白平衡。
包围捕获
包围捕获基于上述手动相机控件的设置,并允许应用程序以各种不同方式捕获瞬间。
简单地说,包围捕获是从图片到图片用各种设置拍摄的静止图像的连拍。
使用 iOS 8 中的包围捕获,应用程序可以预设一系列手动相机控件,发出单个命令,并让当前场景为每个手动预设返回一系列图像。
包围捕获基础知识
再说一次,包围捕获是从图片到图片用不同设置拍摄的静止图像的连拍。 可用的包围捕获类型包括:
- 自动曝光包围 - 所有图像都具有不同的偏差量。
- 手动曝光包围 - 所有图像都具有不同的快门速度(持续时间)和 ISO 量。
- 连拍包围 - 快速连续拍摄的一系列静止图像。
iOS 8 中新的包围捕获控件
所有包围捕获命令都在 AVCaptureStillImageOutput
类中实现。 使用 CaptureStillImageBracket
方法获取具有一组给定设置的一系列图像。
已实现两个新类来处理设置:
AVCaptureAutoExposureBracketedStillImageSettings
- 它具有一个属性ExposureTargetBias
,用于设置自动曝光包围的偏差。AVCaptureManual
ExposureBracketedStillImageSettings
– 它具有两个属性,ExposureDuration
用于ISO
设置手动曝光括号的快门速度和 ISO。
包围捕获控件注意事项
应做事项
下面是在 iOS 8 中使用包围捕获控件时应该做的事情:
- 通过调用
PrepareToCaptureStillImageBracket
方法,让应用为最坏的捕获情况做好准备。 - 假设示例缓冲区将来自同一个共享池。
- 要释放由上一个 prepare 调用分配的内存,请再次调用
PrepareToCaptureStillImageBracket
并向其发送一个对象的数组。
错误做法
下面是在 iOS 8 中使用包围捕获控件时不应该做的事情:
- 不要在单个捕获中搭配使用不同的包围捕获设置类型。
- 不要在单个捕获中请求超过
MaxBracketedCaptureStillImageCount
个图像。
包围捕获详细信息
在 iOS 8 中使用包围捕获时,应考虑以下细节:
- 包围设置暂时替代
AVCaptureDevice
设置。 - 会忽略闪光和静止图像稳定设置。
- 所有图像都必须使用相同的输出格式(jpeg、png 等)
- 视频预览可能会掉帧。
- 所有与 iOS 8 兼容的设备都支持包围捕获。
考虑到这些信息,让我们看看在 iOS 8 中使用包围捕获的示例。
包围捕获示例
准备好常规 AV 捕获设置代码后,可将 UIViewController
添加到应用程序的情节提要中,并按如下所示进行配置:
视图包含以下主要元素:
- 一个
UIImageView
,它将显示视频源。 - 三个
UIImageViews
,它们将显示捕获结果。 - 一个
UIScrollView
,用于容纳视频源和结果视图。 - 一个
UIButton
,用于使用一些预设设置进行包围捕获。
执行以下操作来连接包围捕获的视图控制器:
添加以下 using 语句:
using System; using System.Drawing; using Foundation; using UIKit; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using AVFoundation; using CoreVideo; using CoreMedia; using CoreGraphics; using CoreFoundation; using CoreImage;
添加以下专用变量:
#region Private Variables private NSError Error; private List<UIImageView> Output = new List<UIImageView>(); private nint OutputIndex = 0; #endregion
添加以下计算属性:
#region Computed Properties public AppDelegate ThisApp { get { return (AppDelegate)UIApplication.SharedApplication.Delegate; } } #endregion
添加以下专用方法来生成所需的输出图像视图:
#region Private Methods private UIImageView BuildOutputView(nint n) { // Create a new image view controller var imageView = new UIImageView (new CGRect (CameraView.Frame.Width * n, 0, CameraView.Frame.Width, CameraView.Frame.Height)); // Load a temp image imageView.Image = UIImage.FromFile ("Default-568h@2x.png"); // Add a label UILabel label = new UILabel (new CGRect (0, 20, CameraView.Frame.Width, 24)); label.TextColor = UIColor.White; label.Text = string.Format ("Bracketed Image {0}", n); imageView.AddSubview (label); // Add to scrolling view ScrollView.AddSubview (imageView); // Return new image view return imageView; } #endregion
重写
ViewDidLoad
方法并添加以下代码:public override void ViewDidLoad () { base.ViewDidLoad (); // Hide no camera label NoCamera.Hidden = ThisApp.CameraAvailable; // Attach to camera view ThisApp.Recorder.DisplayView = CameraView; // Setup scrolling area ScrollView.ContentSize = new SizeF (CameraView.Frame.Width * 4, CameraView.Frame.Height); // Add output views Output.Add (BuildOutputView (1)); Output.Add (BuildOutputView (2)); Output.Add (BuildOutputView (3)); // Create preset settings var Settings = new AVCaptureBracketedStillImageSettings[] { AVCaptureAutoExposureBracketedStillImageSettings.Create(-2.0f), AVCaptureAutoExposureBracketedStillImageSettings.Create(0.0f), AVCaptureAutoExposureBracketedStillImageSettings.Create(2.0f) }; // Wireup capture button CaptureButton.TouchUpInside += (sender, e) => { // Reset output index OutputIndex = 0; // Tell the camera that we are getting ready to do a bracketed capture ThisApp.StillImageOutput.PrepareToCaptureStillImageBracket(ThisApp.StillImageOutput.Connections[0],Settings,async (bool ready, NSError err) => { // Was there an error, if so report it if (err!=null) { Console.WriteLine("Error: {0}",err.LocalizedDescription); } }); // Ask the camera to snap a bracketed capture ThisApp.StillImageOutput.CaptureStillImageBracket(ThisApp.StillImageOutput.Connections[0],Settings, (sampleBuffer, settings, err) =>{ // Convert raw image stream into a Core Image Image var imageData = AVCaptureStillImageOutput.JpegStillToNSData(sampleBuffer); var image = CIImage.FromData(imageData); // Display the resulting image Output[OutputIndex++].Image = UIImage.FromImage(image); // IMPORTANT: You must release the buffer because AVFoundation has a fixed number // of buffers and will stop delivering frames if it runs out. sampleBuffer.Dispose(); }); }; }
重写
ViewDidAppear
方法并添加以下代码:public override void ViewDidAppear (bool animated) { base.ViewDidAppear (animated); // Start udating the display if (ThisApp.CameraAvailable) { // Remap to this camera view ThisApp.Recorder.DisplayView = CameraView; ThisApp.Session.StartRunning (); } }
将更改保存到保存中并运行应用程序。
对场景取框,然后点击“捕获包围”按钮:
向右向左轻扫,可以看到包围捕获拍摄的三张图像:
停止应用程序。
上述代码演示了如何在 iOS 8 中配置和使用自动曝光包围捕获。
总结
在本文中,我们介绍了 iOS 8 提供的新的手动相机控件,还介绍了它们执行的操作及其工作原理的基础知识。 我们提供了手动对焦、手动曝光和手动白平衡的示例。 最后,我们提供了一个使用前面讨论的手动相机控件进行包围捕获的示例