搜索
房产
装修
汽车
婚嫁
健康
理财
旅游
美食
跳蚤
二手房
租房
招聘
二手车
教育
茶座
我要买房
买东西
装修家居
交友
职场
生活
网购
亲子
情感
龙城车友
找美食
谈婚论嫁
美女
兴趣
八卦
宠物
手机

DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子

[复制链接]
查看: 73|回复: 0

1万

主题

1万

帖子

4万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
45261
发表于 2020-2-14 22:17 | 显示全部楼层 |阅读模式
前言

到这里盘算着色器的主线进修底子竣事,剩下的就是再补充两个有关图像处置惩罚方面的利用。这里面包含了龙书11的图像暗昧,以及龙书12额外提到的Sobel算子举行边沿检测。严重内容源自于龙书12,项目源码也基于此举行调解。
进修方针:

  • 熟悉图像处置惩罚常用的卷积
  • 熟悉高斯暗昧、Sobel算子
DirectX11 With Windows SDK完整目录
Github项目源码
接待加入QQ群: 727623616 可以一路探讨DX11,以及有什么题目也可以在这里报告。
图像卷积

在图像处置惩罚中,经常需要用到卷积,很多成果都可以大要经过卷积的形式来实现。针对源图像中的每一个像素\(P_{ij}\),盘算以它为中心的m×n矩阵的加权值。此加权值即是经过处置惩罚后图像中第i行、第j列的色彩,假如写成卷积的形式则为:
\[H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i-r,j-c}\]
其中,\(m=2a+1\)且\(n=2b+1\),将m与n逼迫为奇数,以此来保证m×n矩阵总是具有“中心”项。若a=b=r,则只需指定半径r便可以肯定矩阵的巨细。\(W_{rc}\)为m×n矩阵(又称内核、算子)中的权值。为了方便观察盘算及编码,凡是会将内核扭转180°,这样就获得了加倍常用的盘算公式:
\[H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i+r,j+c}\]
若内核的全数权值的和为1,则它可以用来做暗昧处置惩罚;假如权值和大于0小于1,则处置惩罚后的图像会随着色彩的缺失而变暗;假如权值和大于1,则处置惩罚后的图像会随着色彩的增加而加倍明亮。固然也会有权值和即是0甚至大要小于0的情况,比如索贝尔算子。
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200210111651000-49188061

图像暗昧

在保证权值和为1的条件下,我们就能用多种差此外方式来盘算它。其中就有一种广为人知的暗昧运算:高斯暗昧(Gaussian blur)。该算法借助高斯函数\(G(x)=exp(-\frac{x^2}{2\sigma^2})\)来获得权值。下图展现了取不同σ值时高斯函数的对应图像:
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200210111907617-1620909248

可以看到,若σ越大,则曲线越趋于陡峭,给附近点所赋予的权值也就越大。
\[G(x)=exp(-\frac{x^2}{2\sigma^2})=e^{-\frac{x^2}{2\sigma^2}}\]
假如学过几率论的话应当晓得它很像标准正态散布的几率密度,只不外缺了一个系数\(\frac{1}{\sqrt{2\pi}\;\sigma}\)。
现在假定我们要举行范围为1×5的高斯暗昧(即在水平偏向举行1D暗昧),且设σ=1。别离对x=-2,-1,0,1,2求G(x)的值,可以获得:
\[\begin{align}G(-2)&=exp(-\frac{(-2)^2}{2})=e^{-2} \\G(-1)&=exp(-\frac{(-1)^2}{2})=e^{-\frac{1}{2}} \\G(0)&=exp(0)=1 \\G(1)&=exp(-\frac{1^2}{2})=e^{-\frac{1}{2}} \\G(2)&=exp(-\frac{2^2}{2})=e^{-2}\end{align}\]
可是,这些数据还不是趾Λ的权值,由于它们的和不为1:
\[\begin{align}\sum_{x=-2}^{x=2}G(x)&=G(-2)+G(-1)+G(0)+G(1)+G(2)\\&=1+2e^{-\frac{1}{2}}+2e^{-2}\\&\approx 2.48373\end{align}\]
假如将前面5个值都除以它们的和举行规格化处置惩罚,那末我们便会基于高斯函数获得总和为1的各个权值:
\[\begin{align}w_{-2}&=\frac{G(-2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\w_{-1}&=\frac{G(-1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\w_{0}&=\frac{G(0)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.4026\\w_{1}&=\frac{G(1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\w_{2}&=\frac{G(2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\\end{align}\]
对于二维的高斯函数,有
\[\begin{align}G(x, y) &= G(x)\cdot G(y) \\&=exp(-\frac{x^2}{2\sigma^2})\cdot exp(-\frac{x^2}{2\sigma^2}) \\&=e^{-\frac{x^2+y^2}{2\sigma^2}}\end{align}\]
假如我们要举行3x3的高斯暗昧,且设σ=1,则未经过归一化的内核为:
\[\begin{bmatrix}G(-1)G(-1) & G(-1)G(0) & G(-1)G(1) \\G(0)G(-1) & G(0)G(0) & G(0)G(1) \\G(1)G(-1) & G(1)G(0) & G(1)G(1) \\\end{bmatrix} = \begin{bmatrix}G(-1) \\G(0) \\G(1) \\\end{bmatrix}\begin{bmatrix}G(-1) & G(0) & G(1) \\\end{bmatrix}\]
由于上面的内核矩阵可以写成一个列向量乘以一个行向量的形式,是以在做暗昧的时候可以将一个2D暗昧进程分为两个1D暗昧进程。这也就分析该内核具有可分手性

  • 经过1D横向暗昧将输入的图像I举行暗昧处置惩罚:\(I_H=Blur_H(I)\)
  • 对上一步输出的成果再次举行1D纵向暗昧处置惩罚:\(Blur(I)=Blur_V(I_H)\)
是以有:
\[Blur(I)=Blur_V(Blur_H(I))\]
假如暗昧核为一个9×9矩阵,我们就需要对总计81个样本依次举行2D暗昧运算。但经过将暗昧进程分手为两个1D暗昧阶段,便仅需要处置惩罚9+9=18个样本!我们经常要对纹理举行暗昧处置惩罚,而对纹理采样是价格高昂的操纵。是以,经过分离暗昧进程来淘汰纹理采样操纵是一种受用户接待的优化本事。尽管有些暗昧方式不具有可分手性,但只要保证终极图像在视觉上充沛精准,我们常常照旧能以优化性能为方针而简化其暗昧进程。
实现道理

首先,假定所应用的暗昧算法具有可分手性,据此将暗昧操纵分为两个1D暗昧运算:一个横向暗昧运算,一个纵向暗昧运算。假定用户供给了一个纹理A作为输入(凡是是作为SRV形参),以及一个纹理B作为输出(凡是是作为UAV形参)。不外要考虑到有的用户渴望将间接点窜纹理A,将纹理A的SRV和UAV都传入。是以我们照旧需要两个存储中心成果的纹理T0、T1,进程以下:

  • 给纹理A绑定SRV作为输入,而且给纹理T0绑定UAV作为输出。
  • 调节线程组举行横向暗昧操纵。完成后,纹理T0存储了横向暗昧的成果
  • 解绑纹理T0的UAV,将它的SRV作为输入。
  • 若用户指定了UAV,而且暗昧次数为1,则将该UAV作为输出;否则由于后续还需要举行肴杂,则将纹理T1的UAV作为输出。
  • 调节线程组举行纵向暗昧操纵。若当前为末端一次暗昧,且用户指定了UAV,则该UAV的纹理将保存终极的成果;否则T1保存了当前暗昧的成果。解绑UAV后,若仍有残剩暗昧次数,则将纹理T1绑定SRV作为输入,并给纹理T0绑定UAV作为输出,回到步伐2继续;否则就再解绑SRV后竣事。
由于衬着到纹理种的场景于窗口工作区要连结着类似的分辨率,我们需要不时重新构建离屏纹理,而暗昧算法用的姑且纹理T也是如此。在GameApp::OnResize的时候重新调解即可。
假如要处置惩罚的图像宽度为w、宽度为h。对于1D纵向暗昧而言,一个线程组用256个线程来处置惩罚水平偏向上的线段,而且每个线程又负责图像中一个像素的暗昧操纵。是以,为了图像中的每个像素都能获得暗昧处置惩罚,我们需要在x偏向上调节\(ceil(\frac{w}{256})\)个线程组(ceil为上取整函数),且在y偏向上调节h个线程组。假如w不能被256整除,则末端一次调节的线程组会存有过剩的线程(见下图)。我们对于这类情况力所不及,由于线程组的巨细牢固。是以,我们只得把留意力放在着色器代码中越界题目标钳位检测(clamping check)上。
1D纵向暗昧于上述1D横向暗昧的情况类似。在纵向暗昧进程中,线程组就像由256个线程组成的垂直线段,每个线程只负责图像中一个像素的暗昧运算。是以,为了使图像中的每个像素都能获得暗昧处置惩罚,我们需要在y偏向上调节\(ceil(\frac{h}{256})\)个线程组,并在x偏向上调节w个线程组。
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200211110406650-830953945

现在来考虑对一个28x14像素的纹理举行处置惩罚,我们所用的横向、纵向线程组的范围别离为8x1和1x8(采取X×Y的表现格式)。对于水平偏向的处置惩罚进程来说,为了处置惩罚全数的像素,我们需要在x偏向上调节\(ceil(\frac{w}{8})=ceil(\frac{28}{8})=4\)个线程组,并在y偏向上调节14个线程组。由于28并不能被8整除,所以最右侧的线程组中会有\((4\times 8-28)\times 14=56\)个线程声明都不做。对于垂直偏向的处置惩罚进程而言,为了处置惩罚全数的像素,我们需要在y偏向上分拨\(ceil(\frac{h}{8})=ceil(\frac{14}{8})=2\)个线程组,并在x偏向上调节28个线程组。同理,由于14并不能被8整除,所以最下侧的线程组中会有\((2\times 8 - 14)\times 28\)个闲置的线程。沿袭同一思绪便可以将线程组扩大为256个线程的范围来处置惩罚更大的纹理。
BlurFilter::Execute不单盘算出了每个偏向要调节的线程组数目,还开启了盘算着色器的暗昧运算:
  1. void BlurFilter::Execute(ID3D11DeviceContext* deviceContext, ID3D11ShaderResourceView* inputTex, ID3D11UnorderedAccessView* outputTex, UINT blurTimes){    if (!deviceContext || !inputTex || !blurTimes)        return;    // 设备常量缓冲区    D3D11_MAPPED_SUBRESOURCE mappedData;    deviceContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);    memcpy_s(mappedData.pData, sizeof m_CBSettings, &m_CBSettings, sizeof m_CBSettings);    deviceContext->Unmap(m_pConstantBuffer.Get(), 0);    deviceContext->CSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());    ID3D11UnorderedAccessView* nullUAV[1] = { nullptr };    ID3D11ShaderResourceView* nullSRV[1] = { nullptr };    // 第一次暗昧    // 横向暗昧    deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0);    deviceContext->CSSetShaderResources(0, 1, &inputTex);    deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr);    deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1);    deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);    // 纵向暗昧    deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0);    deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf());    if (blurTimes == 1 && outputTex)        deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr);    else        deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr);    deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1);    deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);    // 残剩暗昧次数    while (--blurTimes)    {        // 横向暗昧        deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0);        deviceContext->CSSetShaderResources(0, 1, m_pTempSRV1.GetAddressOf());        deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr);        deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1);        deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);        // 纵向暗昧        deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0);        deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf());        if (blurTimes == 1 && outputTex)            deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr);        else            deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr);        deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1);                deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr);    }    // 解除残剩绑定    deviceContext->CSSetShaderResources(0, 1, nullSRV);}
复制代码
此外C++端源码则间接去项目源码看即可。
HLSL代码

由于水平暗昧与垂直暗昧的实现道理相仿,这里我们只会商水平暗昧。
在上面的代码中,我们可以看到调节的线程组是由256个线程组成的水平“线段”,每个线程都负责图像中一个像素的暗昧操纵。一种低效的实现计划是,每个线程都简单地盘算出以正在处置惩罚的像素为中心的行矩阵(由于我们现在正在举行的是1D横向暗昧处置惩罚,所以要针对行矩阵举行盘算)的加权均匀值。这类法子的弱点是需要屡次拾取同一纹素。
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200214110120411-218110546

仅考虑输入图像中的这两个相邻像素,假定暗昧核为1×7。光是在对这两个像素举行暗冒鼬程中,8个差此外像素中就已经有6个被采集了2次,而且要考虑到拜候装备内存的服从在GPU内存模子中是属于比力慢的一种。
我们可以按照前面一节提到的暗昧处置惩罚计谋,利用同享内存来优化上述算法。这样一来,每个线程便可以在同享内存中读取或存储所需的纹素数据。待全数线程都从同享内存读取到它们所需的纹素后,便可以大要实行暗昧运算了。不能不说,从同享内存中读取数据的速度缓慢。除此之外,另有一件辣手的事变,就是利用具有n = 256个线程的线程组行暗昧运算的时候,却需要n + 2R个纹素数据,这里的R就是暗昧半径:
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200214111551900-1507096628

由于暗昧半径的原因原由,在处置惩罚线程组鸿沟四周的像素时,大要会读取线程组之外存在“越界”情况的像素。治理法子实在也并不复杂。我们只需要分派出能包容n + 2R个元素的同享内存,而且有2R个线程要各获得两个纹素数据。唯一麻烦的地方就是在同享内存时要多花心机,由于组外线程ID此时不能于同享内存中的元素逐一对应了。下图演示了当R=4时,从线程到同享内存的映照进程。
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200214111944531-450919303

在此例中,R = 4。最左侧的4个线程以及最右侧的4个线程,每个都要读取2个纹素数据,并将它们存于同享内存当中。而这8个线程之外的全数线程都只需要读取1个像素,并将其存于同享内存当中。这样一来,我们即可以获得以暗昧半径R对N个像素举行暗昧处置惩罚所需的全数纹素数据。
现在要会商的是末端一种情况,即下图中所示的最左侧于最右侧的线程组在索引输入图像时会发生越界的情况。
DirectX11 With Windows SDK--30 计算着色器:图像模糊、索贝尔算子  热点新闻 1172605-20200214112256687-1854513574

前面提到,从越界的索引处读取数据并不黑白法操纵,而是返回0(对越界索引处举行写入是不会实行任何操纵的,即no-op)。但是,我们在读取越界数据时并不渴望获得数据0,由于这意味着值为0的色彩(即黑色)会影响到鸿沟处的暗昧成果。我们此期间盼能实现出类似于钳位(clamp)纹理寻址形式的成果,即在读取越界的数据时,可以大要获得一个与鸿沟纹素类似的数据。这个计划可以经过对索引举行钳位来加以实现,鄙人面完整的着色器代码可以看到(这里将暗昧半径调大了):
  1. // Blur.hlslicbuffer CBSettings : register(b0){    int g_BlurRadius;        // 最多支持19个暗昧权值    float w0;    float w1;    float w2;    float w3;    float w4;    float w5;    float w6;    float w7;    float w8;    float w9;    float w10;    float w11;    float w12;    float w13;    float w14;    float w15;    float w16;    float w17;    float w18;}Texture2D g_Input : register(t0);RWTexture2D g_Output : register(u0);static const int g_MaxBlurRadius = 9;#define N 256#define CacheSize (N + 2 * g_MaxBlurRadius)
复制代码
[code]// Blur_Horz_CS.hlsl#include "Blur.hlsli"groupshared float4 g_Cache[CacheSize];[numthreads(N, 1, 1)]void CS(int3 GTid : SV_GroupThreadID,    int3 DTid : SV_DispatchThreadID){    // 放在数组中以便于索引    float g_Weights[19] =    {        w0, w1, w2, w3, w4, w5, w6, w7, w8, w9,        w10, w11, w12, w13, w14, w15, w16, w17, w18    };    // 经过填写当地线程存储区来淘汰带宽的负载。若要对N个像素举行暗昧处置惩罚,按照暗昧半径,    // 我们需要加载N + 2 * BlurRadius个像素        // 此线程组运转着N个线程。为了获得额外的2*BlurRadius个像素,就需要有2*BlurRadius个    // 线程都多采集一个像素数据    if (GTid.x < g_BlurRadius)    {        // 对于图像左侧鸿沟存在越界采样的情况举行钳位(Clamp)操纵        int x = max(DTid.x - g_BlurRadius, 0);        g_Cache[GTid.x] = g_Input[int2(x, DTid.y)];    }        if (GTid.x >= N - g_BlurRadius)    {        // 对于图像左侧鸿沟存在越界采样的情况举行钳位(Clamp)操纵        // 震动的是Texture2D居然能经过属性Length拜候宽高        int x = min(DTid.x + g_BlurRadius, g_Input.Length.x - 1);           g_Cache[GTid.x + 2 * g_BlurRadius] = g_Input[int2(x, DTid.y)];    }        // 将数据写入Cache的对应位置    // 针对图形鸿沟处的越界采样情况举行钳位处置惩罚    g_Cache[GTid.x + g_BlurRadius] = g_Input[min(DTid.xy, g_Input.Length.xy - 1)];        // 等待全数线程完成使命    GroupMemoryBarrierWithGroupSync();        // 起头对每个像素举行肴杂    float4 blurColor = float4(0.0f, 0.0f, 0.0f, 0.0f);    for (int i = -g_BlurRadius; i

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2006-2014 全椒百姓网-全椒知名**,发布及时新鲜的全椒新闻资讯 生活信息 版权所有 法律顾问:高律师 客服电话:0791-88289918
技术支持:迪恩网络科技公司  Powered by Discuz! X3.2
快速回复 返回顶部 返回列表