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

关于JPG解码的坑

Loading jpg files gives different results in 3.4.0 and 3.4.2

OpenCV imread读取jpg图像的一个大坑的更多相关文章

  1. OpenCV imread读取图片,imshow展示图片,出现cv:Exception at memory location异常

    问题如上.环境:VS2013. 代码如下: #include "stdafx.h" #include "opencv2\opencv.hpp" using na ...

  2. 【转】OpenCV—imread读取数据为空

    之前遇到一个很郁闷的问题,因为从用OpenCV2.3.1改成OpenCV2.4.4,开始改用Mat和imread来代替Iplimage和cvLoadImage,出了点小问题:imread读入数据总是为 ...

  3. openCV学习——一、图像读取、显示、输出

    openCV学习——一.图像读取.显示.输出   一.Mat imread(const string& filename,int flags=1),用于读取图片 1.参数介绍 filename ...

  4. VS中OpenCV用imread读取不到图片

    转自:https://blog.csdn.net/u012423865/article/details/78116059 在VS中OpenCV用imread读取不到图片 今天在Visual Studi ...

  5. opencv笔记2:图像ROI

    time:2015年 10月 03日 星期六 12:03:45 CST # opencv笔记2:图像ROI ROI ROI意思是Region Of Interests,感兴趣区域,是一个图中的一个子区 ...

  6. [OpenCV学习笔记3][图像的加载+修改+显示+保存]

    正式进入OpenCV学习了,前面开始的都是一些环境搭建和准备工作,对一些数据结构的认识主要是Mat类的认识: [1.学习目标] 图像的加载:imread() 图像的修改:cvtColor() 图像的显 ...

  7. 深入学习OpenCV检测及分割图像的目标区域

    准备1:OpenCV常用图片转换技巧 在进行计算机视觉模型训练前,我们经常会用到图像增强的技巧来获取更多的样本,但是有些深度学习框架中的方法对图像的变换方式可能并不满足我们的需求,所以掌握OpenCV ...

  8. OpenCV 第二课 认识图像的存储结构

    OpenCV 第二课 认识图像的存储结构 Mat Mat 类包含两部分,矩阵头和矩阵体.矩阵头包含矩阵的大小,存储方式和矩阵体存储空间的指针.因此,Mat中矩阵头的大小是固定的,矩阵体大小是不定的. ...

  9. opencv分水岭算法对图像进行切割

    先看效果 说明 使用分水岭算法对图像进行切割,设置一个标记图像能达到比較好的效果,还能防止过度切割. 1.这里首先对阈值化的二值图像进行腐蚀,去掉小的白色区域,得到图像的前景区域.并对前景区域用255 ...

随机推荐

  1. Linux--虚拟环境

    一 . 虚拟环境的安装 如果我们在进行多个django项目的话,只用一个物理环境的话,那么会影响效率,这时候我们局可以应用虚拟环境了 virtualenv #指定清华源下载pip的包 pip3 ins ...

  2. vue 使用微信JSSDK,在IOS端会授权出错

    原因: vue-router切换的时候操作的都是浏览器的历史记录,iOS会把第一次刚进入时的URL作为真实URL,安卓会把当前URL作为真实URL. 所以导致后端在配置好的授权参数获得的config参 ...

  3. python之异常处理和re模块补充

    一.re模块的补充 1.从一个字符串中获取要匹配的内容 findall:返回一个列表 2.search ***** 验证用户输入内容 '^正则规则$':返回一个对象,用group()取值 3.matc ...

  4. 下载图片没有关闭http输入流导致下载超时

    在某次接入第三方厂商数据时,需要根据对方提供的URL地址下载图片,当数据量大时会遇到很多的下载图片超时问题,开始以为是第三方厂商的问题,对方排查了很久之后,说是我这边下载数据全部留在缓存区,导致缓存区 ...

  5. <Android基础>(四) Fragment Part 1

    Fragment 1)Fragment的简单用法 2)动态添加Fragment 3)在Fragment中模拟返回栈 4)Fragment和活动之间通信 第四章 Fragment Fragment是一种 ...

  6. 继续沿用旧的网络访问模式Apache HTTP 客户端,防止Android9闪退

    注意位置,在application 节点里面.

  7. TensorFlow迁移学习的识别花试验

    最近学习了TensorFlow,发现一个模型叫vgg16,然后搭建环境跑了一下,觉得十分神奇,而且准确率十分的高.又上了一节选修课,关于人工智能,老师让做一个关于人工智能的试验,于是觉得vgg16很不 ...

  8. 记一次504 Gateway Time-out

    使用curl请求是超时,查了下资料原来是端口被占用,造成了死锁,记录下 首先要知道为什么会出现死锁? 在我们访问页面的时候这个端口进程就已经被使用,当我们再在页面中curl请求其他页面因为没有其他的端 ...

  9. windows下 cmd 界面的替代者 cmder 推荐!

    介绍 http://cmder.net/ Portable console emulator for Windows Cmder is a software package created out o ...

  10. 代码,java_web

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...