GPUImage 2

Brad Larson

http://www.sunsetlakesoftware.com

@bradlarson

contact@sunsetlakesoftware.com

概述

GPUImage 2 是 GPUImage 框架的第二代产品,该框架是一个开源项目,用于在 Mac、iOS 以及现在的 Linux 上执行 GPU 加速的图像和视频处理。 最初的 GPUImage 框架是用 Objective-C 编写的,目标是 Mac 和 iOS,但这个最新版本完全用 Swift 编写,并且还可以面向 Linux 和未来支持 Swift 代码的平台。

该框架的目标是尽可能轻松地设置并执行针对图像或视频源的实时视频处理或机器视觉。 通过依靠 GPU 来运行这些操作,与受 CPU 限制的代码相比,可以实现 100 倍或更高的性能提升。 这在移动或嵌入式设备中尤其明显。 在 iPhone 4S 上,该框架可以轻松地以超过 60 FPS 的速度处理 1080p 视频。 在 Raspberry Pi 3 上,它可以以超过 20 FPS 的速度对实时 720p 视频执行 Sobel 边缘检测。

许可证

BSD 风格,完整许可证位于框架中的 License.txt 文件中。

目前,GPUImage 使用 Lode Vandevenne 的 LodePNG 在 Linux 上进行 PNG 输出,以及 Paul Hudson 的 SwiftGD 进行图像加载。 lodepng 在 zlib 许可证下发布,而 SwiftGD 在 MIT 许可证下发布。

技术要求

总体架构

该框架依赖于处理管道的概念,其中图像源针对图像消费者,依此类推,直到图像输出到屏幕、图像文件、原始数据或录制的电影。 摄像头、电影、静态图像和原始数据都可以作为此管道的输入。 可以通过一系列较小的操作的组合来构建任意复杂的处理操作。

这是一个面向对象的框架,其中类封装了输入、处理操作和输出。 处理操作使用 Open GL (ES) 顶点和片段着色器在 GPU 上执行其图像操作。

以下显示了在常见应用程序中使用该框架的示例。

在 Mac 或 iOS 应用程序中使用 GPUImage

要将 GPUImage 框架添加到您的 Mac 或 iOS 应用程序,可以将 GPUImage.xcodeproj 项目拖到应用程序的项目中,或者通过“文件 | 添加文件到...”添加它。

之后,转到项目的“构建阶段”,并将 GPUImage_iOS 或 GPUImage_macOS 添加为“目标依赖项”。 将其添加到“链接二进制文件与库”阶段。 添加一个新的“拷贝文件”构建阶段,将其目标设置为“Frameworks”,并将上面的 GPUImage.framework(适用于 Mac)或下面的 GPUImage.framework(适用于 iOS)添加到其中。 最后一步将确保框架部署在您的应用程序捆绑包中。

在引用 GPUImage 类的任何 Swift 文件中,只需添加

import GPUImage

您应该可以开始了。

请注意,您可能需要构建您的项目一次,以解析和构建 GPUImage 框架,以便 Xcode 停止警告您有关框架及其类丢失的信息。

在 Linux 应用程序中使用 GPUImage

此项目支持 Swift Package Manager,因此您应该能够像下面这样在 Package.swift 文件中将其添加为依赖项

.package(url: "https://github.com/BradLarson/GPUImage2.git", from: "0.0.1"), 

以及一个

import GPUImage

在您的应用程序代码中。

在编译框架之前,您需要在您的系统上启动并运行 Swift。 对于桌面 Ubuntu 安装,您可以按照 Apple 在 他们的下载页面上的指南进行操作。

在 Swift 之后,您需要安装 Video4Linux 以访问标准 USB 网络摄像头作为输入

sudo apt-get install libv4l-dev

在 Raspberry Pi 上,您需要确保安装了 Broadcom Videocore 标头和库才能访问 GPU

sudo apt-get install libraspberrypi-dev

对于桌面 Linux 和其他 OpenGL 设备(Jetson 系列),您需要确保安装了 GLUT 和 OpenGL 标头。 该框架当前使用 GLUT 进行输出。 GLUT 可以在 Raspberry Pi 上通过新的实验性 OpenGL 支持来使用,但我发现它比使用 OpenGL ES API 和 Pi 附带的 Videocore 接口慢得多。 此外,如果您启用 OpenGL 支持,您目前将无法使用 Videocore 接口。

设置好所有这些后,您可以使用

swift build

在主 GPUImage 目录中构建框架,或者在 examples/Linux-OpenGL/SimpleVideoFilter 目录中执行相同的操作。 这将构建一个示例应用程序,该应用程序从 USB 摄像头过滤实时视频并将结果实时显示到屏幕上。 应用程序本身将包含在 .build 目录及其特定于平台的子目录中。 查找 SimpleVideoFilter 二进制文件并运行它。

执行常见任务

过滤实时视频

要过滤来自 Mac 或 iOS 摄像头的实时视频,您可以编写如下代码

do {
    camera = try Camera(sessionPreset:AVCaptureSessionPreset640x480)
    filter = SaturationAdjustment()
    camera --> filter --> renderView
    camera.startCapture()
} catch {
    fatalError("Could not initialize rendering pipeline: \(error)")
}

其中 renderView 是您放置在视图层次结构中的某个位置的 RenderView 的实例。 上面实例化了一个 640x480 摄像头实例,创建了一个饱和度滤镜,并将摄像头帧定向到通过饱和度滤镜进行处理,然后显示到屏幕上。 startCapture() 启动摄像头捕获过程。

--> 运算符将图像源链接到图像消费者,并且其中许多可以在同一行中链接。

捕获和过滤静止照片

功能尚未完成。

从视频中捕获图像

(当前在 Linux 上不可用。)

要从实时视频中捕获静止图像,您需要设置一个回调,以便在处理的下一个视频帧上执行。 最简单的方法是使用便利扩展来捕获、编码并将文件保存到磁盘

filter.saveNextFrameToURL(url, format:.PNG)

在底层,这会创建一个 PictureOutput 实例,将其作为目标附加到您的滤镜,将 PictureOutput 的 encodedImageFormat 设置为 PNG 文件,并将 encodedImageAvailableCallback 设置为一个闭包,该闭包接收过滤后的图像的数据并将其保存到 URL。

您可以手动设置此设置以获取编码的图像数据(作为 NSData)

let pictureOutput = PictureOutput()
pictureOutput.encodedImageFormat = .JPEG
pictureOutput.encodedImageAvailableCallback = {imageData in
    // Do something with the NSData
}
filter --> pictureOutput

您还可以通过设置 imageAvailableCallback 以平台原生格式(NSImage、UIImage)获取过滤后的图像

let pictureOutput = PictureOutput()
pictureOutput.encodedImageFormat = .JPEG
pictureOutput.imageAvailableCallback = {image in
    // Do something with the image
}
filter --> pictureOutput

处理静止图像

(当前在 Linux 上不可用。)

有几种不同的方法来处理图像过滤。 最简单的方法是 UIImage 或 NSImage 的便利扩展,它允许您过滤该图像并返回 UIImage 或 NSImage

let testImage = UIImage(named:"WID-small.jpg")!
let toonFilter = SmoothToonFilter()
let filteredImage = testImage.filterWithOperation(toonFilter)

对于更复杂的管道

let testImage = UIImage(named:"WID-small.jpg")!
let toonFilter = SmoothToonFilter()
let luminanceFilter = Luminance()
let filteredImage = testImage.filterWithPipeline{input, output in
    input --> toonFilter --> luminanceFilter --> output
}

一个警告:如果您想在屏幕上显示图像或重复过滤图像,请不要使用这些方法。 来回 Core Graphics 会增加很多开销。 相反,我建议手动设置管道并将其定向到 RenderView 以进行显示,以便将所有内容保留在 GPU 上。

这两种便利方法都包装了多个操作。 要将图片输入到滤镜管道中,您可以实例化一个 PictureInput。 要从管道中捕获图片,您可以使用 PictureOutput。 要手动设置图像处理,您可以使用如下所示的内容

let toonFilter = SmoothToonFilter()
let testImage = UIImage(named:"WID-small.jpg")!
let pictureInput = PictureInput(image:testImage)
let pictureOutput = PictureOutput()
pictureOutput.imageAvailableCallback = {image in
    // Do something with image
}
pictureInput --> toonFilter --> pictureOutput
pictureInput.processImage(synchronously:true)

在上面的代码中,imageAvailableCallback 将在 processImage() 行立即触发。 如果您希望异步完成图像处理,请在上面的代码中省略 synchronously 参数。

过滤和重新编码电影

要过滤现有的电影文件,您可以编写如下代码

do {
	let bundleURL = Bundle.main.resourceURL!
	let movieURL = URL(string:"sample_iPod.m4v", relativeTo:bundleURL)!
	movie = try MovieInput(url:movieURL, playAtActualSpeed:true)
    filter = SaturationAdjustment()
    movie --> filter --> renderView
    movie.start()
} catch {
    fatalError("Could not initialize rendering pipeline: \(error)")
}

其中 renderView 是您放置在视图层次结构中的某个位置的 RenderView 的实例。 上面从应用程序的捆绑包中加载名为 "sample_iPod.m4v" 的电影,创建一个饱和度滤镜,并将电影帧定向到通过饱和度滤镜进行处理,然后显示到屏幕上。 start() 启动电影播放。

编写自定义图像处理操作

该框架使用一系列协议来定义可以输出要处理的图像、接收图像进行处理或同时执行这两者的类型。 这些分别是 ImageSource、ImageConsumer 和 ImageProcessingOperation 协议。 任何类型都可以遵守这些协议,但通常使用类。

许多常见的滤镜和其他图像处理操作可以描述为 BasicOperation 类的子类。 BasicOperation 提供了从一个或多个输入中获取图像帧、使用指定的着色器程序从这些输入渲染矩形图像(四边形)并将该图像提供给其所有目标所需的许多内部代码。 BasicOperation 的变体(例如 TextureSamplingOperation 或 TwoStageOperation)为着色器程序提供了某些类型的操作可能需要的其他信息。

要构建一个简单的一输入滤镜,您甚至可能不需要创建自己的子类。 您所需要做的就是在实例化 BasicOperation 时提供一个片段着色器和所需的输入数量

let myFilter = BasicOperation(fragmentShaderFile:MyFilterFragmentShaderURL, numberOfInputs:1)

着色器程序由匹配的顶点和片段着色器组成,这些着色器被编译并链接在一起形成一个程序。 默认情况下,该框架使用一系列基于输入图像数量的库存顶点着色器来输入到操作中。 通常,您只需要提供用于执行过滤或其他处理的自定义片段着色器。

GPUImage 使用的片段着色器如下所示

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;
uniform lowp float gamma;

void main()
{
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    
    gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);
}

纹理坐标的命名约定是 textureCoordinate、textureCoordinate2 等作为 varyings 从顶点着色器提供。 inputImageTexture、inputImageTexture2 等是表示传递到着色器程序的每个图像的纹理。 可以定义 Uniforms 来控制您正在运行的任何着色器的属性。 您需要提供两个片段着色器,一个用于具有精度限定符的 OpenGL ES,另一个用于没有精度限定符的 OpenGL。

在框架内部,一个自定义脚本将这些着色器文件转换为内联字符串常量,以便将它们与编译后的框架捆绑在一起。 如果你向框架添加新的操作,你需要运行

./ShaderConverter.sh *

在 Operations/Shaders 目录下,以更新这些内联常量。

分组操作

如果你希望将一系列操作分组为一个单元来传递,你可以创建一个 OperationGroup 的新实例。OperationGroup 提供了一个 configureGroup 属性,该属性接受一个闭包,该闭包指定应如何配置组

let boxBlur = BoxBlur()
let contrast = ContrastAdjustment()

let myGroup = OperationGroup()

myGroup.configureGroup{input, output in
    input --> self.boxBlur --> self.contrast --> output
}

传入 OperationGroup 的帧由上述闭包中的输入表示,而从整个组输出的帧由输出表示。 设置完成后,上面的 myGroup 将像任何其他操作一样显示,即使它由多个子操作组成。 然后,可以将此组像单个操作一样传递或使用。

与 OpenGL / OpenGL ES 交互

GPUImage 可以分别通过使用 TextureOutput 和 TextureInput 类从 OpenGL (ES) 导出和导入纹理。 这使你可以从渲染到具有绑定纹理的帧缓冲区对象的 OpenGL 场景录制电影,或过滤视频或图像,然后将其作为纹理馈送到 OpenGL 中以在场景中显示。

这种方法的一个注意事项是,这些过程中使用的纹理必须通过共享组或类似的东西在 GPUImage 的 OpenGL (ES) 上下文与任何其他上下文之间共享。

常用类型

该框架使用几种平台无关的类型来表示常用值。 通常,浮点输入以 Floats 的形式接收。 大小使用 Size 类型指定(通过使用宽度和高度初始化来构造)。 颜色通过 Color 类型处理,你需要为红色、绿色、蓝色以及可选的 alpha 分量提供归一化到 1.0 的颜色值。

可以在 2-D 和 3-D 坐标中提供位置。 如果仅通过指定 X 和 Y 值来创建 Position,它将被处理为 2-D 点。 如果还提供了可选的 Z 坐标,它将被视为 3-D 点。

矩阵有 Matrix3x3 和 Matrix4x4 两种类型。 这些矩阵可以使用浮点数的行优先数组构建,或者(在 Mac 和 iOS 上)可以从 CATransform3D 或 CGAffineTransform 结构初始化。

内置操作

目前,框架中内置了 100 多个操作,分为以下几类

图像生成器

颜色调整

图像处理

混合模式

视觉效果