一:雙線性內插值法
設原圖像的height,width,channels分別爲h,w,c
目標圖像(resize結果)的height,width,channels分別爲h_r,w_r,c_r
則長寬的調整比例分別爲:,
那麼目標圖像中的像素點(a, b, c),對應原圖像的像素點,要取整。
這是k近鄰內插值的方法。
而雙線性內插值,對於縮放後的像素點不再是簡單的取整。
設,即對應原圖像的像素點爲,因爲通道數不縮放,所以下文都不再把通道數寫出來,(2.4,3.6)該像素點其實對應在點(2,3),(2,4),(3,3),(3,4)之間,所以縮放後的點(2.4,3.6)應該由這四個點共同決定,見下圖:
Pixel(E)=0.4*0.4*Pixel(A)+0.4*0.6*Pixel(B)+0.4*0.6*Pixel(C)+0.6*0.6*Pixel(D)
由上圖可以清晰的看出雙線性內插值法將對應原圖像的像素點的小數部分視爲一種佔比關係,我們再梳理一下:
設縮放比例爲scale_h(height的縮放比例),scale_w(width的縮放比例),縮放後的圖像中像素點(x,y),對應的原始圖像中的像素點爲(x*scale_width,y*scale_height),取他的小數部分爲(a,b),取他的整數爲(c,d),則其像素值受到這四個像素點的像素值決定A(c,d),B(c+1,d),C(c,d+1),D(c+1,d+1)
則像素點Pixel(x,y)=Pixel(A)*a*(1-b)+Pixel(B)*(1-a)*(1-b)+Pixel(C)*a*b+Pixel(D)*(1-a)*b
這就是雙線性內插值法的計算過程。
二:darknet框架中的實現
相關算法在src/image.c中,resize_image函數,代碼如下:
image resize_image(image im, int w, int h)
{
//目標大小608*608
image resized = make_image(w, h, im.c);
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);
}
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);
}
}
}
free_image(part);
return resized;
}
我現將darknet中的實現思想表述以下,再來分析代碼。
再借用這附圖:
上述在講解雙線性內插值時,是寬高同時進行縮放的,可以現進行寬縮放或高縮放。在計算過程中,就可以先求中間點H,I的像素值或點G,F的像素值
即Pixel(H)=Pixel(A)*0.4+Pixel(B)*0.6,Pixel(I)=Pixel(C)*0.4+Pixel(D)*0.6,Pixel(E)=Pixel(H)*0.4+Pixel(I)*0.6
或Pixel(G)=Pixel(A)*0.4+Pixel(C)*0.6,Pixel(F)=Pixel(B)*0.4+Pixel(D)*0.6,Pixel(E)=Pixel(G)*0.4+Pixel(F)*0.6
而darknet採用的是先對寬(width)進行縮放,再對高(height)進行縮放
現在來看代碼:
首先來看縮放比例的計算:
float w_scale = (float)(im.w - 1) / (w - 1);
float h_scale = (float)(im.h - 1) / (h - 1);
看到這裏,小朋友你是不是有很多❓,爲什麼要減1啊?
在尋找對應原圖像的像素點時,縮放後的圖像中像素點(x,y),對應的原始圖像中的像素點爲(x*scale_width,y*scale_height),這個(x*scale_width,y*scale_height)是兩個小數,我們對他向下和向上取整,獲得那四個像素點,其中向上取整時,得到的那倆個像素點可能超出了圖像的大小,那麼對於這種點採取像素值取0的方式。那麼對於縮放後的圖像的最外側的像素點,在計算其像素值時,一定會超出圖像大小,那麼就要取0值進行計算,這無端的在外圈像素引入了誤差,從視覺效果來說,縮放後的圖像會有一圈顏色偏暗(黑)的像素圈圈。
所以darknet不對最下端和最右端的像素進行縮放,所以實際進行縮放的圖像大小爲(im.w-1,im.h-1),那麼對於最後一行和最後一列的像素值直接從原圖像的對應位置直接取值,對應代碼爲:
if(c == w-1 || im.w == 1){
val = get_pixel(im, im.w-1, r, k);
}
搞清楚這些後,整個過程就很明朗了,先對width進行縮放:
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);
}
}
}
part用於存儲width縮放的中間值
再對height進行縮放:
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);
}
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);
}
}
}
由於darknet中圖像保存爲一維數組,所以要特別注意圖像像素的索引,darknet中的圖像是以行(height),列(width),通道(channel)進行展開爲一維數組的。可以通過get_pixel函數來理解像素的索引。