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。