在使用Python和C++等语言处理图像时,常常会用到Python自带的Python Imaging Library和专用的图像处理工具库Opencv。有时在做算法实验时,图像预处理的时候使用了PIL库,可是在工程对接中又用了opencv库(C++),这时需要保证实验效果的一致性,往往会碰到各种各样的问题,例如:分类任务中,同一张图片,用pytorch版模型计算的实验结果和C++版SDK跑出的结果不一致,具体来说是softmax层输出的结果有差异(一个N分类问题,虽然两边都能把图片正确判为A类,但是pytorch版给出的值与SDK版给出的值相差在0.02以内)。
那么这个误差来源于何处呢?
猜测1——手写softmax层的bug: 由于模型的输出是未经过softmax层的结果,我需要先在SDK中实现softmax层(由于转换工具的关系,在模型末尾加的softmax层未能正常转换;又由于时间关系,不得已手动实现了softmax层),所以我猜测可能来源于此处。但我写了几个test case测试它与pytorch版torch.nn.functional.softmax的结果,发现两者是一致的,故排除该可能性。
猜测2——模型计算精度的问题:粗略看了一遍SDK的业务代码,并未发现在预处理图片上C++版与python版本的差异,故推测可能是不同服务器GPU版本在网络前向计算时,由于计算速度的优化功能,可能会出现混合精度计算的问题,并最终影响了结果。有个老哥说,V100卡在用自研代码库XXX版本的时候回出现该问题;我通过ldd检查.so文件,用nvidia-smi指令检查显卡(GTX 1080),发现当前情况与老哥说的不一致,故排除该可能性。
既然两边图片预处理步骤一致,模型的计算精度和输出结果的计算步骤也一致,怎么还会存在误差呢?
没办法,只好把该任务建模成:input(图片预处理)——model(模型前向计算)——output(后处理流程)三个部分,输入测试样例进行中间结果比对,以此定位问题。
对齐模块1:图片预处理部分由读取图片+图片resize两步组成。我假设这两步没问题,于是直接打印了输入网络前的图片矩阵,结果发现两者的值存在差异:
(1)不少像素值都不同,虽然差异很小,基本都在1左右;
(2)原图中相邻像素值大范围相等的区域,在转换后基本上没有差异,而相邻像素值变化较大的区域有着明显的差异。
这让我猜测,可能是PIL和opencv(C++)在resize上有差异。
实验1:验证resize功能的一致性。先生成一个300 X 300 X 3的矩阵用作测试样例,矩阵的像素值赋值策略为:
img = np.ones([300, 300, 3], dtype = np.uint8)
for D1 in range(300):
for D2 in range(300):
img[D1][D2][0] = int(225*D2/300)
img[D1][D2][1] = int(180*D2/300)
img[D1][D2][2] = int(105*D2/300)
img = Image.fromarray(img).resize((200, 200), Image.BILINEAR)
通过模型之后发现:SDK结果与pytorch版结果不一致。这说明:图片resize部分,模型前向计算与后处理流程三个点必然有一个点没有对齐,而后处理流程(手写的softmax层)之前用testcase测试过是没问题的,那么怎么验证前两者的功能是否对齐呢?
实验2:验证模型前向计算功能的一致性。我生成了一张200 X 200 X 3的像素值全零的图片作为测试样例(这就避免了resize部分和读取部分可能造成的影响)。SDK和pytorch模型的softmax结果能够对齐,这表明模型前向计算功能对齐了。所以问题出在resize上。
实验3:验证读取图片功能的一致性。我按照实验1的像素赋值策略,生成了200 X 200 X 3的图片作为输入(避免了resize操作)。softmax层输出的结果竟然没有对齐!这说明PIL和opencv(C++)在读取同一张图片上竟然存在像素值不一致的情况!我有些不敢相信,于是又从网上找了张自然拍摄的图片(jpg格式),打印读取之后未经resize的图像矩阵。结果是有部分像素值相差1左右。我在网上搜索资料,发现这个老哥也碰到了类似的问题;这个老哥则做了更细致的实验。他们认为,这主要是由于PIL和opencv使用的不同版本的libjpeg造成的。
实验4:查看libjpeg版本是否一致。
(1)(Linux)查看PIL中libjpeg的版本:https://stackoverflow.com/questions/24396727/find-which-libjpeg-is-being-used-by-pil-pillow
(2)(Linux)查看opencv(C++)中libjpeg的版本:https://www.learnopencv.com/get-opencv-build-information-getbuildinformation/
(3)(Linux)查看opencv(Python)中libjpeg的版本:https://stackoverflow.com/questions/59280235/how-to-check-the-libjpeg-turbo-is-built-into-opencv
实验结果表明,PIL中的版本是libjpeg9,opencv(C++)中的版本是libjpeg(ver62),opencv(Python)中的版本是libjpeg-turbo(ver 2.0.2-62)。
从Wiki中得知各种libjpeg的版本号:
其中,我们的ver62应该就是6b版本。显然PIL和opencv(C++)版使用了不同的libjpeg,在解码图像时可能存在差异(不是失真)。另外wiki中提到:libjpeg-turbo有着较好的向下兼容性,所以较为常用。
消除问题的最好办法是面对问题,现在对齐碰到的问题就是PIL和opencv(C++)在读取和resize两步的不一致。怎么解决该问题呢?盲猜opencv能对齐不同语言的功能模块。
实验5:检测opencv(C++)和opencv-python的读取图片和resize(双线性插值)能否对齐。
实验结果表明,两者可以对齐。
至此问题终于解决了(发出奥利给的声音)!