打破Resize的支配!
一、起因
自己的推理实现在imagenet上eval结果和pytorch的差了0.924个点,看着差距不大但是就非常疑惑。
首先进行两个可能点排查:
- 在Python端使用pytorch和onnxruntime分别对torch和onnx文件进行推理比较,发现一致
- 在c++端使用onnxruntime和自己的推理分别进行推理比较,发现一致
这不是很奇怪吗?
A == B and B == C
那 A≠C 吗?你以为是JavaScript吗。。。

那直接找A和C的不同呢?分别喂相同的数据给pytorch和自己的推理代码,发现存在一些细微差别。通过逐步排查发现在前处理的地方得到的数据就不一致了,那么就先重点排查前处理部分。
对于pytorch的训练一般的前处理就是
- 读图片解码
- resize
- 转tensor
- norm
看着都是比较常规的操作,应该容易对齐才对的。然后逐步排查发现了两个差距点
- resize行为不一致
- round行为不一致
二、老生常谈的一个问题
来看看让@大缺弦(https://zhuanlan.zhihu.com/p/107761106)大老师都头痛的Resize,主要是说在深度学习中的Resize
还有这位国外老哥,直接用了dangers这个词,这里主要是说一般图像的Resize
https://zuru.tech/blog/the-dangers-behind-image-resizing
不管是深度学习里面,还是一般的图像处理中。大家都被这个Resize支配着。
不过下面两节要谈的是一个更具体的点,pillow和opencv的resize差距。
三、简单搜索分析一下
为什么是pillow呢,因为torchvision默认就用的pillow来做图像的resize操作。
这里我已经怀疑pillow的resize和opencv不一致了,于是去Google了一把,发现果然很多人都遇到过类似的问题,那就说明方向对了。
https//github.com/python-pillow/Pillow/issues/2718
https//github.com/python-pillow/Pillow/issues/4445
https//github.com/python-pillow/Pillow/issues/4476
在pillow的issues中找到从17年就有人提出这个问题了,并且还有人试图pr一份修改,但是一直没有修改,为什么呢?
当然是pillow从之前的pil继承了resize的逻辑,认为他们不需要去模仿opencv的行为,并且选择了和大多数人理解不一样的实现方式。
https//zh.wikipedia.org/zh-cn/%25E5%258F%258C%25E7%25BA%25BF%25E6%2580%25A7%25E6%258F%2592%25E5%2580%25BC

大多数人学的都应该是维基百科提到的,双线性插值是在某个点的周围取相邻4个坐标点的值来计算,只是具体的计算方式有不同次数乘法的实现。
但是Pillow在双线性插值的时候用了一个高级的 two pass resize于是乎取值的逻辑就变了。从上面的issue中摘抄一个真实数据来看看
Opencv的结果比较好理解,比如第一个点:5.5 = (0 + 1 + 10 +11) / 4
但是Pillow的结果就比较不常见了。直接看官方的解释
https//github.com/python-pillow/Pillow/blob/b4bf2885f365b23e16772380173e971f89b208bf/src/libImaging/Resample.c%23L655
https//github.com/python-pillow/Pillow/blob/b4bf2885f365b23e16772380173e971f89b208bf/src/libImaging/Resample.c%23L20-L29
具体的核计算在上面的链接中。
一个直观的感受就是在这个示例中,opencv沿用的4点取值,pillow采用的时6点取值。这就导致了他们结果不会完全对等上。
实际上pillow的操作是为了抗锯齿,下面的示例能比较清楚的看出pillow和opencv的差距。

四、如何解决
这里不说Python怎么做,因为你可以非常方便的安装这两个库,并且相互转换。下面只讨论c++部署怎么做。
- 把pillow的c++代码抠出来
- 自己手搓一个
- 找大佬求求看有没有已经实现过了
- Google碰运气
Google说今天运气不错,有现成的,并且还是基于opencv来做的。
https//github.com/zurutech/pillow-resize
这里要注意一下大佬的宏可能和某些opencv版本不匹配了,需要手动修改一下。其它毛病没有。
五、附加的round差异
大家都知道round有很多种
https//en.wikipedia.org/wiki/Floating-point_arithmetic
在维基百科的浮点说明中,Rounding modes 单元有如下的说明:
Alternative rounding options are also available. IEEE 754 specifies the following rounding modes:
- round to nearest, where ties round to the nearest even digit in the required position (the default and by far the most common mode)
- round to nearest, where ties round away from zero (optional for binary floating-point and commonly used in decimal)
- round up (toward +∞; negative results thus round toward zero)
- round down (toward −∞; negative results thus round away from zero)
- round toward zero (truncation; it is similar to the common behavior of float-to-integer conversions, which convert −3.9 to −3 and 3.9 to 3)
比较常见的就是向最近临的偶数取整
然而在不同的语言,不同的版本中,同一个数的舍入情况也不一样。比如Python2.7和Python3.5对舍入操作的描述就不一样。在迁移到c++的时候需要小心的关注一下Python端用的那种舍入模式。避免Python和c++的不一致。
1170

被折叠的 条评论
为什么被折叠?



