この記事は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を有効にする設定を追加したい。
コーデックに関するマクロ/設定値がいくつかある。
- WITH_xxxx: xxxxを有効化する。
- BUILD_xxxx: 3rdpartyフォルダ以下のコードをコンパイル。なければ、shared library参照。
- HAVE_xxxx: xxxxライブラリが存在する
WITH_xxxx(+BUILD_xxxx) -> HAVE_xxxx
HAVE_xxxxは以下のようなコードで導出される。
# --- 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はこちら
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はこちら。
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を生成し、decoders
やencoders
で管理している。これにより、画像フォーマットを柔軟に追加することができる。
ソースコードは、こちら。
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編)" の予定です。よろしくお願いします!