我们研究了vecAddkernel和colorToGreyscaleConversion,其中每个线程只对一个数组元素执行少量算术运算。这些内核很好地服务于其目的:说明基本的CUDA C程序结构和数据并行执行概念。在这一点上,读者应该问一个显而易见的问题——所有CUDA线程是否只相互独立地执行如此简单、微不足道的操作?答案是否定的。在真正的CUDA C程序中,线程通常对其数据执行复杂的算法,并且需要相互合作。在接下来的几章中,我们将研究表现出这些特征的越来越复杂的示例。我们将从图像模糊功能开始。
图像模糊平滑了像素值的突然变化,同时保留了识别图像关键特征所必需的边缘。图3.6说明了图像模糊的效果。简单地说,我们让图像看起来模糊不清。对人眼来说,模糊的图像往往会掩盖精细的细节和现在“大图”印象或图片中的主要主题对象。在计算机图像处理算法中,图像模糊的常见用例是通过用干净的周围像素值纠正有问题的像素值来减少图像中噪声和颗粒渲染效果的影响。在计算机视觉中,图像模糊可用于允许边缘检测和对象识别算法专注于主题对象,而不是受到大量细粒度对象的阻碍。在显示器中,图像模糊有时用于通过模糊图像的其余部分来突出图像的特定部分。
在数学上,图像模糊函数将输出图像像素的值计算为包含输入图像中像素的像素补丁的加权和。正如我们将在第7章中学习的那样,并行模式:卷积,这种加权和的计算属于卷积模式。在本章中,我们将使用简化的方法,取周围像素的NxN补丁的简单平均值,包括我们的目标像素。为了保持算法简单,我们不会根据与目标像素的距离对任何像素的值进行加权,这在高斯模糊等卷积模糊方法中很常见。
图3.7显示了一个使用3 x 3补丁的示例。在(Row,Col)位置计算输出像素值时,我们看到补丁位于(Row,Col)位置的输入像素居中。3×3补丁横跨三行(Row-1,Row,Row+1)和三列(Col-1,Col,Col+1)。举例来说,计算(25,50)输出像素的九个像素的坐标是(24,49),(24,50),(24,51),(25,49),(25,50),(25,51),(26,49),(26,50)和(26,51)。
图3.8显示图像模糊内核。与colorToGreyscaleConversion类似,我们使用每个线程来计算一个输出像素。也就是说,线程到输出数据映射保持不变。因此,在内核的开头,我们看到了Col和Row索引的熟悉计算。我们还看到了熟悉的if-statement,该状态根据图像的高度和宽度验证Col和Row是否都在有效范围内。只有Col和Row索引在值范围内的线程才能参与执行。
如图3.7所示,Col和Row值还生成用于计算线程输出像素的补丁的中央像素位置。图3.8 中嵌套的for-loop第3行和第4行。遍阅补丁中的所有像素。我们假设程序有一个定义的常量,BLUR_SIZE。BLUR_SIZE的值设置为2*BLUR_SIZE给出补丁两侧的像素数。对于3×3补丁,BLUR_SIZE设置为1,而对于7×7补丁,BLUR_SIZE设置为3。外部循环通过补丁的行进行循环。对于每行,内部循环在补丁的列中循环。
在我们的3×3补丁示例中,BLUR_SIZE是1。对于计算输出像素(25,50)的线程,在外循环的第一次迭代中,curRow变量是 Row-BLUR_SIZE = (25 ? 1) = 24。因此,在外循环的第一次迭代中,内循环迭代第24行中的补丁像素。内部循环使用curCol变量从Col-BLUR_SIZE = 50 ? 1 = 49列到 Col+BLUR_SIZE = 51。因此,在外循环的第一次迭代中处理的像素是(24、49)、(24、50)和(24、51)。读者应验证在外循环的第二次迭代中,内循环通过像素(25、49)、(25、50)和(25、51)迭代。最后,在外循环的第三次迭代中,内循环通过像素(26、49)、(26、50)和(26、51)迭代。
第8行使用curRow和curCol的线性化索引来访问当前迭代中访问的输入像素的值。它将像素值累积到运行总和变量pixVal中。第9行记录通过增加像素变量在运行总和中再添加一个像素值。处理完补丁中的所有像素后,第10行通过将像素值除以像素值来计算补丁中像素的平均值。它使用Row和Col的线性化索引将结果写入其输出像素。
第7行包含一个条件声明,以保护第9行和第10行的执行。对于图像边缘附近的输出像素,补丁可能会超出图片的有效范围。图3.9中说明了这一点。假设3×3补丁。在案例1中,左上角的像素被模糊了。预期补丁中的九个像素中有五个在输入图像中不存在。在这种情况下,输出像素的Row和Col值为0和0。在执行嵌套循环期间,九次迭代的CurRow和CurCol值为(-1,-1)、(-1,0)、(-1,1)、(0,-1)、(0,0,)、(0,1)、(1,-1)、(1,0)和(1,1)。请注意,对于图像外的五个像素,其中至少有一个值小于0。if-statement的curRow<0和curCol<0条件捕获这些值,并跳过第8行和第9行的执行。因此,只有四个有效像素的值被累积到运行总和变量中。像素值也正确地增加了四倍,以便在第10行正确计算平均值。
读者应该研究图3.9中的其他案例,并分析blurKernel中嵌套循环的执行行为。请注意,大多数线程将在输入图像中找到其分配的3 x 3补丁中的所有像素。它们将累积嵌套循环中的所有九个像素。然而,对于四个角落的像素,负责的线程将只积累4个像素。对于四个边缘的其他像素,负责的线程将在嵌套循环中积累6个像素。这些变化需要跟踪可变像素累积的实际像素数。