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

この記事は[OpenCV Advent Calendar 2022](https://qiita.com/advent-calendar/2022/opencv)の6日目の記事です。
他の記事は[目次](https://qiita.com/advent-calendar/2022/opencv)にまとめられています。

## TL;DR

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

## はじめに

### 簡単に自己紹介？

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

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

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

[OpenCVのissue](https://github.com/opencv/opencv/issues)を見ていたら、TGA format対応へのfeature requestが来ていた。

https://github.com/opencv/opencv/issues/22722

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

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

## Step 1. CMakeList.txt

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

### CODECに対応するマクロ

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89434/33d867f6-c03b-ea45-f09a-1a05e5cfcd88.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

```cmake: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は[こちら](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/CMakeLists.txt#L306-L308)

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

### CMake.実行時のログに表示

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

```bash
   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は[こちら](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/CMakeLists.txt#L1315-L1323)。

```cmake: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()
```

</div></details>

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

### Codecの登録

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

ソースコードは、[こちら](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/modules/imgcodecs/src/loadsave.cpp#L121-L199)。

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

```mermaid
classDiagram
  direction LR

  class BaseImageDecoder {
    <<abstract>> 
    +newDecoder()* ImageDecoder
  }

  class TiffDecoder {
    +newDecoder() ImageDecoder
  }
  class TiffEncoder {
    +newEncoder() ImageEncoder
  }
  class PngDecoder {
    +newDecoder() ImageDecoder
  }
  class PngEncoder {
    +newEncoder() ImageEncoder
  }

  class ImageCodecInitializer {
    -vector＜BaseImageDecoder＞ decoders
    -vector＜BaseImageEncoder＞ encoders
    +ImageCodecInitializer()
  }

  BaseImageDecoder <|.. TiffDecoder  
  BaseImageDecoder <|.. PngDecoder  

  BaseImageEncoder <|.. TiffEncoder  
  BaseImageEncoder <|.. PngEncoder  

  class BaseImageEncoder {
    <<abstract>> 
    +newDecoder()* ImageEncoder
  }

  ImageCodecInitializer "1" *-- "0..*" BaseImageDecoder
  ImageCodecInitializer "1" *-- "0..*" BaseImageEncoder
```

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

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

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

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

```mermaid
sequenceDiagram
    actor app as User Application
    participant imread as imgcodec<br/>loadsave.cpp<br/>imread()
    participant findDecoder as imgcodec<br/>loadsave.cpp<br/>findDecoder()
    app->>+imread : imread( filaname, flags)
    imread->>+findDecoder:findDecoder( filename )
      loop All decorders
        findDecoder->>+decoder:signatureLength () 
        decoder->>-findDecoder:return length () 
      end
      findDecoder->>findDecoder: read signature
      loop All decorders
        findDecoder->>+decoder:checkSignature (signature)
        decoder-->>-findDecoder:return true/false
      end
    findDecoder-->-imread : return decorder
    imread->>+decoder : readHeader()
    decoder->>decoder : fix m_type/m_width/m_height
    decoder-->>-imread : return true/false
    imread->>imread : create Mat ( flags )
    imread->>+decoder : readData(mat)
    decoder-->>-imread : return true/false
    imread-->>-app:return mat


```



## 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の場合は以下。

```c++
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都合を上手く隠ぺいできる仕組みになっている。

```c++
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でまとめると...

```mermaid
classDiagram
  direction LR
  class BaseImageDecoder {
  <<abstract>> 
    +signatureLength()* size_t
    +checkSignature( const String& signature )* bool
  }

  class TiffDecoder {
    +signatureLength() size_t
    +checkSignature( const String& signature ) bool
  }

  class PngDecoder {
    +signatureLength() size_t
    +checkSignature( const String& signature ) bool
  }

  findDecoder -- BaseImageDecoder : use

  BaseImageDecoder <|.. TiffDecoder  
  BaseImageDecoder <|.. PngDecoder  
```

### (代替手段) m_signature

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

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

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

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

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

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

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

例えば、[jpegの例](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/modules/imgcodecs/src/grfmt_jpeg.cpp#L245-L259) を示す。`libjpeg`を使って読み込んだ構造体`state->info`を使って、 `m_width`,`m_height`,`m_type`が設定される。

```c++
        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でまとめると...

```mermaid
classDiagram
  direction LR
  participant imread 

  class BaseImageDecoder {
    <<abstract>> 
    #int m_height
    #int m_width
    #int m_type
    #String m_filename 
    +readHeader()* bool
  }

  class PngDecoder {
    +readHeader() bool
  }

  imread -- BaseImageDecoder : use
  BaseImageDecoder <|.. PngDecoder
```

### （余談）ファイルじゃなくてもバッファでもよい

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

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

[JpegDecoderのコントラスタ](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/modules/imgcodecs/src/grfmt_jpeg.cpp#L178-L184)を再度確認すると、`m_buf_supported = true` となっている。

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

これは、JpegDecoderの基底クラスでる、BaseImageDecoder経由で、[データソースを指定](https://github.com/opencv/opencv/blob/b0dc474160e389b9c9045da5db49d03ae17c6a6b/modules/imgcodecs/src/grfmt_base.cpp#L63-L77) するときの判定に使われる。

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

https://github.com/opencv/opencv/blob/da4ac6b7eff2e8869567e4faaff73312f9e1ef57/modules/imgcodecs/src/loadsave.cpp#L449-L463

```c++: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でまとめると...

```mermaid
classDiagram
  direction LR
  class BaseImageDecoder {
    <<abstract>> 
    #int m_height
    #int m_width
    #int m_type
    #String m_filename
    +readData(mat)* bool
  }

  class PngDecoder {
    +readData(mat) bool
  }

  imread -- BaseImageDecoder : use
  BaseImageDecoder <|.. PngDecoder  
```

## まとめ

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

https://github.com/opencv/opencv/blob/127d6649a1c83397bf42799ac807af41aa507b30/modules/highgui/src/loadsave.cpp


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

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