作者:京东科技 李大冲

一、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的矩阵操作来实现

  1. 代码如下,使用numpy的广播的原理来实现,然后通过一系列的过滤异常值后,再经过插值进一步的处理得到最终想要的结果。

  2. 这段代码没有进一步的去实现插值的过程,耗时情况为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的更多相关文章

  1. javaScript中的异步编程模式

    1.事件模型 let button = document.getElementById("my-btn"); button.onclick = function(event) { ...

  2. CPU和GPU实现julia

    CPU和GPU实现julia           主要目的是通过对比,学习研究如何编写CUDA程序.julia的算法还是有一定难度的,但不是重点.由于GPU实现了也是做图像识别程序,所以缺省的就是和O ...

  3. 第一篇:GPU 编程技术的发展历程及现状

    前言 本文通过介绍 GPU 编程技术的发展历程,让大家初步地了解 GPU 编程,走进 GPU 编程的世界. 冯诺依曼计算机架构的瓶颈 曾经,几乎所有的处理器都是以冯诺依曼计算机架构为基础的.该系统架构 ...

  4. Point : GPU编程的艺术!一切的历史!

    Point: 渲染渲染,神奇的渲染!! ———————————————— 只要你走的足够远,你肯定能到达某个地方. 1"GPU编程" History ————————— //由于笔 ...

  5. GPU编程自学7 —— 常量内存与事件

    深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...

  6. GPU编程自学6 —— 函数与变量类型限定符

    深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...

  7. GPU编程自学5 —— 线程协作

    深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...

  8. GPU编程自学4 —— CUDA核函数运行参数

    深度学习的兴起,使得多线程以及GPU编程逐渐成为算法工程师无法规避的问题.这里主要记录自己的GPU自学历程. 目录 <GPU编程自学1 -- 引言> <GPU编程自学2 -- CUD ...

  9. GPU 编程相关 简要摘录

    GPU 编程可以称为异构编程,最近由于机器学习的火热,很多模型越来越依赖于GPU来进行加速运算,所以异构计算的位置越来越重要:异构编程,主要是指CPU+GPU或者CPU+其他设备(FPGA等)协同计算 ...

  10. cg语言学习&&阳春白雪GPU编程入门学习

    虽然所知甚少,但康大的<GPU编程与Cg编程之阳春白雪下里巴人>确实带我入了shader的门,在里面我第一次清晰地知道了“语义”的意思,非常感谢. 入门shader,我觉得可以先读3本书: ...

随机推荐

  1. Flink SQL管理平台flink-streaming-platform-web安装搭建

    文章都在个人博客网站:https://www.ikeguang.com/ 同步,欢迎访问. 最近看到有人在用flink sql的页面管理平台,大致看了下,尝试安装使用,比原生的flink sql界面确 ...

  2. js- day03- 将数据变成柱形图

    柱形图的渲染 * {             margin: 0;             padding: 0;         }  .box {             display: fle ...

  3. 使用sanic框架实现分布式爬虫

    bee_server.py from sanic import Sanic from sanic import response from urlpool import UrlPool #初始化url ...

  4. Jgit的使用笔记

    原文:Jgit的使用笔记 - Stars-One的杂货小窝 之前整的一个系统,涉及到git代码的推送,是通过cmd命令去推送的,然后最近在产品验收的时候,测试部门随意填了个git仓库,然后导致仓库代码 ...

  5. 第三模块的下载、requests模块、openpyxl模块

    目录 第三方模块的下载安装 下载第三模块的方式 针对下载第三模块时可能会出现的问题 网络爬虫模块之requests模块 自动化办公领域之openpyxl模块 第三方模块的下载安装 第三方模块:别人写的 ...

  6. B站地区限制破解方法

    B站地区限制破解方法 当我们观看bilibili番剧时,经常会看到"仅限港澳台地区"的字样.那么,有没有一种方法,不需要挂梯子,就可以观看这些被限制的番剧呢? 本教程只适用于bil ...

  7. 集合框架-Collection集合

    集合框架 JAVASE提供了满足各种需求的APl,在使用这些API前,先了解其继承与接口操作架构,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用.集合按照其存储结构可以分为两大类,分 ...

  8. 【Azure Cache for Redis】Python Djange-Redis连接Azure Redis服务遇上(104, 'Connection reset by peer')

    问题描述 使用Python连接Azure Redis服务,因为在代码中使用的是Djange-redis组件,所以通过如下的配置连接到Azure Redis服务: CACHES = { "de ...

  9. Linux存储服务

    存储服务 一.概述 存储:用于存放用户上传的内容(数据),一般应用在网站集群中 为什么要存储? 如果不使用存储,用户上传的数据就直接存放在某一台网站服务器上了,用户下次访问就可能找不到 如果使用存储, ...

  10. QuartzNet在winform中使用(目前版本3.6)

    界面图"没有什么技术含量~   不过还是有部分人不太了解的 接下来一一解析下,勿q 使用步骤: 1.初始化帮助类 QuarztHelper x = new QuarztHelper(); 2 ...