阅读 147

iOS 核心图像教程:自定义过滤器

Core Image是一个强大而高效的图像处理框架。您可以使用框架提供的内置过滤器创建漂亮的效果,也可以创建自定义过滤器和图像处理器。您可以调整颜色、几何形状并执行复杂的卷积。

制作漂亮的滤镜是一门艺术,最伟大的艺术家之一是列奥纳多·达·芬奇 (Leonardo da Vinci)。在本教程中,您将为达芬奇的名画添加一些有趣的元素。

在此过程中,您将:

  • 了解 Core Image 的类和内置过滤器。
  • 使用内置过滤器创建过滤器。
  • 使用自定义颜色内核转换图像的颜色。
  • 使用自定义扭曲内核转换图像的几何形状。
  • 学习调试 Core Image 问题。

注意:由于 Apple 错误,本教程不适用于 Xcode 13 和 iOS 15。您现在必须使用 Xcode 12。

准备好你的画笔,哎呀,我的意思是你的 Xcode 准备好了。是时候潜入 Core Image 的奇妙世界了!

入门

RayVinci 入门

您将看到达芬奇最著名的四部作品。点击一幅画会打开一张纸,但图像的输出是空的。

在本教程中,您将为这些图像创建过滤器,然后查看在输出中应用过滤器的结果。

向下滑动以关闭工作表。接下来,点击右上角的过滤器列表

该按钮应显示可用内置过滤器的列表。但是等等,它目前是空的。你接下来会解决这个问题。:]

介绍核心图像类

在填充过滤器列表之前,您需要了解 Core Image 框架的基本类。

  • CIImage:表示准备好处理或由 Core Image 过滤器生成的图像。一个CIImage对象具有内所有图像的数据,但并不是真正的图像。这就像一个食谱,它包含了做一道菜的所有成分,但不是这道菜本身。

    您将在本教程后面看到如何渲染要显示的图像。

  • CIFilter:获取一张或多张图像,通过应用变换处理每张图像并产生 aCIImage作为其输出。您可以链接多个过滤器并创建有趣的效果。的对象CIFilters是可变的并且不是线程安全的。

  • CIContext:呈现过滤器的处理结果。例如,CIContext帮助从CIImage对象创建 Quartz 2D 图像。

要了解有关这些类的更多信息,请参阅Core Image 教程:入门

现在您已经熟悉了 Core Image 类,是时候填充过滤器列表了。

获取内置过滤器列表

打开RayVinci并选择FilterListView.swift。替换filterListFilterListView用:

让filterList =  CIFilter .filterNames(inCategory: nil ) 
复制代码

在这里,您通过使用filterNames(inCategory:)nil作为类别传递来获取 Core Image 提供的所有可用内置过滤器的列表。您可以查看可用的类别列表中CIFilter开发者文档

打开FilterDetailView.swift。替换Text("Filter Details")body用:

// 1 
if  let ciFilter =  CIFilter (name: filter) { 
  // 2 
  ScrollView { Text (ciFilter.attributes.description) } } else { 
  // 3
  文本(“未知过滤器!”) }
复制代码

在这里,你:

  1. ciFilter使用过滤器名称初始化过滤器 。由于名称是一个字符串并且可能拼写错误,因此初始化程序返回一个可选的。因此,您需要检查过滤器是否存在。
  2. 您可以使用 来检查过滤器的各种属性attributes。如果过滤器存在,您将在此处创建ScrollView并填充Text视图中的属性描述。
  3. 如果过滤器不存在或未知,您将显示一个Text解释该情况的视图。

构建并运行。点按过滤器列表。哇,过滤器太多了!

点按任何过滤器以查看其属性。

获取可用过滤器列表

很了不起,不是吗?你才刚刚开始!在下一部分中,您将使用其中一个内置滤镜让阳光照在“蒙娜丽莎”上。:]

使用内置过滤器

现在您已经看到了可用过滤器的列表,您将使用其中一个来创建一个有趣的效果。

打开ImageProcessor.swift。在顶部,在类声明之前,添加:

enum  ProcessEffect  {  case builtIn case colorKernel case warpKernel case blendKernel }
复制代码

在这里,您声明ProcessEffectenum. 它包含您将在本教程中使用的所有过滤器案例。

将以下内容添加到ImageProcessor

// 1 
private  func  applyBuiltInEffect ( input : CIImage ) { 
    // 2 
  let noir =  CIFilter ( 名称: "CIPhotoEffectNoir" , 参数:[ “输入图像”:输入] ) ? .outputImage 
    // 3 
  let sunGenerate =  CIFilter ( name: "CISunbeamsGenerator" , 参数: [
      "inputStriationStrength" : 1 , "inputSunRadius" : 300 , "inputCenter" : CIVector ( x: input.extent.width - input.extent.width /  5 , y: input.extent.height - input.extent.height /  10 ) ]) ? .outputImage
  // 4 
  let CompositeImage = input.applyingFilter( "CIBlendWithMask" , 参数: [ kCIInputBackgroundImageKey: noir as  Any , kCIInputMaskImageKey: sunGenerate as  Any ]) }
复制代码

在这里,你:

  1. 声明一个将 aCIImage作为输入并应用内置过滤器的私有方法。
  2. 您可以通过创建一个黑暗的,穆迪开始黑色的使用效果CIPhotoEffectNoirCIFilter以字典的形式将字符串作为名称和参数。您从 中获取生成的过滤图像outputImage
  3. 接下来,您使用CISunbeamsGenerator. 这将创建一个阳光遮罩。在参数中,您设置:
    • inputStriationStrength:表示阳光的强度。
    • inputSunRadius:表示太阳的半径。
    • inputCenter:阳光中心的 x 和 y 位置。在这种情况下,您将位置设置在图像的右上角。
  4. 在这里,您可以使用CIBlendWithMask. 您可以input通过将结果设置CIPhotoEffectNoir为背景图像和sunGenerate蒙版图像来应用过滤器。此组合的结果是CIImage.

ImageProcessor具有output,一个已发布的属性,它是UIImage. 您需要将合成结果转换为 aUIImage以显示它。

在 中ImageProcessor,添加以下内容@Published var output = UIImage()

让上下文=  CIContext () 
复制代码

在这里,您创建一个CIContext所有过滤器都将使用的实例。

将以下内容添加到ImageProcessor

ivate  func  renderAsUIImage ( _  image : CIImage ) -> UIImage ? { if  let cgImage = context.createCGImage(image, from: image.extent) { return  UIImage (cgImage: cgImage) }
  返回 零 }
复制代码

在这里,您用于context创建CGImagefrom的实例CIImage

使用cgImage,然后创建一个UIImage. 用户将看到此图像。

显示内置过滤器的输出

将以下内容添加到末尾applyBuiltInEffect(input:)

outputImage = renderAsUIImage(compositeImage) { 输出=输出图像 }
复制代码

这会将compositeImagea转换CIImageUIImageusing renderAsUIImage(_:)。然后将结果保存到output.

将以下新方法添加到ImageProcessor

// 1 
func  process ( Painting : Painting , effect : ProcessEffect ) { 
    // 2 
  guard 
    let paintImage =  UIImage (named: Painting.image), let input =  CIImage (image:paintImage) else { print ( "Invalid input image" ) 返回 }
  switch effect { 
    // 3 
  case .builtIn: applyBuiltInEffect(输入:输入)
  默认值: 打印(“不支持的效果”) } }
复制代码

在这里,你:

  1. 创建一个方法作为 的入口点ImageProcessor。它需要一个实例Painting和一个effect来应用。
  2. 检查有效图像。
  3. 如果效果类型为.builtIn,则调用applyBuiltInEffect(input:)以应用过滤器。

打开PaintWall.swift。下面selectedPainting = paintings[index]在 的action闭包中Button添加:

var effect =  ProcessEffect .builtIn
    if  let Painting = selectedPainting { 
        switch index { 
        case  0 : 
            效果= .builtIn 
        默认: 
            效果= .builtIn 
        }
  ImageProcessor .shared.process(绘画:绘画,效果:效果) 
  }
复制代码

在这里,您将effect.builtIn为第一张画。您还将其设置为默认效果。然后,通过调用应用过滤器process(painting:, effect:)ImageProcessor

构建并运行。点击*“蒙娜丽莎”*。您将看到在输出中应用了一个内置过滤器!

应用内置过滤器

让蒙娜丽莎阳光普照的伟大工作。难怪她在笑!现在是使用CIKernel创建过滤器的时候了。

认识 CIKernel

使用CIKernel,您可以放置​​称为kernel 的自定义代码,以逐个像素地操作图像。GPU 处理这些像素。您使用Metal Shading Language编写内核,与较旧的Core Image Kernel Language相比,它具有以下优势,自 iOS 12 起已弃用:

  • 支持 Core Image 内核的所有强大功能,如连接和平铺。
  • 在构建时预编译,带有错误诊断。这样,您无需等待运行时出现错误。
  • 提供语法高亮和语法检查。

有不同类型的内核:

  • CIColorKernel:改变像素的颜色但不知道像素的位置。
  • CIWarpKernel:改变像素的位置但不知道像素的颜色。
  • CIBlendKernel:以优化的方式混合两个图像。

要创建和应用内核,您需要:

  1. 首先,向项目添加自定义构建规则。
  2. 然后,添加 Metal 源文件。
  3. 加载内核。
  4. 最后,初始化并应用内核。

image.png

接下来,您将实施这些步骤中的每一个。准备好享受有趣的旅程吧!

创建构建规则

您需要编译 Core Image Metal 代码并将其与特殊标志链接。

image.png

在项目导航器中选择RayVinci目标。然后,选择构建规则选项卡。单击*+*添加新的构建规则。

然后,设置第一个新的构建规则:

  1. 进程设置为名称匹配的源文件: . 然后将**.ci.metal*设置为值。
  2. 取消选中Run once per architecture
  3. 添加以下脚本:
xcrun metal -c -I $MTL_HEADER_SEARCH_PATHS -fcikernel "${INPUT_FILE_PATH}" \ -o "${SCRIPT_OUTPUT_FILE_0}"
复制代码
这会使用所需的*-fcikernel*标志调用 Metal 编译器。
复制代码
  1. 输出文件中添加以下内容:
$(DERIVED_FILE_DIR)/${INPUT_FILE_BASE}.air
复制代码
这会产生一个以*.ci.air*结尾的输出二进制文件。
复制代码

image.png 接下来,再次单击*+*添加另一个新的构建规则。

对于第二个新构建规则,请按照以下步骤操作:

  1. 进程设置为名称匹配的源文件: . 然后将**.ci.air*设置为值。
  2. 取消选中Run once per architecture
  3. 添加以下脚本:
xcrun metallib -cikernel "${INPUT_FILE_PATH}" -o "${SCRIPT_OUTPUT_FILE_0}"
复制代码
这将使用所需的*-cikernel*标志调用 Metal 链接器。
复制代码
  1. 输出文件中添加以下内容:
$(METAL_LIBRARY_OUTPUT_DIR)/$(INPUT_FILE_BASE).metallib
复制代码
这会在应用程序包中生成一个以*.ci.metallib*结尾的文件。
复制代码

image.png 接下来,是时候添加 Metal 源了。

添加金属源

首先,您将为颜色内核创建一个源文件。在Project Navigator中,突出RayVinci权下RayVinci项目。

右键单击并选择新建组。将此新组命名为Filters。然后,突出显示该组并添加一个名为ColorFilterKernel.ci.metal的新 Metal 文件。

打开文件并添加:

// 1 
#包含 <CoreImage/CoreImage.h>

// 2 
extern  "C" { 命名空间核心图像{
    // 3 float4 colorFilterKernel(sample_t s){
      // 4 float4 交换颜色; 交换颜色.r = sg; 交换颜色.g = sb; 交换颜色.b = sr; swappedColor.a = sa;
      返回交换颜色; } } }
复制代码

这是代码分解:

  1. 包含 Core Image 标头可让您访问框架提供的类。这会自动包含 Core Image Metal 内核库CIKernelMetalLib.h
  2. 内核需要位于extern "C"外壳内,以便在运行时可以通过名称访问它。接下来,您指定 的命名空间coreimage。您在coreimage命名空间中声明所有扩展以避免与 Metal 发生冲突。
  3. 在这里,您声明colorFilterKernel,它接受类型为的输入sample_tsample_t表示来自输入图像的单个颜色样本。colorFilterKernel返回float4表示像素的 RGBA 值的 。
  4. 然后,您声明一个新的float4swappedColor并交换输入样本中的 RGBA 值。然后返回具有交换值的样本。

接下来,您将编写代码来加载和应用内核。

加载内核代码

要加载和应用内核,首先要创建CIFilter.

在过滤器组中创建一个新的 Swift 文件。将其命名为ColorFilter.swift并添加:

// 1
导入CoreImage

class  ColorFilter : CIFilter  { // 2 
  var inputImage: CIImage ?

  // 3 
  static  var kernel: CIKernel  = { () -> CIColorKernel  in 
    guard  let url =  Bundle .main.url( forResource: "ColorFilterKernel.ci" , withExtension: "metallib" ), let data =  try?  数据(contentsOf:url)else { fatalError(“无法加载metallib”) }

    守卫 让内核= 尝试? CIColorKernel ( 函数名称: "colorFilterKernel" , fromMetalLibraryData: data) else { fatalError ( "无法创建颜色内核" ) }

    返回内核 }()

  // 4
  覆盖 var outputImage: CIImage ? { guard  let inputImage = inputImage else { return  nil } return  ColorFilter .kernel.apply( 范围:inputImage.extent, roiCallback:{ _,RECT的
        回报矩形 }, 参数:[输入图像]) } }
复制代码

在这里,你:

  1. 首先导入 Core Image 框架。

  2. 子类化CIFilter包括两个主要步骤:

    • 指定输入参数。在这里,您使用inputImage.
    • 覆盖outputImage.
  3. 然后,您声明一个静态属性kernel,用于加载ColorFilterKernel.ci.metallib的内容。这样,库只加载一次。然后CIColorKernel使用ColorFilterKernel.ci.metallib的内容创建一个实例。

  4. 接下来,你override outputImage。在这里,您使用apply(extent:roiCallback:arguments:). 该extent决定多少输入图像的被传递给内核。

    您传递了整个图像,因此过滤器将应用于整个图像。roiCallback确定rect渲染rectin所需的输入图像的outputImage。在这里,rectofinputImageoutputImage不会改变,因此您返回相同的值并将inputImage参数数组中的 传递给内核。

现在您已经创建了颜色内核过滤器,您将把它应用到图像上。

应用颜色内核过滤器

打开ImageProcessor.swift。将以下方法添加到ImageProcessor

private  func  applyColorKernel ( input : CIImage ) { 
  let filter =  ColorFilter () 
 filter.inputImage = input 
  if  let outputImage = filter.outputImage, 
   let renderImage = renderAsUIImage(outputImage) { 
  输出=渲染图像 
   } 
  }
复制代码

在这里,您声明applyColorKernel(input:). 这将 aCIImage作为输入。您可以通过创建 的实例来创建自定义过滤器ColorFilter

过滤器outputImage应用了颜色内核。然后创建一个UIImageusing实例renderAsUIImage(_:)并将其设置为输出。

接着,手柄.colorKernelprocess(painting:effect:)如下所示。在上面添加这个新案例default

案例.colorKernel:
  applyColorKernel(输入:输入)
复制代码

在这里,您调用applyColorKernel(input:)以应用自定义颜色内核过滤器。

最后,打开PaintingWall.swift。在's 闭包的switch正下方的语句中添加以下内容:case 0``Button``action

案例 1:
  效果= .colorKernel
复制代码

这将.colorKernel第二幅画的效果设置为 。

构建并运行。现在点击第二幅画*“最后的晚餐”*。您将看到应用的颜色内核过滤器和图像中交换的 RGBA 值。

颜色内核过滤器

很好!接下来,您将对达芬奇的神秘“救世主”创建酷炫的扭曲效果。

创建扭曲内核

与颜色内核类似,您将从添加 Metal 源文件开始。在过滤器组中创建一个名为WarpFilterKernel.ci.metal的新 Metal 文件。打开文件并添加:

# include  <CoreImage/CoreImage.h>
 //1 
extern  "C" {
  命名空间核心图像{
    //2
    float2 warpFilter(destination dest) {
      浮动y = dest.coord().y + tan(dest.coord().y / 10 ) * 20 ;
      浮动x = dest.coord().x + tan(dest.coord().x/ 10 ) * 20 ;
      返回float2(x,y);
    }
  }
}
复制代码

这是您添加的内容:

  1. 就像在颜色内核 Metal 源中一样,您包含 Core Image 标头并将该方法包含在一个extern "C"外壳中。然后指定coreimage命名空间。

  2. 接下来,您warpFilter(_:)使用 type 的输入参数进行声明destination,允许访问您当前正在计算的像素的位置。它返回输入图像坐标中的位置,然后您可以将其用作源。

    您可以使用 访问目标像素的 x 和 y 坐标coord()。然后,您应用简单的数学运算来转换坐标并将它们作为源像素坐标返回,以创建有趣的平铺效果。

    注意:尝试用in替换tan,你会得到一个有趣的失真效果!:] sin``warpFilter(_:)

加载扭曲内核

与您为颜色内核创建的过滤器类似,您将创建一个自定义过滤器来加载和初始化扭曲内核。

在过滤器组中创建一个新的 Swift 文件。将其命名为WarpFilter.swift并添加:

导入CoreImage

// 1
类 WarpFilter : CIFilter  {
   var inputImage: CIImage ?
  // 2 
  static  var kernel: CIWarpKernel  = { () -> CIWarpKernel  in 
    guard  let url =  Bundle .main.url(
      forResource: "WarpFilterKernel.ci" ,
      withExtension: "metallib" ),
     let data =  try?  数据(contentsOf:url)else {
       fatalError(“无法加载metallib”)
    }

    守卫 让内核= 尝试? CIWarp内核(
      函数名称: "warpFilter" ,
      fromMetalLibraryData: data) else {
       fatalError ( “无法创建扭曲内核” )
    }

    返回内核
  }()

  // 3
  覆盖 var outputImage: CIImage ? {
    守卫 让inputImage = inputImage else { return .none }

    返回 WarpFilter .kernel.apply(
      范围:inputImage.extent,
      roiCallback:{ _,RECT的
        回报矩形
      },
      图像:输入图像,
      参数:[])
  }
}
复制代码

在这里,你:

  1. 创建WarpFilterCIFilterwithinputImage作为输入参数的子类。
  2. 接下来,您声明静态属性kernel以加载WarpFilterKernel.ci.metallib的内容。然后创建一个CIWarpKernel使用 的内容的实例.metallib
  3. 最后,您通过覆盖outputImage. 在 中override,您将内核应用于inputImageusingapply(extent:roiCallback:arguments:)并返回结果。

应用扭曲内核过滤器

打开ImageProcessor.swift。将以下内容添加到ImageProcessor

private  func  applyWarpKernel ( input : CIImage ) {
   let filter =  WarpFilter ()
  filter.inputImage = input
   if  let outputImage = filter.outputImage,
     let renderImage = renderAsUIImage(outputImage) {
    输出=渲染图像
  }
}
复制代码

在这里,您声明applyColorKernel(input:),它将CIImage作为输入。然后创建一个WarpFilter和 set的实例inputImage

过滤器outputImage应用了扭曲内核。然后创建一个UIImageusing实例renderAsUIImage(_:)并将其保存到输出。

接下来,将以下案例添加到process(painting:effect:),下面case .colorKernel

案例.warpKernel:
  applyWarpKernel(输入:输入)
复制代码

在这里,您处理.warpKernel并调用applyWarpKernel(input:)应用扭曲内核过滤器的情况。

最后,打开PaintingWall.swift。添加下面的情况下,在switch右下面的语句case 1action

案例 2:
  效果= .warpKernel
复制代码

.warpKernel为第三幅画设置了效果。

构建并运行。点击 Salvator Mundi 的画作。你会看到一个有趣的基于扭曲的瓷砖效果应用。

image.gif 恭喜!您将自己的风格应用于杰作!;]

挑战:实现混合内核

CIBlendKernel是用于混合两个图像进行了优化。作为一个有趣的挑战,为CIBlendKernel. 一些提示:

  1. 创建一个CIFilter接受两个图像的子类:输入图像和背景图像。
  2. 使用内置的可用 CIBlendKernel内核。对于此挑战,请使用内置的乘法混合内核。
  3. 创建一个方法ImageProcessor,将混合内核过滤器应用于图像并将结果设置为输出。您可以使用项目资产中提供的multi_color图片作为滤镜的背景图片。此外,处理.blendKernel.
  4. 将此过滤器应用于PaintingWall.swift 中的第四张图片。

您将在下载的材料中找到在最终项目中实施的解决方案。祝你好运!

调试核心图像问题

了解 Core Image 如何渲染图像可以帮助您在图像未按预期显示时进行调试。最简单的方法是在调试时使用Core Image Quick Look

使用核心图像快速查看

打开ImageProcessor.swift。将断点你在哪里设置线条outputapplyColorKernel(input:)。构建并运行。点击*“最后的晚餐”*。

使用快速查看查看核心图像图

当您遇到断点时,将鼠标悬停在 上outputImage。您会看到一个显示地址的小弹出框。

单击眼睛符号。将出现一个窗口,显示制作图像的图形。很酷吧?

使用 CI_PRINT_TREE

CI_PRINT_TREE是基于与 Core Image Quick Look 相同的基础架构的调试功能。它有多种模式和操作。

选择和编辑RayVinci方案。选择Run选项卡并添加CI_PRINT_TREE作为值为7 pdf的新环境变量。

image.png

CI_PRINT_TREE的值采用以下形式graph_type output_type options

graph_type表示核心图像渲染的阶段。以下是您可以指定的值:

  • 1:显示颜色空间的初始图形。
  • 2 : 一个优化的图表,显示了 Core Image 是如何优化的。
  • 4:显示您需要多少内存的连接图。
  • 7:详细日志记录。这将打印所有上述图表。

对于output_type,您可以指定PDFPNG。它将文档保存到一个临时目录。

构建并运行。在模拟器中选择*“最后的晚餐”。现在,通过使用终端导航到/tmp*来打开 Mac 上的临时目录。

您将看到所有图形为 PDF 文件。打开以*_initial_graph.pdf*作为后缀的文件之一。

image.png

输入在底部,输出在顶部。红色节点代表彩色内核,而绿色节点代表扭曲内核。您还将看到每个步骤的投资回报率和范围。

文章分类
iOS