OpenCV imread读取jpg图像的一个大坑
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
OpenCV imread读取jpg图像的一个大坑的更多相关文章
- OpenCV imread读取图片,imshow展示图片,出现cv:Exception at memory location异常
问题如上.环境:VS2013. 代码如下: #include "stdafx.h" #include "opencv2\opencv.hpp" using na ...
- 【转】OpenCV—imread读取数据为空
之前遇到一个很郁闷的问题,因为从用OpenCV2.3.1改成OpenCV2.4.4,开始改用Mat和imread来代替Iplimage和cvLoadImage,出了点小问题:imread读入数据总是为 ...
- openCV学习——一、图像读取、显示、输出
openCV学习——一.图像读取.显示.输出 一.Mat imread(const string& filename,int flags=1),用于读取图片 1.参数介绍 filename ...
- VS中OpenCV用imread读取不到图片
转自:https://blog.csdn.net/u012423865/article/details/78116059 在VS中OpenCV用imread读取不到图片 今天在Visual Studi ...
- opencv笔记2:图像ROI
time:2015年 10月 03日 星期六 12:03:45 CST # opencv笔记2:图像ROI ROI ROI意思是Region Of Interests,感兴趣区域,是一个图中的一个子区 ...
- [OpenCV学习笔记3][图像的加载+修改+显示+保存]
正式进入OpenCV学习了,前面开始的都是一些环境搭建和准备工作,对一些数据结构的认识主要是Mat类的认识: [1.学习目标] 图像的加载:imread() 图像的修改:cvtColor() 图像的显 ...
- 深入学习OpenCV检测及分割图像的目标区域
准备1:OpenCV常用图片转换技巧 在进行计算机视觉模型训练前,我们经常会用到图像增强的技巧来获取更多的样本,但是有些深度学习框架中的方法对图像的变换方式可能并不满足我们的需求,所以掌握OpenCV ...
- OpenCV 第二课 认识图像的存储结构
OpenCV 第二课 认识图像的存储结构 Mat Mat 类包含两部分,矩阵头和矩阵体.矩阵头包含矩阵的大小,存储方式和矩阵体存储空间的指针.因此,Mat中矩阵头的大小是固定的,矩阵体大小是不定的. ...
- opencv分水岭算法对图像进行切割
先看效果 说明 使用分水岭算法对图像进行切割,设置一个标记图像能达到比較好的效果,还能防止过度切割. 1.这里首先对阈值化的二值图像进行腐蚀,去掉小的白色区域,得到图像的前景区域.并对前景区域用255 ...
随机推荐
- java valueOf()函数
valueOf() 方法用于返回给定参数的原生 Number 对象值,参数可以是原生数据类型, String等. 该方法是静态方法.该方法可以接收两个参数一个是字符串,一个是基数. 语法 该方法有以下 ...
- Django 模板系统
Django模板系统 常用语法 {{}} 变量相关 {%%} 逻辑相关 变量 格式 {{ 变量名 }} 命名规则 包括任何字母数字以及下划线 ("_")的组合 变量名称中不能有空格 ...
- Loj #528. 「LibreOJ β Round #4」求和 (莫比乌斯反演)
题目链接:https://loj.ac/problem/528 题目:给定两个正整数N,M,你需要计算ΣΣu(gcd(i,j))^2 mod 998244353 ,其中i属于[1,N],j属于[1,M ...
- Redis系列八:redis主从复制和哨兵
一.Redis主从复制 主从复制:主节点负责写数据,从节点负责读数据,主节点定期把数据同步到从节点保证数据的一致性 1. 主从复制的相关操作 a,配置主从复制方式一.新增redis6380.conf, ...
- JavaScript对象 Object类型基础
前言 JavaScript 对象是整个语言学习的一个难点.本文主要带大家入门学习Object知识 对象定义 javascript的基本数据类型包括undefined.null.boolean.stri ...
- 点评cat系列-应用集成
========================消息的基本属性========================消息的几个属性:type: 定义消息的 category, 比如 SQL 或 RPC 或 ...
- [数学笔记Mathematical Notes]1-调和级数发散的一个简单证明
定理. 调和级数 $\dps{\vsm{n}\frac{1}{n}}$ 是发散的. 证明. 设 $$\bex a_n=\sum_{k=1}^n\frac{1}{k}, \eex$$ 则 $a_n$ 递 ...
- [再寄小读者之数学篇](2014-06-23 二阶导数估计 [中国科学技术大学2013年高等数学B 考研试题])
设 $f(x)$ 二阶连续可导, $f(0)=f(1)=0$, $\dps{\max_{0\leq x\leq 1}f(x)=2}$. 证明: $$\bex \min_{0\leq x\leq 1}f ...
- [物理学与PDEs]第3章第2节 磁流体力学方程组 2.1 考虑到导电媒质 (等离子体) 的运动对 Maxwell 方程组的修正
1. Maxwell 方程组 $$\bee\label{3_2_1_Maxwell} \bea \Div{\bf D}&=\rho_f,\\ \rot{\bf E}&=-\cfrac ...
- [物理学与PDEs]第1章习题3 常场强下电势的定解问题
在一场强为 ${\bf E}_0$ (${\bf E}_0$ 为常向量) 的电场中, 置入一个半径为 $R$ 的导电球体, 试导出球外电势所满足的方程及相应的定解条件. 解答: 设导电球体为 $B_R ...