个人感觉tensorflow的resize_image方法有个大坑,这里只是以双三次插值为例对图片进行缩放为例,对比opencv以及PIL算法的不同。之所以说tensorflow的插值算法有些坑,是因为他的resize算法,无论怎么配置,都无法和openc以及pil库提供的插值算法进行对齐,常用的视频编解码工具ffmpeg自带的双三次插值接近PIL效果。另外,三者的结果都不相同,opencv个人感觉最好,图像显得锐化一些;tensorflow也是有锐化效果,但是部分细节丢失严重,更加接近于最近邻插值算法。
由于tensorflow在1.0版本是静态图的形式,自定义插值算法变得不是很容易。在分析完三者之间的不同后,这里简单提供一个自定义tensorflow的resize_image方法,效果和opencv是一样的。
因为涉及到部分功能代码重复,我们分为三部分:一,读取yuv数据信息,包括帧数,长宽等。二,resize_image,对图像进行插值缩放。三,保存结果。
一,数据读取:
def getFrameNUm(filename, bitdepth, W, H):
bytesPerPixel = math.ceil(bitdepth / 8)
framePixels = bytesPerPixel * H * W * 3 // 2
fp = open(filename, 'rb')
fp.seek(0, 2)
maxFrameNum = fp.tell()
maxFrameNum = maxFrameNum // framePixels
return maxFrameNum
def readyuv420_byframe(filename, bitdepth, W, H, startframe, totalframe, show=False):
bytesPerPixel = math.ceil(bitdepth / 8)
seekPixels = startframe * H * W * 3 // 2
fp = open(filename, 'rb')
fp.seek(bytesPerPixel * seekPixels)
for i in range(totalframe):
Y = fp.read(H * W * bytesPerPixel)
U = fp.read(H // 2 * W // 2 * bytesPerPixel)
V = fp.read(H // 2 * W // 2 * bytesPerPixel)
Y = np.reshape(np.fromstring(Y, dtype=np.uint8), (H, W))
U = np.reshape(np.fromstring(U, dtype=np.uint8), (H // 2, W // 2))
V = np.reshape(np.fromstring(V, dtype=np.uint8), (H // 2, W // 2))
yield Y, U, V
# 具体读取部分
sdr_video = '/360X360_8bit_sor_cv_down.yuv'
save_hdr_video = '720X720_8bit_sor_cv_down_up.yuv'
sdrFrameNum = getFrameNUm(sdr_video, 8, 360, 360)
sdrF = readyuv420_byframe(sdr_video, 8, 360, 360, 0, sdrFrameNum)
width = 720
height = 720
dim_y_shape = (width, height)
dim_uv_shape = (width // 2, height // 2)
插值算法:
# PIL部分,from PIL import Image
for index in range(sdrFrameNum):
sdrY, sdrU, sdrV = sdrF.__next__()
img_y = Image.fromarray(sdrY)
img_y = img_y.resize(dim_y_shape, Image.BICUBIC)
img_y = np.array(img_y)
img_u = Image.fromarray(sdrU)
img_u = img_u.resize(dim_uv_shape, Image.BICUBIC)
img_u = np.array(img_u)
img_v = Image.fromarray(sdrV)
img_v = img_v.resize(dim_uv_shape, Image.BICUBIC)
img_v = np.array(img_v)
# opencv部分,import cv2
for index in range(sdrFrameNum):
sdrY, sdrU, sdrV = sdrF.__next__()
resize_img_y = cv2.resize(sdrY, dim_y_shape, interpolation=cv2.INTER_CUBIC)
resize_img_u = cv2.resize(sdrU, dim_uv_shape, interpolation=cv2.INTER_CUBIC)
resize_img_v = cv2.resize(sdrV, dim_uv_shape, interpolation=cv2.INTER_CUBIC)
tensorflow部分
hdr_y = tf.placeholder(tf.float32, [None, None, None, 1], name='hdy_img')
hdr_u = tf.placeholder(tf.float32, [None, None, None, 1], name='hdu_img')
hdr_v = tf.placeholder(tf.float32, [None, None, None, 1], name='hdv_img')
input_y_s = tf.image.resize_image_with_pad(hdr_y, des_h, des_w, method=2)
input_u_s = tf.image.resize_image_with_pad(hdr_u, des_h//2, des_w//2, method=2)
input_v_s = tf.image.resize_image_with_pad(hdr_v, des_h//2, des_w//2, method=2)
# input_y_s = tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=False)
# input_u_s = tf.image.resize_bicubic(hdr_u, [des_h//2, des_w//2], align_corners=False)
# input_v_s = tf.image.resize_bicubic(hdr_v, [des_h//2, des_w//2], align_corners=False)
result_y = tf.clip_by_value(input_y_s, 0.0, 1.0)
result_u = tf.clip_by_value(input_u_s, 0.0, 1.0)
result_v = tf.clip_by_value(input_v_s, 0.0, 1.0)
with tf.Session(config=tf.ConfigProto(log_device_placement=False)) as sess:
for step in range(int(total_frame)):
sdrY_ori, sdrU, sdrV = sdrF.__next__()
sdrY = np.reshape(sdrY_ori, (1, src_h, src_w, 1))
sdrU = np.reshape(sdrU, (1, src_h // 2, src_w // 2, 1))
sdrV = np.reshape(sdrV, (1, src_h // 2, src_w // 2, 1))
hy_value, hu_value, hv_value = sdrY / 255.0, sdrU / 255.0, sdrV / 255.0
feed_dict = {hdr_y: hy_value, hdr_u: hu_value, hdr_v: hv_value}
hdr_y_r, hdr_u_r, hdr_v_r = sess.run([result_y, result_u, result_v], feed_dict=feed_dict)
hdrY_pred_img = np.around(hdr_y_r[0, :, :, 0] * 255.0).astype(np.uint8)
hdrU_pred_img = np.around(hdr_u_r[0, :, :, 0] * 255.0).astype(np.uint8)
hdrV_pred_img = np.around(hdr_v_r[0, :, :, 0] * 255.0).astype(np.uint8)
数据保存:
writeyuv420p(save_pre_video, des_w, des_h, hdrY_pred_img, hdrU_pred_img, hdrV_pred_img)
def writeyuv420p(filename, W, H, Y, U, V):
fp = open(filename, 'wb')
fp.write(Y)
fp.write(U)
fp.write(V)
fp.close()
def writeyuv420p_add(filename, W, H, Y, U, V):
fp = open(filename, 'ab+')
fp.write(Y)
fp.write(U)
fp.write(V)
fp.close()
tensorflow有三种插值算法:
一:# 2代表双三次插值 bicubic
tf.image.resize_image_with_pad(hdr_y, des_h, des_w, method=2)
二:
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=False)
三:
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=True)
这里值讨论由于插值而产生的移位情况。这里以opencv和PIL的结果作为标准(不看效果,只看是否移位),为何以这两个为标准?主要是因为他的效果和ffmpeg的结果一致,没有发生移位情况。
一:下采样插值
对于tensorflow下采样,
tf.image.resize_image_with_pad(hdr_y, des_h, des_w, method=2)
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=False)
这两种设置方法,得出的结果是一样的。但是相较于opencv和PIL(以后只以opencv为例),整体像右下角偏移一个像素左右。
相比于:
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=True)
这种设置方法相比于opencv,是往中间进行偏移。直观看,就是四个角或边都往中心方向移位。
这是下采样的结论。
对于上采样,要对比的就比较多了,主要是上采样回去以后,是要和groundtruth相比,也就是和源图像比是否产生了移位。那么对于待采样图像,他是由原图像进行下采样得到,下采样是通过opencv和tensorflow的三种方法得到的结果(除去一个相同的)。
opencv下采样结果,进行上采样
首先对于opencv下采样后,用三种方法进行上采样。采用opencv上采样插值后,没有发生移位(这个我们之前就提到过)
对于一下这两种方法,他们两个结果一样
tf.image.resize_image_with_pad(hdr_y, des_h, des_w, method=2)
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=False)
tensorflow的这两种方法,相对于groundtruth,是有一个左上角的偏移。
在这里你可能也想到了,既然上面的结论,下采样会右下,上采样会左上,那么:
tf.image.resize_bicubic(hdr_y, [des_h, des_w], align_corners=False)
tensorflow的是向四周进行移位的。
几种组合方式的上下采样其实也可以猜想一下了。因为tensorflow的下采样,要么向左下角偏移,要么向中心偏移;上采样偏移正好相反。
为什么会出现这种情况?这是因为在插值过程中需要确定用到原图中的哪几个点,而确定方法有所不同。
目标图像的每一个像素点都需要原图中几个点的信息。需要原图像的那几个点应当首先确定:目标图像的像素点在原图像的哪个位置。
srcX=dstX* (srcWidth/dstWidth)
srcY = dstY * (srcHeight/dstHeight)
中心对齐(OpenCV也是如此):
SrcX=(dstX+0.5)* (srcWidth/dstWidth) -0.5
SrcY=(dstY+0.5) * (srcHeight/dstHeight)-0.5
# 其中,dstX 为rang(0,dstWidth),detY为rang(0,desHeight)
由上面两种公式来进行确定,也就是因为这两种不同的确定方法而导致的位置偏移。其部分详细解释可参考该篇文章
双三次插值的具体算法可以参考这里,主要解释一下这里的权重公式
这里的x是目标图像的某像素点距离原图像参考像素点的位置(16个哦)。并且,每一个还分为x和y两个分量。
这里将双线性插值和双三次插值的Python代码列出,结果比C版本的要慢很多。具体原因待查。并且这里主要是为了理解这些插值的算法公式原理,在边缘方面没有做详细处理。
# 最近邻
def NN_interpolation(img,dstH,dstW):
scrH,scrW,_=img.shape
retimg=np.zeros((dstH,dstW,3),dtype=np.uint8)
for i in range(dstH):
for j in range(dstW):
scrx=round((i+1)*(scrH/dstH))
scry=round((j+1)*(scrW/dstW))
retimg[i,j]=img[scrx-1,scry-1]
return retimg
#双线性
def BiLinear_interpolation(img,dstH,dstW):
scrH,scrW,_=img.shape
img=np.pad(img,((0,1),(0,1),(0,0)),'constant')
retimg=np.zeros((dstH,dstW,3),dtype=np.uint8)
for i in range(dstH):
for j in range(dstW):
scrx=(i+1)*(scrH/dstH)-1
scry=(j+1)*(scrW/dstW)-1
x=math.floor(scrx)
y=math.floor(scry)
u=scrx-x
v=scry-y
retimg[i,j]=(1-u)*(1-v)*img[x,y]+u*(1-v)*img[x+1,y]+(1-u)*v*img[x,y+1]+u*v*img[x+1,y+1]
return retimg
def BiBubic(x):
x=abs(x)
if x<=1:
return 1-2.5*(x**2)+(x**3)*1.5
elif x<2:
return 2-4*x+2.5*(x**2)-(x**3)*0.5
else:
return 0
#双三次
def BiCubic_interpolation(img,dstH,dstW):
scrH,scrW,_=img.shape
retimg=np.zeros((dstH,dstW,3),dtype=np.uint8)
for i in range(dstH):
print('one pixel:',i,'total:',dstH)
for j in range(dstW):
scrx=(i+0.5)*(scrH/dstH) - 0.5
scry=(j+0.5)*(scrW/dstW) - 0.5
x=math.floor(scrx)
y=math.floor(scry)
u=scrx-x
v=scry-y
tmp=0
for ii in range(-1,3):
for jj in range(-1,3):
if x+ii<0 or y+jj<0 or x+ii>=scrH or y+jj>=scrW:
continue
tmp+=img[x+ii,y+jj]*BiBubic(ii-u)*BiBubic(jj-v)
retimg[i,j]=np.clip(tmp,0,255)
return retimg
INTER_AREA
area插值算法简单说一下其原理,引自这里
总之:
缩图的时候相当于均值滤波,如果目标缩放倍数不是整数,那么通过举例来算权重:
如果是放大的话,接近于双线性插值算法。这个在opencv官方介绍中有这么一句话
when the image is zoomed, it is similar to the INTER_NEAREST method
这里只能选择上采样两倍的固定倍率,单通道正方形的灰度图,并采用了卷积的方式进行实现。这是因为一个原点变成四个目标像素点,并且距离不会变化。
def caculate_bic_weight():
x = [1.75, 0.75, 0.25, 1.25]
y = [1.75, 0.75, 0.25, 1.25]
# x = [1.25, 0.25, 0.75, 1.75]
# y = [1.75, 0.75, 0.25, 1.25]
# x = [1.75, 0.75, 0.25, 1.25]
# y = [1.25, 0.25, 0.75, 1.75]
# x = [1.25, 0.25, 0.75, 1.75]
# y = [1.25, 0.25, 0.75, 1.75]
total = 0
for x_idx in range(4):
for y_idx in range(4):
# print("按列排列")
x_weight = bicubic_kernel(x[x_idx])
y_weight = bicubic_kernel(y[y_idx])
xy_weight = x_weight*y_weight
print(xy_weight)
total += xy_weight
print(total)
这里四组x,y,相当于u、v的值,两个权重相乘得到对应源图像上像素的权重。如果确定的目标图像像素在原图像像素的位置就会发现,一个源图像周围分布四个目标像素。四组x,y分别表示左上,右上,左下,右下,在高宽上的权重分量。
def cus_bicubic_center_tf_up_X2(sdr_video,save_hdr_video,mulit_channel=True):
if mulit_channel:
img_y = Image.open(sdr_video).convert('RGB')
img = tf.placeholder(tf.float32, [None, None, None, 3], name='hdy_img')
img_data = np.array(img_y)
img_data = np.expand_dims(img_data, axis=0)
else:
img_y = Image.open(sdr_video).convert('L')
img = tf.placeholder(tf.float32, [None, None, None, 1], name='hdy_img')
img_data = np.array(img_y)
img_data = np.expand_dims(img_data, axis=-1)
img_data = np.expand_dims(img_data, axis=0)
pad1 = np.array([[0, 0], [2, 1], [2, 1], [0, 0]])
pad2 = np.array([[0, 0], [2, 1], [1, 2], [0, 0]])
pad3 = np.array([[0, 0], [1, 2], [2, 1], [0, 0]])
pad4 = np.array([[0, 0], [1, 2], [1, 2], [0, 0]])
filter_bic_1 = tf.constant([[[[0.00054931640625, -0.00531005859375, -0.02032470703125, 0.00164794921875],
[-0.00531005859375, 0.05133056640625, 0.19647216796875, -0.01593017578125],
[-0.02032470703125, 0.19647216796875, 0.75201416015625, -0.06097412109375],
[0.00164794921875, -0.01593017578125, -0.06097412109375, 0.00494384765625]]]])
filter_bic_2 = tf.constant([[[[0.00164794921875, -0.02032470703125, -0.00531005859375, 0.00054931640625],
[-0.01593017578125, 0.19647216796875, 0.05133056640625, -0.00531005859375],
[-0.06097412109375, 0.75201416015625, 0.19647216796875, -0.02032470703125],
[0.00494384765625, -0.06097412109375, -0.01593017578125, 0.00164794921875]]]])
filter_bic_3 = tf.constant([[[[0.00164794921875, -0.01593017578125, -0.06097412109375, 0.00494384765625],
[-0.02032470703125, 0.19647216796875, 0.75201416015625, -0.06097412109375],
[-0.00531005859375, 0.05133056640625, 0.19647216796875, -0.01593017578125],
[0.00054931640625, -0.00531005859375, -0.02032470703125, 0.00164794921875]]]])
filter_bic_4 = tf.constant([[[[0.00494384765625, -0.06097412109375, -0.01593017578125, 0.00164794921875],
[-0.06097412109375, 0.75201416015625, 0.19647216796875, -0.02032470703125],
[-0.01593017578125, 0.19647216796875, 0.05133056640625, -0.00531005859375],
[0.00164794921875, -0.02032470703125, -0.00531005859375, 0.00054931640625]]]])
filter_bic_1 = tf.reshape(filter_bic_1, [4, 4, 1, 1])
filter_bic_2 = tf.reshape(filter_bic_2, [4, 4, 1, 1])
filter_bic_3 = tf.reshape(filter_bic_3, [4, 4, 1, 1])
filter_bic_4 = tf.reshape(filter_bic_4, [4, 4, 1, 1])
img_pad_1 = tf.pad(img, pad1, mode='SYMMETRIC', name='pad_hdrx1')
img_pad_2 = tf.pad(img, pad2, mode='SYMMETRIC', name='pad_hdrx2')
img_pad_3 = tf.pad(img, pad3, mode='SYMMETRIC', name='pad_hdry1')
img_pad_4 = tf.pad(img, pad4, mode='SYMMETRIC', name='pad_hdry2')
if mulit_channel:
filter_bic_mui_1 = tf.concat([filter_bic_1, filter_bic_1, filter_bic_1], axis=2)
filter_bic_mui_2 = tf.concat([filter_bic_2, filter_bic_2, filter_bic_2], axis=2)
filter_bic_mui_3 = tf.concat([filter_bic_3, filter_bic_3, filter_bic_3], axis=2)
filter_bic_mui_4 = tf.concat([filter_bic_4, filter_bic_4, filter_bic_4], axis=2)
hdr_pad_1 = tf.nn.depthwise_conv2d(img_pad_1, filter=filter_bic_mui_1, strides=[1, 1, 1, 1], padding='VALID')
hdr_pad_2 = tf.nn.depthwise_conv2d(img_pad_2, filter=filter_bic_mui_2, strides=[1, 1, 1, 1], padding='VALID')
# axis y
hdr_pad_3 = tf.nn.depthwise_conv2d(img_pad_3, filter=filter_bic_mui_3, strides=[1, 1, 1, 1], padding='VALID')
hdr_pad_4 = tf.nn.depthwise_conv2d(img_pad_4, filter=filter_bic_mui_4, strides=[1, 1, 1, 1], padding='VALID')
else:
hdr_pad_1 = tf.nn.conv2d(img_pad_1, filter=filter_bic_1, strides=[1, 1, 1, 1], padding='VALID')
hdr_pad_2 = tf.nn.conv2d(img_pad_2, filter=filter_bic_2, strides=[1, 1, 1, 1], padding='VALID')
hdr_pad_3 = tf.nn.conv2d(img_pad_3, filter=filter_bic_3, strides=[1, 1, 1, 1], padding='VALID')
hdr_pad_4 = tf.nn.conv2d(img_pad_4, filter=filter_bic_4, strides=[1, 1, 1, 1], padding='VALID')
hdr_result = tf.concat([hdr_pad_1, hdr_pad_2, hdr_pad_3, hdr_pad_4],axis=-1)
hdr_result = tf.depth_to_space(hdr_result, 2)
# hdr_result = SubpixelConv2d(hdr_result, 2)
hdr_result = tf.clip_by_value(hdr_result, 0.0, 1.0)
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
feed_dict = {img: img_data/255.0}
hdr_result_re = sess.run(hdr_result,feed_dict= feed_dict)
hdr_result_re = hdr_result_re[0]*255.0
if mulit_channel:
hdr_result_re = hdr_result_re.astype(np.uint8)
hdr_result_re = cv2.cvtColor(hdr_result_re, cv2.COLOR_RGB2BGR)
cv2.imwrite(save_hdr_video,hdr_result_re)
将四个点堆放成四个通道,然后通过subpixel操作。tf.depth_to_space,可以也可以通过该方法实现:
def SubpixelConv2d(inputs, scale=2, n_out_channel=1):
shape_img = tf.shape(inputs)
y = tf.reshape(inputs, [-1, shape_img[2], scale, scale * n_out_channel])
y = tf.transpose(y, [0, 2, 1, 3])
y = tf.reshape(y, [-1, shape_img[1] * scale, shape_img[2] * scale, n_out_channel])
return y
这里的tf的padding方式也挺有讲究。不过只要理解了双三次插值的具体方法以及边界处理方式,还是挺好想象的。
这里只是单通道的灰度图,如果想采用多通道的可以将filter_bic通过concat进行连接,并且使用
tf.nn.depthwise_conv2d()
总结:
这种只能固定整数倍缩放才能够实现通过卷积的方式进行上下采样。这是因为在求各元素权重比例时是根据u、v的来确定的。
scrx=(i+0.5)*(scrH/dstH) - 0.5
scry=(j+0.5)*(scrW/dstW) - 0.5
x=math.floor(scrx)
y=math.floor(scry)
u=scrx-x
v=scry-y
只要u、v确定了,其权重也就确定了。那么就可以组成filter_bic。