什么是 NPP ?

NVIDIA NPP 是一个函数库,用于执行 CUDA 加速的 2D 图像和信号处理。

该库的主要功能集侧重于图像处理,并广泛适用于这些领域的开发人员。NPP 将随着时间的推移而发展,以涵盖各种问题领域中更多计算密集型任务。NPP 库的编写旨在最大化灵活性,同时保持高性能。

NPP 可以通过以下两种方式之一使用:

  • 一个独立的库,用于以最小的努力为应用程序添加 GPU 加速。使用此方法,开发人员可以在数小时内将 GPU 加速添加到他们的应用程序中。

  • 一个协作库,用于与开发人员的 GPU 代码高效地互操作。

这两种方法都允许开发人员利用 NVIDIA GPU 的海量计算资源,同时缩短开发时间。阅读完本主页后,建议您阅读下面的“通用 API 约定”页面以及“图像处理特定 API 约定”页面或“信号处理特定 API 约定”页面,具体取决于您期望执行的处理类型。最后,如果您选择此页面顶部的“模块”选项卡,您可以找到支持您需求的 NPP 操作的可用函数类型。

什么是 NPP? general_conventions_lb nppi_conventions_lb npps_conventions_lb

文件

NPP API 在以下文件中定义:

头文件

  • npp.h

  • nppdefs.h

  • nppcore.h

  • nppi.h

  • npps.h

所有这些头文件都位于以下 CUDA 工具包目录中:

/include/

库文件

NPP 的功能分为 3 个不同的库组:

  • 一个核心库 (NPPC),包含来自 npp.h 头文件的基本功能以及其他两个库使用的通用功能。

  • 图像处理库 NPPI。来自 nppi.h 头文件或各种名为“nppi_xxx.h”的头文件的任何函数都捆绑到 NPPI 库中。

  • 信号处理库 NPPS。来自 npps.h 头文件或各种名为“npps_xxx.h”的头文件的任何函数都捆绑到 NPPS 库中。

在 Windows 平台上,NPP 存根库位于 CUDA 工具包的库目录中

/lib/nppc.lib
/lib/nppial.lib
/lib/nppicc.lib
/lib/nppidei.lib
/lib/nppif.lib
/lib/nppig.lib
/lib/nppim.lib
/lib/nppist.lib
/lib/nppisu.lib
/lib/nppitc.lib
/lib/npps.lib

匹配的 DLL 位于 CUDA 工具包的二进制目录中。例如:

* /bin/nppial64_111_<build_no>.dll  // Dynamic image-processing library for 64-bit Windows.

在 Linux 平台上,动态库位于 lib 目录中,名称包含主版本号和次版本号以及构建号

* /lib/libnppc.so.11.1.<build_no>   // NPP dynamic core library for Linux

库组织

注意:静态 NPP 库依赖于一个名为 cuLIBOS (libculibos.a) 的通用线程抽象层库,该库现在作为工具包的一部分分发。因此,当静态库被链接时,必须将 cuLIBOS 提供给链接器。为了最大限度地减少库加载和 CUDA 运行时启动时间,建议尽可能使用静态库。为了在使用动态库时提高加载和运行时性能,NPP 提供了一整套 NPPI 子库。仅链接到应用程序使用的函数所在的子库可以显着缩短加载时间和运行时启动性能。一些 NPPI 函数在内部调用其他 NPPI 和/或 NPPS 函数,因此您可能需要链接到一些额外的库,具体取决于您的应用程序进行的函数调用。NPPI 子库根据 NPPI 头文件的拆分方式分为几个部分。子库列表如下:

  • NPPC,NPP 核心库,链接任何应用程序时必须包含,函数在 nppCore.h 中列出,

  • NPPIAL,nppi_arithmetic_and_logical_operations.h 中的算术和逻辑运算函数,

  • NPPICC,nppi_color_conversion.h 中的颜色转换和采样函数,

  • NPPIDEI,nppi_data_exchange_and_initialization.h 中的数据交换和初始化函数,

  • NPPIF,nppi_filtering_functions.h 中的滤波和计算机视觉函数,

  • NPPIG,nppi_geometry_transforms.h 中找到的几何变换函数,

  • NPPIM,nppi_morphological_operations.h 中找到的形态学运算函数,

  • NPPIST,nppi_statistics_functions.h 和 nppi_linear_transforms.h 中的统计和线性变换,

  • NPPISU,nppi_support_functions.h 中的内存支持函数,

  • NPPITC,nppi_threshold_and_compare_operations.h 中的阈值和比较运算函数,

例如,在 Linux 上,要使用 NPP 针对动态库编译一个小型颜色转换应用程序 foo,可以使用以下命令:

nvcc foo.c  -lnppc -lnppicc -o foo

而要针对静态 NPP 库进行编译,则必须使用以下命令:

nvcc foo.c  -lnppc_static -lnppicc_static -lculibos -o foo

也可以使用原生主机 C++ 编译器。根据主机操作系统,链接行可能需要一些额外的库,如 pthread 或 dl。建议在 Linux 上使用以下命令:

g++ foo.c  -lnppc_static -lnppicc_static -lculibos -lcudart_static -lpthread -ldl 
-I <cuda-toolkit-path>/include -L <cuda-toolkit-path>/lib64 -o foo

NPP 是一个无状态 API,从 NPP 6.5 开始,NPP 在函数调用之间记住的唯一状态是当前流 ID,即在最近的 nppSetStream() 调用中设置的流 ID 以及关于该流的一些设备特定信息。默认流 ID 为 0。如果应用程序打算将 NPP 与多个流一起使用,则应用程序有责任使用下面描述的完全无状态的应用程序管理流上下文接口,或者在希望更改流 ID 时调用 nppSetStream()。任何不使用应用程序管理流上下文的 NPP 函数调用都将使用最近一次调用 nppSetStream() 设置的流,而 nppGetStream() 和其他不包含应用程序管理流上下文参数的 “nppGet” 类型函数调用也将始终使用该流。

所有 NPP 函数都应该是线程安全的。

注意:在 NPP 12.4 中,为了支持非常大的图像,NPP 12.4 及更高版本已将统计函数和少数其他函数的 GetBufferSize 调用的返回值从 int 更改为 size_t。信号长度也已从 int 更改为 size_t。这应该是使用这些版本的 NPP 重新构建应用程序所需的唯一更改。

注意:NPP 12.1 是最后一个支持不包含 NPP 流上下文参数的 NPP API 调用的 NPP 版本。此外,NPP 即将发布一个 API 变体,该变体提供许多 API 调用的折叠组合参数版本。例如,像 nppiAdd_8u_C3R_Ctx(pSrc1, nSrc1Step, pSrc2, nSrc2Step, pDst, nDstStep, oSizeROI, nppStreamCtx) 这样的调用将变为 nppiAdd_Ctx(NPP_8U, NPP_CH_3, pSrc1, nSrc1Step, pSrc2, nSrcStep2, pDst, nDstStep, oSizeROI, nppStreamCtx)。这使得添加对新数据类型和通道数的支持更简单,并显着减少冗余文档。

注意:NPP 11.6 的新功能是:

nppiContoursImageMarchingSquaresInterpolation_32f_C1R_Ctx
nppiContoursImageMarchingSquaresInterpolation_64f_C1R_Ctx

注意:NPP 10.1 的新功能是在 Volta 及更高版本的 GPU 架构中,在某些 NPP 图像处理函数中支持 fp16 (__half) 数据类型。支持 __half 数据类型像素的 NPP 图像函数具有 16f 类型的函数名称,并且需要将指向该数据类型像素的指针作为 NPP 数据类型 Npp16f 传递给 NPP。以下是如何将 __half 类型的图像指针传递给 NPP 16f 函数的示例,该函数应适用于包括 Armv7 在内的所有编译器。

nppiAdd_16f_C3R(reinterpret_cast<const Npp16f *>((const void *)(pSrc1Data)), nSrc1Pitch,
                reinterpret_cast<const Npp16f *>((const void *)(pSrc2Data)), nSrc2Pitch,
                reinterpret_cast<Npp16f *>((void *)(pDstData)),  nDstPitch,
                oDstROI);

应用程序管理的流上下文 应用程序管理的流上下文

注意:NPP 10.1 的另一个新功能是支持应用程序管理的流上下文。应用程序管理的流上下文使 NPP 在内部真正无状态,从而实现快速、无开销的流上下文切换。虽然建议所有新的 NPP 应用程序代码都使用应用程序管理的流上下文,但现有的应用程序代码可以继续使用 nppSetStream()nppGetStream() 来管理流上下文(现在也没有开销),但随着时间的推移,NPP 可能会弃用旧的非应用程序管理流上下文 API。新的和旧的流管理技术可以在应用程序中混合使用,但任何使用旧 API 的 NPP 调用都将使用最近一次调用 nppSetStream() 设置的流,并且 nppGetStream() 调用也将返回该流 ID。所有以 _Ctx 结尾的 NPP 函数名称都期望将应用程序管理的流上下文作为参数传递给该函数。新的 NppStreamContext 应用程序管理的流上下文结构在 nppdefs.h 中定义,应由应用程序初始化为 CUDA 设备 ID 和与特定流关联的值。应用程序可以使用多个固定流上下文,或者在要使用不同流时动态更改特定流上下文中的值。

注意:NPP 10.2 及更高版本在 NppStreamContext 结构中包含一个名为 nStreamFlags 的附加元素,该元素也必须由应用程序初始化。如果不这样做,可能会不必要地降低某些 NPP 函数的性能。

注意:对于在 WDDM 模式下工作的设备,NPP 在 Windows 上不支持非阻塞流。

请注意,一些 “GetBufferSize” 样式的函数现在具有与之关联的应用程序管理的流上下文,应与关联的应用程序管理的流上下文 NPP 函数将使用的流上下文相同。

请注意,NPP 对应用程序管理的流上下文结构中的参数进行最少的检查,因此应用程序有责任确保在将它们传递给 NPP 函数时它们是正确且有效的。

请注意,从 NPP 11.0 开始,NPP 已弃用 nppicom JPEG 压缩库,请改用 NVJPEG 库。

支持的 NVIDIA 硬件

NPP 在所有支持 CUDA 的 NVIDIA 硬件上运行。有关详细信息,请参阅 http://www.nvidia.com/object/cuda_learn_products.html

通用约定

内存管理

所有 NPP 函数的设计都遵循与其他 NVIDIA CUDA 库(如 cuFFT 和 cuBLAS)相同的准则。即,这些 API 中的所有指针参数都是设备指针。

此约定使各个开发人员能够就内存管理做出明智的选择,从而最大限度地减少内存传输次数。它还允许用户在 CUDA 运行时提供的各种内存传输机制(例如,同步或异步内存传输、零拷贝和固定内存等)方面具有最大的灵活性。

使用 NPP 处理数据的最基本步骤如下:

  1. 使用以下命令将输入数据从主机传输到设备:

    cudaMemCpy(...)
    
  2. 使用一个或多个 NPP 函数或自定义 CUDA 内核处理数据

  3. 使用以下命令将结果数据从设备传输到主机:

    cudaMemCpy(...)
    

暂存缓冲区和主机指针

NPP 的某些原语需要额外的设备内存缓冲区(暂存缓冲区)用于计算,例如信号和图像缩减(Sum、Max、Min、MinMax 等)。为了让 NPP 用户最大程度地控制内存分配和性能,用户有责任分配和删除这些临时缓冲区。这样做的好处之一是库不会在用户不知情的情况下分配内存。它还允许重复调用同一原语的开发人员仅分配一次暂存区,从而提高性能并减少潜在的设备内存碎片。

暂存缓冲区内存是非结构化的,可以以未初始化的形式传递给原语。这允许将相同的暂存缓冲区与任何需要暂存内存的原语重复使用,只要它的大小足够即可。

给定原语(例如 nppsSum_32f())的最小暂存缓冲区大小可以通过配套函数(例如 nppsSumGetBufferSize_32f())获得。缓冲区大小通过主机指针返回,因为暂存缓冲区的分配是通过 CUDA 运行时主机代码执行的。

调用信号求和原语并分配和释放必要暂存内存的示例:

// pSrc, pSum, pDeviceBuffer are all device pointers. 
Npp32f * pSrc; 
Npp32f * pSum; 
Npp8u * pDeviceBuffer;
size_t nLength = 1024;

// Allocate the device memroy.
cudaMalloc((void **)(&pSrc), sizeof(Npp32f) * nLength);
nppsSet_32f(1.0f, pSrc, nLength);  
cudaMalloc((void **)(&pSum), sizeof(Npp32f) * 1);

// Compute the appropriate size of the scratch-memory buffer, note that nBufferSize and nLength data types have changed from int to size_t. 
size_t nBufferSize;
nppsSumGetBufferSize_32f(nLength, &nBufferSize);
// Allocate the scratch buffer 
cudaMalloc((void **)(&pDeviceBuffer), nBufferSize);

// Call the primitive with the scratch buffer
nppsSum_32f(pSrc, nLength, pSum, pDeviceBuffer);
Npp32f nSumHost;
cudaMemcpy(&nSumHost, pSum, sizeof(Npp32f) * 1, cudaMemcpyDeviceToHost);
printf("sum = %f\n", nSumHost); // nSumHost = 1024.0f;

// Free the device memory
cudaFree(pSrc);
cudaFree(pDeviceBuffer);
cudaFree(pSum);

函数命名

由于 NPP 是 C API,因此不允许为不同的数据类型进行函数重载,因此 NPP 命名约定解决了区分同一算法或原语函数的不同变体但针对各种数据类型的需求。原语的不同变体的消除歧义是通过包含数据类型和其他消除歧义信息的后缀来完成的。

除了变体后缀外,所有 NPP 函数都以字母 “npp” 为前缀。属于 NPP 图像处理模块的原语在 npp 前缀中添加字母 “i”,即以 “nppi” 为前缀。类似地,信号处理原语以 “npps” 为前缀。

通用命名方案为:

npp<module info><PrimitiveName>_<data-type info>[_<additional flavor info>](<parameter list>)

数据类型信息使用与基本 NPP 数据类型相同的名称。例如,数据类型信息 “8u” 将表示原语对 Npp8u 数据进行操作。

如果原语消耗的数据类型与它产生的数据类型不同,则两种类型将按消耗到产生的数据类型顺序列出。

有关 “附加变体信息” 的详细信息,请针对每个 NPP 模块提供,因为每个问题域都使用不同的变体信息后缀。

整数结果缩放

NPP 信号处理和图像处理原语通常对整数数据进行操作。此整数数据通常是某些物理量(例如亮度)的定点小数表示。由于表示的这种定点性质,许多数值运算(例如加法或乘法)如果作为常规整数处理,则往往会产生超出原始定点范围的结果。

在结果超出原始范围的情况下,这些函数会将结果值钳制回有效范围。例如,16 位无符号整数的最大正值为 32767。4 * 10000 = 40000 的乘法运算将超出此范围。结果将被钳制为 32767。

为了避免因钳制而导致的信息丢失,大多数整数原语都允许结果缩放。具有结果缩放的原语在其名称中带有 “Sfs” 后缀,并提供一个参数 “nScaleFactor” 来控制缩放量。在将运算结果钳制到有效的输出数据范围之前,通过将它们与 \(2^{\mbox{-nScaleFactor}}\) 相乘。

示例:原语 nppsSqr_8u_Sfs() 计算信号(值的 1D 数组)中 8 位无符号样本值的平方。8 位值的最大值为 255。\(255^2 = 65025\) 的平方,如果不执行结果缩放,则将被钳制为 255。为了将最大值 255 映射到结果中的 255,需要指定整数结果缩放因子为 8,即将每个结果乘以 \(2^{-8} = \frac{1}{2^8} = \frac{1}{256}\)。信号值 255 平方并缩放的最终结果将是:

\[255^2\cdot 2^{-8} = 254.00390625\]
这将四舍五入为最终结果 254。

中等灰度值 128 将导致:

\[128^2 * 2^{-8} = 64\]

舍入模式

许多 NPP 函数需要将浮点值转换为整数。舍入模式 枚举列出了 NPP 支持的舍入模式。并非 NPP 中所有执行舍入作为其功能一部分的原语都允许用户指定使用的舍入模式。相反,它们使用 NPP 的默认舍入模式,即 NPP_RND_FINANCIAL。

舍入模式参数

NPP 函数的一个子集执行舍入作为其功能的一部分,它们允许用户通过 舍入模式 类型的参数来指定使用的舍入模式。

图像处理约定

函数命名

与图像处理相关的函数使用许多后缀来指示原语的各种不同变体,而不仅仅是不同的数据类型。变体后缀使用以下缩写:

  • “A” 如果图像是 4 通道图像,则表示结果 alpha 通道不受原语的影响。

  • “Cn” 图像由 n 通道打包像素组成,其中 n 可以是 1、2、3 或 4。

  • “Pn” 图像由 n 个独立的图像平面组成,其中 n 可以是 1、2、3 或 4。

  • “C”(在通道信息之后)表示原语仅对颜色通道之一(“感兴趣通道”)进行操作。所有其他输出通道不受原语的影响。

  • “I” 表示原语“就地”工作。在这种情况下,图像数据指针通常命名为 pSrcDst,以指示图像数据同时充当源和目标。

  • “M” 表示“掩码操作”。这些类型的原语具有额外的 “掩码图像” 作为输入。目标图像中的每个像素都对应于掩码图像中的一个像素。仅处理具有相应非零掩码像素的像素。

  • “R” 表示原语仅在矩形感兴趣区域或 “ROI” 上操作。所有 ROI 原语都采用 NppiSize 类型的附加输入参数,该参数指定原语应处理的矩形区域的宽度和高度。有关原语如何在 ROI 上操作的详细信息,请参阅::ref: ‘roi_specification’。

  • “Sfs” 表示结果值在写出之前通过固定缩放和饱和度进行处理。

上述后缀始终按字母顺序出现。例如,一个 4 通道原语,不影响 alpha 通道,具有掩码操作、就地操作以及缩放/饱和度和 ROI,将具有后缀:“AC4IMRSfs”。

图像数据

图像数据通过一对参数传递到 NPPI 原语和从 NPPI 原语传递:

  1. 指向图像底层数据类型的指针。

  2. 以字节为单位的行步长(有时也称为行跨距)。

这种相当低级的图像数据传递方式背后的总体思路是易于采用到现有软件项目中:
  • 传递指向底层像素数据类型的原始指针,而不是结构化(按颜色)通道像素数据,允许在各种情况下使用该函数,从而避免有风险的类型转换或昂贵的图像数据复制。

  • 单独传递数据指针和行步长,而不是更高级别的图像结构,再次允许轻松采用,而无需特定的图像表示,从而避免了从主机应用程序到 NPP 特定图像表示的笨拙的打包和解包图像数据。

行步长

行步长(也称为 “行跨距” 或 “行步幅”)允许奇数大小图像的行通过在线尾添加一些未使用的字节,从而在良好对齐的地址上开始。这种类型的行填充长期以来一直是数字图像处理中的常见做法,并非 GPU 图像处理所特有。

行步长是一行中的字节数,**包括填充**。解释此数字的另一种方式是说它是图像中连续行的第一个像素之间的字节数,或者通常是像素的任何列中两个相邻像素之间的字节数。

行步长存在的总体原因是,均匀对齐的像素行可以优化内存访问模式。

即使 NPP 中的所有函数都适用于任意对齐的图像,但只有使用良好对齐的图像数据才能获得最佳性能。使用 NPP 图像分配器或 CUDA 运行时的 2D 内存分配器分配的任何图像数据都已良好对齐。

特别是在较旧的支持 CUDA 的 GPU 上,未对齐数据的性能下降可能是巨大的(数量级)。

传递给 NPPI 原语的所有图像数据都需要提供行步长。重要的是要记住,此行步长始终以字节而不是像素为单位指定。

图像数据参数名称

在以下部分中详细介绍了 NPP 中图像数据传递的三个一般情况。

传递源图像数据

这些是算法使用的图像。

源图像指针

源图像数据通常通过名为以下内容的指针传递:

pSrc 
源图像指针通常定义为常量,强制原语不更改该指针指向的任何图像数据。例如:
nppiPrimitive_32s_C1R(const Npp32s * pSrc, ...) 
如果原语消耗多个图像作为输入,则源指针按如下方式编号:
pSrc1, pScr2, ... 

源批量图像指针

批量源图像数据通常通过 NppiImageDescriptor 类型的指针传递,命名为:

pSrcBatchList 
源批量指针通常定义为常量,强制原语不更改该指针指向的任何源数据。例如:
nppiYUVToRGBBatch_8u_C3R(NppiSize oSizeROI, const NppiImageDescriptor* pSrcBatchList, ...) 
所有处理批量数据的原语都需要在单独的参数中提供批量大小。

源平面图像指针数组

平面源图像数据通常通过名为以下内容的指针数组传递:

pSrc[] 
平面源图像指针数组通常定义为常量指针的常量数组,强制原语不更改这些指针指向的任何图像数据。例如:
nppiPrimitive_8u_P3R(const Npp8u * const pSrc[3], ...) 
数组中的每个指针都指向不同的图像平面。

源平面图像指针

多平面源图像数据通过一组名为以下名称的指针传递

pSrc1, pSrc2, ... 
平面源图像指针通常定义为一组常量指针之一,每个指针指向不同的输入图像平面。

源图像行步长

源图像行步长是图像中连续行之间的字节数。源图像行步长参数是

nSrcStep 
或者在多个源图像的情况下
nSrcStep1, nSrcStep2, ... 

源平面图像行步长数组

源平面图像行步长数组是一个数组,其中数组的每个元素都包含输入图像中特定平面的连续行之间的字节数。源平面图像行步长数组参数是

rSrcStep[] 

源平面图像行步长

源平面图像行步长是多平面输入图像的特定平面中连续行之间的字节数。源平面图像行步长参数是

nSrcStep1, nSrcStep2, ... 

传递目标图像数据

这些是算法生成的图像。

目标图像指针

目标图像数据通常通过名为以下名称的指针传递

pDst 
如果原语生成多个图像作为输出,则目标指针的编号如下
pDst1, pDst2, ... 

目标批处理图像指针

目标批处理图像数据通常通过 NppiImageDescriptor 类型的指针传递,名为

pDstBatchList 
所有处理批量数据的原语都需要在单独的参数中提供批量大小。

目标平面图像指针数组

平面目标图像数据指针通常通过名为以下名称的指针数组传递

pDst[] 
数组中的每个指针都指向不同的图像平面。

目标平面图像指针

目标平面图像数据通常通过指向多平面输出图像的每个平面的指针传递,名为

pDst1, pDst2, ...  

目标图像行步长

目标图像行步长参数是

nDstStep 
或者在多个目标图像的情况下
nDstStep1, nDstStep2, ... 

目标平面图像行步长

目标平面图像行步长是多平面输出图像中特定平面的连续行之间的字节数。目标平面图像行步长参数是

nDstStep1, nDstStep2, ... 

传递原位图像数据

原位图像指针

在原位处理的情况下,源和目标由同一指针提供服务,因此指向原位图像数据的指针称为

pSrcDst 

原位图像行步长

原位行步长参数是

nSrcDstStep 

传递掩码图像数据

一些图像处理原语具有支持 masked_operation 的变体。

掩码图像指针

掩码图像数据通常通过名为以下名称的指针传递

pMask 

掩码图像行步长

掩码图像行步长参数是

nMaskStep 

传递感兴趣通道数据

一些图像处理原语支持 channel_of_interest。

感兴趣通道编号

感兴趣通道数据通常是一个整数(1、2 或 3)

nCOI 

图像数据对齐要求

NPP 要求像素数据遵守某些对齐约束。

对于 2 通道和 4 通道图像,以下对齐要求成立

data_pointer % (\#channels * sizeof(channel type)) == 0
例如,具有底层类型 Npp8u (8 位无符号) 的 4 通道图像将要求所有像素都位于 4 的倍数的地址上(4 通道 * 1 字节大小)。

作为所有像素与其自然大小对齐的逻辑结果,2 通道和 4 通道图像的图像行步长也需要是像素大小的倍数。

对于 1 通道和 3 通道图像,仅要求像素指针与底层数据类型对齐,即 pData % sizof(data type) == 0。因此,行步长也需要满足此要求。

图像数据相关错误代码

所有在图像数据上运行的 NPPI 原语都验证图像数据指针是否正确对齐,并测试该点是否为空。它们还验证行跨距是否正确对齐,并防止步长小于或等于 0。验证失败会导致返回以下错误代码之一,并且不执行原语:如果数据步长为 0 或负数,则返回 NPP_STEP_ERROR。如果行步长不是 2 通道和 4 通道图像的像素大小的倍数,则返回 NPP_NOT_EVEN_STEP_ERROR。如果图像数据指针为 0 (NULL),则返回 NPP_NULL_POINTER_ERROR。如果图像数据指针地址不是 2 通道和 4 通道图像的像素大小的倍数,则返回 NPP_ALIGNMENT_ERROR。

感兴趣区域 (ROI)

实际上,处理图像的矩形子区域通常比处理完整图像更常见。NPP 的绝大多数图像处理原语都允许处理此类子区域,也称为感兴趣区域或 ROI。

所有支持 ROI 处理的原语都在其名称后缀中标记为“R”。在大多数情况下,ROI 作为单个 NppiSize 结构传递,该结构提供 ROI 的宽度和高度。这就提出了一个问题,即原语如何知道此 (宽度、高度) 矩形在图像中的位置。“ROI 的起始像素”由图像数据指针隐式给出。即,用户只需偏移图像数据指针以指向 ROI 的第一个像素,而不是显式传递左上角(最低内存地址)的像素坐标。

实际上,这意味着对于图像(pSrcnSrcStep)和 ROI 的起始像素位于位置 (x, y),将传递

pSrcOffset = pSrc + y * nSrcStep + x * PixelSize; 

作为原语的图像数据源。PixelSize 通常计算为

PixelSize = NumberOfColorChannels * sizeof(PixelDataType). 

例如,对于像 nppiSet_16s_C4R() 这样的原语,我们将有

  • NumberOfColorChannels == 4;

  • sizeof(Npp16s) == 2;

  • 因此 PixelSize = 4 * 2 = 8;

ROI 相关错误代码

所有在图像数据的 ROI 上运行的 NPPI 原语都验证 ROI 大小和图像的步长大小。验证失败会导致返回以下错误代码之一,并且不执行原语:如果 ROI 宽度或 ROI 高度为负数,则返回 NPP_SIZE_ERROR。如果 ROI 宽度超过图像的行步长,则返回 NPP_STEP_ERROR。用数学术语表示,(widthROI * PixelSize) > nLinStep 表示错误。

掩码操作

一些原语支持掩码操作。这些变体的后缀中的“M”表示掩码操作。支持掩码操作的原语会消耗通过 mask_image_pointer 和 mask_image_line_step 提供的附加输入图像。掩码图像被这些原语解释为布尔图像。Npp8u 类型的值被解释为布尔值,其中值 0 表示 false,任何非零值表示 true。

除非另有说明,否则操作仅在空间上对应的掩码像素为 true(非零)的像素上执行。例如,掩码复制操作将仅复制 ROI 中具有相应非零掩码像素的那些像素。

感兴趣通道 API

一些原语允许将操作限制为多通道图像中单个感兴趣的通道。这些原语的后缀为字母“C”(在通道信息之后,例如 nppiCopy_8u_C3CR())。感兴趣的通道通常通过偏移图像数据指针以直接指向感兴趣的通道而不是 ROI 中第一个像素的基址来选择。一些原语还显式指定选定的通道号并通过整数传递它,例如 nppiMean_StdDev_8u_C3CR()

选择通道源图像指针

这是指向源图像的第一个像素内感兴趣通道的指针。例如,如果 pSrc 是指向三通道图像的 ROI 内第一个像素的指针。使用适当的选择通道复制原语,可以将此源图像的第二个通道复制到由 pDst 给出的目标图像的第一个通道中,方法是将指针偏移一位

nppiCopy_8u_C3CR(pSrc + 1, nSrcStep, pDst, nDstStep, oSizeROI); 

选择通道源图像

一些原语允许用户通过指定通道号 (nCOI) 来选择感兴趣的通道。这种方法通常用于图像统计函数中。例如,

nppiMean_StdDev_8u_C3CR(pSrc, nSrcStep, oSizeROI, nCOI, pDeviceBuffer, pMean, pStdDev ); 

感兴趣通道编号可以是 1、2 或 3。

选择通道目标图像指针

这是指向目标图像的第一个像素内感兴趣通道的指针。例如,如果 pDst 是指向三通道图像的 ROI 内第一个像素的指针。使用适当的选择通道复制原语,可以将数据从由 pSrc 给出的源图像的第一个通道复制到此目标图像的第二个通道中,方法是将目标指针偏移一位

nppiCopy_8u_C3CR(pSrc, nSrcStep, pDst + 1, nDstStep, oSizeROI); 

源图像采样

大量的 NPP 图像处理函数至少消耗一个源图像并生成一个输出图像(例如 nppiAddC_8u_C1RSfs()nppiFilterBox_8u_C1R())。属于此类的所有 NPP 函数也对 ROI 进行操作(请参阅 :ref: _roi_specification),对于这些函数,应将其视为描述目标 ROI。换句话说,ROI 描述了目标图像中的矩形区域,并且此区域内的所有像素都由所讨论的函数写入。

为了成功使用此类函数,重要的是要了解用户定义的目标 ROI 如何影响算法正在读取的输入图像中的像素。为了简化 ROI 传播的讨论(即,给定目标 ROI,源中的 ROI 是什么),区分两种主要情况是有意义的

  1. 点对点操作:这些是像 nppiAddC_8u_C1RSfs() 这样的原语。每个输出像素都需要读取一个输入像素。

  2. 邻域操作:这些是像 nppiFilterBox_8u_C1R() 这样的原语,它们需要从源图像中读取一组像素才能生成单个输出。

点对点操作

如上所述,点对点操作从输入图像中消耗单个像素(或者如果所讨论的操作具有多个输入图像,则从每个输入图像中消耗单个像素),以生成单个输出像素。

邻域操作

在邻域操作的情况下,为了计算单个输出像素,在输入图像(或图像)中读取多个输入像素(像素的“邻域”)。image_filtering_functions 和 image_morphological_operations 的所有函数都是邻域操作。

这些函数中的大多数都具有影响邻域大小和相对位置的参数:掩码大小结构和锚点结构。这两个参数在接下来的小节中进行了更详细的描述。

掩码大小参数

许多 NPP 邻域操作允许用户通过通常名为 oMaskSizeNppiSize 类型的参数来指定邻域的大小。在这些情况下,从源读取的像素邻域的大小正好是掩码的大小。假设掩码锚定在位置 (0, 0)(请参阅下面的 anchor_point_parameter)并且大小为 (w, h),即

assert(oMaskSize.w == w);
assert(oMaskSize.h == h);
assert(oAnchor.x == 0);
assert(oAnchor.y == 0);

邻域操作将读取以下源像素以计算目标像素 \( D_{i,j} \)

\[\begin{split} \begin{array}{lllll} S_{i,j} & S_{i,j+1} & \ldots & S_{i,j+w-1} \\ S_{i+1,j} & S_{i+1,j+1} & \ldots & S_{i+1, j+w-1} \\ \vdots & \vdots & \ddots & \vdots \\ S_{i+h-1, j} & S_{i+h-1, j+1} & \ldots & S_{i+h-1, j+w-1} \end{array} \end{split}\]

锚点参数

许多执行邻域操作的 NPP 原语允许用户通过通常名为 oAnchorNppiPoint 类型的参数来指定邻域的相对位置。开发人员可以使用锚点来选择掩码(请参阅 mask_size_parameter)相对于当前像素索引的位置。

使用与 mask_size_parameter 中相同的示例,但这次的锚点位置为 (a, b)

assert(oMaskSize.w == w);
assert(oMaskSize.h == h);
assert(oAnchor.x == a);
assert(oAnchor.y == b);

将读取源图像中的以下像素

\[\begin{split} \begin{array}{lllll} S_{i-a,j-b} & S_{i-a,j-b+1} & \ldots & S_{i-a,j-b+w-1} \\ S_{i-a+1,j-b} & S_{i-a+1,j-b+1} & \ldots & S_{i-a+1, j-b+w-1} \\ \vdots & \vdots & \ddots & \vdots \\ S_{i-a+h-1, j-b} & S_{i-a+h-1, j-b+1} & \ldots & S_{i-a+h-1, j-b+w-1} \end{array} \end{split}\]

超出图像边界的采样

一般的 NPP 原语,特别是 NPP 邻域操作,要求所有读取和写入的像素位置都有效且在相应图像的边界内。在定义的图像数据区域之外进行采样会导致未定义的行为,并可能导致系统不稳定。

这在实践中提出了一个问题:在处理全尺寸图像时,不能选择与源图像相同大小的目标 ROI。由于邻域操作从扩大的源 ROI 读取像素,因此必须缩小目标 ROI,以使扩大的源 ROI 不会超出源图像的大小;或者,如果邻域操作函数支持 Border 版本,则可以使用此版本,而无需 ROI 调整,并选择适当的边界保护模式。

对于缩小目标图像大小不可接受且 Border 版本的函数不可用的情况,NPP 提供了一组边界扩展 Copy 原语。例如 nppiCopyConstBorder_8u_C1R()nppiCopyReplicateBorder_8u_C1R()nppiCopyWrapBorder_8u_C1R()。用户可以使用这些原语通过三种扩展模式之一“扩展”源图像的大小。然后可以将扩展的图像安全地传递给邻域操作,从而生成全尺寸结果。

信号处理约定

信号数据

信号数据通过指向信号数据类型的指针传递给 NPPS 原语并从 NPPS 原语传递。

这种相当低级的信号数据传递方式背后的总体思路是易于采用到现有软件项目中

  • 传递数据指针而不是更高级别的信号结构可以轻松采用,因为它不需要特定的信号表示形式(可能包括总信号大小偏移或其他附加信息)。这避免了将信号数据从宿主应用程序尴尬地打包和解包到 NPP 特定的信号表示形式。

信号数据的参数名称

在以下部分中详细介绍了 NPP 中图像数据传递的三个一般情况。

这些是算法消耗的信号。

源信号指针

源信号数据通常通过名为以下名称的指针传递

pSrc 
源信号指针通常定义为常量,强制原语不更改该指针指向的任何图像数据。例如
nppsPrimitive_32s(const Npp32s * pSrc, ...) 
如果原语消耗多个信号作为输入,则源指针的编号如下
pSrc1, pScr2, ... 

目标信号指针

目标信号数据通常通过名为以下名称的指针传递

pDst 
如果原语消耗多个信号作为输入,则源指针的编号如下
pDst1, pDst2, ... 

原位信号指针

在原位处理的情况下,源和目标由同一指针提供服务,因此指向原位信号数据的指针称为

pSrcDst 

信号数据对齐要求

NPP 要求信号样本数据自然对齐,即任何指针

NppType * p; 
到信号中的样本需要满足
assert(p % sizeof(p) == 0); 

信号数据相关错误代码

所有在信号数据上运行的 NPPI 原语都验证信号数据指针是否正确对齐,并测试该点是否为空。

验证失败会导致返回以下错误代码之一,并且不执行原语:如果图像数据指针为 0 (NULL),则返回 NPP_NULL_POINTER_ERROR。如果信号数据指针地址不是信号数据类型大小的倍数,则返回 NPP_ALIGNMENT_ERROR。

信号长度

绝大多数 NPPS 函数都采用

nLength 
参数,该参数告诉原语要处理多少个从给定数据指针开始的信号样本。

长度相关错误代码

所有采用长度参数的 NPPS 原语都验证此输入。

验证失败会导致返回以下错误代码,并且不执行原语:如果长度为负数,则返回 NPP_SIZE_ERROR。