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場景的圖片)的第一個像素值看是否一致,測試圖像和測試結果記錄分別如下:

1.jpg

我這里打印上面這張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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM