自适应阈值效果图 demo
avatar

这几天抽空看了下GpuImage的filter,移植了高斯模糊与自适应阈值的vulkan compute shader实现,一个是基本的图像处理,一个是组合基础图像处理聚合,算是比较有代表性的二种.

高斯模糊实现与优化
大部分模糊效果主要是卷积核的实现,相应值根据公式得到.

int ksize = paramet.blurRadius * 2 + 1;
if (paramet.sigma <= 0) {
paramet.sigma = ((ksize - 1) * 0.5 - 1) * 0.3 + 0.8;
}
double scale = 1.0f / (paramet.sigma * paramet.sigma * 2.0);
double cons = scale / M_PI;
double sum = 0.0;
std::vector karray(ksize * ksize);
for (int i = 0; i < ksize; i++) {
for (int j = 0; j < ksize; j++) {
int x = i - (ksize - 1) / 2;
int y = j - (ksize - 1) / 2;
karray[i * ksize + j] = cons * exp(-scale * (x * x + y * y));
sum += karray[i * ksize + j];
}
}
sum = 1.0 / sum;
for (int i = ksize * ksize - 1; i >= 0; i–) {
karray[i] *= sum;
}
其中对应compute shader代码.

#version 450

layout (local_size_x = 16, local_size_y = 16) in;// gl_WorkGroupSize
layout (binding = 0, rgba8) uniform readonly image2D inTex;
layout (binding = 1, rgba8) uniform image2D outTex;
layout (binding = 2) uniform UBO
{
int xksize;
int yksize;
int xanchor;
int yanchor;
} ubo;

layout (binding = 3) buffer inBuffer{
float kernel[];
};

void main(){
ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
ivec2 size = imageSize(outTex);
if(uv.x >= size.x || uv.y >= size.y){
return;
}
vec4 sum = vec4(0);
int kInd = 0;
for(int i = 0; i< ubo.yksize; ++i){
for(int j= 0; j< ubo.xksize; ++j){
int x = uv.x-ubo.xanchor+j;
int y = uv.y-ubo.yanchor+i;
// REPLICATE border
x = max(0,min(x,size.x-1));
y = max(0,min(y,size.y-1));
vec4 rgba = imageLoad(inTex,ivec2(x,y)) * kernel[kInd++];
sum = sum + rgba;
}
}
imageStore(outTex, uv, sum);
}
这样一个简单的高斯模糊就实现了,结果就是我在用Redmi 10X Pro在摄像头1080P下使用21的核长是不到一桢的处理速度.

高斯模糊的优化都有现成的讲解与实现,其一就是图像处理中的卷积核分离,一个m行乘以n列的高斯卷积可以分解成一个1行乘以n列的行卷积,计算复杂度从原来的O(k^2)降为O(k),其二就是用shared局部显存减少访问纹理显存的操作,注意这块容量非常有限,如果不合理分配,能并行的组就少了.考虑到Android平台,使用packUnorm4x8/unpackUnorm4x8优化局部显存占用.

其核分成一列与一行,具体相应实现请看VkSeparableLinearLayer类的实现,由二个compute shader组合执行.

int ksize = paramet.blurRadius * 2 + 1;
std::vector karray(ksize);
double sum = 0.0;
double scale = 1.0f / (paramet.sigma * paramet.sigma * 2.0);
for (int i = 0; i < ksize; i++) {
int x = i - (ksize - 1) / 2;
karray[i] = exp(-scale * (x * x));
sum += karray[i];
}
sum = 1.0 / sum;
for (int i = 0; i < ksize; i++) {
karray[i] *= sum;
}
rowLayer->updateBuffer(karray);
updateBuffer(karray);
其glsl主要逻辑实现来自opencv里opencv_cudafilters模块里cuda代码改写,在这只贴filterRow的实现,filterColumn的实现和filterRow类似,有兴趣的朋友可以自己翻看.

#version 450

layout (local_size_x = 16, local_size_y = 16) in;// gl_WorkGroupSize
layout (binding = 0, rgba8) uniform readonly image2D inTex;
layout (binding = 1, rgba8) uniform image2D outTex;
layout (binding = 2) uniform UBO
{
int xksize;
int anchor;
} ubo;

layout (binding = 3) buffer inBuffer{
float kernel[];
};

const int PATCH_PER_BLOCK = 4;
const int HALO_SIZE = 1;
// 共享块,扩充左边右边HALO_SIZE(分为左边HALO_SIZE,中间自身PATCH_PER_BLOCK,右边HALO_SIZE)
shared uint row_shared[16][16
(PATCH_PER_BLOCK+HALO_SIZE*2)];//vec4[local_size_y][local_size_x]

// 假定19201080,gl_WorkGroupSize(16,16),gl_NumWorkGroups(120/4,68),每一个线程宽度要管理4个
// 核心的最大宽度由HALO_SIZE
gl_WorkGroupSize.x决定
void main(){
ivec2 size = imageSize(outTex);
uint y = gl_GlobalInvocationID.y;
if(y >= size.y){
return;
}
// 纹理正常范围的全局起点
uint xStart = gl_WorkGroupID.x * (gl_WorkGroupSize.xPATCH_PER_BLOCK) + gl_LocalInvocationID.x;
// 每个线程组填充HALO_SIZE
gl_WorkGroupSize个数据
// 填充每个左边HALO_SIZE,需要注意每行左边是没有纹理数据的
if(gl_WorkGroupID.x > 0){//填充非最左边块的左边
for(int j=0;j<HALO_SIZE;++j){
vec4 rgba = imageLoad(inTex,ivec2(xStart-(HALO_SIZE-j)gl_WorkGroupSize.x,y));
row_shared[gl_LocalInvocationID.y][gl_LocalInvocationID.x + j
gl_WorkGroupSize.x] = packUnorm4x8(rgba);
}
}else{ // 每行最左边
for(int j=0;j<HALO_SIZE;++j){
uint maxIdx = max(0,xStart-(HALO_SIZE-j)gl_WorkGroupSize.x);
vec4 rgba = imageLoad(inTex,ivec2(maxIdx,y));
row_shared[gl_LocalInvocationID.y][gl_LocalInvocationID.x + j
gl_WorkGroupSize.x] = packUnorm4x8(rgba);
}
}
// 填充中间与右边HALO_SIZE块,注意每行右边的HALO_SIZE块是没有纹理数据的
if(gl_WorkGroupID.x + 2 < gl_NumWorkGroups.x){
// 填充中间块
for(int j=0;j<PATCH_PER_BLOCK;++j){
vec4 rgba = imageLoad(inTex,ivec2(xStart+j*gl_WorkGroupSize.x,y));
uint x = gl_LocalInvocationID.x + (HALO_SIZE+j)*gl_WorkGroupSize.x;
row_shared[gl_LocalInvocationID.y][x] = packUnorm4x8(rgba);
}
// 右边的扩展中,还在纹理中
for(int j=0;j<HALO_SIZE;++j){
vec4 rgba = imageLoad(inTex,ivec2(xStart+(PATCH_PER_BLOCK+j)*gl_WorkGroupSize.x,y));
uint x = gl_LocalInvocationID.x + (PATCH_PER_BLOCK+HALO_SIZE+j)gl_WorkGroupSize.x;
row_shared[gl_LocalInvocationID.y][x] = packUnorm4x8(rgba);
}
}else{// 每行右边的一个块
for (int j = 0; j < PATCH_PER_BLOCK; ++j){
uint minIdx = min(size.x-1,xStart+j
gl_WorkGroupSize.x);
uint x = gl_LocalInvocationID.x + (HALO_SIZE+j)*gl_WorkGroupSize.x;
row_shared[gl_LocalInvocationID.y][x] = packUnorm4x8(imageLoad(inTex,ivec2(minIdx,y)));
}
for(int j=0;j<HALO_SIZE;++j){
uint minIdx = min(size.x-1,xStart+(PATCH_PER_BLOCK+j)*gl_WorkGroupSize.x);
uint x = gl_LocalInvocationID.x + (PATCH_PER_BLOCK+HALO_SIZE+j)gl_WorkGroupSize.x;
row_shared[gl_LocalInvocationID.y][x] = packUnorm4x8(imageLoad(inTex,ivec2(minIdx,y)));
}
}
// groupMemoryBarrier();
memoryBarrier();
for (int j = 0; j < PATCH_PER_BLOCK; ++j){
uint x = xStart + j
gl_WorkGroupSize.x;
if(x<size.x){
vec4 sum = vec4(0);
for(int k=0;k<ubo.xksize;++k){
uint xx = gl_LocalInvocationID.x + (HALO_SIZE+j)gl_WorkGroupSize.x - ubo.anchor + k;
sum = sum+unpackUnorm4x8(row_shared[gl_LocalInvocationID.y][xx]) * kernel[k];
}
imageStore(outTex, ivec2(x,y),sum);
}
}
}
一般compute shader常用图像处理操作来说,我们一个线程处理一个像素,在这里PATCH_PER_BLOCK=4,表示一个线程操作4个像素,所以线程组的分配也会改变,针对图像块就是WorkGroupSize
PATCH_PER_BLOCK这块正常取对应数据,其中HALO_SIZE块在row中是左右二边,如果是最左边和最右边需要考虑取不到的情况,我采用的逻辑对应opencv的边框填充REPLICATE模式,余下的块的HALO_SIZE块都不是对应当前线程组对应的图像块.column的上下块同理,可以看到最大核的大小限定在HALO_SIZEx2+WorkGroupSize,如果真有超大核的要求,可以变大HALO_SIZE.

不过优化完后,我发现在PC平台应用会有噪点,特别是核小的时候.我分别针对filterRow/filterColumn做测试应用,发现只有filterColumn有问题,而代码我反复检测也没发现那有逻辑错误,更新逻辑查看filterColumn各种测试中,我发现在groupMemoryBarrier后,隔gl_WorkGroupSize.y的数据能拿到,但是行+1拿的是有噪点的,断定问题出在同步局部共享显存上,前面核大不会出现这问题也应该是核大导致局部共享显存变大导致并行线程组数少,groupMemoryBarrier改为memoryBarrier还是不行,后改为barrier可行,按逻辑上来说,应该是用groupMemoryBarrier就行,不知是不是和硬件有关,不为奇怪的是为啥filterRow的使用groupMemoryBarrier没问题了,二者唯一区别一个是扩展宽度,一个扩展长度,有思路的朋友欢迎解答.

在1080P下取核长为21(半径为10)的高斯模糊查看PC平台没有优化及优化的效果.

avatar

avatar

其中没优化的需要12.03ms,而优化后的是0.60+0.61=1.21ms,差不多10倍左右的差距,符合前面k/2的优化值,之所以快到理论值,应该要加上优化方向二使用局部共享显存减少访问纹理显存这个.

把更新后的实现再次放入Redmi 10X Pro,同样1080P下21核长下,可以看到不是放幻灯片了,差不多有10桢了吧,没有专业工具测试,后续有时间完善测试比对.

AdaptiveThreshold 自适应阈值化
可以先看下GPUImage3里的实现.

public class AdaptiveThreshold: OperationGroup {
public var blurRadiusInPixels: Float { didSet { boxBlur.blurRadiusInPixels = blurRadiusInPixels } }

let luminance = Luminance()
let boxBlur = BoxBlur()
let adaptiveThreshold = BasicOperation(fragmentFunctionName:"adaptiveThresholdFragment", numberOfInputs:2)public override init() {blurRadiusInPixels = 4.0super.init()self.configureGroup{input, output ininput --> self.luminance --> self.boxBlur --> self.adaptiveThreshold --> outputself.luminance --> self.adaptiveThreshold}
}

}
可以看到实现不复杂,根据输入图片得到亮度,然后boxBlur,然后把亮度图与blur后的亮度图交给adaptiveThreshold处理就完成了,原理很简单,但是要求层可以内部加入别的处理层以及多输入,当初设计时使用Graph计算图时就考虑过多输入多输出的问题,这个是支持的,内部层加入别的处理层,这是图层组合能力,这个我当初设计是给外部使用者用的,在这稍微改动一下,也是比较容易支持内部层类组合.

void VkAdaptiveThresholdLayer::onInitGraph() {
VkLayer::onInitGraph();
// 输入输出
inFormats[0].imageType = ImageType::r8;
inFormats[1].imageType = ImageType::r8;
outFormats[0].imageType = ImageType::r8;
// 这几个节点添加在本节点之前
pipeGraph->addNode(luminance.get())->addNode(boxBlur->getLayer());
// 更新下默认UBO信息
memcpy(constBufCpu.data(), &paramet.offset, conBufSize);
}

void VkAdaptiveThresholdLayer::onInitNode() {
luminance->getNode()->addLine(getNode(), 0, 0);
boxBlur->getNode()->addLine(getNode(), 0, 1);
getNode()->setStartNode(luminance->getNode());
}
和别的处理层一样,不同的是添加这个层时,根据onInitNode设定Graph如何自动连接前后层.

相应的luminance/adaptiveThreshold以及专门显示只有一个通道层的图像处理大家有兴趣自己翻看,比较简单就不贴了.

有兴趣的可以在samples/vulkanextratest里,PC平台修改Win32.cpp,Android平台修改Android.cpp查看不同效果.后续有时间完善android下的UI使之查看不同层效果.

Vulkan移植GpuImage(一)高斯模糊与自适应阈值相关推荐

  1. 图像二值化方法及适用场景分析(OTSU Trangle 自适应阈值分割)

    图像二值化 应用场景 二值图像定义 阈值获取的方法 手动阈值法 自动阈值法 灰度均值法 基于直方图均值法 OTSU Triangle 自适应均值阈值分割方法 总结 参考文献 应用场景 二值图像处理与分 ...

  2. 【千律】OpenCV基础:图像阈值分割 -- 自适应阈值分割 -- 代码实现

    环境:Python3.8 和 OpenCV 内容:自适应阈值分割代码实现 import cv2 as cv import numpy as np import matplotlib.pyplot as ...

  3. 全局阈值和自适应阈值

    1.均值法 计算图片的色彩平均值,然后大于阈值的设置为255,小于阈值的设置为0. 2.OTSU 通过寻找类内最小方差:即先将图像按照色彩画出直方图.按色彩值分成两个大类,使每个类的方差最小. 3.三 ...

  4. cv2.threshholding()简单阈值、自适应阈值,Octus阈值

    @[TOC](cv2.threshholding()简单阈值.自适应阈值,Octus阈值 这篇博客将延续上一篇简单阈值处理,继续介绍自适应阈值及Octus阈值: 简单阈值详情见: https://bl ...

  5. Python使用openCV把原始彩色图像转化为灰度图、使用OpenCV把图像二值化(仅仅包含黑色和白色的简化版本)、基于自适应阈值预处理(adaptive thresholding)方法

    Python使用openCV把原始彩色图像转化为灰度图.使用OpenCV把图像二值化(仅仅包含黑色和白色的简化版本).基于自适应阈值预处理(adaptive thresholding)方法 目录

  6. OpenCV自适应阈值化函数adaptiveThreshold详解,并附实例源码

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 2016-6-14日:又发现一种阈值分割法,最大 ...

  7. otsu自适应阈值分割的算法描述和opencv实现,及其在肤色检测中的应用

    from:http://blog.csdn.net/onezeros/article/details/6136770 otsu算法选择使类间方差最大的灰度值为阈值,具有很好的效果 算法具体描述见ots ...

  8. [转载+原创]Emgu CV on C# (五) —— Emgu CV on 局部自适应阈值二值化

    局部自适应阈值二值化 相对全局阈值二值化,自然就有局部自适应阈值二值化,本文利用Emgu CV实现局部自适应阈值二值化算法,并通过调节block大小,实现图像的边缘检测. 一.理论概述(转载自< ...

  9. 【图像处理】——图像的二值化操作及阈值化操作(固定阈值法(全局阈值法——大津法OTSU和三角法TRIANGLE)和自适应阈值法(局部阈值法——均值和高斯法))

    目录 一.二值化的概念(实际上就是一个阈值化操作) 1.概念: 2.实现方法 3.常用方法 二.阈值类型 1.常见阈值类型(主要有五种类型) (1)公式描述 (2)图表描述 2.两种特殊的阈值算法(O ...

最新文章

  1. 运维老鸟告诉你这个经典Zookeeper问题的根因
  2. 《认清C++语言》的random_shuffle()和transform()算法
  3. 如果我问你:排序算法的「稳定性」有何意义?你怎么回答?
  4. 在hive中对日期数据进行处理,毫秒级时间转化为yyyy-MM-dd格式
  5. c语言二分法_14个经典C语言算法你就不看一眼?(附详细代码)
  6. 干粉灭火器(泡沫灭火器)工作原理
  7. 自从上了 SkyWalking,睡觉真香!!来,通过 Excel 来认识神器——POI
  8. nyoj(简单数学)Oh, my Paper!
  9. 第一章 计算机组成原理 ---- 概述
  10. vscode下使用gcc进行Npcap网络编程开发的环境配置
  11. idea 2018汉化包(附使用教程)
  12. 硬盘容量统计神器WinDirStat
  13. RadarNet: Efficient Gesture Recognition Technique Utilizing a Miniaturized Radar Sensor
  14. 数美科技:全栈防御体系怎么样护航游戏ROI增长
  15. linux 命令修改IP(最有效方法)
  16. js获取当前是第几周
  17. Bellman Equation 贝尔曼方程
  18. BAT大厂的架构大数据你有了解么?解析大数据技术及算法
  19. 出现Joi.validate is a not function解决办法
  20. 我的创业日记(序)——人生在于一种体验

热门文章

  1. jquery锚点定位
  2. 唐院桥隧系系主任张万久教授生平
  3. kivy之Slider滑块实操练习
  4. linux分区方案 500g,Linux分区方案最节省的分区方案
  5. 笔记本蓝屏,开不了机的处理记录
  6. 【SpringBoot】一文了解SpringBoot热部署
  7. HTTP错误汇总(404、302、200……)
  8. office2013专业批量授权版的安装步骤
  9. 卷积神经网络典型应用———AlexNet
  10. android这个软件在哪里设置,怎么设置安卓手机软件的默认安装位置