TL;DR 长话短说
别用版本闭区间[3.0.0, 3.4.1]之内的OpenCV读取.jpg图像,如果你care那种肉眼看不出差异的单像素差异。2.4.x和>=3.4.2的则是OK的。
问题描述
同一张jpg图片用OpenCV读取,不同版本OpenCV得到像素值不一样;而这些肉眼难以察觉的差异对计算结果有影响时,请注意检查使用的jpg解码器的情况:
- (1)OpenCV 2.4.x系列版本,编解码库是libjpeg,API/ABI版本是v6b
- (2)OpenCV 版本区间 [3.0.0, 3.4.1],解码库是libjpeg,API/ABI版本是v9
- (3)OpenCV 版本>=3.4.2,3rdparty中同时给了libjpeg和libjpeg-turbo解码器,libjpeg用API/ABI版本v9,libjpeg-turbo用API/ABI版本v6;Windows预编译包用的是libjpeg-turbo
因此,使用OpenCV的Windows预编译包情况下编解码一张jpg图像,OpenCV版本闭区间[3.0.0, 3.4.1]内得到一种结果(jpeg API/ABI v9),OpenCV版本区间(2.4.x] ∪ [3.4.2, -]得到另一种结果(jpeg API/ABI v6b),是可能出现的。说“可能”存在是因为大部分图、大部分像素不存在差别,少量图、少量像素存在问题,也许运气好手头的图片确实没问题。
对于像素值敏感的计算过程(例如深度学习推理引擎推理结果),这种差异应当避免,一旦碰上要及时避开;排查的过程消磨时间,而官方changelog中也没有找到很细致的提醒,因此记录为此文。
How to verity 如何检查
提供3种方法,每一种方法代表了不同的验证思路,相同之处则是“发现问题-不放过问题-进一步研究”。
方法1:调用cv::getBuildInformation()
函数
OpenCV贴心的帮我们封装好了一个名为getBuildInformation()
的函数,打印各种依赖库是否有找到、找到的版本等信息。我们从中找出“Media I/O”开头的一段,“JPEG:”开头的版本信息就是我们要找到。比对不同版本OpenCV的这部分输出,我得到(个人添加了wrong/correct注释)
//-------------------
opencv 3.0.0 (wrong)
Media I/O:
ZLib: build (ver 1.2.8)
JPEG: build (ver 90)
WEBP: build (ver 0.3.1)
PNG: build (ver 1.5.12)
TIFF: build (ver 42 - 4.0.2)
JPEG 2000: build (ver 1.900.1)
OpenEXR: build (ver 1.7.1)
GDAL: NO
//------------------
opencv 3.4.1 (wrong)
Media I/O:
ZLib: build (ver 1.2.11)
JPEG: build (ver 90)
WEBP: build (ver encoder: 0x020e)
PNG: build (ver 1.6.34)
TIFF: build (ver 42 - 4.0.9)
JPEG 2000: build (ver 1.900.1)
OpenEXR: build (ver 1.7.1)
//------------------
opencv 3.4.2 (correct)
Media I/O:
ZLib: build (ver 1.2.11)
JPEG: build-libjpeg-turbo (ver 1.5.3-62)
WEBP: build (ver encoder: 0x020e)
PNG: build (ver 1.6.34)
TIFF: build (ver 42 - 4.0.9)
JPEG 2000: build (ver 1.900.1)
OpenEXR: build (ver 1.7.1)
HDR: YES
SUNRASTER: YES
PXM: YES
//------------------
opencv 2.4.9 (correct)
Media I/O:
ZLib: build (ver 1.2.7)
JPEG: build (ver 62)
PNG: build (ver 1.5.12)
TIFF: build (ver 42 - 4.0.2)
JPEG 2000: build (ver 1.900.1)
OpenEXR: build (ver 1.7.1)
//------------------
opencv 2.4.13.6 (correct)
Media I/O:
ZLib: build (ver 1.2.7)
JPEG: build (ver 62)
PNG: build (ver 1.5.27)
TIFF: build (ver 42 - 4.0.2)
JPEG 2000: build (ver 1.900.1)
OpenEXR: build (ver 1.7.1)
可以看到ver62和ver90两种。ver62对应libjpeg API/ABI v6b, ver90对应v9。
此函数的原理略为hack,通过把编译OpenCV时cmake阶段提取出的各种依赖库的版本信息汇总到一个名为opencv_string.inc
的文件中,再通过#include opencv_string.inc
的形式,作为字符串常量予以返回。(需要自行源码编译OpenCV debug版本进行查看)
方法2:自行翻看源码
通过git clone一份opencv源码,在不同版本间切换源码,然后翻看3rdparty目录,发现opencv 3.4.2版本开始有了libjpeg-turbo子目录。
git checkout -b 3.4.2 3.4.2
cd 3rdparty
ls
进一步查看:3rdparty/libjpeg/jpeglib.h
,查找JPEG_LIB_VERSION
,opencv2.4.x是62, opencv3.x是90。3rdparty/libjpeg-turbo/CMakeLists.txt
,设定了JPEG_LIB_VERSION
为62。
当然,libjpeg-turbo库本身是兼容libjpeg库的,默认兼容v6b,兼容v7和v8的话只要给cmake传递-DWITH_JPEG7=1
或-DWITH_JPEG8=1
就可以了,而至于v9,从libjpeg-turbo主页上可以看到作者们认为“没卵用,并不必现有的标准无损格式多产生什么”,那我们也就不纠结这一点了。
方法3:比对实际项目中的图像像素
比对CNN推理引擎输出结果时,发现SSD网络loc层结果,小数点后几位,PC和设备上结果不一致。loc代表了目标检测预测结果,虽然你看它是一个小数,却要乘以缩放系数来得到原图中的bounding box坐标;而如果loc层的结果偏差了一点点,结果就可能是bounding box有明显视觉偏差,或者跑出图像边界。逐层往前排查,发现网络输入就有问题:同一张.jpg图像,读到内存中像素值有些不一样(第一个像素值就不一样T_T)。所以从这里入手,才发现OpenCV的jpg编解码的大坑。
纠结一番才开始怀疑OpenCV有问题;但预编译的OpenCV并不提供每个VS版本的库,所以需要耐心配置多个版本的VS和多个版本的OpenCV(耐心的重要性;后来发现这种预编译包没法调试进源码,还是自行编译舒服,但需要手动解依赖则是另一个事情了T_T),一通配置测试后,比对imread读取下图(一张ADAS场景的图片)的第一个像素值看是否一致,测试图像和测试结果记录分别如下:
我这里打印上面这张jpg图片的第一个像素值,opencv249打印出来是158(认为是正确,因为板子上跑的版本就是这个结果,是正确的标准,DSP优化库要以这个为标准的),而opencv310打印出来是192。对应的测试代码:
#include <stdio.h>
#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main() {
std::string im_pth = "F:/xx/work/20190315/1.jpg";
cv::Mat src_image = cv::imread(im_pth);
IplImage* shadow_image = cvLoadImage(im_pth.c_str(), CV_LOAD_IMAGE_COLOR);
std::cout << cv::getBuildInformation() << std::endl << std::endl << std::endl;
for (int i = 0; i < shadow_image->height * shadow_image->width * 3; i++){
fprintf(flog, "%f\n", (float)(unsigned char)(shadow_image->imageData[i]));
if (i == 0) {
// 158 is correct
// 192 is wrong
printf("%u\n", (unsigned int)(unsigned char)(shadow_image->imageData[i]));
}
}
fclose(flog);
printf("-------------------end-------------------\n");
return 0;
}
references
Opencv3.0.0‘s bug of imread function
Problem caused by the change from libjpeg to libjpeg-turbo
Loading jpg files gives different results in 3.4.0 and 3.4.2