因为是着重于源码分析,所以假定读者对于 PriorBox(也有叫 Anchor、DefaultBox)已有基本的认识,此文只是从代码的角度来补充一些细节。且看以下我自己在训练某任务时关于 PriorBoxLayer的一段设置,虽并未覆盖全部参数,但作为后面代码分析时的参考已然够用。
layer {
name: "conv5_cc_relu_mbox_priorbox"
type: "PriorBox"
bottom: "conv5_cc"
bottom: "data"
top: "conv5_cc_relu_mbox_priorbox"
prior_box_param {
min_size: 8
min_size: 10
min_size: 15
min_size: 21
min_size: 29
min_size: 37
min_size: 47
min_size: 57
min_size: 68
min_size: 80
aspect_ratio: 4 #宽高比
flip: false #默认为 true
clip: false #是否进行越界处理
variance: 0.1
variance: 0.1
variance: 0.2
variance: 0.2
offset: 0.5 #中心
}
}
LayerSetUp
template <typename Ftype, typename Btype>
void PriorBoxLayer<Ftype, Btype>::LayerSetUp(const vector<Blob*>& bottom,
const vector<Blob*>& top) {
const PriorBoxParameter& prior_box_param =
this->layer_param_.prior_box_param();
CHECK_GT(prior_box_param.min_size_size(), 0) << "must provide min_size.";
for (int i = 0; i < prior_box_param.min_size_size(); ++i) {
min_sizes_.push_back(prior_box_param.min_size(i));
CHECK_GT(min_sizes_.back(), 0) << "min_size must be positive.";
}
aspect_ratios_.clear();
aspect_ratios_.push_back(1.); //1是默认比例,即正方形
flip_ = prior_box_param.flip(); //是否取倒数(e.g. 1/3->flip->3)
//存放aspect_ratio数据至aspect_ratios_
for (int i = 0; i < prior_box_param.aspect_ratio_size(); ++i) {
float ar = prior_box_param.aspect_ratio(i);
bool already_exist = false;
for (int j = 0; j < aspect_ratios_.size(); ++j) {
if (fabs(ar - aspect_ratios_[j]) < 1e-6) {
already_exist = true; //大概意思就是不要重复定义aspect_ratio
break;
}
}
if (!already_exist) {
aspect_ratios_.push_back(ar);
if (flip_) {
//这个参数不要轻意用,ratio会翻一倍
aspect_ratios_.push_back(1./ar);
}
}
}
//prior box的数量,注意还需要加上max_sizes的数量才是最终数量
num_priors_ = aspect_ratios_.size() * min_sizes_.size();
if (prior_box_param.max_size_size() > 0) {
CHECK_EQ(prior_box_param.min_size_size(), prior_box_param.max_size_size());
//max_size当然是要大于min_size才算进来
for (int i = 0; i < prior_box_param.max_size_size(); ++i) {
max_sizes_.push_back(prior_box_param.max_size(i));
CHECK_GT(max_sizes_[i], min_sizes_[i])
<< "max_size must be greater than min_size.";
num_priors_ += 1;
}
}
clip_ = prior_box_param.clip();
//variance与后期真实框计算有关,要么给个1值,要么就给4个值
if (prior_box_param.variance_size() > 1) {
// Must and only provide 4 variance.(大于1那就必需给4个值)
CHECK_EQ(prior_box_param.variance_size(), 4);
for (int i = 0; i < prior_box_param.variance_size(); ++i) {
CHECK_GT(prior_box_param.variance(i), 0);
variance_.push_back(prior_box_param.variance(i));
}
} else if (prior_box_param.variance_size() == 1) {
CHECK_GT(prior_box_param.variance(0), 0);
variance_.push_back(prior_box_param.variance(0));
} else {
// Set default to 0.1.
variance_.push_back(0.1);
}
//干啥?为啥要有个img_h,img_w,img_size参数,有何意义?
if (prior_box_param.has_img_h() || prior_box_param.has_img_w()) {
CHECK(!prior_box_param.has_img_size())
<< "Either img_size or img_h/img_w should be specified; not both.";
img_h_ = prior_box_param.img_h();
CHECK_GT(img_h_, 0) << "img_h should be larger than 0.";
img_w_ = prior_box_param.img_w();
CHECK_GT(img_w_, 0) << "img_w should be larger than 0.";
} else if (prior_box_param.has_img_size()) {
const int img_size = prior_box_param.img_size();
CHECK_GT(img_size, 0) << "img_size should be larger than 0.";
img_h_ = img_size;
img_w_ = img_size;
} else {
img_h_ = 0;
img_w_ = 0;
}
if (prior_box_param.has_step_h() || prior_box_param.has_step_w()) {
CHECK(!prior_box_param.has_step())
<< "Either step or step_h/step_w should be specified; not both.";
step_h_ = prior_box_param.step_h();
CHECK_GT(step_h_, 0.) << "step_h should be larger than 0.";
step_w_ = prior_box_param.step_w();
CHECK_GT(step_w_, 0.) << "step_w should be larger than 0.";
} else if (prior_box_param.has_step()) {
const float step = prior_box_param.step();
CHECK_GT(step, 0) << "step should be larger than 0.";
step_h_ = step;
step_w_ = step;
} else {
step_h_ = 0;
step_w_ = 0;
}
offset_ = prior_box_param.offset();
}
这里有几个要注意的地方:
1. 代码中默认会设置一个 aspect_ratio=1,也就是正方形。在计算生成的 PriorBox 数量的时候不要忘记这个默认比例。
2.参数 flip 相当于将你自定义的所有 aspect_ratio 取倒数后加入到总的 aspect_ratio 中去,这样你定义的 aspect_ratio 相当于就翻了一倍了。这个参数默认是 TRUE,总感觉这个有点坑。毕竟 aspect_ratio 和 1/aspect_ratio 比例上完全不是一回事。
3.参数 clip 表示对于生成那些超出边界的 PriorBox 要不要做坐标截图,例如坐标小于则截断为0,大于图像的宽/高,则截断成图像的宽/高。
4. 在生成的 priorbox 的个数(num_priors_)是个有意思的地方,归结为两部分:1).aspect_ratio的数量(自定义的 aspect_ratio加上默认为1的 aspect_ratio) 乘以定义的 min_size的数量;2).如果还定义了 max_size,则要加入 max_size 的数量。
Reshape
template <typename Ftype, typename Btype>
void PriorBoxLayer<Ftype, Btype>::Reshape(const vector<Blob*>& bottom,
const vector<Blob*>& top) {
//取feature map的大小
const int layer_width = bottom[0]->width();
const int layer_height = bottom[0]->height();
vector<int> top_shape(3, 1); //定义了一个初始容量为3,默认值为1的vector
// Since all images in a batch has same height and width, we only need to
// generate one set of priors which can be shared across all images.
// 因为一个batch里面的所有图片的宽和高都一样,这里在生成anchor(实际上就是4个坐标值)
// 的时候只要生成一组就够了!
top_shape[0] = 1; //所以这里的第一维就只是1
// 2 channels. First channel stores the mean of each prior coordinate.
// Second channel stores the variance of each prior coordinate.
top_shape[1] = 2; //
top_shape[2] = layer_width * layer_height * num_priors_ * 4; //前面是anchor的数量,4是表示一个anchor要用4个坐标来表示
CHECK_GT(top_shape[2], 0);
top[0]->Reshape(top_shape);
}
这段代码用于定义PriorBox 层输出(top[0])的 shape,输出 shape 为(1,2,N)。top_shape[1]等于2,其中第一个通道用于存储 prior box 的坐标,第2个通道存储坐标对应的 variance 值。那这里的 N 是什么呢?它是 PriorBox 层要生成的总的 prior box 的坐标,所以它的计算公式为:layer_width * layer_height * num_priors_ * 4。
Forward
template <typename Ftype, typename Btype>
void PriorBoxLayer<Ftype, Btype>::Forward_cpu(const vector<Blob*>& bottom,
const vector<Blob*>& top) {
const int layer_width = bottom[0]->width();
const int layer_height = bottom[0]->height();
int img_width, img_height;
//img_h 和 img_w 可以不用给定,直接从 data 里面取
if (img_h_ == 0 || img_w_ == 0) {
//bottom[1]是data即训练数据
img_width = bottom[1]->width();
img_height = bottom[1]->height();
} else {
img_width = img_w_;
img_height = img_h_;
}
float step_w, step_h;
if (step_w_ == 0 || step_h_ == 0) {
//网络输入图像相对于featuremap的缩放比例
step_w = static_cast<float>(img_width) / layer_width;
step_h = static_cast<float>(img_height) / layer_height;
} else {
step_w = step_w_;
step_h = step_h_;
}
Ftype* top_data = top[0]->mutable_cpu_data<Dtype>();
int dim = layer_height * layer_width * num_priors_ * 4; //
int idx = 0;
for (int h = 0; h < layer_height; ++h) {
for (int w = 0; w < layer_width; ++w) {
//offset默认值是0.5,因为要寻求的是中心点
//featuremap上每个点都映射回原图中对应感受野的一个中心点
//所以,这里 center_x,center_y都是像素坐标
float center_x = (w + offset_) * step_w;
float center_y = (h + offset_) * step_h;
float box_width, box_height;
for (int s = 0; s < min_sizes_.size(); ++s) {
//在prototxt中设的min_size参数都是像素级的大小
int min_size_ = min_sizes_[s];
//首先计算默认比例为1的anchor,你如果什么其它aspect_ratio参数都不设置的话,
//默认会有一个aspect_ratio为1,也就是出正方形的anchor
// first prior: aspect_ratio = 1, size = min_size
box_width = box_height = min_size_;
//坐标按原图的height,width进行归一化
//这里相当于是aspect_ratio为1的情形,作者默认就加入了!
// xmin
top_data[idx++] = (center_x - box_width / 2.) / img_width;
// ymin
top_data[idx++] = (center_y - box_height / 2.) / img_height;
// xmax
top_data[idx++] = (center_x + box_width / 2.) / img_width;
// ymax
top_data[idx++] = (center_y + box_height / 2.) / img_height;
//如果定义了 max_size则与 min_size是一一对应
if (max_sizes_.size() > 0) {
CHECK_EQ(min_sizes_.size(), max_sizes_.size()); //必须相等
int max_size_ = max_sizes_[s];
// second prior: aspect_ratio = 1, size = sqrt(min_size * max_size)
// 正方形最小边是min_size,但是即使设定了max_size,这可不是说最长边就是max_size
//哟,而是sqrt(min_size * max_size)
//这里还是相对于 aspectio 为1的情形....
box_width = box_height = sqrt(min_size_ * max_size_);
// xmin
top_data[idx++] = (center_x - box_width / 2.) / img_width;
// ymin
top_data[idx++] = (center_y - box_height / 2.) / img_height;
// xmax
top_data[idx++] = (center_x + box_width / 2.) / img_width;
// ymax
top_data[idx++] = (center_y + box_height / 2.) / img_height;
}
//注意,上面两步min_sizes和max_sizes求出来的都是正方形的框,
//因为目前为止还没有用到aspect_ratio,这正是下面要做的生成非正方形的框
//min_size是设定的一个正方形的边长大小,ar是w/h的值,具体到计算anchor的h,w
//的时候要根据简单的面积公式来求解.
// rest of priors
for (int r = 0; r < aspect_ratios_.size(); ++r) {
float ar = aspect_ratios_[r];
//aspect_ratio 为1的情形已经计算过了,所以要 skip
if (fabs(ar - 1.) < 1e-6) {
continue;
}
box_width = min_size_ * sqrt(ar);
box_height = min_size_ / sqrt(ar);
// xmin
top_data[idx++] = (center_x - box_width / 2.) / img_width;
// ymin
top_data[idx++] = (center_y - box_height / 2.) / img_height;
// xmax
top_data[idx++] = (center_x + box_width / 2.) / img_width;
// ymax
top_data[idx++] = (center_y + box_height / 2.) / img_height;
}
}
}
}
// clip表示是否进行越界处理,因为按照默认逻辑,边界附近必然有些框的边框会超出图像自身的大小
// clip the prior's coordidate such that it is within [0, 1]
if (clip_) {
for (int d = 0; d < dim; ++d) {
//如果top_data[d]小于0,则std::max<Dtype>(top_data[d], 0.)为0,
//所以std::min<Dtype>(0,1)为0,哈哈,小于0则clip成0,就是超出边界的
//坐标值直接设定成边界值
top_data[d] = std::min<Dtype>(std::max<Dtype>(top_data[d], 0.), 1.);
}
}
// set the variance.
// 将每个点生成的坐标值加上variance的值
// 调用了blob的offset函数,计算出第2个channel初始位置的偏移量,使后面使用
// top_data[0]就可以设置variance_
/*
参数reshape函数来看,top的shape是1x2xN
First channel stores the mean of each prior coordinate. 为什么叫mean?
Second channel stores the variance of each prior coordinate.
*/
top_data += top[0]->offset(0, 1);
if (variance_.size() == 1) {
caffe_set(dim, Dtype(variance_[0]), top_data);
} else {
int count = 0;
// feature map每个位置上都有4个variance值,当然,对于每个位置来说是
// 一组同样的值,它们在prototxt文件中给定
for (int h = 0; h < layer_height; ++h) {
for (int w = 0; w < layer_width; ++w) {
for (int i = 0; i < num_priors_; ++i) {
for (int j = 0; j < 4; ++j) {
top_data[count] = variance_[j];
++count;
}
}
}
}
}
}
参考文献:
https://zhuanlan.zhihu.com/p/33544892