OpenCVのzlib-ng連携で高速化を図る

この記事は[OpenCV Advent Calendar 2024](https://qiita.com/advent-calendar/2024/opencv)の16日目の記事です。

## はじめに

OpenCV 4.10.0以降からzlib-ng連携機能が使用できるようになりました。本記事ではOpenCVのzlib-ng連携の紹介および効果計測を行っていきます。

## zlib-ngとは

zlib-ng(<https://github.com/zlib-ng/zlib-ng>)は、zlib(<https://zlib.net/>)とAPI互換を保ちつつ、モダンなCPU命令を使用した実装により高速化を図ったライブラリです。
<https://github.com/zlib-ng/zlib-ng>にて"ng"はnext generationを意味することが書かれています。

## OpenCVにおけるzlib-ng連携の歴史

2024/12/16時点におけるOpenCVのzlib-ng連携の歴史を以下にまとめます。

1. <https://github.com/opencv/opencv/issues/22573>でzlibからzlib-ngへの置き換え提案が行われました
1. <https://github.com/opencv/opencv/pull/24782>でzlib-ng対応が行われました（zlib-ng 2.1.6）
1. <https://github.com/opencv/opencv/pull/26113>でzlib-ngバージョンアップが行われました（zlib-ng 2.2.1）

そのため、stableバージョンの場合、OpenCV 4.10.0以降であればzlib-ng連携の機能が使用できます。また、今後リリースが計画されているOpenCV 4.11.0でzlib-ngバージョンアップが適用される見込みです。

## OpenCVのzlib-ng連携有効化

OpenCVのcoreモジュールでzlib-ngを有効にする場合、CMakeオプションで`WITH_ZLIB_NG=ON`を指定します。`WITH_ZLIB_NG=ON`としてCMakeを実行したときのメッセージ例は以下の通りです。このメッセージから`zlib-ng`が参照されるようになっていることがわかります。

```cmake
  Media I/O: 
    ZLib-Ng:                     build (zlib ver 1.3.1.zlib-ng, zlib-ng ver 2.2.1)
```

さらにOpenCVのimgcodecsモジュールでもzlib-ngを有効にする場合は以下のオプションも追加します。

- PNG
  - `WITH_ZLIB_NG=ON`、`WITH_PNG=ON`、`BUILD_PNG=ON`(libpng)
    - `BUILD_PNG=ON`は、`opencv/3rdparty`以下のlibpngがzlib-ngをリンクするため必要
  - `WITH_ZLIB_NG=ON`、`WITH_SPNG=ON`、`BUILD_SPNG=ON`(libspng)
    - `BUILD_SPNG=ON`は、`opencv/3rdparty`以下のlibspngがzlib-ngをリンクするため必要
- TIFF
  - `WITH_ZLIB_NG=ON`、`WITH_TIFF=ON`、`BUILD_TIFF=ON`
    - `BUILD_TIFF=ON`は、`opencv/3rdparty`以下のlibtiffがzlib-ngをリンクするため必要
- OpenEXR
  - `WITH_ZLIB_NG=ON`、`WITH_OPENEXR=ON`、`BUILD_OPENEXR=ON`
    - `BUILD_OPENEXR=ON`は、`opencv/3rdparty`以下のopenexrがzlib-ngをリンクするため必要

## zlib-ng連携の仕組み

ここではOpenCVがzlibの代わりにzlib-ngとどのように連携しているかの仕組みについて解説します。

### OpenCVにおけるzlib-ng格納場所

zlib-ngのソースコードは`opencv/3rdparty`以下（<https://github.com/opencv/opencv/tree/4.10.0/3rdparty/zlib-ng>）に格納されています。

### zlib-ng切り替え

OpenCVでzlib、zlib-ngどちらを使うかはどのように切り替えているのでしょうか？OpenCVの<https://github.com/opencv/opencv/blob/4.10.0/cmake/OpenCVFindLibsGrfmt.cmake>に以下の記述があります。

```cmake
if(WITH_ZLIB_NG)
  ocv_clear_vars(ZLIB_LIBRARY ZLIB_LIBRARIES ZLIB_INCLUDE_DIR)
  set(ZLIB_LIBRARY zlib CACHE INTERNAL "")
  add_subdirectory("${OpenCV_SOURCE_DIR}/3rdparty/zlib-ng")
  set(ZLIB_INCLUDE_DIR "${${ZLIB_LIBRARY}_BINARY_DIR}" CACHE INTERNAL "")
  set(ZLIB_INCLUDE_DIRS ${ZLIB_INCLUDE_DIR})
  set(ZLIB_LIBRARIES ${ZLIB_LIBRARY})

  ocv_parse_header_version(ZLIB "${${ZLIB_LIBRARY}_SOURCE_DIR}/zlib.h.in" ZLIB_VERSION)
  ocv_parse_header_version(ZLIBNG "${${ZLIB_LIBRARY}_SOURCE_DIR}/zlib.h.in" ZLIBNG_VERSION)

  set(HAVE_ZLIB_NG YES)
```

上記のコードで`WITH_ZLIB_NG=ON`の場合に`opencv/3rdparty/zlib-ng`を参照するようになっていることがわかります。冒頭で書いたようにzlib-ngはzlibとAPI互換があるため基本的にはこれだけでOKです。

## 効果計測

ここでは、zlib-ng対応の効果を検証するために以下の環境で処理速度の計測を行います。

### 計測環境、計測条件

計測環境を下表にまとめます。

|名称|スペック、バージョン|
|---|---|
|CPU|Intel Core i7-9800X CPU @ 3.80GHz|
|メモリ|32GB|
|OS|Ubuntu 22.04 64bit|
|コンパイラ|g++ 11.4.0|
|OpenCVバージョン|OpenCVリポジトリの4.xブランチでzlib-ngバージョンアップ適用時点（<https://github.com/opencv/opencv/commit/85923c8f3054bc52c9c30c2a8e932dbd96cc26fc>）|
|zlib|1.3.1|
|zlib-ng|2.2.1|


本記事では`WITH_ZLIB_NG=ON`、`WITH_ZLIB_NG=OFF`それぞれで以下の処理を実行し、処理時間を計測します。入力データは3チャンネルかつ、1080p（1920 x 1080）、4K（3840 x 2160）のデータを用います。

- cv::imencode、cv::imdecode
  - 今回は利用ケースが多いであろうPNGのみ
- cv::imwrite、cv::imread
  - 今回は利用ケースが多いであろうPNGのみ
- cv::FileStorage

### 計測結果

#### cv::imencode、cv::imdecode

エンコードのコードは以下の通りです。

```c++
std::vector<int> params = std::vector<int>(2);
params[0] = cv::IMWRITE_PNG_COMPRESSION;
params[1] = 1;
std::vector<uchar> buf;
cv::imencode(".png", img, buf, params);
```

そして、デコードのコードは以下の通りです。

```c++
cv::Mat dst = cv::imdecode(cv::Mat(buf), cv::IMREAD_COLOR);
```

処理時間を下表に示します。この結果からimencodeはzlib-ngの方が大幅に高速に処理できており、imdecodeもわずかにzlib-ngの方が高速に処理できていることがわかります。

|条件|処理時間(ms)<br>（zlib利用）|処理時間(ms)<br>（zlib-ng利用）|
|---|---:|---:|
|1080p(1920, 1080), imencode|136.82|88.75|
|4K(3840, 2160), imencode|517.71|309.792|
|1080p(1920, 1080), imdecode|38.33|36.90|
|4K(3840, 2160), imdecode|133.65|118.09|

#### cv::imwrite、cv::imread

画像書き込みのコードは以下の通りです。

```c++
std::vector<int> params = std::vector<int>(2);
params[0] = cv::IMWRITE_PNG_COMPRESSION;
params[1] = 1;
cv::imwrite("img.png", img, params);
```

そして、画像読み込みのコードは以下の通りです。

```c++
cv::imread("img.png", cv::IMREAD_COLOR);
```

処理時間を下表に示します。この結果からimwriteはzlib-ngの方が大幅に高速に処理できており、imreadもわずかにzlib-ngの方が高速に処理できていることがわかります。

|条件|処理時間(ms)<br>（zlib利用）|処理時間(ms)<br>（zlib-ng利用）|
|---|---:|---:|
|1080p(1920, 1080), imwrite|157.59|103.65|
|4K(3840, 2160), imwrite|607.81|385.32|
|1080p(1920, 1080), imread|39.43|37.82|
|4K(3840, 2160), imread|134.12|117.34|

#### cv::FileStorage

<https://docs.opencv.org/4.10.0/da/d56/classcv_1_1FileStorage.html#a587b4f5793a894fbe245e95cb30d6bc7>で

> Name of the file to open or the text string to read the data from. Extension of the file (.xml, .yml/.yaml or .json) determines its format (XML, YAML or JSON respectively). Also you can append .gz to work with compressed files, for example myHugeMatrix.xml.gz.

という説明があり、実はcv::FileStorageで圧縮形式を扱う場合にzlib、zlib-ngを使用します。
書き込みのコードは以下の通りです。

```c++
cv::FileStorage fs("img.xml.gz", cv::FileStorage::WRITE);
fs << "img" << img; 
```

そして、読み込みのコードは以下の通りです。

```c++
cv::FileStorage fs("img.xml.gz", cv::FileStorage::READ);
cv::Mat img;
fs["img"] >> img;
```

処理時間を下表に示します。この結果からwriteはzlib-ngの方が大幅に高速に処理できており、readもわずかにzlib-ngの方が高速に処理できていることがわかります。

|条件|処理時間(ms)<br>（zlib利用）|処理時間(ms)<br>（zlib-ng利用）|
|---|---:|---:|
|1080p(1920, 1080), cv::FileStorage write|838.87|394.33|
|4K(3840, 2160), cv::FileStorage write|3100.47|1528.96|
|1080p(1920, 1080), cv::FileStorage read|698.78|664.40|
|4K(3840, 2160), cv::FileStorage read|2731.44|2595.08|

## おわりに

本記事ではOpenCVのzlib-ng連携の紹介および効果計測について述べました。この記事が読者のよりよいOpenCVライフに繋がれば幸いです。明日、12/17は@Kazuhitoさんの担当です、お楽しみに。