用于双目重建中的GPU编程:julia-cuda
作者:京东科技 李大冲
一、Julia是什么
julia是2010年开始面世的语言,作为一个10后,Julia必然有前辈们没有的特点。Julia被期望塑造成原生的有C++的运行速度、python的易交互性以及胶水性。最重要的是,julia也是nVidia-cuda官方选中的接口语言,这点让cuda编程变得稍微容易一些。
二、项目背景
双目立体视觉是比较常规获取深度信息的算法。这是一种模仿人的双眼的方法,根据对同一个点在不同的视角进行观察,并在不同的观察视角上的观察差异推算出深度,这在图1中更清晰一点。被动双目的缺点是需要纹理特征去判断左右相机里是否为同一个点。也就是,当没有问题、纹理不够明显或者纹理重复时,这种方法难以工作。
图 1 被动双目示意图。 观测点在一个相机和在另外一个相机上的成像是有明显可区分性的,二者的观察差异(L1和L2)形成了视差,进而可形成深度信息
被动双目的缺点是某些场景下无效,主动双目便出现了。如果我们投射一些人工设定编码的图案,根据实现约定通过解码可以对视野内是所有点赋予唯一的标识符号,那被动双目的问题不就解决了吗。事实上,人们很快发现可以使用空间上编码和时间上编码两种方式。空间上编码,就像人们观察银河命名星座一样,去投射随机生成的星星点点的光斑,没有纹理的夜空照射进了完全可辨识的图案。如同星座往往是一个空间范围很大的概念,空间编码的方式无法做到非常精细。所以,时间编码的方式应运而生,时间编码是投射一组图案,让被照射的区域,被照射的明暗变化情况是完全不一样的。像图2中的二进制编码(或行业内称为格雷码),再加上灰度变化更加丰富的编码方式以及相机在竖方向上对齐之后是只需要考虑横轴方向上的差异这两点,可以实现全视野的点完全可区分。
图 2 空间编码和时间编码。空间编码如同银河中的星星点点,通过组合星点变为星座,人们可以对深邃夜空不同位置依靠银河区分开。时间编码是投射多组图案,让每一个点是时间上都有唯一的变化情况。就像右图,这16个方格都是完全可区分的。
三、效率问题
使用主动双目的结构去重建,一个很大的影响运行效率的问题是需要对左右图像求算视差(原理如图1所示)。具体求算视差的操作是要对左相机里的每一个点,去找右相机中同一行中与之距离最近的点的横坐标,然后继续插值之后找到距离更近的插值坐标,最后把这两个坐标的差异当作视差进行后续的计算。
使用时间编码的主动双目,按照实现约定的编码进行解码之后,其效果如图3所示,这个环节就是在这对数据上进行处理。以此数据为例,有效区域大概是700850,完全遍历所需要的时间复杂度大概是O(700850*850),直接估算就很耗时。
图 3 时间编码解码后的图像示意图。左右相机拍摄的图案编程了解码值。接下来需要在这对数据上计算视差,而确定相同编码值需要大量的遍历,对相同编码进一步的优化还要对每个点进行小范围的插值处理,时间复杂度很大。
图 4 视差图效果
1. 使用for训练的方式
使用for循环,需要3层。经测试后,在TX2上,使用c++来完成大概需要7s多。
2. 使用numpy的矩阵操作来实现
代码如下,使用numpy的广播的原理来实现,然后通过一系列的过滤异常值后,再经过插值进一步的处理得到最终想要的结果。
这段代码没有进一步的去实现插值的过程,耗时情况为1.35s。
def disp(l_, r_, colstartR, colendR, colstartL, colendL, rowstart, rowend):
# 聚焦在有效值区域
l = l_[rowstart: rowend, colstartL: colendL]
r = r_[rowstart: rowend, colstartR: colendR]
# 算一下阈值。阈值帮助确定判断太远的点就不能当作是目标值
thres = (np.max(l) - np.min(l)) / (colendL - colstartL) * 10
h, w = l.shape
ll = l.T.reshape(w, h, 1)
# print(l.shape, r.shape)
# 加快运算,缩小图片,4倍下采样
quart_r = cv2.resize(r, (w // 4, h))
# 计算距离
distance = np.abs(ll - quart_r)
# 计算点位
idx_old = np.argmin(distance, axis=-1).T * 4
# 去除指向同一个点的重复值
_, d2 = np.unique(idx_old, return_index=True)
idx = np.zeros_like(idx_old).reshape(-1)
idx[d2] = idx_old.reshape(-1)[d2]
idx = idx.reshape(idx_old.shape)
# 计算最值
minV = np.min(distance, axis=-1).T
ori_idx = np.tile(np.arange(w).reshape(w, 1), h).T
disparity = ori_idx - idx + colstartL - colstartR
# 去除异常点
disparity[minV > thres] = 0# 大于阈值的
disparity[l < 1e-3] = 0 # 左图点无效的
# 插值
# 方法:在最值附近的区域去-5:5的值,二次线性插值上采样,采完之后再计算一遍上面的步骤,算完之后取小数进行计算
# 暂时没有实现这一步
return disparity, l, r
3. 使用julia-cuda实现
这段主要是cuda核函数的部分,主要包括以下部分:
1)用到了512*700个cuda线程来实现,增加的并行计算抵消了单个点位计算耗时。
2)还用到了共享显存,降低读取数据的耗时,因为右图中的每一行都会被左图中的同行中的每一个元素加载一遍,对于元素for循环是840*840的时间空间复杂度。这儿加载一遍,就可以让这些线程同时看到了。
3)对于上述过程中提到的插值一事,使用cuda的纹理内容轻松搞定,因为纹理内存的一个特性就是插值。即对于一个向量,可以取得到下标不是整数时的值,这些值就是自动插值出现的,且此过程的资源消耗非常低,因为这部分的初始设计是为了图像渲染和可视化。
4)通过这部分代码,实现相同的效果在TX2上的耗时是165ms,快了近10倍。
function bench_match_smem(cfg, phaL, phaR, w, h, winSize, pha_dif)
texarr2D = CuTextureArray(phaR)
tex2D = CuTexture(texarr2D; interpolation = CUDA.LinearInterpolation())
cp, minv, maxv = cfg.cpdiff, cfg.minv, cfg.maxv
colstart, colend = cfg.colstart, cfg.colend
rowstart, rowend = cfg.rowstart, cfg.rowend
mindis, maxdis = cfg.mindis, cfg.maxdis
col_ = cld((colend - colstart), 32) * 32
row_ = cld(rowend - rowstart, 32) * 32
stride = Int(cld((maxv - minv + 1), 512))
threadsPerBlock = round(Int32, cld((maxv - minv + 1) / stride, 32) * 32)
blocksPerGrid = row_
println("blocks = $blocksPerGrid threads = $threadsPerBlock left $(col_) right $(maxv - minv) h=$(row_)")
@cuda blocks = blocksPerGrid threads = threadsPerBlock shmem =
(threadsPerBlock * sizeof(Float32)) phaseMatch_smem!(cp, mindis, maxdis, minv, maxv, colstart, colend, rowstart, rowend, phaL, tex2D, threadsPerBlock,blocksPerGrid,stride, w, h, winSize, pha_dif)
CUDA.synchronize()
return
end
#进行立体相位匹配
function phaseMatch_smem!(cp, mindis, maxdis, minv, maxv, colstart, colend, rowstart, rowend, phaL, phaR, threadsPerBlock, blocksPerGrid, stride, w, h, winSize, pha_dif)
#---------------------------------------------------
# cp: diparity map
# mindis, maxdis 最近最远视差
#minv, maxv 仿射变换计算得到的R图中有效横向范围
# colstart, colend, rowstart, rowend仿射变换计算得到的左图中有效横向、竖向范围
#phaL, phaR 左右图像
#w, h图像大小
#winSize, pha_dif 3*3的框; 阈值:约等于20个像素的平均相位距离和
# Set up shared memory cache for this current block.
#---------------------------------------------------
wh = fld(winSize, 2)
cache = @cuDynamicSharedMem(Float32, threadsPerBlock)
left_stride = 64
minv = max(1,minv)#必须是有效值,且是julia下的下标计数方式
colstart= max(1,wh*stride)
# 数据读入共享内存
j = blockIdx().x + rowstart # 共用的行序号
i = threadIdx().x + colstart# 左图的列序号
while(j <= min(rowend,h - wh))
#数据拷贝到共享内存中去,并将由threadsPerBlock共享
ri = (threadIdx().x - 1) * stride + minv
tid = threadIdx().x
while(tid <= threadsPerBlock && ri <= maxv)
cache[tid] = phaR[j, ri]
tid+=threadsPerBlock
ri+=threadsPerBlock
end
# synchronise threads
sync_threads()
maxv = min(fld(maxv - minv,stride) * stride + minv,maxv)
# 计算最小匹配项
while(i <= min(colend,w - (wh*stride)))
min_v = 10000
XR = -1
VV = phaL[j, i]
if(VV > 0.001f0)
kStart = max(minv, i - maxdis) + 1
kEnd = min(maxv, i - mindis) - 1
for k = kStart:kEnd #遍历一整行
RK = cache[cld(k - minv + 1,stride)]#从0开始计数
if RK <= 0.001f0
continue
end
dif = abs(VV - RK)
if dif < pha_dif
sum = 0.0f0
sn = 1
for ki in 0:(winSize - 1)
R_local = cache[cld(k - minv + 1 - wh + ki,stride)]
(R_local < 1e-5) && continue
#phaL[j + kj - wh, i + ki - wh*stride]
VR = VV - R_local
sum = sum + abs(VR)# * VR
sn += 1
end
v = sum / sn
if v < min_v
min_v = v
XR = k
end
end
end
#需要作插值
#https://discourse.julialang.org/t/base-function-in-cuda-kernels/21866
if XR > 0
XR_new = bisection(VV, phaR, Float32(j), Float32(XR - 3), Float32(XR + 3))
# 注意,这里直接做了视差处理了
state = (i - XR_new) > 0
@inbounds cp[j, i] = state ? (i - XR_new) : 0.0f0
end
end
i+=threadsPerBlock
end
j+=blocksPerGrid
end
sync_threads()
return
end
4. 使用金字塔原理再次加速
1)这部分和上述代码的区别是,没有让少于列数目的线程进行多次运算,(上述代码中有个自增操作,是让线程进行了多次运算,原因是可分配线程总数不够)
2)整体是金字塔模式,即相邻者相似的原理。
3)时间消耗在TX2上降低到了72ms,相比于前一种方法又有了58%的降幅。
#进行立体相位匹配
function phaseMatch_smem!(cp, mindis, maxdis,
minv, maxv, colstart, colend,
rowstart, rowend, phaL, phaR,
threadsPerBlock, blocksPerGrid, stride,
w, h, winSize, pha_dif)
#---------------------------------------------------
# cp: diparity map
# mindis, maxdis 最近最远视差
#minv, maxv 仿射变换计算得到的R图中有效横向范围
# colstart, colend, rowstart, rowend仿射变换计算得到的左图中有效横向、竖向范围
#phaL, phaR 左右图像
#w, h图像大小
#winSize, pha_dif 3*3的框; 阈值:约等于20个像素的平均相位距离和
# Set up shared memory cache for this current block.
#---------------------------------------------------
wh = fld(winSize, 2)
cache = @cuDynamicSharedMem(Float32, threadsPerBlock)
minv = max(1,minv)#必须是有效值,且是julia下的下标计数方式
colstart = max(1, colstart, wh*stride)
rowstart = max(1, rowstart, wh)#需要算3*3的矩阵,目前没有计算,所以不用严格满足>wh
# i 最大、最小区间,所需要的迭代次数,或者可以看成所需处理的步长
left_stride = Int(cld(colend - colstart, threadsPerBlock))
j = (blockIdx().x - 1) + rowstart # 共用的行序号
i = (threadIdx().x - 1) * left_stride + colstart# 左图的列序号
while(j <= min(rowend,h - wh))
#数据拷贝到共享内存中去,并将由threadsPerBlock共享
ri = (threadIdx().x - 1) * stride + minv
tid = threadIdx().x
while(tid <= threadsPerBlock && ri <= maxv)
cache[tid] = phaR[j, ri]
tid+=threadsPerBlock
ri+=threadsPerBlock
end
# synchronise threads
sync_threads()
maxv = min(fld(maxv - minv,stride) * stride + minv,maxv)
# 计算最小匹配项
#end_i = i + left_stride
#while(i <= min(colend, w - (wh*stride))) #end_i - 1,
if(i <= min(colend, w - (wh*stride))) #end_i - 1,
min_v = 10000
XR = -1
VV = phaL[j, i]
if(VV > 0.001f0)
kStart = max(minv, i - maxdis)
kEnd = min(maxv, i - mindis)
for k = kStart:kEnd #遍历一整行
RK = cache[cld(k - minv + 1,stride)]#从0开始计数
if RK <= 0.001f0
continue
end
dif = abs(VV - RK)
if dif < pha_dif
sum = 0.0f0
sn = 1
for ki in 0:(winSize - 1)
R_local = cache[cld(k - minv + 1 - wh + ki,stride)]
(R_local < 1e-5) && continue
#phaL[j + kj - wh, i + ki - wh*stride]
VR = VV - R_local
sum = sum + abs(VR)# * VR
sn += 1
end
#v = sqrt(sum)/ sn
v = sum / sn
if v < min_v
min_v = v
XR = k
end
end
end
#需要作插值
#https://discourse.julialang.org/t/base-function-in-cuda-kernels/21866
if XR > 0
for offset in 0:left_stride - 1
i += offset
VV = phaL[j, i]
XR_new = bisection(VV, phaR, Float32(j), Float32(XR - 3), Float32(XR + 3))
# 注意,这里直接做了视差处理了
state = (i - XR_new) > 0
@inbounds cp[j, i] = state ? (i - XR_new) : 0.0f0
end
end
end
end
j+=blocksPerGrid
end
sync_threads()
return
end
四、总结
julia作为一个交互性强的语言,在上述目的的达成上,它至少是做到了。对于上述算是一个标准的cuda加速过程,用cuda-c进行编写的话,也是类似的过程,典型操作。但是c++编写的可视化、调试、最终的编译要麻烦的多得多得多。julia达到高效率的目的的同时,让编写的过程没有那么痛苦,堪称完美的一次体验。
用于双目重建中的GPU编程:julia-cuda的更多相关文章
- javaScript中的异步编程模式
1.事件模型 let button = document.getElementById("my-btn"); button.onclick = function(event) { ...
- CPU和GPU实现julia
CPU和GPU实现julia 主要目的是通过对比,学习研究如何编写CUDA程序.julia的算法还是有一定难度的,但不是重点.由于GPU实现了也是做图像识别程序,所以缺省的就是和O ...
- 第一篇:GPU 编程技术的发展历程及现状
前言 本文通过介绍 GPU 编程技术的发展历程,让大家初步地了解 GPU 编程,走进 GPU 编程的世界. 冯诺依曼计算机架构的瓶颈 曾经,几乎所有的处理器都是以冯诺依曼计算机架构为基础的.该系统架构 ...
- Point : GPU编程的艺术!一切的历史!
Point: 渲染渲染,神奇的渲染!! ———————————————— 只要你走的足够远,你肯定能到达某个地方. 1"GPU编程" History ————————— //由于笔 ...
- GPU编程自学7 —— 常量内存与事件
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学6 —— 函数与变量类型限定符
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学5 —— 线程协作
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU编程自学4 —— CUDA核函数运行参数
深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...
- GPU 编程相关 简要摘录
GPU 编程可以称为异构编程,最近由于机器学习的火热,很多模型越来越依赖于GPU来进行加速运算,所以异构计算的位置越来越重要:异构编程,主要是指CPU+GPU或者CPU+其他设备(FPGA等)协同计算 ...
- cg语言学习&&阳春白雪GPU编程入门学习
虽然所知甚少,但康大的<GPU编程与Cg编程之阳春白雪下里巴人>确实带我入了shader的门,在里面我第一次清晰地知道了“语义”的意思,非常感谢. 入门shader,我觉得可以先读3本书: ...
随机推荐
- postgresql函数:定期删除模式下指定天数前的表数据及分区物理表
一.现有函数-- 1.现有函数调用select ods.deletePartitionIfExists('fact_ship' || '_' || to_char(CURRENT_DATE - INT ...
- 【每日一题】【字符串与数字互转】【去除空格】【大数处理】2021年12月12日-8. 字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数). 函数 myAtoi(string s) 的算法如下: ...
- 介绍一款高性能分布式MQTT Broker(带web)
SMQTTX介绍 SMQTTX是基于SMQTT的一次重大技术升级,基于Java开发的分布式MQTT集群,是一款高性能,高吞吐量,并且可以完成二次开发的优秀的开源MQTT broker,主要采用技术栈: ...
- python软件开发目录规范
软件开发目录规范 1.文件及目录的名字可以变换 但是思想是不变的 分类管理 2.目录规范主要规定开发程序的过程中针对不同的文件功能需要做不同的分类 myproject项目文件夹 1,bin文件夹 -- ...
- .NET周报【12月第1期 2022-12-08】
国内文章 CAP 7.0 版本发布通告 - 支持延迟消息,性能炸了? https://www.cnblogs.com/savorboard/p/cap-7-0.html) 今天,我们很高兴宣布 CAP ...
- Redis的数据持久化
介绍 Redis 的数据持久化方案 Redis 的数据持久化主要有两大机制,AOF 日志和 RDB 快照. AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态. RDB 持久化 ...
- 使用linux的ffmpeg进行B站直播推流
很久之前买了个友善的开发板R2S,一直在家吃灰.最近看到网上有用ffmpeg进行直播推流的案例,想把吃灰的的开发板利用起来,于是有了这篇教程. 第一步:安装ffmpeg sudo apt update ...
- 时钟同步服务器ntp安装文档
应用场景 同步时钟很有必要,如果服务器的时间差过大会出现不必要的问题 大数据产生与处理系统是各种计算设备集群的,计算设备将统一.同步的标准时间用于记录各种事件发生时序, 如E-MAIL信息.文件创建和 ...
- cookie、session,、token,还在傻傻分不清?
摘要:session 和 token 本质上是没有区别的,都是对用户身份的认证机制,只是他们实现的校验机制不一样而已. 本文分享自华为云社区<Session/Cookie/Token 还傻傻分不 ...
- 算法之Dijkstra及其堆优化和SPFA:图上单源最短路径神器
签到题-- 题目传送门 SPFA算法 本人曾经写过一篇有关Bellman-ford的博,但就算是挂了优化的ford也只能过这道题的弱化版. 今天就先填个坑,先讲SPFA. 在这里我直接认为你们已经有一 ...