この記事は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を有効にする設定を追加したい。
コーデックに関するマクロ/設定値がいくつかある。
- 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を持った実装を実現している。
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を形成する。
// 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っぽい楽しそうなトピックです!! よろしくお願いします!