Xamarin.iOS 中的手动相机控件

借助 iOS 8 中的 AVFoundation Framework 提供的手动相机控件,移动应用程序可以完全控制 iOS 设备的相机。 这种精细的控制级别可用于创建专业级别的相机应用程序,并通过在拍摄静止图像或视频时调整相机的参数来提供艺术家构图。

在开发科学或工业应用程序时,这些控件也很有用;在这些应用程序中,结果不太关注图像的正确性或美观,而更倾向于突出显示正在拍摄的图像的某些特征或元素。

AVFoundation 捕获对象

无论是用 iOS 设备上的相机拍摄视频还是静止图像,用于捕获这些图像的过程基本上都是相同的。 对于使用默认自动相机控件或使用新的手动相机控件的应用程序来说也是如此:

AVFoundation 捕获对象概述

输入内容通过 AVCaptureConnectionAVCaptureDeviceInput 获取到 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 并添加所需的代码:

  1. 在解决方案资源管理器中双击 AppDelegate.cs 文件,将其打开进行编辑。

  2. 将下列 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;
    
  3. 将以下专用变量和计算属性添加到 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
    
  4. 重写已完成的方法并将其更改为:

    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;
    }
    
  5. 保存对文件所做的更改。

有了此代码,可以轻松实现手动相机控件来进行试验和测试。

手动对焦

通过允许最终用户直接控制对焦,应用程序可以对所拍摄的图像提供更多的艺术控制。

例如,专业摄影师可柔化图像的焦点来实现背景虚化效果。 或者,创建焦点拉动效果

对于科学家或医学应用程序的编写者,应用程序可能希望以编程方式移动镜头进行实验。 无论哪种方式,新的 API 都允许最终用户或应用程序在拍摄图像时控制对焦。

对焦的工作原理

在讨论 iOS 8 应用程序中控制对焦的细节之前, 让我们快速了解对焦在 iOS 设备中的工作原理:

焦点在 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 添加到应用程序的情节提要中,并按如下所示进行配置:

可以将 UIViewController 添加到应用程序情节提要中,并按此处的手动焦点示例进行配置。

视图包含以下主要元素:

  • 一个 UIImageView,它将显示视频源。
  • 一个 UISegmentedControl,它将对焦模式从“自动”更改为“锁定”。
  • 一个 UISlider,它将显示并更新当前镜头位置。

执行以下操作来连接手动对焦控制的视图控制器:

  1. 添加以下 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;
    
  2. 添加以下专用变量:

    #region Private Variables
    private NSError Error;
    private bool Automatic = true;
    #endregion
    
  3. 添加以下计算属性:

    #region Computed Properties
    public AppDelegate ThisApp {
        get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
    }
    public Timer SampleTimer { get; set; }
    #endregion
    
  4. 重写 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();
        };
    }
    
  5. 重写 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 ();
        }
    }
    
  6. 当相机处于自动模式时,随着相机调整焦点,滑块将自动移动:

    当相机调整此示例应用中的焦点时,滑块将自动移动

  7. 点击“锁定”段并拖动位置滑块可手动调整镜头位置:

    手动调整镜头位置

  8. 停止应用程序。

上面的代码演示了当相机处于自动模式时如何监视镜头位置,或者在相机处于锁定模式时如何使用滑块控制镜头位置。

手动曝光

曝光是指图像相对于光源亮度的亮度,它取决于有多少光照射到传感器上、持续多长时间,以及传感器的增益水平(ISO 映射)。 通过提供对曝光的手动控制,应用程序可以为最终用户提供更多自由,并允许他们实现风格化的外观。

使用手动曝光控件,从不真实的亮度到黑暗阴郁的环境,用户都可以拍摄图像:

显示从不真实的明亮到黑暗阴郁的曝光的图像示例

同样,可以使用科学应用程序的编程控件或通过应用程序用户界面提供的手动控件自动完成此操作。 无论哪种方式,新的 iOS 8 曝光 API 都提供对相机曝光设置的精细控制。

曝光的工作原理

在讨论 iOS 8 应用程序中控制曝光的细节之前, 让我们快速了解曝光的工作原理:

曝光的工作原理

共同控制曝光的三个基本元素包括:

  • 快门速度 - 这是快门打开来让光线进入相机传感器的时间长度。 快门打开的时间越短,进入的光线越少,图像就越清晰(运动模糊越少)。 快门打开的时间越长,光进入的光线就越多,发生的运动模糊就越多。
  • ISO 映射 - 这是从胶片摄影借用的术语,是指胶片中的化学物质对光的敏感度。 如果胶片中的 ISO 值较低,则具有更少的颗粒和更精细的色彩再现;如果数字传感器上的 ISO 值较低,传感器噪声更少,但亮度更低。 ISO 值越高,图像越亮,但传感器噪声越多。 数字传感器上的“ISO”是电子增益的度量值,而不是物理特征。
  • 镜头光圈 - 这是镜头开口的大小。 在所有 iOS 设备上,镜头光圈都是固定的,因此只有两个值可用来调整曝光:快门速度和 ISO。

连续自动曝光的工作原理

在了解手动曝光的工作原理之前,最好先了解一下 iOS 设备上的连续自动曝光的工作原理。

iOS 设备中连续自动曝光的工作原理

首先是自动曝光块,它的工作是计算理想的曝光,并不断被馈送测光统计信息。它使用这些信息来计算 ISO 和快门速度的最佳混合,以获得良好的照明场景。 此循环称为 AE 循环。

锁定曝光的工作原理

接下来,让我们看看锁定曝光在 iOS 设备上的工作原理。

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 添加到应用程序的情节提要中,并按如下所示进行配置:

可以将 UIViewController 添加到应用程序情节提要中,并按此处的手动曝光示例进行配置。

视图包含以下主要元素:

  • 一个 UIImageView,它将显示视频源。
  • 一个 UISegmentedControl,它将对焦模式从“自动”更改为“锁定”。
  • 4 个 UISlider 控件,它们显示和更新偏移量、持续时间、ISO 和偏差。

执行以下操作来连接手动对焦控制的曝光控制器:

  1. 添加以下 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;
    
  2. 添加以下专用变量:

    #region Private Variables
    private NSError Error;
    private bool Automatic = true;
    private nfloat ExposureDurationPower = 5;
    private nfloat ExposureMinimumDuration = 1.0f/1000.0f;
    #endregion
    
  3. 添加以下计算属性:

    #region Computed Properties
    public AppDelegate ThisApp {
        get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
    }
    public Timer SampleTimer { get; set; }
    #endregion
    
  4. 重写 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();
        };
    }
    
  5. 重写 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 ();
        }
    }
    
  6. 当相机处于自动模式时,随着相机调整曝光,滑块将自动移动:

    随着相机调整曝光,滑块将自动移动

  7. 点击“锁定”段并拖动“偏差”滑块来手动调整自动曝光的偏差:

    手动调整自动曝光的偏差

  8. 点击“自定义”段并拖动“持续时间”和“ISO”滑块来手动控制曝光:

    拖动“持续时间”和“ISO”滑块来手动控制曝光

  9. 停止应用程序。

上面的代码演示了当相机处于自动模式时如何监视曝光设置,以及当相机处于锁定或自定义模式时如何使用滑块控制曝光。

手动白平衡

通过白平衡控制,用户可以调整图像中的色彩平衡,使它们看起来更真实。 不同的光源有不同的色温,必须调整用于捕获图像的相机设置来补偿这些差异。 同样,通过允许用户控制白平衡,他们可以进行自动例程无法做出的专业调整来实现艺术效果。

显示手动白平衡调整的示例图像

例如,日光偏蓝,而钨丝白炽灯则偏暖,呈黄橙色。 (令人困惑的是,“冷色”比“暖色”有更高的色温。色温是一种物理测量,而不是感知。)

人类的大脑非常擅长补偿色温的差异,但相机没法做到这一点。 相机的工作原理是增强相反光谱上的颜色来调节色差。

新的 iOS 8 曝光 API 让应用程序能够控制该过程,并提供对相机白平衡设置的精细控制。

白平衡的工作原理

在讨论 iOS 8 应用程序中控制白平衡的细节之前, 让我们快速了解白平衡的工作原理:

在颜色感知的研究中,CIE 1931 RGB 颜色空间和 CIE 1931 XYZ 颜色空间是第一批用数学方法定义的颜色空间。 它们是 1931 年由国际照明委员会 (CIE) 创建的。

CIE 1931 RGB 颜色空间和 CIE 1931 XYZ 颜色空间

上图显示了人眼可见的所有颜色,从深蓝色到亮绿色再到亮红色。 如上图所示,图上的任何点都可以用 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.GetTemperatureAndTintValuesCaptureDevice.GetDeviceWhiteBalanceGains 方法在色温和色调、色度和 RGB 增益颜色空间之间进行转换。

注意

要转换的值越接近普朗克轨迹,转换例程越准确。

灰卡支持

Apple 使用“灰度世界”(Gray World) 这个词引用 iOS 8 中内置的灰卡支持。 它让用户能够将焦点放在覆盖画面中心至少 50% 的物理灰卡上,并使用它来调整白平衡。 灰卡的目的是达到白色,显得中立。

这可以在应用程序中实现,方法是提示用户在相机前面放置一张物理灰卡,监视 GrayWorldDeviceWhiteBalanceGains 属性,并等待值稳定下来。

然后,应用程序将使用 GrayWorldDeviceWhiteBalanceGains 属性中的值来应用更改,从而锁定 SetWhiteBalanceModeLockedWithDeviceWhiteBalanceGains 方法的白平衡增益。

必须先锁定捕获设备进行配置,然后才能更改白平衡。

手动白平衡示例

准备好常规 AV 捕获设置代码后,可将 UIViewController 添加到应用程序的情节提要中,并按如下所示进行配置:

可以将 UIViewController 添加到应用程序情节提要中,并按此处的手动白平衡示例进行配置。

视图包含以下主要元素:

  • 一个 UIImageView,它将显示视频源。
  • 一个 UISegmentedControl,它将对焦模式从“自动”更改为“锁定”。
  • 两个 UISlider 控件,它们将显示和更新色温和色调。
  • 一个 UIButton,用于采样灰卡(灰度世界)空间并使用这些值设置白平衡。

执行以下操作来连接手动对焦控制的白平衡控制器:

  1. 添加以下 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;
    
  2. 添加以下专用变量:

    #region Private Variables
    private NSError Error;
    private bool Automatic = true;
    #endregion
    
  3. 添加以下计算属性:

    #region Computed Properties
    public AppDelegate ThisApp {
        get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
    }
    public Timer SampleTimer { get; set; }
    #endregion
    
  4. 添加以下专用方法来设置新的白平衡温度和色调:

    #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
    
  5. 重写 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 ();
            }
        };
    }
    
  6. 重写 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 ();
        }
    }
    
  7. 将更改保存到保存中并运行应用程序。

  8. 当相机处于自动模式时,随着相机调整白平衡,滑块将自动移动:

    随着相机调整白平衡,滑块将自动移动

  9. 点击“锁定”段并拖动“色温”和“色调”滑块来手动调整白平衡:

    拖动“色温”和“色调”滑块以手动调整白平衡

  10. 在“锁定”段仍处于选中状态时,在相机前面放置一张物理灰卡,然后点击“灰卡”按钮,将白平衡调整为灰度世界:

    点击“灰卡”按钮,将白平衡调整为“灰度世界”

  11. 停止应用程序。

上面的代码演示了当相机处于自动模式时如何监视白平衡设置,在相机处于锁定模式时如何使用滑块控制白平衡。

包围捕获

包围捕获基于上述手动相机控件的设置,并允许应用程序以各种不同方式捕获瞬间。

简单地说,包围捕获是从图片到图片用各种设置拍摄的静止图像的连拍。

包围捕获的工作原理

使用 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 添加到应用程序的情节提要中,并按如下所示进行配置:

可以将 UIViewController 添加到应用程序情节提要中,并按此处的包围捕获示例进行配置。

视图包含以下主要元素:

  • 一个 UIImageView,它将显示视频源。
  • 三个 UIImageViews,它们将显示捕获结果。
  • 一个 UIScrollView,用于容纳视频源和结果视图。
  • 一个 UIButton,用于使用一些预设设置进行包围捕获。

执行以下操作来连接包围捕获的视图控制器:

  1. 添加以下 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;
    
  2. 添加以下专用变量:

    #region Private Variables
    private NSError Error;
    private List<UIImageView> Output = new List<UIImageView>();
    private nint OutputIndex = 0;
    #endregion
    
  3. 添加以下计算属性:

    #region Computed Properties
    public AppDelegate ThisApp {
        get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
    }
    #endregion
    
  4. 添加以下专用方法来生成所需的输出图像视图:

    #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
    
  5. 重写 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();
            });
        };
    }
    
  6. 重写 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 ();
        }
    }
    
    
  7. 将更改保存到保存中并运行应用程序。

  8. 对场景取框,然后点击“捕获包围”按钮:

    对场景取框,然后点击“捕获包围”按钮

  9. 向右向左轻扫,可以看到包围捕获拍摄的三张图像:

    从右向左轻扫,可以看到利用包围捕获拍摄的三张图像

  10. 停止应用程序。

上述代码演示了如何在 iOS 8 中配置和使用自动曝光包围捕获。

总结

在本文中,我们介绍了 iOS 8 提供的新的手动相机控件,还介绍了它们执行的操作及其工作原理的基础知识。 我们提供了手动对焦、手动曝光和手动白平衡的示例。 最后,我们提供了一个使用前面讨论的手动相机控件进行包围捕获的示例