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. JAVA多线程-内存模型、三大特性、线程池

    一.线程的三大特性 原子性.可见性.有序性 1)原子性,即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行.原子性其实就是保证数据一致.线程安全一部分. 2)可见性,即 ...

  2. DAY15、模块

    一.函数的补充 1.函数回调: 提前在另一个函数中写出函数的调用,再根据实际的需求去考虑函数体的实现 def download(fn=None): print('开始下载') my_sleep(1) ...

  3. Go语言的通道(1)-无缓冲通道

    前言: 上文中我们采用了[原子函数]已经[共享锁]两种方式分别对多个goroutine进行了同步,但是在go语言中提供了另一种更好的方式,那就是使用通道(Channel). 一.通道是什么? 其实无论 ...

  4. mysql 基本语句

    求知若渴 虚心若愚   博客园 首页 新随笔 联系 管理 随笔-391  文章-0  评论-7  mysql sql常用语句大全   SQL执行一次INSERT INTO查询,插入多行记录 inser ...

  5. 【并发编程】【JDK源码】JDK的(J.U.C)java.util.concurrent包结构

    本文从JDK源码包中截取出concurrent包的所有类,对该包整体结构进行一个概述. 在JDK1.5之前,Java中要进行并发编程时,通常需要由程序员独立完成代码实现.当然也有一些开源的框架提供了这 ...

  6. python4 分支结构,循环结构 for循环

    ## 复习 ```python'''1.变量名命名规范 -- 1.只能由数字.字母 及 _ 组成 -- 2.不能以数字开头 -- 3.不能与系统关键字重名 -- 4._开头有特殊含义 -- 5.__开 ...

  7. Linux记录-open-falcon开源监控系统部署

    参考https://book.open-falcon.org/zh_0_2/quick_install/prepare.html一.安装后端1.环境准备yum -y install redisyum ...

  8. JGUI源码:Accordion鼠标中键滚动和手机端滑动实现(2)

    本文是抽屉组件在PC端滚动鼠标中键.手机端滑动时,滚动数据列表实现方法,没有使用iscroll等第三方插件,支持火狐,谷歌,IE8+等浏览器. 演示在:www.jgui.com Github地址:ht ...

  9. Python 3中bytes/string的区别

    原文:http://eli.thegreenplace.net/2012/01/30/the-bytesstr-dichotomy-in-python-3 python 3中最重要的新特性可能就是将文 ...

  10. Class.forname和ClassLoader.loadClass的源码分析

    最近在研读<深入理解java虚拟机:JVM高继特性与最佳实践>第二版, 今天想起来很久前,写数据库连接,使用Class.forName,当时没有深究,所以便简单的看了下源码,顺便做了以下记 ...