IDA-3D技术细节分析

IDA-3D技术细节分析

这里主要针对其实例视差深度估计, Instance Disparity Depth Estimation进行分析

IDA-3D技术细节分析_第1张图片

如上图所示,其流程为:

  • 输入左右眼的图片
  • 分别通过Stereo RCNN的Stereo RPN得到一堆Anchors,分为两支:
    • 利用MaskRCNN的ROI Align,之后过网络进行多个变量的回归,包括(2D box, 偏转角度,长宽高,2D的x和y坐标)
    • 通过IDA模块,即实例深度注意(Instance-Depth-Aware)的模块,然后单独对深度z进行回归

文章的重点即放在IDA模块上,如图下方所示,由两个阶段构成这一模块:

  • 4D cost volume
  • 3D CNN+maxpooling

第一阶段的4D cost volume,而volume可以翻译成体积

引用原文的一段话, “Instead of computing the correspondence of each pixel between two images, we measure the correspondence of the same instance between two images, paying more attention to the global spatial information of the object.”
也就是说这里计算视差的时候,会考虑全局的整体的信息,而不是逐像素的计算

那么怎么构造这个cost volume呢?

Therefore, after forming a cost volume of dimensionality disparity×height×width×feature size by concatenating the left and right feature maps across each disparity level …

可以看到,4D分别代表(disparity,height,width,feature size)
可是disparity(视差)这个定义还是比较模糊,但是可以知道的是,文章想表达的意思是在不同视差级别上对左右眼的特征图进行连接
也就是说,文章必然将视差分成了几个等级,我们直接到代码中来看

def get_boxes_for_cost_volum(left_boxes, right_boxes, depth_bin_rate, calib_list):
    depth_max = 87
    max_depth = len(depth_bin_rate)
    proposals_left = []
    proposals_right = []
    depth_bin_list = []
    #box_num = 0
    for left_box, right_box, calib in zip(left_boxes, right_boxes, calib_list):
        mode = left_box.mode
        assert mode == 'xyxy'
        xmin = torch.min(left_box.bbox[:,0], right_box.bbox[:,0])
        ymin = torch.min(left_box.bbox[:,1], right_box.bbox[:,1])
        xmax = torch.max(left_box.bbox[:,2], right_box.bbox[:,2])
        ymax = torch.max(left_box.bbox[:,3], right_box.bbox[:,3])

首先这个函数输入时左右眼图片的Proposals,深度块比率,相机内参列表
开始了第一个循环,读取每一个box,并求出左右眼图片的box的坐标的极大极小值

        depth_bin_per_image_min = calib['b'] * calib['fu'] / ((xmax - xmin) * 0.9).view(-1,1)
        depth_bin_per_image = depth_max - (depth_max - depth_bin_per_image_min) * depth_bin_rate
        disp_bin_per_image = calib['b'] * calib['fu'] / depth_bin_per_image / 2
        depth_bin_list.append(depth_bin_per_image)

这里的calib[‘b’]是指baseline,即两个相机光心的距离,而calib[‘fu’]是指x方向的焦距,即光心到成像平面的距离
xmax-xmin意味着求出了两个box的并集的一个宽度,如下图所示:
IDA-3D技术细节分析_第2张图片

通过第一行代码,我们可以知道其实类似于通过视差法来求深度,这里先普及一下视差法
IDA-3D技术细节分析_第3张图片

如上图所示,即双目相机的成像模型, O L O_L OL O R O_R OR分别时左右的光心, f f f是焦距, u L u_L uL u R u_R uR是成像的坐标
那么利用相似三角形,容易得到如下等式
z − f z = b − u L + u R b \frac{z-f}{z}=\frac{b-u_L+u_R}{b} zzf=bbuL+uR

注意,这里的 u R u_R uR是负数,所以图里面是 − u R -u_R uR

故有
z = b f d , d = u L − u R z = \frac{bf}{d}, d = u_L-u_R z=dbf,d=uLuR
我们便通过简单的视差得到了深度,这里的视差即P点在两个相机上的投影的距离差

回到代码之中,这一句

depth_bin_per_image_min = calib['b'] * calib['fu'] / ((xmax - xmin) * 0.9).view(-1,1)

其分母比较奇怪,通过之前我们推出的等式可以看出,深度越深,其d值越小,也就是投影的距离差比较小
而这里面分母并不是同个像素的距离差,而是左右眼box并集的宽度,可以理解为从左眼box最左边的一个像素到右眼最右边的一个像素
故,这应该是一个边界,也就是深度的最小估计,换句话说,如果目标都在box中的话,该值代表着根据目标最大的移动可能,计算出来的最近深度。代码在最后除了个0.9,原因暂时不明,这里我们可以先忽略。

看看随后的三条语句

depth_bin_per_image = depth_max - (depth_max - depth_bin_per_image_min) * depth_bin_rate disp_bin_per_image = calib['b'] * calib['fu'] / depth_bin_per_image / 2         
depth_bin_list.append(depth_bin_per_image)

这里面多出来个depth_bin_rate, 查看配置文件,发现应该是一个0~1的数组

DEPTH_BIN_RATE: ( 0.06, 0.10, 0.14, 0.18, 0.22, 0.26, 0.30, 0.34, 0.38,
                     0.42, 0.46, 0.50, 0.54, 0.58, 0.62, 0.66, 0.70, 0.74,
                     0.78, 0.82, 0.86, 0.90, 0.94, 0.98)

那么第一条语句的计算是什么意思呢,这里不妨给出其数学形式
d = d m a x − ( d m a x − d m i n ) × r a t e d = d_{max} - (d_{max}-d_{min})\times rate d=dmax(dmaxdmin)×rate
如果rate=1, 那么 d = d m i n d=d_{min} d=dmin, rate=0, 那么 d = d m a x d=d_{max} d=dmax

那么结果就比较清晰了,该语句的作用是生成最小深度到最大深度的一个离散的区间值,有点像numpy的linspace
随后,利用不同的深度值,反推出投影距离差d的值,除以2是为了后续的左右偏移
之后把深度的离散区间加到数组里面

随后

        bbox_shift_left_per_image = []
        bbox_shift_rigth_per_image = []
        for i in range(len(depth_bin_rate)):
            xmin_shift_left = xmin + disp_bin_per_image[:,i]
            xmax_shift_left = torch.clamp(xmax + disp_bin_per_image[:, i], max=left_box.size[0] - 1)
            bbox_shift_left = torch.stack((xmin_shift_left, ymin, xmax_shift_left, ymax), dim = 1)
            bbox_shift_left_per_image.append(BoxList(bbox_shift_left, left_box.size, mode="xyxy"))
            xmin_shift_right = torch.clamp(xmin - disp_bin_per_image[:, i], min=0)
            xmax_shift_right = xmax -disp_bin_per_image[:, i]
            bbox_shift_right = torch.stack((xmin_shift_right, ymin, xmax_shift_right, ymax), dim = 1)
            bbox_shift_rigth_per_image.append(BoxList(bbox_shift_right, right_box.size, mode="xyxy"))
       
        proposals_left.append(bbox_shift_left_per_image)
        proposals_right.append(bbox_shift_rigth_per_image)

开始遍历不同的深度估计值,即我们之前得到离散的深度区间
之后根据不同深度反推出来的视差(或者称之为像素偏移,投影偏移),分别计算x-min, x-max的左右偏移后估计值
进而得到左右偏移后的一个并集框(即一个最小框同时包含左右眼的边界框),这里记为左偏移-并集框和右偏移-并集框
针对每张图片的每一个框,都分别计算出左偏移-并集框和右偏移-并集框

最后

    proposals_left = list(zip(*proposals_left))
    proposals_right = list(zip(*proposals_right))
    depth_bin = depth_bin_list[0]
    for i in range(1,len(depth_bin_list)):
        depth_bin = torch.cat((depth_bin,depth_bin_list[i]),0) 
    return proposals_left, proposals_right, depth_bin

for循环里面干的事是将不同框的深度离散区间按顺序全连接到一起
最后返回 左偏移-并集框(针对不同框,不同深度下的偏移框),右偏移-并集框以及深度区间

这一函数的本质其实就是,4D cost volume的前半部分
IDA-3D技术细节分析_第4张图片

在左眼图片将并集框右移,在右眼图片将并集框左移,如果深度估计正确的话,则会重合在一起,也就是上图的红色标记框

到这里我们基本上知道IDA模块的一部分了,我们还需要分析其中如何进行匹配,3D卷积的细节

不妨继续看一下代码

    def forward(self, features, proposals, calib):
        proposals_left, proposals_right = proposals
        features_left, features_right = features
        proposals_shift_left, proposals_shift_right, depth_bin = get_boxes_for_cost_volum(proposals_left,proposals_right,self.depth_bin_rate, calib)
        
        features_left_reduce = []
        features_right_reduce = []
        for feature_left, fearure_right in zip(features_left, features_right):
            features_left_reduce.append(self.dim_reduce(feature_left))
            features_right_reduce.append(self.dim_reduce(fearure_right))
        features_left_reduce = tuple(features_left_reduce)
        features_right_reduce = tuple(features_right_reduce)
        num_channels = self.reduced_channel
        cost = Variable(torch.FloatTensor(depth_bin.size()[0], num_channels*3, \
                                                 self.max_depth, self.resolution, self.resolution).zero_()).cuda()
        idx = 0
        for proposals_s_l, proposals_s_r in zip(proposals_shift_left, proposals_shift_right):
            x_l = self.pooler(features_left_reduce, proposals_s_l)
            x_r = self.pooler(features_right_reduce, proposals_s_r)
            cost[:, :num_channels,idx,:,:] = x_l
            cost[:, num_channels : num_channels*2,idx,:,:] = x_r
            cost[:, num_channels*2 : num_channels*3,idx,:,:] = x_l-x_r
            idx += 1
        
        disp = self.depth_cost(cost, depth_bin, num_channels)
        disp = disp.split([len(box) for box in proposals_left], dim = 0)
        return disp

我们已经解析了get_boxes_for_cost_volum函数的细节,之后继续看

首先会对左右眼的特征图进行降维,这里贴出dim_reduce的代码

self.dim_reduce = nn.Sequential(nn.Conv2d(in_channels, 64, kernel_size=3, stride=1),
                        FrozenBatchNorm2d(64), nn.ReLU(inplace=True),
                        nn.Conv2d(64, 32, kernel_size=1, stride=1),
                        FrozenBatchNorm2d(32), nn.ReLU(inplace=True))

应该是将Channels降到了32,至于宽高我们暂时先不考虑,这里self.reduced_channel也是设置成32

然后声明一个cost变量,结构为(框的个数,32*3,最大深度,width,height)
这里的最大深度是离散区间的个数

接着是

for proposals_s_l, proposals_s_r in zip(proposals_shift_left, proposals_shift_right):
    x_l = self.pooler(features_left_reduce, proposals_s_l)
    x_r = self.pooler(features_right_reduce, proposals_s_r)
    cost[:, :num_channels,idx,:,:] = x_l
    cost[:, num_channels : num_channels*2,idx,:,:] = x_r
    cost[:, num_channels*2 : num_channels*3,idx,:,:] = x_l-x_r
    idx += 1

将我们之前得到的左右偏移的并集框拿出来,每一次代表着拿一个深度的所有框的左右偏移的并集框
idx可以看作是深度

这里降通道数后的特征图会和所有框在第idx个深度上进行pooler操作
我们来看一下pooler的代码,先看一下初始化的部分

class Pooler(nn.Module):
    """
    Pooler for Detection with or without FPN.
    It currently hard-code ROIAlign in the implementation,
    but that can be made more generic later on.
    Also, the requirement of passing the scales is not strictly necessary, as they
    can be inferred from the size of the feature map / size of original image,
    which is available thanks to the BoxList.
    """
    def __init__(self, output_size, scales, sampling_ratio):
        """
        Arguments:
            output_size (list[tuple[int]] or list[int]): output size for the pooled region
            scales (list[float]): scales for each Pooler
            sampling_ratio (int): sampling ratio for ROIAlign
        """
        super(Pooler, self).__init__()
        poolers = []
        for scale in scales:
            poolers.append(
                ROIAlign(
                    output_size, spatial_scale=scale, sampling_ratio=sampling_ratio
                )
            )
        self.poolers = nn.ModuleList(poolers)
        self.output_size = output_size
        # get the levels in the feature map by leveraging the fact that the network always
        # downsamples by a factor of 2 at each level.
        lvl_min = -torch.log2(torch.tensor(scales[0], dtype=torch.float32)).item()
        lvl_max = -torch.log2(torch.tensor(scales[-1], dtype=torch.float32)).item()
        self.map_levels = LevelMapper(lvl_min, lvl_max)

可以看到初始化的参数有三个,输出大小,范围,采样率
首先针对不同的范围配置ROIAlign这个对象,计算出两个常数,我们先搁置

这里面涉及了ROIAlign,FPN,还有LevelMapper,我们需要先简单过一遍必要的知识

首先说一下FPN,全称是Feature Pyramid Network,特征金字塔网络,是cvpr17年的文章
IDA-3D技术细节分析_第5张图片

如上图所示,FPN提出了一种新颖的利用多尺度信息的方法,即顶层特征通过上采样和低层特征做融合,而且每层都是独立预测的

IDA-3D技术细节分析_第6张图片

有的工作是只在自顶向下的最后一层做预测,这里直接是每一层融合都做一遍预测
而融合的方式为顶层特征先做两倍的上采样,调整为低层的大小,然后对应的低层特征做1x1卷积之后直接加上去,得到融合的结果
IDA-3D技术细节分析_第7张图片

这些融合的结果有什么用呢,作者将其和RPN进行结合,即每一个融合的结果去过一遍RPN得到一些proposals
作者在不同级别的融合结果上应用了大小不同的anchor来输出对应的proposals,即推荐区域

这里作者给出多个层的表示,即最顶层到最底层的融合结果,可以表示为 P 2 , P 3 , P 4 , P 5 P_2, P_3, P_4, P_5 P2,P3,P4,P5,对应的特征图的宽度为32,64,128,256

这里不同级别的特征图包含的东西也不一样,作者给出了需要进行ROIPooling的层数 k = k 0 + l o g 2 ( w h / 224 ) k=k_0+log_2(\sqrt{wh}/224) k=k0+log2(wh /224),这里224是ImageNet的预训练size,针对ImageNet,可设置 k 0 = 4 k_0=4 k0=4

我们继续讲讲ROIPooling,其作用在于输入的特征图尺寸不固定,但是输出的尺寸是固定的,后续一般接着各类回归层
其原理为根据输出的尺寸,对输入的特征图进行分割,粗暴地取整之后做max pooling

如下图所示:
IDA-3D技术细节分析_第8张图片

假设区域是(0,3)和(7,8)所确定,而目标区域是2x2的大小
直接做除法然后取整,分割成四块区域并做max-pooling得到结果

该种方法由于取整时造成的精度误差,于是后续有人提出了ROIAlign
即不取整数,然后每一个区域取四个点,四个点中每个点的像素值由相邻的四个像素值双线性插值得到

IDA-3D技术细节分析_第9张图片

我们来看看ROIAlign的代码实现

首先对forward的计算代码进行解析

template 
__global__ void RoIAlignForward(const int nthreads, const T* bottom_data,
    const T spatial_scale, const int channels,
    const int height, const int width,
    const int pooled_height, const int pooled_width,
    const int sampling_ratio,
    const T* bottom_rois, T* top_data) {
  CUDA_1D_KERNEL_LOOP(index, nthreads) {
    // (n, c, ph, pw) is an element in the pooled output
    int pw = index % pooled_width;
    int ph = (index / pooled_width) % pooled_height;
    int c = (index / pooled_width / pooled_height) % channels;
    int n = index / pooled_width / pooled_height / channels;
    const T* offset_bottom_rois = bottom_rois + n * 5;
    int roi_batch_ind = offset_bottom_rois[0];
    // Do not using rounding; this implementation detail is critical
    T roi_start_w = offset_bottom_rois[1] * spatial_scale;
    T roi_start_h = offset_bottom_rois[2] * spatial_scale;
    T roi_end_w = offset_bottom_rois[3] * spatial_scale;
    T roi_end_h = offset_bottom_rois[4] * spatial_scale;
    // T roi_start_w = round(offset_bottom_rois[1] * spatial_scale);
    // T roi_start_h = round(offset_bottom_rois[2] * spatial_scale);
    // T roi_end_w = round(offset_bottom_rois[3] * spatial_scale);
    // T roi_end_h = round(offset_bottom_rois[4] * spatial_scale);
    // Force malformed ROIs to be 1x1
    T roi_width = max(roi_end_w - roi_start_w, (T)1.);
    T roi_height = max(roi_end_h - roi_start_h, (T)1.);
    T bin_size_h = static_cast(roi_height) / static_cast(pooled_height);
    T bin_size_w = static_cast(roi_width) / static_cast(pooled_width);
    const T* offset_bottom_data = bottom_data + (roi_batch_ind * channels + c) * height * width;
    // We use roi_bin_grid to sample the grid and mimic integral
    int roi_bin_grid_h = (sampling_ratio > 0) ? sampling_ratio : ceil(roi_height / pooled_height); // e.g., = 2
    int roi_bin_grid_w = (sampling_ratio > 0) ? sampling_ratio : ceil(roi_width / pooled_width);
    // We do average (integral) pooling inside a bin
    const T count = roi_bin_grid_h * roi_bin_grid_w; // e.g. = 4
    T output_val = 0.;
    for (int iy = 0; iy < roi_bin_grid_h; iy ++) // e.g., iy = 0, 1
    {
      const T y = roi_start_h + ph * bin_size_h + static_cast(iy + .5f) * bin_size_h / static_cast(roi_bin_grid_h); // e.g., 0.5, 1.5
      for (int ix = 0; ix < roi_bin_grid_w; ix ++)
      {
        const T x = roi_start_w + pw * bin_size_w + static_cast(ix + .5f) * bin_size_w / static_cast(roi_bin_grid_w);
        T val = bilinear_interpolate(offset_bottom_data, height, width, y, x, index);
        output_val += val;
      }
    }
    output_val /= count;
    top_data[index] = output_val;
  }
}

由于这种池化比较特殊(前向的计算和反向的梯度传播),所以需要自己手写底层的cuda实现(当然也可以是cpu版本的,这里就只拿cuda版本作为例子),看上去就像是c++的形式

可以看到,输入为11个参数

const int nthreads,  // 池化后特征图像素数量,即ROI数量*池化后高度*池化后宽度*通道数
const T* bottom_data,  // 需要进行池化的特征图的首地址,一维数组,结构为(b*c*h*w)
const T spatial_scale,  // 原特征图的高度/缩放后特征图的高度
const int channels,  // 特征图的通道数
const int height,   // 高度
const int width,  // 宽度
const int pooled_height,  // 池化后的高度
const int pooled_width,  // 池化后的宽度
const int sampling_ratio,  // 采样的比率
const T* bottom_rois,   // 存储ROIs的首地址,一维数组,大小为(roi数量*5),这里的5是指index, x1, y1, x2, y2
T* top_data  // 结果的首地址,是一维数组,其大小为(roi数量*池化后高度*池化后宽度*通道数)

之后是一层

CUDA_1D_KERNEL_LOOP(index, nthreads) { ... }

其定义为

#define CUDA_1D_KERNEL_LOOP(i, n)                          \  
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < n; \
       i += blockDim.x * gridDim.x)

本质就是一个for循环,这里面大家比较陌生的是block,thread和grid。这其实是Cuda的布局,如下图所示
IDA-3D技术细节分析_第10张图片

上面整体是一个grid,一个grid分为多个block,每个block又分为多个thread
所以上述循环即,初始化当前位置i,然后逐个grid去访问(即每次跨越一个grid的距离,访问的相对位置不变)

接着是一些初始化的变量

// (n, c, ph, pw) is an element in the pooled output
int pw = index % pooled_width;
int ph = (index / pooled_width) % pooled_height;
int c = (index / pooled_width / pooled_height) % channels;
int n = index / pooled_width / pooled_height / channels;

这里的index是线程号,根据当前线程号判断应该计算top_data(结果)的哪一个位置
即当前计算第n个roi中的第c个通道的ph,pw块

const T* offset_bottom_rois = bottom_rois + n * 5;  // 将指针移到当前计算的ROI数据的首地址
int roi_batch_ind = offset_bottom_rois[0];  // 获得ROI的index

之后便有

T roi_start_w = offset_bottom_rois[1] * spatial_scale;
T roi_start_h = offset_bottom_rois[2] * spatial_scale;
T roi_end_w = offset_bottom_rois[3] * spatial_scale;
T roi_end_h = offset_bottom_rois[4] * spatial_scale;

计算ROI的四个顶点对应的缩放后的坐标

// Force malformed ROIs to be 1x1
T roi_width = max(roi_end_w - roi_start_w, (T)1.);
T roi_height = max(roi_end_h - roi_start_h, (T)1.);
T bin_size_h = static_cast(roi_height) / static_cast(pooled_height);
T bin_size_w = static_cast(roi_width) / static_cast(pooled_width);
const T* offset_bottom_data = bottom_data + (roi_batch_ind * channels + c) * height * width;

这一步是避免0宽度或者0高度的出现,并计算出池化所需要的bin的数目
接着将特征图数据指针移到对应的ROI的index对应的c通道的数据的首地址
即bottom_data + (roi_batch_ind * channels + c)*height*width

接着

// We use roi_bin_grid to sample the grid and mimic integral
int roi_bin_grid_h = (sampling_ratio > 0) ? sampling_ratio : ceil(roi_height / pooled_height); // e.g., = 2
int roi_bin_grid_w = (sampling_ratio > 0) ? sampling_ratio : ceil(roi_width / pooled_width);

计算出roi的一个bin的宽高(向上取整,举个例子,如果该bin的宽1.5,那么就是2,即和原特征图的两个像素有交集)

// We do average (integral) pooling inside a bin
const T count = roi_bin_grid_h * roi_bin_grid_w; // e.g. = 4

计算出一个池化后的bin覆盖了多少个之前的像素(或者说有交集)
然后开始计算池化

T output_val = 0.;
for (int iy = 0; iy < roi_bin_grid_h; iy ++) // e.g., iy = 0, 1
{
  const T y = roi_start_h + ph * bin_size_h 
                               + static_cast(iy + .5f) * bin_size_h / static_cast(roi_bin_grid_h); // e.g., 0.5, 1.5
  for (int ix = 0; ix < roi_bin_grid_w; ix ++)
  {
    const T x = roi_start_w + pw * bin_size_w 
                               + static_cast(ix + .5f) * bin_size_w / static_cast(roi_bin_grid_w);
    T val = bilinear_interpolate(offset_bottom_data, height, width, y, x, index);
    output_val += val;
  }
}
output_val /= count;
top_data[index] = output_val;

开始计算ROI中一个bin对应的池化结果,这里我们需要理解这个for循环
这个循环主要是遍历bin里面的所有像素,然后平均地对每一个像素分配一个点,这里面并不是取像素的中心点
而是按照有交集的像素个数进行平均分配,如下图的红点所示:

IDA-3D技术细节分析_第11张图片

给出双线性插值的细节

template 
__device__ T bilinear_interpolate(const T* bottom_data,
    const int height, const int width,  // 特征图的高度和宽度
    T y, T x,  // 采样点的坐标,浮点数
    const int index /* index for debug only*/) {
    // deal with cases that inverse elements are out of feature map boundary, 即超出特征图边界的返回0
    if (y < -1.0 || y > height || x < -1.0 || x > width) {
        //empty
        return 0;
    }
    // 处理边界点
    if (y <= 0) y = 0;
    if (x <= 0) x = 0;
    // 向下取整
    int y_low = (int) y;
    int x_low = (int) x;
    int y_high;
    int x_high;
    if (y_low >= height - 1) {  // 处理边界点
        y_high = y_low = height - 1;  
        y = (T) y_low;
    } else {
       y_high = y_low + 1;  // 如果不是边界,则记下高度+1的y值
    }
    if (x_low >= width - 1) {
        x_high = x_low = width - 1;
        x = (T) x_low;
    } else {
        x_high = x_low + 1;
    }
    // 通过上述操作,如果不是边界点,则应该存在4个点的信息
    // 而针对边界点,右上,右下,左下边界点对应4,3,3个点的信息
    T ly = y - y_low;  // 
    T lx = x - x_low;
    T hy = 1. - ly, hx = 1. - lx;
    // do bilinear interpolation
    T v1 = bottom_data[y_low * width + x_low];  // 获得四个点的像素信息
    T v2 = bottom_data[y_low * width + x_high];
    T v3 = bottom_data[y_high * width + x_low];
    T v4 = bottom_data[y_high * width + x_high];
    T w1 = hy * hx, w2 = hy * lx, w3 = ly * hx, w4 = ly * lx;  // 计算权重
    T val = (w1 * v1 + w2 * v2 + w3 * v3 + w4 * v4);  // 计算插值
    return val;
}

至此,我们解决ROIAlign的计算部分了,其本质上是基于双线性插值的最大池化。

还需要补上pooler部分的forward细节

def forward(self, x, boxes):
    """
    Arguments:
        x (list[Tensor]): feature maps for each level
        boxes (list[BoxList]): boxes to be used to perform the pooling operation.
    Returns:
        result (Tensor)
    """
    num_levels = len(self.poolers)  # 对应尺度的个数,在IDA-3D里面是四个尺度(0.25, 0.125, 0.0625, 0.03125)
    rois = self.convert_to_roi_format(boxes)  # 转化为roi格式,即(index,x1,y1,x2,y2)
    if num_levels == 1:  # 如果只有一层池化,则直接返回
        return self.poolers[0](x[0], rois)
    levels = self.map_levels(boxes)  # 根据box的大小确定用FPN的哪一层,这里是个层数的list
    num_rois = len(rois)  # ROI的数量,即框的数量
    num_channels = x[0].shape[1]  # 通道数
    output_size = self.output_size[0]  # 输出的大小,这里是16
    dtype, device = x[0].dtype, x[0].device  # 数据类型和设备
    result = torch.zeros(
        (num_rois, num_channels, output_size, output_size),
        dtype=dtype,
        device=device,
    )  # 创建一个零数组,大小为roi数量*通道数*16*16
    for level, (per_level_feature, pooler) in enumerate(zip(x, self.poolers)):  # 遍历不同缩放级别的池化器
        idx_in_level = torch.nonzero(levels == level).squeeze(1)  # 得到应该输出当前层的box的id列表
        rois_per_level = rois[idx_in_level]  # 得到对应id列表的roi列表
        result[idx_in_level] = pooler(per_level_feature, rois_per_level).to(dtype)  # 利用不同级别的pooler得到结果
    return result

回到之前,我们做完不同深度估计的左右偏移框

def forward(self, features, proposals, calib):
    proposals_left, proposals_right = proposals
    features_left, features_right = features
    proposals_shift_left, proposals_shift_right, depth_bin 
         = get_boxes_for_cost_volum(proposals_left,proposals_right,self.depth_bin_rate, calib)
   # ...
    for proposals_s_l, proposals_s_r in zip(proposals_shift_left, proposals_shift_right):
        x_l = self.pooler(features_left_reduce, proposals_s_l)  # 输入参数为降完通道数的特征图,以及左偏移的并集框
        x_r = self.pooler(features_right_reduce, proposals_s_r)  # 这里的结果应该是(B*C*16*16)对第idx个深度估计而言
        # Cost的大小为(ROI数量,通道数*3,最大深度(即离散深度区间的数目),16,16)
        # 下面的操作即是按池化后的左偏移并集框,右偏移并集框,以及左右的差值,按照通道方向连接在了一起
        cost[:, :num_channels,idx,:,:] = x_l
        cost[:, num_channels : num_channels*2,idx,:,:] = x_r
        cost[:, num_channels*2 : num_channels*3,idx,:,:] = x_l-x_r
        idx += 1

    disp = self.depth_cost(cost, depth_bin, num_channels)
    disp = disp.split([len(box) for box in proposals_left], dim = 0)
    return disp

接着我们需要给出depth_cost的细节

def depth_cost(self, cost, depth_bin, num_channels):
    cost = cost.contiguous()  # 转变为连续存储,大概是加快处理速度?
    # 对逐个ROI的逐个深度求范数,分别对L,R进行操作,再将L和R乘积的范数除以(L的范数*R的范数)
    # 求范数后的结果的大小应该是(B,Depth Level)
    x_l_norm = torch.sqrt(torch.sum(cost[:, :num_channels,:,:,:]*cost[:, :num_channels,:,:,:],(1,3,4))) 
    x_r_norm = torch.sqrt(torch.sum(cost[:, num_channels:num_channels*2,:,:,:]*cost[:, num_channels:num_channels*2,:,:,:],(1,3,4)))
    x_cross  = torch.sum(cost[:, :num_channels,:,:,:]*cost[:, num_channels:num_channels*2,:,:,:],(1,3,4))/torch.clamp(x_l_norm*x_r_norm,min=0.01)  
    x_cross = x_cross.unsqueeze(1).unsqueeze(3).unsqueeze(4)  # 对维度进行扩展,即剩下(B, 1, Depth Level, 1, 1)
    #cost1 = cost
    cost = self.dres0(cost)  # b, 96, depth, 16, 16 -> b, 128, depth(24), 16, 16
    cost = self.max_pool1(cost)  # b, 128, depth(24), 16, 16 -> b, 128, 24, 8, 8
    #cost2 = cost
    cost = cost * x_cross  # b, 128, 24, 8, 8

    cost = self.dres1(cost) + cost  # b, 128, 24, 8, 8 类似残差连接
    cost = self.max_pool2(cost)  # b, 128, 24, 4, 4
    #cost3 = cost
    cost = self.dres2(cost)  # b, 1, 24, 4, 4
    cost_disp = torch.squeeze(cost, 1)  # b, 24, 4, 4
    cost_disp = self.avg_pool(cost_disp)  # b, 24, 1, 1
    #cost4 = cost_disp
    cost_disp = cost_disp.squeeze(-1)  # b, 24, 1
    cost_disp = cost_disp.squeeze(-1)  # b, 24
    disp_prob = F.softmax(cost_disp,-1)  # b, 24 得到每个ROI框不同深度估计的概率

    disp = Variable(torch.FloatTensor(disp_prob.size()[0]).zero_()).cuda()  # b
    for i in range(self.max_depth):
        disp += disp_prob[:,i] * depth_bin[:,i]  # 加权求和,因为已经过了一遍softmax,可以直接得到深度估计
    disp = disp.contiguous()
    # 这时候disp的结构就是一个Batch大小的数组,Batch大小也就是ROI框的数量
    return disp

最后,按图片将不同box的深度进行分组并返回

disp = disp.split([len(box) for box in proposals_left], dim = 0)

你可能感兴趣的:(图像处理,深度学习,机器学习,深度学习,算法,cuda,人工智能,python)