作者 | 王小二 编辑 | 汽车人
原文链接:https://zhuanlan.zhihu.com/p/666296180
点击下方卡片,关注“自动驾驶之心”公众号
ADAS巨卷干货,即可获取
点击进入→自动驾驶之心【全栈算法】技术交流群
本文只做学术分享,如有侵权,联系删文
导读
打破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中摘抄一个真实数据来看看
INPUT:
[[ 0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
[10. 11. 12. 13. 14. 15. 16. 17. 18. 19.]
[20. 21. 22. 23. 24. 25. 26. 27. 28. 29.]
[30. 31. 32. 33. 34. 35. 36. 37. 38. 39.]
[40. 41. 42. 43. 44. 45. 46. 47. 48. 49.]
[50. 51. 52. 53. 54. 55. 56. 57. 58. 59.]
[60. 61. 62. 63. 64. 65. 66. 67. 68. 69.]
[70. 71. 72. 73. 74. 75. 76. 77. 78. 79.]
[80. 81. 82. 83. 84. 85. 86. 87. 88. 89.]
[90. 91. 92. 93. 94. 95. 96. 97. 98. 99.]]
Pillow:
[[ 7.857143 9.642858 11.642858 13.642858 15.428572]
[25.714285 27.5 29.5 31.5 33.285713]
[45.714287 47.5 49.5 51.5 53.285713]
[65.71429 67.5 69.5 71.5 73.28571 ]
[83.57143 85.35714 87.35714 89.35714 91.14285 ]]
OpenCV:
[[ 5.5 7.5 9.5 11.5 13.5]
[25.5 27.5 29.5 31.5 33.5]
[45.5 47.5 49.5 51.5 53.5]
[65.5 67.5 69.5 71.5 73.5]
[85.5 87.5 89.5 91.5 93.5]]
Opencv的结果比较好理解,比如第一个点:5.5 = (0 + 1 + 10 +11) / 4
但是Pillow的结果就比较不常见了。直接看官方的解释
Regarding the first post, Pillow performs two passes over the image - horizontal, and then vertical.
So why is the first value 7.857143?
The coefficients generated by our bilinear function are 0.428571, 0.428571 and 0.142857.
Applying that horizontally,
0 * 0.428571 + 1 * 0.428571 + 2 * 0.142857 = 0.714285
10 * 0.428571 + 11 * 0.428571 + 12 * 0.142857 = 10.714275
20 * 0.428571 + 21 * 0.428571 + 22 * 0.142857 = 20.714265
and then vertically,
0.714285 * 0.428571 + 10.714275 * 0.428571 + 20.714265 * 0.142857 = 7.85712714287
You suggest that Pillow should only consider 0, 1, 10 and 11 to get the first pixel value. Instead, Pillow is also considering 2, 12, 20, 21 and 22. That is different to OpenCV, but I see no reason why it should be thought of as incorrect. 0, 1, 10 and 11 are still all considered equally.
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++的不一致。
① 全网独家视频课程
BEV感知、毫米波雷达视觉融合、多传感器标定、多传感器融合、多模态3D目标检测、点云3D目标检测、目标跟踪、Occupancy、cuda与TensorRT模型部署、协同感知、语义分割、自动驾驶仿真、传感器部署、决策规划、轨迹预测等多个方向学习视频(扫码即可学习)

② 国内首个自动驾驶学习社区
近2000人的交流社区,涉及30+自动驾驶技术栈学习路线,想要了解更多自动驾驶感知(2D检测、分割、2D/3D车道线、BEV感知、3D目标检测、Occupancy、多传感器融合、多传感器标定、目标跟踪、光流估计)、自动驾驶定位建图(SLAM、高精地图、局部在线地图)、自动驾驶规划控制/轨迹预测等领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球,这是一个真正有干货的地方,与领域大佬交流入门、学习、工作、跳槽上的各类难题,日常分享论文+代码+视频,期待交流!

③【自动驾驶之心】技术交流群
自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、BEV感知、多模态感知、Occupancy、多传感器融合、transformer、大模型、点云处理、端到端自动驾驶、SLAM、光流估计、深度估计、轨迹预测、高精地图、NeRF、规划控制、模型部署落地、自动驾驶仿真测试、产品经理、硬件配置、AI求职交流等方向。扫码添加汽车人助理微信邀请入群,备注:学校/公司+方向+昵称(快速入群方式)
④【自动驾驶之心】平台矩阵,欢迎联系我们!