yolov3 src yolo layer


原文链接: yolov3 src yolo layer

史上最详细的Yolov3边框预测分析 - 知乎
5分钟GET AI - YOLO 如何做到一眼识别 - 知乎

关于神经网络是如何做到预测的,这一期我们不做科学家,先做码农把它理解为一种黑魔法操作:

第 0 层接收 416*416 大小,3 通道(RGB)的彩色图片,开启神经网络魔法旅程
经过第 1~14 层魔法变换后,得到一张 13*13 大小,425 通道的特征图片(feature map)
第 15 层对该特征图片进行终极奥义魔法,最终输出一维预测数组(predictions)包含 13*13*5*85 个数值。是的没错,作者的代码里把多维数组(矩阵)降到一维数组。

Step 4. 生成检测框(bounding box)

经过神经网络黑魔法操作后,我们得到了 13*13*5*85 个数值,深入分析这 4 个数字的意义,它们代表了 YOLO 思想的精髓

13 * 13:代表了特征图(feature map)的宽*高,一共有 13 * 13 个特征单元。YOLO 将原始图片(416*416)平均划分成 13*13 个区域(cell),每个特征单元对应的一个图片区域(cell)
5:代表了 5 个形状不同的检测框(bounding box),YOLO 在每个图片区域(cell)都会生成 5 个形状不同的检测框(bounding box)以该区域(cell)的中心点为检测框(bounding box)的中心点去检测物体,所以 YOLO 一共会用 13*13*5 个检测框(bounding box)去检测一张图片
85:分拆成 3 部分理解,4 + 1 + 80
4:每个检测框(bounding box)包含 4 个坐标值 (x, y, width, height)
1:每个检测框(bounding box)都有 1 个检测物体自信值(0~1),理解为检测到物体的自信概率。
80:每个检测框(bounding box)都有 80 个分类检测概率值(0~1),理解为检测框内的物体分别可能是每个分类的概率。
总结成一句话:一张 416*416 的图片,被平均划分成 13*13 个图片区域(cell),每个图片区域生成 5 个检测框(bounding box),每个检测框包含 85 个值(4 个坐标值 + 1 个检测物体自信值 + 80 个分类检测值),最后得到的一维预测数组(predictions)代表了图片中检测到的物体,数组共包含 13*13*5*85 个数值 predictions[0] ~ predictions[13*13*5*85-1]

接下来我们解剖一维预测数组(predictions)中的每一个数值,这是个消耗脑细胞工程,我在循环中输出了所有生成的 13*13*5 个检测框(bounding box):

row:检测框(bounding box)在 13 行中的哪一行
col:检测框(bounding box)在 13 列中的哪一列
cell:检测框(bounding box)在 13*13 个特征单元中的哪一个(在哪一个图片区域)
box:可以看到每个图片区域内循环了5次分别生成了5个检测框(bounding box)
index:每个检测框(bounding box)的索引值, 0~13*13*5-1
box_index:在一维预测数组(predictions)中,检测框(bounding box) 4 个坐标值的索引起始值(起始指针)tx ty tw th
obj_index:在一维预测数组(predictions)中,检测框(bounding box) 1 个检测物体自信值的索引起始值(起始指针)tc 代表是否有对象
class_index:在一维预测数组(predictions)中,检测框(bounding box) 80 个分类概率值的索引起始值(起始指针) Ci

直接上我的分析答案:

predictions[0] ~ predictions[13*13*1*85-1]:包含形状0检测框(bounding box)在图片中的所有预测值,共 14365 个数值
predictions[13*13*1*85] ~ predictions[13*13*2*85-1]:包含形状1检测框(bounding box)在图片中的所有预测值,共 14365 个数值
predictions[13*13*2*85] ~ predictions[13*13*3*85-1]:包含形状2检测框(bounding box)在图片中的所有预测值,共 14365 个数值
predictions[13*13*3*85] ~ predictions[13*13*4*85-1]:包含形状3检测框(bounding box)在图片中的所有预测值,共 14365 个数值
predictions[13*13*4*85] ~ predictions[13*13*5*85-1]:包含形状4检测框(bounding box)在图片中的所有预测值,共 14365 个数值
每 14365 个预测值分三部分:[13*13*4个数值, 13*13*1个数值, 13*13*80个数值],依次表示在所有 13*13 个区域(cell)预测到的 4个坐标值 + 1个物体检测自信值 + 80分类检测概率值,循环中计算出的 box_index, obj_index 和 class_index 分别是这三部分起始索引值(起始指针)
文字解释清楚后,如果没有理解的话,我们直接上图(干嘛不早上图)。

我取了其中3个图片区域(cell),区域坐标分别是 (1,1), (6,6), (9,9) ,生成 5 个检测框(bounding box)后的图片,注意到图片的尺寸仍然是416*416:

然后所有的检测框(bounding box)会根据原图片尺寸(640*424)进行一次校正:

YOLO 使用概率阈值(thresh)对所有检测框(bounding box)的 80 个分类概率值进行筛选,并制定了一套筛选规则:

第一轮海选,每个检测框(bounding box)的80个分类内部先进行PK,选出一个概率值最高的冠军分类
第二轮预赛,冠军分类与概率阈值(thresh)进行PK,数值大于概率阈值的检测框(bounding box)才有资格进入决赛。概率阈值默认值为0.24(24%),在命令行运行时可以使用 -thresh 参数进行调整
这里我把通过预赛的检测框(bounding box)显示在图片上,可以看出只要是>0.24默认阈值概率的分类概率值,就已经很准确了。

分类 dog:有2个检测框(bounding box)通过预赛
分类 person:有3个检测框(bounding box)通过预赛
分类 horse:有5个检测框(bounding box)通过预赛

Step 5. 检测结果排序

可以看到,如果检测框(bounding box)只通过预赛的话,会出现一个物体多个检测框的情况,显然我们需要最后的决赛来为每个物体PK出一个王者,决赛用到的PK方法叫做 IoU,表示两个检测框重合率(0~1),值越高越重合。Read more about IoU

通过输出分析,能看出决赛是如何进行的:

分类 class: 1 (person) 有三个检测框(bounding box)#251, #420, #433 进入了最终的决赛
进行qsort对各自的检测概率降序排序
两两计算IoU,如果重合度 IoU>0.3,就将概率低的选手淘汰
最终得出了分类 class: 1 (person) 的唯一王者检测框(bounding box)#420

Step 6. 画出检测结果

最后的最后,就是将3个检测到的分类王者检测框(bounding box)画到原图上,并配上美美的颜色和抬头,保存为大家熟悉的 predictions.png。

”只有冠军才会被大家记住,AI的世界同样如此“

PART FINAL: Do it yourself
对于神经网络黑魔法好奇的同学,可以穿越去 Convolution Neural Network in YOLO,Have fun!

-->附赠修改代码,猛戳下载<--

// Converts output of the network to detection boxes
// w,h: image width,height
// netw,neth: network width,height
// relative: 1 (all callers seems to pass TRUE)
void correct_yolo_boxes(detection *dets, int n, int w, int h, int netw, int neth, int relative, int letter)
{
    int i;
    int new_w = 0;
    int new_h = 0;
    // Compute scale given image w,h vs network w,h
    // I think this "rotates" the image to match network to input image w/h ratio
    // new_h and new_w are really just network width and height
    if (letter)//一般我们不用这个
    {
        if (((float)netw / w) < ((float)neth / h))
        {
            new_w = netw;
            new_h = (h * netw) / w;
        }
        else
        {
            new_h = neth;
            new_w = (w * neth) / h;
        }
    }
    else//用这个resize
    {
        new_w = netw;
        new_h = neth;
    }

    //计算宽高比率
    float deltaw = netw - new_w;
    float deltah = neth - new_h;
    float ratiow = (float)new_w / netw;
    float ratioh = (float)new_h / neth;

    //操作的数量
    for (i = 0; i < n; ++i)
    {
        //获取该方框
        box b = dets[i].bbox;

        //计算
        b.x = (b.x - deltaw / 2. / netw) / ratiow;
        b.y = (b.y - deltah / 2. / neth) / ratioh;
        b.w *= 1 / ratiow;
        b.h *= 1 / ratioh;

        // relative seems to always be == 1, I don't think we hit this condition, ever.
        if (!relative)//没用
        {
            b.x *= w;
            b.w *= w;
            b.y *= h;
            b.h *= h;
        }

        //保存
        dets[i].bbox = b;
    }
}


int get_yolo_detections(layer l, int w, int h, int netw, int neth, float thresh, int *map, int relative, detection *dets, int letter)
{
    int i, j, n;

    //获取yolo层的输出数据
    float *predictions = l.output;

    int count = 0;

    for (i = 0; i < l.w*l.h; ++i)
    {
        int row = i / l.w;
        int col = i % l.w;
        for(n = 0; n < l.n; ++n)
        {
            //获取对象索引
            int obj_index  = entry_index(l, 0, n*l.w*l.h + i, 4);

            //获取对象置信度
            float objectness = predictions[obj_index];

            //如果置信度大于阈值才进行操作
            if (objectness > thresh)
            {
                //获取窗口索引
                int box_index = entry_index(l, 0, n*l.w*l.h + i, 0);

                //获取位置信息
                dets[count].bbox = get_yolo_box(predictions, l.biases, l.mask[n], box_index, col, row, l.w, l.h, netw, neth, l.w*l.h);

                //获取没有对象
                dets[count].objectness = objectness;

                //获取类别数量
                dets[count].classes = l.classes;

                //对每一个类别进行判断
                for (j = 0; j < l.classes; ++j)
                {
                    //获取该类别的索引
                    int class_index = entry_index(l, 0, n*l.w*l.h + i, 4 + 1 + j);

                    //计算置信度
                    float prob = objectness*predictions[class_index];

                    //是否大于阈值
                    dets[count].prob[j] = (prob > thresh) ? prob : 0;
                }
                ++count;
            }
        }
    }
    //计算方框位置信息
    correct_yolo_boxes(dets, count, w, h, netw, neth, relative, letter);
    return count;
}




关于yolov3的thresh和hier_thresh
追踪thresh和hier_thresh的来源可以发现

这两个参数最终决定是否使用的地方在src/network.c的fill_network_boxes函数:

void fill_network_boxes(network *net, int w, int h, float thresh, float hier, int *map, int relative, detection *dets)
{
    int j;
    for(j = 0; j < net->n; ++j){
        layer l = net->layers[j];
        if(l.type == YOLO){//yolov3
            int count = get_yolo_detections(l, w, h, net->w, net->h, thresh, map, relative, dets);
            dets += count;
        }
        if(l.type == REGION){//yolov2
            get_region_detections(l, w, h, net->w, net->h, thresh, map, hier, relative, dets);
            dets += l.w*l.h*l.n;
        }
        if(l.type == DETECTION){//yolov1
            get_detection_detections(l, w, h, thresh, dets);
            dets += l.w*l.h*l.n;
        }
    }
}

这里的l.type是根据yolov*.cfg网络文件来定义的,分别解释为:
在yolov3中为YOLO
在yolov2中检测层为REGION
在yolov1中为DETECTION

所以参数thresh在yolo1.cfg、yolov2-voc.cfg、yolov3-voc.cfg都有用到,hier_thresh仅在yolov2-voc.cfg用到

yolov3中测试时如果需要设置阈值,仅仅设置thresh就可以了

AB大神AlexeyAB在回答问题的时候也有提到过类似内容,可见https://github.com/AlexeyAB/darknet/issues/991

在yolov3的python接口中的darknet.py中的detect()函数中包含参数hier_thresh,具体函数如下:

def detect(net, meta, image, thresh=.5, hier_thresh=.5, nms=.45):
    im = load_image(image, 0, 0)
    num = c_int(0)
    pnum = pointer(num)
    predict_image(net, im)
    dets = get_network_boxes(net, im.w, im.h, thresh, hier_thresh, None, 0, pnum)
    num = pnum[0]
    if (nms): do_nms_obj(dets, num, meta.classes, nms);
 
    res = []
    for j in range(num):
        for i in range(meta.classes):
            if dets[j].prob[i] > 0:
                b = dets[j].bbox
                res.append((meta.names[i], dets[j].prob[i], (b.x, b.y, b.w, b.h)))
    res = sorted(res, key=lambda x: -x[1])
    free_image(im)
    free_detections(dets, num)
    return res
看到使用hier_thresh参数的是get_network_boxes()函数,该函数通过libdarknet.so导入到python环境中,原始的函数定义实现在network.c中,具体为:

/**
 * w, h: 原始待检测图片的宽高
 * thresh: 默认为0.5
 * hier: 默认为0.5
 * map: NULL
 * relative: 1,box坐标值是否是相对的(归一化后的)
 * num: 存储object数量
 **/
detection *get_network_boxes(network *net, int w, int h, 
  float thresh, float hier, int *map, int relative, int *num){
  detection *dets = make_network_boxes(net, thresh, num);
  fill_network_boxes(net, w, h, thresh, hier, map, relative, dets);
  return dets;
}
可见参数 hier_thresh 传给get_network_boxes()中的参数hier,具体首通过network.c中的make_network_boxes()得到多有的检测框,具体函数实现为:

detection *make_network_boxes(network *net, float thresh, int *num){
    layer l = net->layers[net->n - 1];
    int i;
    int nboxes = num_detections(net, thresh);       // 获取所有>thresh预测box数量,for YOLOv2 case: 13*13*5
    if(num) *num = nboxes;
    detection *dets = calloc(nboxes, sizeof(detection)); // 创建检测类
    for(i = 0; i < nboxes; ++i){
        dets[i].prob = calloc(l.classes, sizeof(float)); // 开辟内存空间,用于保存概率/ 检测置信度
        if(l.coords > 4){
            dets[i].mask = calloc(l.coords-4, sizeof(float));
        }
    }
    return dets;
}
然后hier和boxes信息dets传递给fill_network_boxes()函数使用,而fill_network_boxes()函数同样位于network.c中,具体函数为:

void fill_network_boxes(network *net, int w, int h, float thresh, float hier, 
  int *map, int relative, detection *dets){
    int j;
    for(j = 0; j < net->n; ++j){
        layer l = net->layers[j];
        if(l.type == YOLO){       // YOLOv2
            int count = get_yolo_detections(l, w, h, net->w, net->h, thresh, map, relative, dets);
            dets += count;
        }
        if(l.type == REGION){      // YOLOv2
            get_region_detections(l, w, h, net->w, net->h, thresh, map, hier, relative, dets);
            dets += l.w*l.h*l.n;
        }
        if(l.type == DETECTION){   // YOLOv1
            get_detection_detections(l, w, h, thresh, dets);
            dets += l.w*l.h*l.n;
        }
    }
}
可见,hier参数最终传递至get_region_detections()中,这对应yolov2中的region检测层,具体函数在region_layer.c中实现,函数具体为:

void get_region_detections(layer l, int w, int h, int netw, int neth, float thresh, int *map, float tree_thresh, int relative, detection *dets)
{
    int i,j,n,z;
    float *predictions = l.output;
    if (l.batch == 2) {
        float *flip = l.output + l.outputs;
        for (j = 0; j < l.h; ++j) {
            for (i = 0; i < l.w/2; ++i) {
                for (n = 0; n < l.n; ++n) {
                    for(z = 0; z < l.classes + l.coords + 1; ++z){
                        int i1 = z*l.w*l.h*l.n + n*l.w*l.h + j*l.w + i;
                        int i2 = z*l.w*l.h*l.n + n*l.w*l.h + j*l.w + (l.w - i - 1);
                        float swap = flip[i1];
                        flip[i1] = flip[i2];
                        flip[i2] = swap;
                        if(z == 0){
                            flip[i1] = -flip[i1];
                            flip[i2] = -flip[i2];
                        }
                    }
                }
            }
        }
        for(i = 0; i < l.outputs; ++i){
            l.output[i] = (l.output[i] + flip[i])/2.;
        }
    }
    for (i = 0; i < l.w*l.h; ++i){ // 13*13,最后一层的feature map/grid
      int row = i / l.w;  // 行
      int col = i % l.w;  // 列
      for(n = 0; n < l.n; ++n){ // grid cell上的预测box,5个,可看作是输出通道数
        int index = n*l.w*l.h + i;  // 位置下标
        for(j = 0; j < l.classes; ++j){
          // dets:所有预测box的数组
          dets[index].prob[j] = 0;  // 分类概率初始化为0
        }
        int obj_index  = entry_index(l, 0, n*l.w*l.h + i, l.coords); // 预测box的confidence下标
        int box_index  = entry_index(l, 0, n*l.w*l.h + i, 0);  // 预测box 下标
        int mask_index = entry_index(l, 0, n*l.w*l.h + i, 4);  // coords>4时才用到
        // predictions就是输出数组
        float scale = l.background ? 1 : predictions[obj_index]; // 预测box的confidence值
        // 从数据数据中获取当前box的x,y,w,h
        dets[index].bbox = get_region_box(predictions, l.biases, n, box_index, col, row, 
          l.w, l.h, l.w*l.h);
        dets[index].objectness = scale > thresh ? scale : 0;    // 判断是否是object
        if(dets[index].mask){  // coords>4时才用到
          for(j = 0; j < l.coords - 4; ++j){
            dets[index].mask[j] = l.output[mask_index + j*l.w*l.h];//从输出中拷贝掩码值
          }
        }
        // 第一种分类,其概率数据的位置下标
        int class_index = entry_index(l, 0, n*l.w*l.h + i, l.coords + !l.background);
        if(l.softmax_tree){  // cfg/yolo9000专有
          // 根据YOLOv2(二)中的式(5)进行概率链式相乘,得到每个分类的最终预测概率
          hierarchy_predictions(predictions + class_index, l.classes, l.softmax_tree, 0, 
    l.w*l.h);
          if(map){  // 手动提供的分类id的映射
            // 对于分类数量为200的某个数据集,将分类id映射到YOLO9000中,但是源码这里map为NULL
            for(j = 0; j < 200; ++j){
              int class_index = entry_index(l, 0, n*l.w*l.h + i, l.coords + 1 + map[j]);
              float prob = scale*predictions[class_index];
              dets[index].prob[j] = (prob > thresh) ? prob : 0;
            }
          } else {  // 自动获取最大预测概率对应的分类(尽可能的细粒度分类)
            // 获取最大预测概率对应的分类id
            int j =  hierarchy_top_prediction(predictions + class_index, 
                       l.softmax_tree, tree_thresh, l.w*l.h);
            dets[index].prob[j] = (scale > thresh) ? scale : 0;
          }
        } else {   // 非 cfg/yolo9000 网络结构
          if(dets[index].objectness){  // confidence大于阈值,认为有object
            for(j = 0; j < l.classes; ++j){
              // 当前预测box的第j种分类位置下标
              int class_index = entry_index(l, 0, n*l.w*l.h + i, l.coords + 1 + j);
              // Pr(object)*Pr(class_j|object)
              float prob = scale*predictions[class_index];
              dets[index].prob[j] = (prob > thresh) ? prob : 0;
            }
          }
        }
      }
    }
    // 以上dets中各个预测box的坐标是针对network input尺寸的,
    // 然而还需要校正得到以原始图片尺寸为基准的box坐标
    correct_region_boxes(dets, l.w*l.h*l.n, w, h, netw, neth, relative);
可见hier传递给了tree_thresh,而最终使用tree_thresh参数的是hierarchy_top_prediction()函数,该函数获取最大预测概率对应的分类id。该函数位于tree.c中,函数具体为:

int hierarchy_top_prediction(float *predictions, tree *hier, float thresh, int stride)
{
    float p = 1;
    int group = 0;
    int i;
    while(1){
        float max = 0;
        int max_i = 0;
 
        for(i = 0; i < hier->group_size[group]; ++i){
            int index = i + hier->group_offset[group];
            float val = predictions[(i + hier->group_offset[group])*stride];
            if(val > max){ //得到概率最大的预测框
                max_i = index;
                max = val;
            }
        }
        if(p*max > thresh){  //判别得到的最大概率预测框概率是否大于thresh
            p = p*max;
            group = hier->child[max_i];
            if(hier->child[max_i] < 0) return max_i;
        } else if (group == 0){
            return max_i;
        } else {
            return hier->parent[hier->group_offset[group]];
        }
    }
    return 0;
}
可见tree_thresh具体传递给thresh参数,其具体作用是判别最大概率预测框是否大于thresh。
`