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.lang.IllegalStateException: LifecycleProcessor not initialized - call 'refresh' before invoking lifecycle methods via the context: Root WebApplicationContext: startup date [Mon Oct 01 16:32:37 CS

    使用idea工具更改项目包名时报 :java.lang.ClassNotFoundException 解决方案: 1.删除项目的target目录,这个目录存放的是编译后的文件 2.清除缓存 3.重新编 ...

  2. Go语言中的Iota

    一.复习常量 提到Iota这个关键字,就必须要复习一下Go语言的常量. 1.Go语言的常量一般使用const声明 2.Go语言的常量只能是布尔型.数字型(整数型.浮点型和复数)和字符串型 3.Go语言 ...

  3. Django admin组件使用

    ADMIN 组件 介绍 admin 组件实现了更方便的WEB后台数据管理方式 settings.py 中第一个组件就是 : INSTALLED_APPS = [ 'django.contrib.adm ...

  4. [linux]解除linux对多次登录密码错误的账户的锁定

    其他wheel账户下,执行: sudo pam_tally2 --user=username --reset

  5. python之路day04--列表的增删改查,嵌套、元组的嵌套、range、for循环嵌套

    列表增删改查 增加 append li = ['taibai','zy','nvshen'] li.append('aa') print(li) #['taibai', 'zy', 'nvshen', ...

  6. (二分查找 结构体) leetcode33. Search in Rotated Sorted Array

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...

  7. Decision tree(决策树)算法初探

    0. 算法概述 决策树(decision tree)是一种基本的分类与回归方法.决策树模型呈树形结构(二分类思想的算法模型往往都是树形结构) 0x1:决策树模型的不同角度理解 在分类问题中,表示基于特 ...

  8. IP地址转为二进制,去掉0b补齐八位拼接,再转为十进制

    #!/usr/bin/env python# -*- coding:utf-8 -*- ip = '192.168.0.1' # 转为二进制:# 方法一'''eve = ip.split('.')s ...

  9. [物理学与PDEs]第1章第8节 静电场和静磁场 8.1 静电场

    1. 静电场: 由静止电荷产生的稳定电场. 2. 此时, Maxwell 方程组为 $$\bex \Div{\bf D}=\rho_f,\quad \rot{\bf E}={\bf 0}. \eex$ ...

  10. 使用PHP中的ajax做登录页面、验证用户名是否可用、动态调用数据库

    1.ajax的基础知识 ajax是结合了jquery.php等几种技术延伸出来的综合运用的技术,不是新的内容.ajax也是写在<script>标签里面的. 如果使用ajax一定是要有1个处 ...