什么是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 Toolkit目录中:

/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中的内存支持函数,

  • nppi_threshold_and_compare_operations.h 中的 NPPITC、阈值和比较操作函数,

例如,在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即将发布一个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架构中fp16(__half)数据类型的支持,部分NPP图像处理函数现已兼容该数据类型。支持__half数据类型像素的NPP图像函数,其函数名称为16f类型,且需要将该数据类型的像素指针以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 runs on all CUDA capable NVIDIA hardware. For details please see 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中的某些原语在进行计算时需要额外的设备内存缓冲区(临时缓冲区),例如信号和图像缩减(求和、最大值、最小值、最小最大值等)。为了让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()用于计算信号中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\]
which would be rounded to a final result of 254.

中等灰度值128将导致

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

舍入模式

许多NPP函数需要将浮点数值转换为整数。Rounding Modes枚举列出了NPP支持的舍入模式。并非所有NPP中执行舍入操作的原语都允许用户指定使用的舍入模式,它们会默认使用NPP的金融舍入模式NPP_RND_FINANCIAL。

舍入模式参数

NPP函数的一个子集在执行其功能时包含舍入操作,这些函数允许用户通过Rounding Modes类型的参数来指定所使用的舍入模式。

图像处理规范

函数命名

图像处理相关函数使用多个后缀来表示基本操作的不同变体,而不仅仅是不同的数据类型。这些变体后缀采用以下缩写:

  • 如果图像是4通道图像,"A"表示原始通道的结果alpha通道不受影响。

  • "Cn"表示图像由n个通道的打包像素组成,其中n可以是1、2、3或4。

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

  • "C"(跟随通道信息之后)表示该原语仅对其中一个颜色通道(即"关注通道")进行操作。所有其他输出通道不受此原语影响。

  • "I"表示该原语操作是"原地(in-place)"进行的。在这种情况下,图像数据指针通常命名为pSrcDst,表示图像数据同时作为源和目标。

  • “M”表示“掩码操作”。这类原语额外需要一个“掩码图像”作为输入。目标图像中的每个像素都对应掩码图像中的一个像素。只有对应掩码像素值非零的像素才会被处理。

  • "R"表示该原语仅对矩形感兴趣区域(ROI)进行操作。所有ROI原语都额外接受一个NppiSize类型的输入参数,用于指定原语应处理的矩形区域的宽度和高度。有关原语如何在ROI上操作的详细信息,请参阅::ref: 'roi_specification'。

  • "Sfs"表示结果值在写出之前经过固定缩放和饱和处理。

The suffixes above always appear in alphabetical order. E.g. a 4 channel primitive not affecting the alpha channel with masked operation, in place and with scaling/saturation and ROI would have the postfix: “AC4IMRSfs”.

图像数据

图像数据通过一对参数传入和传出NPPI原语:

  1. 指向图像基础数据类型的指针。

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

The general idea behind this fairly low-level way of passing image data is ease-of-adoption into existing software projects:
  • 直接传递底层像素数据类型的原始指针,而非按颜色分层的结构化通道像素数据,使得该函数能在多种场景下使用,避免了风险性的类型转换或高昂的图像数据拷贝开销。

  • 单独传递数据指针和行步长,而非使用更高层次的图像结构,这样无需特定的图像表示形式,从而避免了将图像数据从主机应用程序打包和解包为NPP特定图像表示的繁琐操作,便于轻松采用。

行步骤

行步长(也称为"行跨度"或"行步幅")通过在行末添加若干未使用的字节,使不规则尺寸图像的行能够从对齐良好的地址开始。这种行填充方式在数字图像处理领域长期属于常见做法,并非GPU图像处理所特有。

行步长是指一行中的字节数包括填充部分。 另一种理解方式是,它表示图像中连续行首像素之间的字节数,或者更一般地说,表示任意像素列中两个相邻像素之间的字节数。

存在行步进(line step)的普遍原因是,像素行均匀对齐可以优化内存访问模式。

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

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

传递给NPPI原语的所有图像数据都需要提供行步长。需要特别注意的是,这个行步长始终以字节为单位指定,而不是像素。

图像数据的参数名称

以下章节详细介绍了在NPP中传递图像数据的三种常见情况。

传递源图像数据

这些是算法消耗的图像。

源图像指针

源图像数据通常通过名为

的指针传递
pSrc 
The source image pointer is generally defined constant, enforcing that the primitive does not change any image data pointed to by that pointer. E.g.
nppiPrimitive_32s_C1R(const Npp32s * pSrc, ...) 
In case the primitive consumes multiple images as inputs the source pointers are numbered like this:
pSrc1, pScr2, ... 

源批量图像指针

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

pSrcBatchList 
The source batch pointer is generally defined constant, enforcing that the primitive does not change any source data pointed to by that pointer. E.g.
nppiYUVToRGBBatch_8u_C3R(NppiSize oSizeROI, const NppiImageDescriptor* pSrcBatchList, ...) 
All primitives processing batch data require providing the size of the batch in a separate parameter.

源平面图像指针数组

平面源图像数据通常通过一个名为

的指针数组传递
pSrc[] 
The planar source image pointer array is generally defined a constant array of constant pointers, enforcing that the primitive does not change any image data pointed to by those pointers. E.g.
nppiPrimitive_8u_P3R(const Npp8u * const pSrc[3], ...) 
Each pointer in the array points to a different image plane.

源平面图像指针

多平面源图像数据通过一组名为

的指针传递
pSrc1, pSrc2, ... 
The planar source image pointer is generally defined as one of a set of constant pointers with each pointer pointing to a different input image plane.

源图像行步长

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

nSrcStep 
or in the case of multiple source images
nSrcStep1, nSrcStep2, ... 

源平面图像行步长数组

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

rSrcStep[] 

源平面图像行步长

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

nSrcStep1, nSrcStep2, ... 

传递目标图像数据

这些是由算法生成的图像。

目标图像指针

目标图像数据通常通过名为

pDst 
In case the primitive generates multiple images as outputs the destination pointers are numbered like this:
pDst1, pDst2, ... 

目标批次图像指针

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

pDstBatchList 
All primitives processing batch data require providing the size of the batch in a separate parameter.

目标平面图像指针数组

平面目标图像数据指针通常通过一个名为

的指针数组传递
pDst[] 
Each pointer in the array points to a different image plane.

目标平面图像指针

目标平面图像数据通常通过指针传递到名为

的多平面输出图像的每个平面
pDst1, pDst2, ...  

目标图像行步长

目标图像行步长参数是

nDstStep 
or in the case of multiple destination images
nDstStep1, nDstStep2, ... 

目标平面图像行步长

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

nDstStep1, nDstStep2, ... 

传入原地图像数据

原地图像指针

对于原地处理的情况,源数据和目标数据由同一指针提供,因此指向原地图像数据的指针被称为:

pSrcDst 

原地图像行步长

原地行步参数是

nSrcDstStep 

传递掩码图像数据

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

掩码图像指针

遮罩图像数据通常通过名为

pMask 

掩膜图像行步长

mask-image线条步长参数是

nMaskStep 

传递感兴趣通道数据

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

关注通道编号

关注通道的数据通常是一个整数(1、2或3):

nCOI 

图像数据对齐要求

NPP要求像素数据遵循特定的对齐约束。

对于2通道和4通道图像,需遵循以下对齐要求:

data_pointer % (\#channels * sizeof(channel type)) == 0
E.g. a 4 channel image with underlying type Npp8u (8-bit unsigned) would require all pixels to fall on addresses that are multiples of 4 (4 channels * 1 byte size).

由于所有像素都与其自然大小对齐的逻辑结果,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的绝大多数图像处理原语都支持处理此类子区域,这些子区域也被称为感兴趣区域或ROIs。

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

在实践中,这意味着对于图像(pSrc, nSrcStep)和ROI起始像素位于位置(x, y)的情况,需要传递

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

作为图元(image-data)的数据源。PixelSize通常计算为

PixelSize = NumberOfColorChannels * sizeof(PixelDataType). 

例如,对于像 nppiSet_16s_C4R() 这样的基本操作,我们会得到

  • NumberOfColorChannels == 4;

  • sizeof(Npp16s) == 2;

  • 因此 PixelSize = 4 * 2 = 8;

ROI相关错误代码

所有对图像数据ROI区域进行操作NPPI原语都会验证ROI尺寸和图像步长。验证失败将返回以下错误代码之一且不执行原语操作:如果ROI宽度或高度为负值则返回NPP_SIZE_ERROR;如果ROI宽度超过图像行步长则返回NPP_STEP_ERROR。用数学表达式表示(widthROI * PixelSize) > nLinStep即表示错误。

掩码操作

部分基础操作支持掩码处理。这些变体名称后缀中的"M"表示掩码操作。支持掩码操作的基础运算会通过mask_image_pointer和mask_image_line_step参数额外接收一个输入图像。这些基础运算将掩码图像视为布尔图像,其中Npp8u类型的数值会被解释为布尔值:0值表示false,任何非零值表示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上操作(参见:_roi_specification),对于这些函数而言,ROI应被视为描述目标ROI。换句话说,ROI描述了目标图像中的一个矩形区域,该区域内的所有像素都将由相关函数写入。

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

  1. 逐点操作:这些是类似nppiAddC_8u_C1RSfs()的基本运算。每个输出像素只需要读取一个输入像素。

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

逐点运算

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

邻域操作

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

这些函数大多包含影响邻域大小和相对位置的参数:一个掩码尺寸结构和一个锚点结构。这两个参数将在接下来的小节中更详细地描述。

掩码大小参数

许多NPP邻域操作允许用户通过通常名为oMaskSize的参数(类型为NppiSize)来指定邻域大小。在这些情况下,从源图像读取的像素邻域正好是掩码的大小。假设掩码锚定在位置(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不超过源图像尺寸。或者,如果邻域操作函数支持边界处理版本,则可在不调整ROI的情况下使用该版本,但需选择适当的边界保护模式。

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

信号处理约定

信号数据

信号数据通过指向信号数据类型的指针传入和传出NPPS原语。

这种相当底层的信号数据传递方式背后的总体思路是便于集成到现有软件项目中:

  • 直接传递数据指针而非更高级的信号结构体,可以避免要求特定的信号表示方式(可能包含总信号大小偏移量或其他额外信息),从而便于采用。这避免了将信号数据从主机应用程序打包和解包到NPP特定信号表示方式的繁琐过程。

信号数据的参数名称

以下章节详细介绍了在NPP中传递图像数据的三种常见情况。

这些是算法所消耗的信号。

源信号指针

源信号数据通常通过名为

的指针传递
pSrc 
The source signal pointer is generally defined constant, enforcing that the primitive does not change any image data pointed to by that pointer. E.g.
nppsPrimitive_32s(const Npp32s * pSrc, ...) 
In case the primitive consumes multiple signals as inputs the source pointers are numbered like this:
pSrc1, pScr2, ... 

目标信号指针

目标信号数据通常通过名为

的指针传递
pDst 
In case the primitive consumes multiple signals as inputs the source pointers are numbered like this:
pDst1, pDst2, ... 

原地信号指针

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

pSrcDst 

信号数据对齐要求

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

NppType * p; 
to a sample in a signal needs to fulfill:
assert(p % sizeof(p) == 0); 

信号数据相关错误代码

所有处理信号数据的NPPI原语都会验证信号数据指针是否正确对齐,并测试该指针是否为空。

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

信号长度

绝大多数NPPS函数都接受一个

nLength 
parameter that tells the primitive how many of the signal’s samples starting from the given data pointer are to be processed.

长度相关错误代码

所有接收长度参数的NPPS原语都会验证此输入。

验证失败将返回以下错误代码且不执行原语操作:如果长度为负值,则返回NPP_SIZE_ERROR。