LoginSignup
8
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-04

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

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を持った実装を実現している。

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

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

Step 3. findEncoder()

imwrite()の第一引数はファイル名である。この拡張子を見て、どの画像フォーマットを使うのかを選択する。例えば、JPEG画像であれば.jpg、png画像であれば.pngが付与される。なお、ファイル名が存在しないバッファへの圧縮の場合でも、.jpgという形で識別子を指定する必要がある(そして、jpgだけを指定して上手く動かない、までが既定路線である)。

さて、getDescription()だが、これはBaseImageEncoder()クラスで、m_descrptionを返す関数と定義されている。

Encoder m_description
BMP Windows bitmap (*.bmp;*.dib)
JPEG JPEG files (*.jpeg;*.jpg;*.jpe)
TIFF TIFF Files (*.tiff;*.tif)

    ImageCodecInitializer& codecs = getCodecs();
    for( size_t i = 0; i < codecs.encoders.size(); i++ )
    {
        String description = codecs.encoders[i]->getDescription();
        const char* descr = strchr( description.c_str(), '(' );


        while( descr )
        {
            descr = strchr( descr + 1, '.' );
            if( !descr )
                break;
            int j = 0;
            for( descr++; j < len && isalnum(descr[j]) ; j++ )
            {
                int c1 = tolower(ext[j]);
                int c2 = tolower(descr[j]);
                if( c1 != c2 )
                    break;
            }
            if( j == len && !isalnum(descr[j]))
                return codecs.encoders[i]->newEncoder();
            descr += j;
        }
    }


    return ImageEncoder();

Step 4. encoder->write()

paramの解析

encoderには、imwriteflagを使ってオプション(param)を指定できる。

このparamは、vector<int>形式で管理されていて、IDとVALUEをペアで扱う必要がある。例えばこんな感じで使う。

  std::vector<int> params;
  params.push_back( IMWRITE_PNG_COMPRESSION );
  params.push_back( 95 );
  params.push_back( IMWRITE_PNG_STRATEGY );
  params.push_back( IMWRITE_PNG_STRATEGY_DEFAULT );

これに対して、2個ずつループを回して、IDとVALUEを取り込む。

                for( size_t i = 0; i < params.size(); i += 2 )
                {
                    if( params[i] == IMWRITE_PNG_COMPRESSION )
                    {
                        compression_strategy = IMWRITE_PNG_STRATEGY_DEFAULT; // Default strategy
                        compression_level = params[i+1];
                        compression_level = MIN(MAX(compression_level, 0), Z_BEST_COMPRESSION);
                    }
                    if( params[i] == IMWRITE_PNG_STRATEGY )
                    {
                        compression_strategy = params[i+1];
                        compression_strategy = MIN(MAX(compression_strategy, 0), Z_FIXED);
                    }
                    if( params[i] == IMWRITE_PNG_BILEVEL )
                    {
                        isBilevel = params[i+1] != 0;
                    }
                }

matの出力

さて、あとは画像データを、各ライブラリに対して供給すれば画像ファイルが作成できる。これは各ライブラリの都合に合わせるとなる。

png出力だと、このあたりが実質コア部分となる。

                    buffer.allocate(height);
                    for( y = 0; y < height; y++ )
                        buffer[y] = img.data + y*img.step;


                    png_write_image( png_ptr, buffer.data() );
                    png_write_end( png_ptr, info_ptr );

マルチページTIFFなどもこのコードで共通化されている、流石ですね!!ということで、技術紹介はここまでと。

まとめ

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

10年越えてもその構造を大きく直す必要がない、ということは、当時からきちんと考え、設計されていたという事なのかもしれませんね!

このシステムであれば、新たな画像フォーマットを追加する事も難しい話ではありません。

次回は"OpenCVにおける画像ファイル入出力の仕組み(imread編)" の予定です。よろしくお願いします!

8
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
8
4