LoginSignup
6
4

More than 1 year has passed since last update.

OpenCVにおける画像ファイル入出力の仕組み(imread編)

Last updated at Posted at 2022-12-05

この記事はOpenCV Advent Calendar 2022の6日目の記事です。
他の記事は目次にまとめられています。

TL;DR

  • この記事では、 OpenCVの画像入力の流れをまとめています。

はじめに

簡単に自己紹介?

画像処理系の組み込みエンジニアです。OpenCV communityにもちょくちょくコメントしています。以後、よろしくお願いします!

そういえば、imread()/imwrite()とか画像コーデック対応、勉強してないな。

それは、1件のissueから始まった。

OpenCVのissueを見ていたら、TGA format対応へのfeature requestが来ていた。

今回、画像フォーマット対応(入出力)の仕組みを勉強して行きたい。例えば、 上手く画像データが読み込めない、書き込めない とかあっても中身を知っていれば安心ですね!!

記事が長くなるので、「読み込み」の前編と、「書き出し」の後編の、2回に分けて説明します。

Step 1. CMakeList.txt

CMakeList.txtの説明は補足なので、興味がある方はクリックして展開して下さい imwrite()とimread()で同じ内容です

CODECに対応するマクロ

cmakeコマンドで、CODECを有効にする設定を追加したい。

image.png

コーデックに関するマクロ/設定値がいくつかある。

  • WITH_xxxx: xxxxを有効化する。
  • BUILD_xxxx: 3rdpartyフォルダ以下のコードをコンパイル。なければ、shared library参照。
  • HAVE_xxxx: xxxxライブラリが存在する

WITH_xxxx(+BUILD_xxxx) -> HAVE_xxxx

HAVE_xxxxは以下のようなコードで導出される。

https://github.com/opencv/opencv/blob/da4ac6b7eff2e8869567e4faaff73312f9e1ef57/cmake/OpenCVFindLibsGrfmt.cmake#L40-L84

OpenCVFindLibsGrfmt.cmake

# --- libjpeg (optional) ---
if(WITH_JPEG)
  if(BUILD_JPEG)
    ocv_clear_vars(JPEG_FOUND)
  else()
    ocv_clear_internal_cache_vars(JPEG_LIBRARY JPEG_INCLUDE_DIR)
    include(FindJPEG)
  endif()

 <略>
  set(HAVE_JPEG YES)
endif()

CMakeList.txtはこちら

CMakeLists.txt
OCV_OPTION(WITH_JPEG "Include JPEG support" ON
  VISIBLE_IF TRUE
  VERIFY HAVE_JPEG)

CMake.実行時のログに表示

いつも、cmakeコマンドで表示されるアレを実現するところです。

   Media I/O:
     ZLib:                        /usr/lib/x86_64-linux-gnu/libz.so (ver 1.2.11)
     JPEG:                        build-libjpeg-turbo (ver 2.1.3-62)
       SIMD Support Request:      YES
       SIMD Support:              YES
     WEBP:                        build (ver encoder: 0x020f)
     PNG:                         /usr/lib/x86_64-linux-gnu/libpng.so (ver 1.6.38)
     TIFF:                        build (ver 42 - 4.2.0)

CMakeList.txtはこちら

CMakeLists.txt
if(WITH_JPEG OR HAVE_JPEG)
  if(NOT HAVE_JPEG)
    status("    JPEG:" NO)
  elseif(BUILD_JPEG)
    status("    JPEG:" "build-${JPEG_LIBRARY} (ver ${JPEG_LIB_VERSION})")
  else()
    status("    JPEG:" "${JPEG_LIBRARY} (ver ${JPEG_LIB_VERSION})")
  endif()
endif()

Step 2. 各種Codecの登録と呼び出し

Codecの登録

imgcodecsの中にある、struct ImageCodecInitializer構造体で、Encoder/Decoderを生成し、decodersencodersで管理している。これにより、画像フォーマットを柔軟に追加することができる。

ソースコードは、こちら

loadsave.cpp
struct ImageCodecInitializer
{
    /**
     * Default Constructor for the ImageCodeInitializer
    */
    ImageCodecInitializer()
    {
        /// BMP Support
        decoders.push_back( makePtr<BmpDecoder>() );
        encoders.push_back( makePtr<BmpEncoder>() );
 :

Codec登録周辺をUMLで表現すると...

BaseImageDecoderを基底クラスとして、各Codecを派生することで共通I/Fを持った実装を実現している。

imread()からの呼び出しシーケンス

アプリケーションがimread()を呼び出したときの、大まかなシーケンスは以下となる。

imread()から3段階に処理が分かれる事を、理解してほしい。後でもう少し(Codecよりに)説明するので、全部を理解する必要はないので、「へー」くらいでOK。

  • Decoder検出のための、signatureLength()checkSignature()
  • ヘッダ解析のための、readHeader()
  • データ読み込みのための、readData()

Step 3. signature解析( signatureLength() / checkSignature() )

signatureLength()

各Codecのデータ形式を自動的に判別するために、ヘッダ先頭に書かれている識別子Signatureを読み出す。しかし、signatureの長さは各Codecでバラバラである。例えば.以下のようになる。

Signature長 フォーマット
3 BMP/PNG/...
4 TIFF
32 WEBP

そこでfindDecoder()は、各DecoderからsignatureLength()を呼び出し、最大値を求めることで、必要なSignature長を求める。例えば、上記の例であれば、最大長は32byteとなる。

例えば、Tiff Decoderの場合は以下。

size_t TiffDecoder::signatureLength() const
{
    return 4;
}

checkSignature()

findDecoder()は、最大長分のsignatureを読み出した後、各decoderのcheckSignature()実装を呼び出す。返り値がtrueを返したdecoderが、検出結果となり、以後の展開処理で用いられる。

ここで、画像フォーマットの中には、複数のsignature形式が存在する場合もある。例えば、Tiff Decoderの場合は以下。この場合、Little Endian形式、Big Endian系形式 さらに通常のTIFF形式BigTIFF形式を掛け合わせた4通りに対応している。このように、Signature判別をロジックで実装する事で、Decoder都合を上手く隠ぺいできる仕組みになっている。

bool TiffDecoder::checkSignature( const String& signature ) const
{
    return signature.size() >= 4 &&
        (memcmp(signature.c_str(), fmtSignTiffII, 4) == 0 ||
        memcmp(signature.c_str(), fmtSignTiffMM, 4) == 0 ||
        memcmp(signature.c_str(), fmtSignBigTiffII, 4) == 0 ||
        memcmp(signature.c_str(), fmtSignBigTiffMM, 4) == 0);
}

signature周りをUMLでまとめると...

(代替手段) m_signature

ここまでで「画像形式を判別するためのsignatureなんて、単純にメモリ比較だけでいいでしょ?毎回実装するのは面倒じゃないの?」と思った方もいるだろう。 大正解 である。

この場合、各Decoderはm_signatureにデータを格納すればよい。これによりsignatureLength() + checkSignature() の実装を端折る事も出来る。例えば、jpegDecoderの場合、{0xFF, 0xDB, 0xFF} の3文字を比較できればよいので、この3文字をm_signatureに代入することで、シグネチャ対応は完了となる。

JpegDecoder::JpegDecoder()
{
    m_signature = "\xFF\xD8\xFF";
    m_state = 0;
    m_f = 0;
    m_buf_supported = true;
}

Step 4. ヘッダ解析(readHeader())

signatureLength() / checkSignature() での処理によって適切なDecoderが選出された後は、当該Decoderを使ってヘッダ解析・データ解析が行われる。

ヘッダ解析の主な役割は2つである。

  • データ(m_filename)情報から、画像の幅(m_width)・高さ(m_height)・色空間・色深度(m_type)などを設定する。
  • 適切ではないデータ形式であった場合には、エラーを返す。

例えば、jpegの例 を示す。libjpegを使って読み込んだ構造体state->infoを使って、 m_width,m_height,m_typeが設定される。

        if (state->cinfo.src != 0)
        {
            jpeg_save_markers(&state->cinfo, APP1, 0xffff);
            jpeg_read_header( &state->cinfo, TRUE );

            state->cinfo.scale_num=1;
            state->cinfo.scale_denom = m_scale_denom;
            m_scale_denom=1; // trick! to know which decoder used scale_denom see imread_
            jpeg_calc_output_dimensions(&state->cinfo);
            m_width = state->cinfo.output_width;
            m_height = state->cinfo.output_height;
            m_type = state->cinfo.num_components > 1 ? CV_8UC3 : CV_8UC1;
            result = true;
        }
    }

ヘッダ解析をUMLでまとめると...

(余談)ファイルじゃなくてもバッファでもよい

m_filenameには解析するべきデータが入っている。とはいえ、例えば imdecode()関数のようにファイルではなくメモリから展開したいケースもある。これはどうすればよいのか?

正解は、メンバ変数m_bufと、それを有効化するためのm_buf_supportedである。

JpegDecoderのコントラスタを再度確認すると、m_buf_supported = true となっている。

JpegDecoder::JpegDecoder()
{
    m_signature = "\xFF\xD8\xFF";
    m_state = 0;
    m_f = 0;
    m_buf_supported = true;
}

これは、JpegDecoderの基底クラスでる、BaseImageDecoder経由で、データソースを指定 するときの判定に使われる。

bool BaseImageDecoder::setSource( const String& filename )
{
    m_filename = filename;
    m_buf.release();
    return true;
}

bool BaseImageDecoder::setSource( const Mat& buf )
{
    if( !m_buf_supported )
        return false;
    m_filename = String();
    m_buf = buf;
    return true;
}

Step 5. (imread()側) 要求画像の決定

imread()では、第2引数にパラメータを指定できる。例えば、強制的にモノクロで読み出す、カラーで読み出す、など等。どのような形式でデータを詰め込んでほしいのかを求めて、引数matを形成する。

loadsave.cpp

    // grab the decoded type
    int type = decoder->type();
    if( (flags & IMREAD_LOAD_GDAL) != IMREAD_LOAD_GDAL && flags != IMREAD_UNCHANGED )
    {
        if( (flags & IMREAD_ANYDEPTH) == 0 )
            type = CV_MAKETYPE(CV_8U, CV_MAT_CN(type));


        if( (flags & IMREAD_COLOR) != 0 ||
           ((flags & IMREAD_ANYCOLOR) != 0 && CV_MAT_CN(type) > 1) )
            type = CV_MAKETYPE(CV_MAT_DEPTH(type), 3);
        else
            type = CV_MAKETYPE(CV_MAT_DEPTH(type), 1);
    }

Step 6. データ解析(readData())

さて、ここまでで以下のパラメータが定まった。これにそって読み出していく。これらは各画像Decoderの実装依存となる。

  • m_filename/m_height/m_width/m_type -> 入力形式(画像ファイル)
  • readData()の引数mat -> 出力形式(バッファ形態)

データ解析をUMLでまとめると...

まとめ

OpenCVの画像読み込みの構造は、2010年当時(OpenCVのコード管理をGithubに持ってきたとき)からずーっと変わっていないです。拡張性などを考慮した設計がきちんとなされれば、ここまで長く使い続ける事もできるのですね!

お忙しい中、ご精読、ありがとうございました!!

次回は、@uranishi さんの 「Google Colaboratory+OpenCVでWebカメラ画像からリアルタイム顔検出」 の予定です。おお、まさしくOpenCVっぽい楽しそうなトピックです!! よろしくお願いします!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4