OpenCVを使って連番画像をアニメーションWebPとして出力する方法

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

# 1. はじめに

Webサイトで動く画像と言えばGIFアニメーションというほど，動画を動画としてではなく画像としてリンクのに使われているものGIFフォーマットです．
しかしGIFが登場して[早30年](https://ja.wikipedia.org/wiki/Graphics_Interchange_Format)．
さすがにこのフォーマットには，そろそろWebから勇退してもらわないとインターネットを流れるパケット量的に困ります．

画像の連番を動画として再生するためのフォーマットとして，GoogleのWebPは，この古代のアニメーションGIFを代替しかけています．
この記事では，OpenCVを使って連番画像をWebPアニメーションとして保存できるようにする方法を紹介します．
これを使えば，WebページやGitHubのReadme等でアプリや論文などの説明をわかりやすくするための動く画像を手軽にできるようになるでしょう．
（あと，アニメーションWebPはGithubデスクプットのコミットのプレビューでもアニメーションで確認できます．）

# 2. WebP

[WebP](https://ja.wikipedia.org/wiki/WebP)はGoogleが開発した，これまでの一般的なフォーマットを代替するために作られた画像フォーマットです．
JPEGのように自然画を高圧に，PNGのように可逆圧縮可能なように，GIFのようにアニメーション機能を持つようにと，これまでに使われてきたフォーマットのすべての上位互換となるようにして作られたフォーマットです．

登場から早10年．いつの間にか，ほぼすべてのブラウザでWebPの画像表示に対応しています．

[WebPの対応状況](https://caniuse.com/webp)

画像が動く機能自体は非常に大事ですが，その綺麗さ，画像サイズも重要です．
動くことが保証されるなら，大事なのはクオリティとサイズのコントロールです．
GIFは，まずフルカラー画像に対応しておらず，$256^3$の色情報を256色に原色しないと画像として表示できません．
加えて，動画であっても動画としての相関性が使えず，1枚1枚をGIFとして圧縮しているためサイズが膨大になります．
WebPはフルカラー対応に加えて画像を動画フォーマットのように動画間の似た色を予測しながら小さくするためGIFよりも小さくきれいに表示できます．
これは，WebPはYouTube内の動画エンコード形式であるVP8のイントラ符号化を元にした画像符号化フォーマットだからです．

なお，静止画としての圧縮性能はVP8よりも新しいH.265/HEVCをベースにしたHEIFのほうが高いです．
しかしブラウザ全然対応してません．
[HEIFの対応状況](https://caniuse.com/heif)

# 3. OpenCVの対応状況
OpenCVも画像の保存としてWebPを選択できるようになっており，`imwrite("a.webp")`などとすればWebPで保存できます．
しかし，その保存のためのオプションとして画像品質を0から100の値で指定する`IMWRITE_WEBP_QUALITY`オプションしかないため，アニメーションWebPには対応していません．
WebPには入力画像の色空間（RGBとYUV420）を指定できたり，圧縮の方法を指定出来たり，デブロッキングフィルタやシャープネスフィルタを指定できたり様々な機能がありますがOpenCVではその機能を使うことができません．

各機能と性能は下記で紹介しています（10年前なのでだいぶ古い）．
[WebPの各機能の紹介](https://www.slideshare.net/FukushimaNorishige/webp)

また，下記構造体はWebPで圧縮する画像オプションを指定するためのものであり，ここを制御することでアルゴリズムの詳細を設定可能です．
（今回はほとんど指定してませんが）
```cpp
struct WebPConfig {
  int lossless;           // Lossless encoding (0=lossy(default), 1=lossless).
  float quality;          // between 0 and 100. For lossy, 0 gives the smallest
                          // size and 100 the largest. For lossless, this
                          // parameter is the amount of effort put into the
                          // compression: 0 is the fastest but gives larger
                          // files compared to the slowest, but best, 100.
  int method;             // quality/speed trade-off (0=fast, 6=slower-better)

  WebPImageHint image_hint;  // Hint for image type (lossless only for now).

  // Parameters related to lossy compression only:
  int target_size;        // if non-zero, set the desired target size in bytes.
                          // Takes precedence over the 'compression' parameter.
  float target_PSNR;      // if non-zero, specifies the minimal distortion to
                          // try to achieve. Takes precedence over target_size.
  int segments;           // maximum number of segments to use, in [1..4]
  int sns_strength;       // Spatial Noise Shaping. 0=off, 100=maximum.
  int filter_strength;    // range: [0 = off .. 100 = strongest]
  int filter_sharpness;   // range: [0 = off .. 7 = least sharp]
  int filter_type;        // filtering type: 0 = simple, 1 = strong (only used
                          // if filter_strength > 0 or autofilter > 0)
  int autofilter;         // Auto adjust filter's strength [0 = off, 1 = on]
  int alpha_compression;  // Algorithm for encoding the alpha plane (0 = none,
                          // 1 = compressed with WebP lossless). Default is 1.
  int alpha_filtering;    // Predictive filtering method for alpha plane.
                          //  0: none, 1: fast, 2: best. Default if 1.
  int alpha_quality;      // Between 0 (smallest size) and 100 (lossless).
                          // Default is 100.
  int pass;               // number of entropy-analysis passes (in [1..10]).

  int show_compressed;    // if true, export the compressed picture back.
                          // In-loop filtering is not applied.
  int preprocessing;      // preprocessing filter (0=none, 1=segment-smooth)
  int partitions;         // log2(number of token partitions) in [0..3]
                          // Default is set to 0 for easier progressive decoding.
  int partition_limit;    // quality degradation allowed to fit the 512k limit on
                          // prediction modes coding (0: no degradation,
                          // 100: maximum possible degradation).
  int use_sharp_yuv;      // if needed, use sharp (and slow) RGB->YUV conversion
};
```
# 4. コード
下記に，アニメーションWebPを保存するコードを示します．
入力として，出力ファイル名，入力連番画像（vector<Mat>），各種パラメータを指定します．
パラメータは`IMWRITE_WEBP_QUALITY`に加えて，下記を指定できるようにしています．
パラメータは偶数が項目，奇数がその変数値になってます．
詳細は下記を参照してください．
[参照記事：13年前からの潜在バグを修正しようとして、9年前の潜在バグに悩まされた話。](https://qiita.com/hon_no_mushi/items/65fc9f1a73100ef1852d)

IMWRITE_WEBP_COLORSPACEの１に相当するYUV_SHARPに関しては下記を参照してください．
（このオプションはだいぶ計算時間がかかるため，入力画像がそのままRGB画像ならRGBを直接圧縮したほうがいいかも．）
[参照記事：WebP の色劣化問題の改善](https://qiita.com/yoya/items/f5ef9b66b556eda9e6bf)

また，作成に当たってWebP公式にある連番画像を入力してアニメーションWebPを作成するツールである`img2webp.c`のコードも参考になります．

```cpp
//圧縮アルゴリズム（0-6）の選択．デフォルトは4．0がが一番高速に動き，6になると遅いが高品質
#define IMWRITE_WEBP_METHOD 65 //0(fast)-6(slower, better), default 4
//色空間．0から2の順で品質が上がり圧縮率が下がる．
#define IMWRITE_WEBP_COLORSPACE 66 //0: YUV, 1: YUV_SHARP, 2: RGB
//連番動画の繰り返し回数．0は無限回繰り返す．
#define IMWRITE_WEBP_LOOPCOUNT 67 //0: infinit
//連番動画の1枚が何msで表示されるか．33なら30FPSで表示
#define IMWRITE_WEBP_TIMEMSPERFRAME 68//time(ms) per frame default(33)
```

作成した関数`int imwriteAnimationWebp`．戻り値はファイルサイズ．

```cpp
#include "webp.hpp"
#include "./webp/encode.h"
#include "./webp/decode.h"
#include "./webp/mux.h"
#pragma comment(lib,"./webp/libwebp.lib")
#pragma comment(lib,"./webp/libwebpmux.lib")

using namespace cv;
using namespace std;

	//ImwriteFlags::IMWRITE_WEBP_QUALITY=64
#define IMWRITE_WEBP_METHOD 65 //0(fast)-6(slower, better), default 4
#define IMWRITE_WEBP_COLORSPACE 66 //0: YUV, 1: YUV_SHARP, 2: RGB
#define IMWRITE_WEBP_LOOPCOUNT 67 //0: infinit
#define IMWRITE_WEBP_TIMEMSPERFRAME 68//time(ms) per frame default(33)

	//return writting data size
    int imwriteAnimationWebp(std::string name, std::vector<cv::Mat>& src, const std::vector<int>& parameters = std::vector<int>());
	{
		float quality = 100.f;
		int loop_count = 0;//0: infinit
		int timems_per_frame = 33;
		int method = 4;
		int colorspace = 0;//0: YUV, 1: RGB, 2: YUV_SHARP
		for (int i = 0; i < parameters.size(); i += 2)
		{
			if (parameters[i] == IMWRITE_WEBP_QUALITY)
			{
				quality = (float)parameters[i + 1];
			}
			if (parameters[i] == IMWRITE_WEBP_METHOD)
			{
				method = parameters[i + 1];
			}
			if (parameters[i] == IMWRITE_WEBP_LOOPCOUNT)
			{
				loop_count = parameters[i + 1];
			}
			if (parameters[i] == IMWRITE_WEBP_TIMEMSPERFRAME)
			{
				timems_per_frame = parameters[i + 1];
			}
			if (parameters[i] == IMWRITE_WEBP_COLORSPACE)
			{
				colorspace = parameters[i + 1];
			}
		}
		const int width = src[0].cols;
		const int height = src[0].rows;

		WebPPicture pic;
		if (!WebPPictureInit(&pic))
		{
			cerr << "picture init" << endl;
			return -1;  // version error
		}
		pic.width = width;
		pic.height = height;

		// allocated picture of dimension width x height
		if (!WebPPictureAlloc(&pic))
		{
			cerr << "picture alloc" << endl;
			return -1;   // memory error
		}

		WebPAnimEncoderOptions enc_options = { {0} };
		WebPAnimEncoderOptionsInit(&enc_options);
		//enc_options.kmax = 1;
		//enc_options.kmin = 256/2+1;//kmin >= kmax / 2 + 1
		WebPAnimEncoder* enc = WebPAnimEncoderNew(width, height, &enc_options);

		WebPConfig config;
		WebPConfigInit(&config);
		config.quality = quality;//0.f-100.f
		config.method = method;//0(fast)-6(slower, better), default 4
		if (!WebPValidateConfig(&config))
		{
			cerr << "config error" << endl;
			return -1;   // config error
		}

		Mat temp;
		for (int i = 0; i < src.size(); i++)
		{
			cvtColor(src[i], temp, COLOR_BGR2RGBA);

			if (!WebPPictureImportRGBA(&pic, temp.data, temp.step))
			{
				cerr << "error: WebPPictureImportRGBA" << endl;
			}

			if (colorspace == 0) WebPPictureARGBToYUVA(&pic, WebPEncCSP::WEBP_YUV420);
			if (colorspace == 1) WebPPictureSharpARGBToYUVA(&pic);

			WebPAnimEncoderAdd(enc, &pic, timems_per_frame * i, &config);
		}

		// add a last fake frame to signal the last duration
		WebPAnimEncoderAdd(enc, NULL, timems_per_frame, NULL);

		WebPData webp_data = { 0 };
		WebPDataInit(&webp_data);

		//write data
		WebPAnimEncoderAssemble(enc, &webp_data);

		//Mux assemble
		WebPMux* mux = WebPMuxCreate(&webp_data, 1);
		WebPMuxAnimParams anim_params;
		anim_params.loop_count = loop_count;
		WebPMuxSetAnimationParams(mux, &anim_params);
		WebPMuxAssemble(mux, &webp_data);

		FILE* fp = fopen(name.c_str(), "wb");
		fwrite(webp_data.bytes, sizeof(char), webp_data.size, fp);
		const int ret = (int)webp_data.size;
		fclose(fp);
		WebPPictureFree(&pic);
		WebPAnimEncoderDelete(enc);
		WebPDataClear(&webp_data);
		WebPMuxDelete(mux);
		return ret;
	}
```

なお，`WebPAnimEncoderOptions`の`kmin`，`kmax`でイントラフレーム（キーフレーム）の間隔を指定できます．
余りに激しく動くような動画はこれを指定したほうがいいかもしれませんが，まぁデフォルト値のint最大値が指定したままで困ることは少ない．
```cpp
 enc_options = { {0} };
		WebPAnimEncoderOptionsInit(&enc_options);
		//enc_options.kmax = 1;
		//enc_options.kmin = 256/2+1;//kmin >= kmax / 2 + 1
```

# 5. 結果
## 5.1 GIF
まず，GIF　動画を示します．ファイルサイズは，2.80 MBです．
色を256色のパレットとディザリングによって表現しているため，特徴的なノイズが目立ちます．
これは，ffmpegを使って，000.bmp, 001.bmp ...というファイルを下記コマンドで変換しました．
なお，連番画像の真ん中にはわかりやすいようにフレーム番号をレンダリングした画像を変換しています．
```
ffmpeg -r 10 -i %03d.png out.gif
```

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67908/e1ebf476-c9a4-cca4-c1ce-200ef5b7223c.gif)

## 5.2 WebP
次にWebPを下記オプション（一番いいアルゴリズムでカラースペースをYUV，YUV_Sharp，RGB）と変えて，品質を10と100で実行しました．

```cpp
vector<int> parameters;
parameters.push_back(IMWRITE_WEBP_COLORSPACE);
parameters.push_back(0);//or 1 or 2
parameters.push_back(IMWRITE_WEBP_METHOD);
parameters.push_back(6);
parameters.push_back(IMWRITE_WEBP_QUALITY);
parameters.push_back(10);//or 100
parameters.push_back(IMWRITE_WEBP_TIMEMSPERFRAME);
parameters.push_back(67);//15 fps
```

結果は以下になります．
YUVは一番ファイルサイズが小さく，Sharpは変換に若干時間がかかります．
最大の品質で圧縮してもGIFよりもファイルサイズは小さくなっています．
（一番きれいになるはずのデータはRGBですが，今回は色域の関係でほぼわかりません．パワポなどの色彩がはっきりしているようなものを選べばおそらくわかります．）

```
write YUV: 3.5395 sec
1721.56 Kbyte
write YUV Sharp: 3.84242 sec
1824.51 Kbyte
write RGB: 3.56159 sec
1791.13 Kbyte

write YUV: 11.1865 sec
216.447 Kbyte
write YUV Sharp: 11.676 sec
219.244 Kbyte
write RGB: 11.3013 sec
219.162 Kbyte
```

結果の動画を示します．YUV_Sharpは割愛してます．
YUV，RGB，このデータでは見た目が分かりませんね．
どちらも，GIFよりは圧倒的にきれいなことが分かります．
さすがに，品質10は劣化が分かるのと残像が残っています．
これはキーフレームの設定をしていないからですが，この動画は動画圧縮としてかなり手ごわいデータを選んだので普通の動画ならもう少しうまくいきます．
（この動画はカメラ80台で取った同時刻の80台を切り替えた動画です．カメラごとにいろが微妙に違うのと，カメラが固定されていない動きになるため，類似領域が少なくなって小さくなりづらくなります．）

WebP (YUV100)
<img src="https://raw.githubusercontent.com/norishigefukushima/OpenCP/master/testOpenCP/webp/yuv100.webp" alt="webpyuv100">
WebP (RGB100)
<img src="https://raw.githubusercontent.com/norishigefukushima/OpenCP/master/testOpenCP/webp/rgb100.webp" alt="webprgb100">
WebP (YUV10)
<img src="https://raw.githubusercontent.com/norishigefukushima/OpenCP/master/testOpenCP/webp/yuv10.webp" alt="webpyuv10">
WebP (RGB10)
<img src="https://raw.githubusercontent.com/norishigefukushima/OpenCP/master/testOpenCP/webp/rgb10.webp" alt="webprgb10">
※ QiitaのマークダウンではWebpを貼れないのため（2022/12/17時点），HTMLタグで貼ること．

# 6. まとめ
本記事では，OpenCVで連番動画をアニメーションWebPとして保存する方法を紹介しました．
これで，論文などを説明するページをアニメーションを含めて説明することが楽になります．
（これ作った最大の目的がこれ）

これをOpenCVにコトリビュートするためにはテストコードやエラーチェックを追加する必要があるのですが，時間ができたらやろうと思ってもなかなか．．．
自分が使えればいいやのコードは，下記に公開済みです．
（このサンプルとほぼ一緒です．）
[https://github.com/norishigefukushima/OpenCP/blob/master/include/webp.hpp](https://github.com/norishigefukushima/OpenCP/blob/master/include/webp.hpp)

なお，PNG拡張のAPNGや[JPEG XL](https://ja.wikipedia.org/wiki/JPEG_XL)も代替候補ですが，普及具合はまだまだです．
ただし，APNGはIE除いて全部最近のブラウザでは使える模様です．
* [JPEG XL](https://caniuse.com/jpegxl)
* [APNG](https://caniuse.com/apng)

次の記事は，@kimizuka さんの
「OpenCV.jsを試すぞ」です．


**Windows用備考**
Webpの配布済みのlibファイルはスタティックリンクのためのライブラリのため，DLL中でそのまま使うと下記がでます．
```
'libcmt.lib' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。
```
dllとして使いたいときは，cmake等でsolusionを作ってdllとして作って使うようにしましょう．