在iOS应用开发中,处理实时摄像头流是一个常见的需求,尤其是在进行物体检测、人脸识别等场景时。本文将介绍如何在iOS应用中捕获并预览实时摄像头流,并为后续使用YOLO模型进行物体检测做准备。
为了运行本文中的示例代码,需要具备以下条件:
从Storyboard的角度来看,应用程序非常简单。它只包含一个视图控制器(ViewController),并且在该控制器上有一个预览视图(Preview View)控件。将使用这个视图来显示实时摄像头流。
所有负责处理摄像头输入和视频预览的代码都在Controllers/VideoCapture
类中,该类实现了AVCaptureVideoDataOutputSampleBufferDelegate
。以下是存储其设置的成员变量:
private let captureSession = AVCaptureSession()
private var videoPreviewLayer: AVCaptureVideoPreviewLayer! = nil
private let videoDataOutput = AVCaptureVideoDataOutput()
private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
private var videoFrameSize: CGSize = .zero
在类构造函数中调用的setupPreview
方法将所有元素绑定在一起。首先,它获取第一个可用的后置摄像头作为输入设备:
var deviceInput: AVCaptureDeviceInput!
let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
do {
// ...
}
然后,它开始配置过程,强制从摄像头获取640x480的帧。这样的分辨率对于YOLO v2模型来说是足够的,因为它使用的是缩放到416x416像素的图像。注意,由于固定了竖屏方向,在videoFrameSize
变量中存储了48x640作为输入尺寸,以备将来使用:
captureSession.beginConfiguration()
captureSession.sessionPreset = .vga640x480
self.videoFrameSize = CGSize(width: 480, height: 640)
配置继续建立一个单元素队列来处理帧(alwaysDiscardLateVideoFrames
标志)。这意味着在当前帧的处理完成之前,后续的帧将被丢弃。
captureSession.addInput(deviceInput)
if captureSession.canAddOutput(videoDataOutput) {
captureSession.addOutput(videoDataOutput)
videoDataOutput.alwaysDiscardsLateVideoFrames = true
videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
} else {
print("Could not add video data output to the session")
captureSession.commitConfiguration()
return
}
固定竖屏方向将使处理和绘制物体检测预测变得更容易。
在接下来的步骤中,setup
方法创建了一个videoPreviewLayer
实例,并将其作为子层添加到应用程序视图(viewLayer
)中:
self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.videoPreviewLayer.videoGravity = .resizeAspectFill
videoPreviewLayer.frame = viewLayer.bounds
viewLayer.addSublayer(videoPreviewLayer)
使用.resizeAspectFill
值确保视频填充整个可用屏幕。由于没有iPhone的屏幕比例等于1.33:1(由640x480分辨率推断),每个帧在竖屏视图中将在两侧被裁剪。如果使用.resizeAspect
,整个帧将可见,但上面和下面会有空白条。
在VideoCapture
类中,还需要三个方法:
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// 将在这里处理帧
}
public func captureOutput(_ captureOutput: AVCaptureOutput, didDrop didDropSampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// 可以在这里处理丢弃的帧
}
public func startCapture() {
if !captureSession.isRunning {
captureSession.startRunning()
}
}