入门
在本教程中,您将使用 Vision Frame 来学习如何:
- 使用 . 地球地球的图像
VNTranslationalImageRegistrationRequest
。 -
CIFilter
使用 Metal 内核创建自定义。 - 使用此过滤器组合多个图像以移除任何移动对象。
:由于您需要在使用服务器和Metal,因此必须在实际设备而不是在您的教程中显示您正在运行它。
01.jpg你应该会看到一些看起来像摄像头应用程序的东西。有一个简单的按钮,周围有一个白色的环,它全屏显示摄像头输入。
你肯定已经发现了它并在下面找到了CameraViewController。那是因为它设置为在任务程序中寻找两行要查看的位置configureCaptureSession()
。
camera.activeFrameDuration = 时间(值:1,最大时间:5)
camera.activeVideoFrameDuration = CMTime(值:1,时间亮:5)
第一行强制最大帧速率为每秒五帧。第二行将最小帧速率定义为相同。两条线一起要求相机以所需的帧速率运行。
如果您点击录制按钮,您应该会看到外面的白色环顺时针填充。然而,当它完成时,什么也没有发生。
你现在必须为此做点什么。
将图像保存到文件应用程序
为了帮助您在进行过程中调试应用程序,最好将您正在使用的图像保存到“文件”应用程序中。幸运的是,这比听起来容易得多。
将以下两个键添加到您的Info.plist:
- 应用程序支持 iTunes 文件共享。
- 支持就地打开文档。
将它们的值都设置为YES
。完成后,文件应如下所示:
第一个键为Documents目录中的文件启用文件共享。第二个让您的应用程序从文件提供程序打开原始文档,而不是接收副本。当这两个选项都启用时,存储在应用程序的Documents目录中的所有文件都会出现在文件应用程序中。这也意味着其他应用程序可以访问这些文件。
现在您已授予Files应用程序访问Documents目录的权限,是时候在其中保存一些图像了。
与启动项目捆绑在一起的是一个struct
名为ImageSaver
. 实例化时,它会生成一个通用唯一标识符 (UUID) 并使用它在Documents目录下创建一个目录。这是为了确保您不会覆盖以前保存的图像。您将ImageSaver
在您的应用程序中使用将图像写入文件。
在CameraViewController.swift中,在类的顶部定义一个新变量,如下所示:
var saver: ImageSaver ?
然后,滚动到recordTapped(_:)
并在方法末尾添加以下内容:
保护程序= ImageSaver ()
每次点击录制按钮时,您都会在此处创建一个新ImageSaver
的,以确保每个录制会话都将图像保存到新目录。
接下来,滚动到并在初始语句captureOutput(_:didOutput:from:)
之后添加以下代码:if
// 1个
守卫
let imageBuffer = CMSampleBufferGetImageBuffer (sampleBuffer),
let cgImage = CIImage (cvImageBuffer: imageBuffer).cgImage()
else {
return
}
// 2
let image = CIImage (cgImage: cgImage)
// 3
saver ? .write(图像)
使用此代码,您可以:
- 从捕获的样本缓冲区中提取
CVImageBuffer
并将其转换为CGImage
. - 将 转换
CGImage
为CIImage
. - 将图像写入Documents目录。
注意:为什么必须将样本缓冲区转换为 a CIImage
,然后转换为 a CGImage
,最后又转换回 a CIImage
?这与谁拥有数据有关。当您将样本缓冲区转换为 aCIImage
时,图像会存储对样本缓冲区的强引用。不幸的是,对于视频捕获,这意味着几秒钟后,它将开始丢帧,因为分配给样本缓冲区的内存不足。通过使用 a渲染CIImage
a ,您可以复制图像数据,并且可以释放样本缓冲区以再次使用。CGImage``CIIContext
现在,构建并运行应用程序。点击录制按钮,完成后,切换到文件应用程序。在Evanesco文件夹下,您应该会看到一个以 UUID 命名的文件夹,其中包含 20 个项目。
03.pngUUID 命名文件夹
如果您查看此文件夹,您会发现在 4 秒的录制过程中捕获的 20 帧。
04.jpg捕获的帧
注意:如果您没有立即看到该文件夹,请使用“文件”应用顶部的搜索栏。
嗯不错。那么你能用 20 张几乎相同的图像做什么呢?
照片堆叠
在计算摄影中,照片堆叠是一种技术,其中捕获、对齐和组合多个图像以创建不同的所需效果。
例如,HDR 图像是通过在不同曝光水平下拍摄多张图像并将每张图像的最佳部分组合在一起而获得的。这就是您可以在 iOS 中同时查看阴影和明亮天空中的细节的方式。
天文摄影也大量使用照片堆叠。图像曝光时间越短,传感器拾取的噪点就越少。所以天文摄影师通常会拍摄一堆短曝光图像并将它们堆叠在一起以增加亮度。
在微距摄影中,很难同时对焦整个图像。使用照片堆叠,摄影师可以拍摄几张不同焦距的图像,然后将它们组合起来,生成非常清晰的非常小的物体图像。
要将图像组合在一起,您首先需要对齐它们。如何?iOS 提供了一些有趣的 API 来帮助你。
使用视觉对齐图像
Vision框架有两个不同的 API 用于对齐图像:VNTranslationalImageRegistrationRequest
和VNHomographicImageRegistrationRequest
. 前者更容易使用,如果你假设应用程序的用户会相对静止地握住 iPhone,它应该足够好。
注意:如果您从未使用过 Vision 框架,请查看Face Detection Tutorial Using the Vision Framework for iOS,了解有关 Vision 请求如何工作的一些信息。
为了使您的代码更具可读性,您将创建一个新类来处理捕获的图像的对齐和最终组合。
创建一个新的空Swift 文件并将其命名为ImageProcessor.swift。
删除任何提供的导入语句并添加以下代码:
导入CoreImage
导入Vision
类 ImageProcessor {
var frameBuffer: [ CIImage ] = []
var alignedFrameBuffer: [ CIImage ] = []
var completion: (( CIImage ) -> Void ) ?
var isProcessingFrames = false
var frameCount: Int {
return frameBuffer.count
}
}
在这里,您导入Vision框架并定义ImageProcessor
类以及一些必要的属性:
- frameBuffer将存储原始捕获的图像。
- alignedFrameBuffer将包含对齐后的图像。
- 完成是一个处理程序,将在图像对齐和组合后调用。
- isProcessingFrames将指示图像当前是否正在对齐和组合。
- frameCount是捕获的图像数。
接下来,将以下方法添加到ImageProcessor
类中:
func add ( _frame : CIImage ) {
if isProcessingFrames {
return
}
frameBuffer.append(帧)
}
此方法将捕获的帧添加到帧缓冲区,但前提是您当前未处理帧缓冲区中的帧。
还是在类里面,添加处理方法:
func processFrames ( completion : (( CIImage ) -> Void ) ? ) {
// 1
isProcessingFrames = true
self .completion = completion
// 2
let firstFrame = frameBuffer.removeFirst()
对齐的FrameBuffer.append(firstFrame)
// 3
for frame in frameBuffer {
// 4
let request = VNTranslationalImageRegistrationRequest (targetedCIImage: frame)
do {
// 5
let sequenceHandler = VNSequenceRequestHandler ()
// 6
try sequenceHandler.perform([request], on: firstFrame)
}捕捉{
打印(error.localizedDescription)
}
// 7
alignImages(请求:请求,帧:帧)
}
// 8
清理()
}
看起来步骤很多,但这种方法相对简单。添加所有捕获的帧后,您将调用此方法。它将处理每一帧并使用Vision框架对齐它们。具体来说,在这段代码中,您:
- 设置
isProcessingFrames
布尔变量以防止添加更多帧。您还可以保存完成处理程序以供以后使用。 - 从帧缓冲区中删除第一帧并将其添加到对齐图像的帧缓冲区。所有其他帧将与这一帧对齐。
- 循环遍历帧缓冲区中的每一帧。
- 使用框架创建新的视觉请求以确定简单的平移对齐。
- 创建序列请求处理程序,它将处理您的对齐请求。
- 执行Vision请求以将帧与第一帧对齐并捕获任何错误。
-
alignImages(request:frame:)
使用请求和当前帧调用。此方法尚不存在,您将很快解决。 - 清理。这个方法还需要写。
准备好应对了alignImages(request:frame:)
吗?
在下面添加以下代码processFrames(completion:)
:
func alignImages ( request : VNRequest , frame : CIImage ) {
// 1
guard
let results = request.results as? [ VNImageTranslationAlignmentObservation ],
让result = results.first
else {
return
}
// 2
let alignedFrame = frame.transformed(by: result.alignmentTransform)
// 3
alignedFrameBuffer.append(alignedFrame)
}
你在这里:
- 解开您
for
在processFrames(completion:)
. - 使用Vision框架计算的仿射变换矩阵变换帧。
- 将此翻译后的帧附加到对齐的帧缓冲区。
最后两种方法是您的应用所需的Vision代码的核心。您执行请求,然后使用结果来修改图像。现在剩下的就是清理自己。
将以下方法添加到ImageProcessor
类的末尾:
功能 清理(){
帧缓冲区= []
对齐帧缓冲区= []
isProcessingFrames = 错误
完成= nil
}
在cleanup()
中,您只需清除两个帧缓冲区,重置标志以指示您不再处理帧并将完成处理程序设置为nil
。
在您可以构建和运行您的应用程序之前,您需要ImageProcessor
在您的CameraViewController
.
打开CameraViewController.swift。在类的顶部,定义以下属性:
让图像处理器=图像 处理器()
接下来,找到captureOutput(_:didOutput:from:)
. 您将对这个方法进行两个小改动。
在该行下方添加以下let image = ...
行:
imageProcessor.add(图像)
在对 的调用下方stopRecording()
,仍在if
语句中,添加:
imageProcessor.processFrames(完成:displayCombinedImage)
构建并运行您的应用程序,然后……什么也没有发生。不用担心,波特先生。您仍然需要将所有这些图像组合成一个杰作。要了解如何做到这一点,您必须继续阅读!
注意:如果您想查看对齐的图像与原始捕获的比较,您可以ImageSaver
在ImageProcessor
. 这将允许您将对齐的图像保存到 Documents 文件夹并在 Files 应用程序中查看它们。
照片堆叠的工作原理
有几种不同的方法可以将图像组合或堆叠在一起。到目前为止,最简单的方法是将图像中每个位置的像素平均在一起。
例如,如果您有 20 张图像要堆叠,您可以将所有 20 张图像的坐标 (13, 37) 处的像素平均在一起,以获得堆叠图像在 (13, 37) 处的平均像素值。
05.png像素堆叠
如果您对每个像素坐标执行此操作,您的最终图像将是所有图像的平均值。您拥有的图像越多,平均值就越接近背景像素值。如果某物在相机前移动,它只会出现在几张图像中的同一位置,因此它对整体平均值的贡献不大。这就是移动物体消失的原因。
这就是您实现堆叠逻辑的方式。
堆叠图像
现在是真正有趣的部分!您将把所有这些图像组合成一个奇妙的图像。您将使用金属着色语言 (MSL)创建自己的核心映像内核。
您的简单内核将计算两个图像的像素值的加权平均值。当您将一堆图像平均在一起时,任何移动的物体都应该消失。背景像素会更频繁地出现并主导平均像素值。
创建核心映像内核
您将从使用 MSL 编写的实际内核开始。MSL 与 C++ 非常相似。
将一个新的金属文件添加到您的项目中,并将其命名为AverageStacking.metal。保留模板代码并将以下代码添加到文件末尾:
#包括 <CoreImage/CoreImage.h>
extern "C" { namespace coreimage {
// 1
float4 avgStacking ( sample_t currentStack, sample_t newImage, float stackCount) {
// 2
float4 avg = ((currentStack * stackCount) + newImage) / (stackCount + 1.0 );
// 3
avg = float4 (avg.rgb, 1 );
// 4
返回平均值;
}
}}
使用此代码,您可以:
- 定义一个名为 的新函数
avgStacking
,它将返回一个包含 4 个浮点值的数组,代表像素颜色红色、绿色和蓝色以及一个 alpha 通道。该功能将一次应用于两张图像,因此您需要跟踪所有看到的图像的当前平均值。该currentStack
参数表示该平均值,stackCount
而是一个数字,表示如何使用图像来创建currentStack
. - 计算两个图像的加权平均值。由于
currentStack
可能已经包含来自多个图像的信息,因此您将其乘以stackCount
以赋予其适当的权重。 - 将 alpha 值添加到平均值以使其完全不透明。
- 返回平均像素值。
注意:理解这个函数对于两个图像之间的每一对对应像素将被调用一次是非常重要的。数据类型是来自图像的sample_t
像素样本。
好的,现在你有了一个内核函数,你需要创建一个CIFilter
来使用它!向项目中添加一个新的Swift 文件并将其命名为AverageStackingFilter.swift。删除 import 语句并添加以下内容:
导入CoreImage
类 AverageStackingFilter:CIFilter {
让内核:CIBlendKernel
var inputCurrentStack:CIImage?
var inputNewImage: CIImage ?
var inputStackCount = 1.0
}
在这里,您正在定义您的新CIFilter
类和一些您需要的属性。请注意三个输入变量如何对应于内核函数中的三个参数。巧合?;]
至此,Xcode 可能正在抱怨这个类缺少初始化程序。所以,是时候解决这个问题了。将以下内容添加到类中:
override init () {
// 1
保护 let url = Bundle .main.url(forResource: "default" ,
withExtension: "metallib" ) else {
fatalError ( "Check your build settings." )
}
do {
// 2
let data = try Data (contentsOf: url)
// 3
kernel = try CIBlendKernel (
函数名称:“avgStacking”,
来自MetalLibraryData:数据)
} catch {
print (error.localizedDescription)
fatalError ( "确保函数名匹配" )
}
// 4
超级. 初始化()
}
使用此初始化程序,您可以:
- 获取已编译和链接的 Metal 文件的 URL。
- 读取文件的内容。
- 尝试
CIBlendKernel
从avgStacking
Metal 文件中的函数创建一个,如果失败则恐慌。 - 打电话给超级
init
。
等一下……你什么时候编译和链接你的 Metal 文件的?不幸的是,你还没有。不过,好消息是您可以让 Xcode 为您做这件事!
编译你的内核
要编译和链接您的 Metal 文件,您需要在Build Settings中添加两个标志。所以去那边吧。
搜索Other Metal Compiler Flags并将-fcikernel添加到其中:
06.png金属编译器标志
接下来,单击+按钮并选择Add User-Defined Setting:
07.png添加用户自定义设置
调用设置MTLLINKER_FLAGS并将其设置为-cikernel:
08.png金属链接器标志
现在,下次您构建项目时,Xcode 将编译您的 Metal 文件并自动链接它们。
不过,在您执行此操作之前,您仍然需要在 Core Image 过滤器上做一些工作。
回到AverageStackingFilter.swift,添加以下方法:
函数输出 图像()-> CIImage?{
守卫让inputCurrentStack = inputCurrentStack,
让inputNewImage = inputNewImage
else {
返回零
}
返回内核.apply(
范围:inputCurrentStack.extent,
参数:[inputCurrentStack,inputNewImage,inputStackCount])
}
这个方法非常重要。也就是说,它将您的内核函数应用于输入图像并返回输出图像!如果它不这样做,那将是一个无用的过滤器。
呃,Xcode 还在抱怨!美好的。将以下代码添加到类中以使其平静下来:
需要 初始化?( coder aDecoder : NSCoder ) {
fatalError ( "init(coder:) has not been implemented" )
}
你不需要能够从一个 unarchiver 初始化这个 Core Image 过滤器,所以你只需实现最低限度的让 Xcode 满意。
使用您的过滤器
打开ImageProcessor.swift并将以下方法添加到ImageProcessor
:
func combineFrames () {
// 1
var finalImage = alignedFrameBuffer.removeFirst()
// 2
let filter = AverageStackingFilter ()
//3
for (i, image) in alignedFrameBuffer.enumerated() {
// 4
filter.inputCurrentStack = finalImage
filter.inputNewImage =图像
filter.inputStackCount = Double (i + 1 )
// 5
finalImage = filter.outputImage() !
}
// 6
清理(图片:finalImage)
}
你在这里:
- 使用对齐的成帧器缓冲区中的第一个图像初始化最终图像,并在此过程中将其删除。
- 初始化您的自定义核心图像过滤器。
- 循环遍历对齐的帧缓冲区中的每个剩余图像。
- 设置过滤器参数。注意最终图像设置为当前堆栈图像。重要的是不要交换输入图像!堆栈计数也设置为数组索引加一。这是因为您在方法开始时从对齐的帧缓冲区中删除了第一张图像。
- 用新的过滤器输出图像覆盖最终图像。
-
cleanup(image:)
合并所有图像后使用最终图像调用。
您可能已经注意到它cleanup()
不带任何参数。通过替换cleanup()
以下内容来解决此问题:
功能 清理(图片:CIImage){
帧缓冲区= []
对齐帧缓冲区= []
isProcessingFrames = false
if let completion = completion {
DispatchQueue .main.async {
完成(图像)
}
}
完成= 无
}
唯一的变化是新添加的参数和if
在主线程上调用完成处理程序的语句。其余的保持原样。
在底部processFrames(completion:)
,将调用替换为cleanup()
:
组合帧()
这样,您的图像处理器将在对齐所有捕获的帧后将它们组合起来,然后将最终图像传递给完成函数。
呸!构建并运行这个应用程序,让那些人、汽车和任何在你的镜头中移动的东西消失!
09.gif结论
但是,如果您想尝试改进您的应用程序,有几种方法可以做到这一点:
- 用于
VNHomographicImageRegistrationRequest
计算透视扭曲矩阵以对齐捕获的帧。这应该会在两个帧之间创建更好的匹配,只是使用起来有点复杂。 - 计算模式像素值而不是平均值。众数是最常出现的值。这样做会消除图像中移动对象的所有影响,因为它们不会被平均化。这应该会创建一个看起来更干净的输出图像。提示:将 RGB 转换为 HSL,并根据色调 (H) 值的小范围计算模式。
结论
您可以使用教程中的所有代码部分下载最终项目。
这里也推荐一些面试相关的内容,祝各位网友都能拿到满意offer!
GCD面试要点
block面试要点
Runtime面试要点
RunLoop面试要点
内存管理面试要点
MVC、MVVM面试要点
网络性能优化面试要点
网络编程面试要点
KVC&KVO面试要点
数据存储面试要点
混编技术面试要点
设计模式面试要点
UI面试要点
网友评论