跳转到内容

使用 Kernel#

本主题介绍什么是 kernel 以及如何在 VisualApplets 中使用 kernel 进行邻域操作,例如 Noise Reduction、模糊和边缘检测。

阅读完本主题后,您将了解如何构建 kernel、使用滤波器运算符对其进行处理,并在以下操作之前将其转换回单个像素 DmaToPC.

Kernel 结构#

kernel 是图像中某个位置周围相邻像素值的小窗口。许多滤波器(如 Noise Reduction、模糊、边缘检测)都是根据该邻域计算新的输出值。

对于 3×3 kernel,窗口包含九个值:该位置的当前像素加上八个邻域像素。对于奇数大小的 kernel,当前像素位于窗口的中心。

Kernel 在图像上从左到右、逐行移动。在每个位置,滤波器都会使用该像素周围的邻域:

3×3 Kernel 窗口在 9×9 图像上移动

在 VisualApplets 中,该邻域由 link properties 定义。如何构建窗口以及当前像素在其中的位置取决于算子。相关详细信息见下文。

Kernel Columns 和 Kernel Rows#

Kernel 大小是 link properties. KernelColumns 是邻域窗口的宽度。 KernelRows 是其高度。因此,3×3 kernel 使用 KernelColumns = 3 且 KernelRows = 3.

Kernel Size 是一个 Link Property

在 VisualApplets 中,N 是 kernel 行数,M 是 kernel 列数。Kernel 元素通过索引而不是坐标进行寻址。索引按逐行顺序从左上角运行到右下角。

请务必查看运算符文档,了解 kernel 索引到图像位置的精确映射。

并非每个算子都接受或转发大于 1×1 的 kernel。每个算子都在其 Supported Link Format 表格中记录了允许的值,位于 Operator Reference中。如果将 kernel 链路连接到仅允许 Kernel Columns = 1 和 Kernel Rows = 1的运算符,VisualApplets 将报告 link format 冲突。

有关所有 link properties 的概述,请参见 Parameterization 教程主题。

Kernels 与 Parallelism#

Parallelism 和 kernels 都会增加链路在每个时钟周期内传输的数据量,但它们描述的是不同的概念:

Parallelism Kernel
问题 每个时钟周期通过多少个独立像素? 一个位置属于多少个相邻值?
典型用途 匹配相机 / DMA 带宽 提供需要邻域的滤波器
Link properties Parallelism KernelColumns, KernelRows

你可以将 Parallelism 保持为 1,并通过使用 kernel 在每个时钟周期传输更多数据:Link 传输一个邻域(例如 3×3 个值)作为单个数据字。每个时钟周期的数据量增加是因为每个时钟周期传输的值更多,而不是因为每个时钟周期处理了更多新的图像位置。Parallelism 教程主题中的像素速率带宽公式不乘以 kernel 大小。

您也可以将两者结合使用。运算符 CastKernel 可以在输入和输出之间重新解释 Parallelism 和 kernel 大小的乘积,只要该乘积保持不变即可。例如,Parallelism 为 1 且 kernel 为 3×2 可以变为 Parallelism 为 6 且 kernel 为 1×1。

Cast Kernel 转换 Kernel 和 Parallel Components

有关详细信息,请参阅算子参考中的 CastKernel 以及 Parallelism 教程主题。

构建 Kernels#

要从正常的像素流创建邻域数据,请使用在其上设置 kernel 大小的算子 输出 Link。 FIRkernelNxM 构建一个居中的 2D 邻域窗口。 LineNeighboursNx1 和 PixelNeighbours1xM 属于特例:它们收集先前的行或先前的像素,而不是居中的窗口。这些算子在当前像素所在位置、算子类型、FPGA 资源消耗和延迟以及边缘处理(见下文)方面各有不同。请选择符合您用例的算子:居中的 2D 滤波器邻域、跨先前行或像素的比较,或是完整 FIR 样式窗口的高资源效率替代方案。

FIRkernelNxM#

算子 FIRkernelNxM (Library Filter)在每个像素周围创建一个矩形 Kernel 窗口,包括相邻像素。尽管其名称如此,它 不 实现 FIR 滤波器。它准备滤波器算子所需的 Kernel 数据。对于奇数 Kernel 大小,当前像素位于 中心 Kernel 的。对于偶数大小的 Kernel,没有确切的中心;中心位置向左上方偏移。

使用 3×3 Average Filter 进行平滑处理:

使用 FIRkernelNxM 进行 3×3 平均滤波器平滑

该设计构建了一个居中的 3×3 邻域, FIRkernelNxM (Filterkernel),将其与 FIRoperatorNxM (Average)求平均值,然后将结果缩放并转换回 8 位。仿真探针会比较平均前后的像素值:尖锐的局部峰值变得更平滑、更均匀。

您可以通过编辑输出链接来定义 Kernel 大小(Kernel Columns 和 Kernel Rows)。算子会缓冲所需的像素,直到完整的邻域可用。在图像边界处,可能会缺少邻居。参数 EdgeHandling 选择如何填充缺失值(EdgeMirrored 或 EdgeConstant)。

FIRkernelNxM 是一个 M 型 算子。构建像 3×3 这样的居中邻域需要进行缓冲,因此比下面的行或像素邻居算子消耗更多 FPGA 资源并增加更多延迟。使用 FIRkernelNxM 当滤波器真正需要 居中的 2D 邻域时 — 例如在 FIRoperatorNxM 以实现模糊、平均或边缘检测。

有关该算子的详细信息,请参阅算子参考中的 FIRkernelNxM。

LineNeighboursNx1 和 PixelNeighbours1xM#

LineNeighboursNx1 和 PixelNeighbours1xM 属于特例。它们不构建通常意义上的居中 Kernel。

LineNeighboursNx1 创建一个包含 N 行和 1 列的 Kernel。当前像素位于 Kernel 索引 0;其他元素是来自先前行(上方)的像素。 PixelNeighbours1xM 创建一个 1 行和 M 列的 kernel。当前像素位于 Kernel 索引 0;其他元素是同行的先前像素(在左侧)。

与不同, FIRkernelNxM,这些算子 不 以当前像素为中心,不提供尚未处理的“未来”邻域。它们只查看 过去 (先前的行或先前的像素)。它们是 O 型 算子,不会增加缓冲中心邻域所产生的行或像素延迟。这使得它们的结构更简单,且在 FPGA 资源消耗上通常比 FIRkernelNxM更省资源。您仍然可以形成更大的邻域(例如通过将两个算子组合成一个 3×3 窗口),但在行或图像的开头,过去的邻居尚不可用,因此这些 Kernel 元素会保持为空,直到收集到足够多的历史数据。图像边界之外的缺失像素将设置为参数 Constant;不存在镜像边缘处理。

典型用例:

  • 比较或缩减 先前行 并分别带有 LineNeighboursNx1 — 例如查找当前像素及其上方行的最小像素值:

    上的值

  • 比较或缩减 同行的先前像素 并分别带有 PixelNeighbours1xM.

    使用 PixelNeighbours1xM 比较像素值

  • 输入诸如以下内容的精简算子: MIN 或 MAX:这些算子在输入端期望接收一个 kernel,从邻域中选择一个值(例如最小的像素),并输出一个 1×1 结果。在该步骤之后,kernel 会被再次解析。

    相邻行之间的最小像素值:

    使用 LineNeighboursNx1 获取相邻行的最小像素值

    相邻像素之间的最大像素值:

    使用 PixelNeighbours1xM 获取相邻像素的最大像素值

要构建二维 kernel,您可以将以下内容组合起来: LineNeighboursNx1 后面紧跟 PixelNeighbours1xM.

有关这些算子的详细信息,请参阅算子参考中的 LineNeighboursNx1 以及算子参考中的 PixelNeighbours1xM。

ExpandToKernel#

算子 ExpandToKernel 将单个像素输入扩展为任意 kernel 大小。它 复制 输入值到每个 kernel 组件。您可以在输出 link 上定义新的 kernel 大小。

使用 ExpandToKernel 当下游算子在输入端期望接收一个 kernel,但您只有一个 单个 像素值时(例如阈值、均值或参考像素)。常见的模式是扩展该常量或参考值,使其与来自以下源的现有邻域相匹配: FIRkernelNxM, LineNeighboursNx1或 PixelNeighbours1xM。然后,您可以应用诸如以下内容的算子: CMP, SUB或 ADD 在整个 kernel 上按元素进行处理(例如将每个邻居与相同的阈值进行比较)。

使用 ExpandToKernel 将邻域值与阈值进行比较

有关该算子的详细信息,请参阅算子参考中的 ExpandToKernel。

处理 Kernels#

构建 kernel 后,您可以对其进行滤波、提取子集、合并或拆分 kernel 流,或者重新排序 kernel 组件。本节中的算子用于处理现有的 kernel。在 DmaToPC,您必须将内核减小至 1×1 — 参见 连接到 DMA.

FIRoperatorNxM#

算子 FIRoperatorNxM 将每个 kernel 元素与系数相乘并对乘积求和。您可以使用以下内容定义这些系数: Coefficients 参数。

算子的输出 FIRoperatorNxM 的 kernel 大小为 1×1,这意味着每个位置有一个经过滤波的像素值。使用 FIRkernelNxM, LineNeighboursNx1或 PixelNeighbours1xM 来生成所需的输入 kernel。

当链路上已有邻域且每个位置需要一个经过滤波的输出像素时,请将此算子用于经典的 FIR 风格滤波器(均值、高斯、Sobel 及类似滤波器):

klzzwxh:0096 Sobel X 滤波器中的算子

屏幕截图显示了 VisualApplets 随附的设计示例 Examples\Processing\Filter\EdgeDetection\SobelGradientX。

有关该算子的详细信息,请参阅算子参考中的 FIRoperatorNxM。

SelectSubKernel#

即: SelectSubKernel 算子用于提取输入 kernel 的矩形子集。其参数 FirstROW 和 FirstCOL 定义了子集的偏移量。新的 kernel 大小在输出链路上设置。

使用 SelectSubKernel 您提取了 中心 内核元素(通常是该图像位置的原始像素)— 当您不再需要完整的邻域时。

一个典型的用例是应用滤波器,同时仍保留 原始 图像。在此之后 FIRkernelNxM 构建了邻域之后,您将分支内核链路。一条路径计算滤波后的结果。另一条路径使用 SelectSubKernel 来提取 1×1 子内核——即该图像位置处的原始像素。这样,您便同时拥有了滤波后的数据和未滤波的图像。

在滤波器旁边保留原始图像数据:

使用 SelectSubKernel 在滤波器旁边保留原始图像数据

该设计使用 FIRkernelNxM构建 3×3 邻域,然后进行分支。上层路径使用 SelectSubKernel 来提取原始像素(1×1 子内核)。下层路径应用 FIRoperatorNxM 作为 Sobel 滤波器。

有关该算子的详细信息,请参阅算子参考中的 SelectSubKernel。

MergeKernel 和 SplitKernel#

算子 MergeKernel 将多个输入链路合并为一个输出内核。为此,其中一个内核维度(行或列)必须在所有输入上匹配;另一个维度的输入可以不同。带有 Alignment 值 horizontal 或 vertical 的参数定义了如何串联输入。

相反的算子是 SplitKernel. SplitKernel 它将 N×M 内核拆分为 N×M 个单独的输出链路,每个链路的内核大小为 1×1。输出的数量必须等于内核组件的数量,并且在其实例化算子时是固定的。

一个典型的用例是将 同一图像的几种变体 (例如不同的灰度或处理版本)组合到一个内核中,以便您可以对它们进行比较或一起处理。使用 SplitKernel 。

结合使用时, MergeKernel 和 SplitKernel 还可以替代原本可能需要 SYNC 算子输出链路上的 Bit Width: SYNC 的设计,后者通常在其输入上需要缓冲区,因此会消耗 FPGA 资源。当流可以作为内核组件承载时,合并和拆分内核通常是更节省资源的方法。

仅将此模式用于 同一图像的几种变体 它们仍共享一个公共源,例如相机流的两个不同处理路径。请 不 使用 MergeKernel 来合并两个独立的相机。独立的相机仍然是异步源,并且仍然需要 SYNC (或具有异步输入的另一个算子)和缓冲区。参见 算子类型与同步 教程主题。

合并异步路径时的同步错误:

合并异步路径时的同步错误

在这里,两个分支以不同的方式处理同一图像,然后馈送至一个共同的 CMP operator. 由于一条路径上的 M 型算子使分支变为异步状态,VisualApplets 会报告同步错误(紫色连线)——您无法直接连接它们。此示例仍然是一个相机,而不是两个独立的相机。

通过缓冲区和 SYNC 解决同步错误:

通过缓冲区和 SYNC 解决同步错误

一种有效的修补方法是在每个路径上放置一个 ImageFifo (或另一个缓冲区),然后使用 SYNC 在……之前 CMP重新同步。这很管用,但额外的缓冲区会消耗 FPGA 资源。

通过 MergeKernel 和 SplitKernel 解决同步错误:

通过 MergeKernel 和 SplitKernel 解决同步错误

资源高效的替代方案是将两个变体作为内核组件承载: MergeKernel 将两条路径组合为一个内核连线,处理在该连线上继续,并且 SplitKernel 在 CMP之前再次分离组件。 SYNC 并且不需要额外的同步缓冲区。

有关同步和同步错误的详细信息,请参阅算子类型和同步教程主题。

有关运算符的详细信息,请参阅运算符参考中的 MergeKernel 和运算符参考中的 SplitKernel。

CastKernel#

CastKernel 重新组织传入数据的 Parallelism 和内核大小。您可以在输出连线上更改内核大小。内核行数、内核列数和 Parallelism 的乘积在输入和输出端必须完全相同。

使用 CastKernel 您可以将 Parallelism 转换为 kernel。例如,具有 1×1 kernel 的 Parallelism 48 会变为具有 48 个 kernel 列的 Parallelism 1。相同的 48 个像素值仍然在单个时钟周期内移动,但它们现在成为了 kernel 组件,而不是并行像素。这在仅允许特定 Parallelism 的 Memory 运算符之前非常有用。

在内存算子之前将 Parallelism 转换为内核:

使用 CastKernel 将 Parallelism 48 转换为 48 列 Kernel

在屏幕截图中, PARALLELup 将 Parallelism 提升至 48(内核 1×1)。 CastKernel 将其重新解释为具有 48 个内核列的 Parallelism 1,以便后续的内存算子可以接收该数据。

有关该运算符的详细信息,请参阅运算符参考中的 CastKernel。

KernelRemap#

KernelRemap 重新映射输入和输出之间的内核组件。内核尺寸保持不变。通过 SourceSelect 参数,您可以为每个输出内核组件分配一个输入内核索引。

使用此算子,您可以镜像或旋转内核内容、将一个输入元素映射到多个输出,或丢弃未使用的输入元素。

使用 KernelRemap 交换内核列:

使用 KernelRemap 交换 Kernel 列

屏幕截图中的设计展示了如何 KernelRemap 交换 kernel columns:第 0 列与第 3 列互换位置(内部列随之移动)。映射关系来自 SourceSelect 参数,此处为 0, 1, 2, 3 → 3, 2, 1, 0。仿真探针显示了重映射前后的 kernel 组件。

有关该运算符的详细信息,请参阅运算符参考中的 KernelRemap。

连接到 DMA#

原始 kernel 链路必须 不 直接进入 DmaToPC. DmaToPC 仅接受 Kernel Columns = 1 和 Kernel Rows = 1.

因此,务必在 kernel 输出和 DmaToPC之间放置以下算子之一。这些算子将 kernel 转换为单像素或可进一步缩减的较小 kernel:

  • FIRoperatorNxM
  • SelectSubKernel
  • SplitKernel
  • MIN 或 MAX
  • Median

示例设计#

典型的滤波路径如下所示:

Filter Operation

  1. 相机输出单像素(1×1 kernel)。
  2. 诸如 LineBuffer 或 ImageBuffer之类的缓冲区可将相机与处理过程解耦,并提供邻域算子所需的行数据。
  3. FIRkernelNxM 在其输出链路上从相邻像素构建一个 kernel,例如 3×3 或 5×5。
  4. FIRoperatorNxM 应用滤波系数并在每个位置输出一个像素。
  5. DmaToPC 将单像素数据流传输至主机。

详细的滤波器示例(例如均值滤波、高斯滤波、Sobel、自适应阈值等)列在运算符参考中 FIRkernelNxM 和运算符参考中 FIRoperatorNxM 的 Examples of Use 部分中。

设计指南

在链路上创建大于 1×1 的 kernel 之前,请务必检查 Supported Link Format 的后续运算符。尤其是在 DmaToPC之前,需将 kernel 缩减为 1×1。