经darknet训练后的网络,在利用opencv接口进行检测时,和官网检测结果不一致,故分析下网络实际加载图像方法。
在和他人员交流时,其反馈说利用AlexeyAB版darknet训练后利用opencv检测时不会出现检测结果不一致问题(经测试确实可行),后期将转到分析AlexeyAB版程序,以下分析是基于官网代码。AB版参考:https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-cmake 主要在darknet的基础上做了很多有用的扩充。更多AB版了解可参考https://blog.csdn.net/just_sort/article/details/104109070 后期自己也将对AB版代码进行分析。
1、darknet如何加载图像?
说明:darknet中的image数据最终转换为RGB格式的按比例缩放的float型数据。空余部分填充为0.5或127
首先darknet在include/darknet.h中定义了image结构体,用于存放图片数据及图片相关信息。
typedef struct {
int w;
int h;
int c; //c: 图像的通道数
float *data;//data指针:指向图像第一个像素位置的指针
} image;
在darknet中,image结构体存储图像数据顺序是按照以下规则:1)按通道顺序存储,即先存1通道,再2通道,最后3通道。
在opencv中,每个像素点直接存储三个通道的值,且为BGR格式。在利用opencv读取图像时,将图像转为了RGB格式。具体测试结果不一样应该还是数据转换问题,目前还在分析该原因。下面主要分析darknet中数据转换部分。
检测命令1:./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg。这个命令结合代码非常好理解,首先可以看examples/darknet.c的主函数main中。程序是根据给定的argv来决定程序的走向,如本例argv[1]为detect,也就是说程序会转到detector.c中的test_dector函数中。test_detector()函数中包含了我们熟悉的一个端到端的过程。即,初始化模型、读取模型权重、读取图片、图片resize、将图片喂给模型进行前向传播、根据模型输出得到目标位置及类别、将目标位置及类别标注在图片上。load_image_color(input,0,0)根据图片路径加载input,在include/darknet.h和image.c中进行定义,通过调用load_image()函数(定义于include/darknet.h和image.c中)读取图片。
image im = load_image_color(input,0,0);
image sized = letterbox_image(im, net->w, net->h);//等比例缩放图像至网络大小
在image.cpp中定义函数定义load_image_color()等函数,在image_opencv.cpp中定义了load_image_cv()函数。下述代码说明了不同函数间的调用关系。
image load_image_color(char *filename, int w, int h)
{
return load_image(filename, w, h, 3);
}
image load_image(char *filename, int w, int h, int c)
{
#ifdef OPENCV
image out = load_image_cv(filename, c);//若定义opencv,调用
#else
image out = load_image_stb(filename, c);//若没有定义opencv,调用
#endif
if((h && w) && (h != out.h || w != out.w)){//参数h w均为0,正常加载图像后不会d调用iaoyong resize_image()函数,而是调用letterbox_image()进行等比例缩放
image resized = resize_image(out, w, h);
free_image(out);
out = resized;
}
return out;
}
void rgbgr_image(image im)//转换BRG到RBG
{
int i;
for(i = 0; i < im.w*im.h; ++i){
float swap = im.data[i];
im.data[i] = im.data[i+im.w*im.h*2];
im.data[i+im.w*im.h*2] = swap;
}
}
image load_image_stb(char *filename, int channels)
{
int w, h, c;
unsigned char *data = stbi_load(filename, &w, &h, &c, channels);
if (!data) {
fprintf(stderr, "Cannot load image \"%s\"\nSTB Reason: %s\n", filename, stbi_failure_reason());
exit(0);
}
if(channels) c = channels;
int i,j,k;
image im = make_image(w, h, c);//image结构中的像素数据是按通道组合在一起的,且进行了均一化处理
for(k = 0; k < c; ++k){
for(j = 0; j < h; ++j){
for(i = 0; i < w; ++i){
int dst_index = i + w*j + w*h*k;
int src_index = k + c*i + c*w*j;
im.data[dst_index] = (float)data[src_index]/255.;
}
}
}
free(data);
return im;
}
//在image_opencv.cpp中定义了load_image_cv()函数
image load_image_cv(char *filename, int channels)
{
int flag = -1;
if (channels == 0) flag = -1;
else if (channels == 1) flag = 0;
else if (channels == 3) flag = 1;
else {
fprintf(stderr, "OpenCV can't force load with %d channels\n", channels);
}
Mat m;
m = imread(filename, flag);//加载图像,若加载失败,返回空的矩阵,stored in B G R order.
if(!m.data){
fprintf(stderr, "Cannot load image \"%s\"\n", filename);
char buff[256];
sprintf(buff, "echo %s >> bad.list", filename);
system(buff);
return make_image(10,10,3);
//exit(0);
}
image im = mat_to_image(m);转换为image结构体图像数据
return im;
}
image mat_to_image(Mat m)
{
IplImage ipl = m;//IplImage是OpenCV中C语言的图像类型;cv::Mat是OpenCV中C++语言的图像类型;
image im = ipl_to_image(&ipl);//转换为image结构图像,并将图像数据转为0~1
rgbgr_image(im);//convert BGR to RGB
return im;
}
image ipl_to_image(IplImage* src)
{
int h = src->height;
int w = src->width;
int c = src->nChannels;
image im = make_image(w, h, c);//在image.c中定义,生成
unsigned char *data = (unsigned char *)src->imageData;
int step = src->widthStep;
int i, j, k;
for(i = 0; i < h; ++i){
for(k= 0; k < c; ++k){
for(j = 0; j < w; ++j){
im.data[k*w*h + i*w + j] = data[i*step + j*c + k]/255.;//归一化0~1
}
}
}
return im;
}
image make_empty_image(int w, int h, int c)//构建空的image结构
{
image out;
out.data = 0;
out.h = h;
out.w = w;
out.c = c;
return out;
}
image make_image(int w, int h, int c)//在构建数据为image结构
{
image out = make_empty_image(w,h,c);
out.data = calloc(h*w*c, sizeof(float));//在动态存储区中长度为n*“size”字节的连续区域,并初始化为0
return out;
}
加载完函数后进行按比例缩放,不足部分按平均灰度值0.5或127进行填充。
//将原图进行一定比例的缩放,返回的图片尺寸为网络尺寸(w,h)
image letterbox_image(image im, int w, int h)
{
int new_w = im.w;
int new_h = im.h;
if (((float)w/im.w) < ((float)h/im.h)) {//这个说明高度比例大于宽度比例,所以new_h要重新设置
new_w = w;
new_h = (im.h * w)/im.w;
} else {
new_h = h;
new_w = (im.w * h)/im.h;
}
image resized = resize_image(im, new_w, new_h);//缩放图像
image boxed = make_image(w, h, im.c);
fill_image(boxed, .5);//将初始化后的boxed图像像素点置为0.5,即,平均灰度
//int i;
//for(i = 0; i < boxed.w*boxed.h*boxed.c; ++i) boxed.data[i] = 0;
embed_image(resized, boxed, (w-new_w)/2, (h-new_h)/2); //将放缩后的图片复制入boxed图片正中央
free_image(resized);
return boxed;
}
//w,h为按原图等比例缩放的照片,即,w=im.w*scale,h=im.h*scale
image resize_image(image im, int w, int h)
{
image resized = make_image(w, h, im.c); //构建大小为w,h的image图像,且初始化为0
image part = make_image(w, im.h, im.c);//先缩放列
int r, c, k;
float w_scale = (float)(im.w - 1) / (w - 1);
float h_scale = (float)(im.h - 1) / (h - 1);
for(k = 0; k < im.c; ++k){
for(r = 0; r < im.h; ++r){
for(c = 0; c < w; ++c){
float val = 0;
if(c == w-1 || im.w == 1){
val = get_pixel(im, im.w-1, r, k);
} else {
float sx = c*w_scale;//映射到原图的列
int ix = (int) sx;//列取整
float dx = sx - ix;//求出小数部分
val = (1 - dx) * get_pixel(im, ix, r, k) + dx * get_pixel(im, ix+1, r, k);//利用插值公式,在列方向上插值
}
set_pixel(part, c, r, k, val);
}
}
}
for(k = 0; k < im.c; ++k){
for(r = 0; r < h; ++r){
float sy = r*h_scale;
int iy = (int) sy;
float dy = sy - iy;
for(c = 0; c < w; ++c){
float val = (1-dy) * get_pixel(part, c, iy, k);
set_pixel(resized, c, r, k, val);//首先获取上一行的象素值*(1-dy)
}
if(r == h-1 || im.h == 1) continue;
for(c = 0; c < w; ++c){
float val = dy * get_pixel(part, c, iy+1, k);
add_pixel(resized, c, r, k, val);//然后获取下一行的象素值*dy
}
}
}
free_image(part);
return resized;
}
static float get_pixel(image m, int x, int y, int c)
{
assert(x < m.w && y < m.h && c < m.c);
return m.data[c*m.h*m.w + y*m.w + x];
}
static void set_pixel(image m, int x, int y, int c, float val)
{
if (x < 0 || y < 0 || c < 0 || x >= m.w || y >= m.h || c >= m.c) return;
assert(x < m.w && y < m.h && c < m.c);
m.data[c*m.h*m.w + y*m.w + x] = val;
}
static void add_pixel(image m, int x, int y, int c, float val)//在原像素灰度值基础上加入某值
{
assert(x < m.w && y < m.h && c < m.c);
m.data[c*m.h*m.w + y*m.w + x] += val;
}
void fill_image(image m, float s)//将image图像置为0.5的灰度值
{
int i;
for(i = 0; i < m.h*m.w*m.c; ++i) m.data[i] = s;
}
//move image from source to dest according to the offset (dx,dy)
void embed_image(image source, image dest, int dx, int dy)//
{
int x,y,k;
for(k = 0; k < source.c; ++k){
for(y = 0; y < source.h; ++y){
for(x = 0; x < source.w; ++x){
float val = get_pixel(source, x,y,k);
set_pixel(dest, dx+x, dy+y, k, val);
}
}
}
}
总结:darknet中的image数据最终转换为RBG格式的按比例缩放的float型数据。