今回はリファレンス画像無しで画質を評価する手法を紹介しようと思います。
先日、OpenCV4.1がリリースされました。
changelogを見ると、quality という新しいモジュールが opencv_contrib レポジトリに追加されたようです。
New Image Quality Analysis module (quality) has been added to the opencv_contrib, referenceless BRISQUE algorithm has been implemented as well as PSNR, SSIM and others
qualityモジュールでは画像の品質を測るための機能が提供されており、PSNRやSSIM等の古典的なアルゴリズムに加えて、リファレンス画像が不要なBRISQUEアルゴリズムも含まれています。BRISQUEは名前だけは聞いたことはありましたけど実際に使ったことはなかったです。
環境
* CentOS 7.5 (x86_64/Xeon 8core CPU/16GB RAM)
* GCC 4.8.5
* CMake 3.13.1
* Python 3.6.5 (Anaconda custom)
いつものようにLinuxサーバ環境上に構築しました。qualityモジュールの動作にはCUDA環境は不要です。構築方法は以前のエントリー(OpenCV 4.0のQRコード検出、G-APIを試す)を参考にしていただければ。opencv_contrib のモジュールもリンクするのでCMakeオプション(OPENCV_EXTRA_MODULES_PATH)を一つ追加する必要があります。
BRISQUEの概要
ここではBRISQUEの概要を簡単に紹介します。詳細は元論文を参照ください。
- 1.A. Mittal, A. K. Moorthy and A. C. Bovik, “ No-Reference Image Quality Assessment in the Spatial Domain ”, IEEE Transactions on ImageProcessing, 2012 (to appear).
BRISQUEはImage Quality Assessment(IQA)のアルゴリズムの一つで、リファレンス画像が不要であるNR IQA(No-Reference/Blind IQA)に分類されます。ちなみに、PSNRやSSIMなどはリファレンス画像が必要となるのでFR IQA(Full-Reference IQA)と呼ばれています。SSIMは輝度値の平均や分散などの基本統計量を使っていますがBRISQUEもそこは同様です。BRISQUEではNSS(Natural Scene Statistics)ベースのMSCN係数というものを定義しており、これはMean Substracted Contrast Normalizationの略で、単純に輝度値を平均0 分散1に正規化するわけではなく、ちょっと手を加えた以下の式で定義されています。
Iは画像の行列(i,j)成分における輝度値、wは局所領域(KxL)におけるガウシアンカーネルの重みで、つまりガウシアンフィルタをかけた画像の輝度値を平均として使っています。Cは0除算を避けるための定数項なのであまり気にしなくてよいです。
そして近隣画素間でMSCN係数の積(実装上は行列全体をシフトして要素積)を取ります。以下の horizontal H(右), vertical V(下), main-diagonal D1(右下), secondary-diagonal D2(左下) の4方向で定義されています。
これで1つの入力画像に対してMSCN係数の積が詰まった行列が4つ得られます。この行列データのヒストグラムが正規分布に従うという観測結果に基づき、確率分布の当てはめ(フィッティング;Gaussian Fitting)を行います。この辺りはBRISQUE固有のロジックというよりは、確率モデルに基づく画像処理では割とよくある考え方かと思います。元論文ではAGGD(Asymmetric Generalized Gaussian Distribution)を仮定してモーメントマッチングベースの手法でパラメータ推定を行ったようです。
BRISQUEではこのMSCN係数の確率分布から36次元の特徴量(特徴ベクトル)を作って学習させます。内訳は最初の2要素が一般化正規分布(GCD:Generalized Gaussian Distribution)のパラメータ2つ(形状、分散)、次の4要素が非対称一般化正規分布(AGCD:Asymmetric Generalized Gaussian Distribution)のパラメータ4つ(形状、平均、左方向分散、右方向分散)、4方向の要素積を各々計算しているので 2 + 4*4 で18、半分の解像度にリサイズしたデータに対して再度同じ計算をしてx2、合計で36次元となっています。
教師信号にはMOS/DMOSという主観的スコアを用いて回帰により学習させるので、画質に対する人間の知覚と相関します。学習アルゴリズムは元論文も今回紹介するOpenCV実装もSVM(SVR:サポートベクター回帰)を使っています。ツールとしては元論文ではLIBSVM、OpenCVではcv::ml
のSVM実装を使っているようです。
動作確認
サンプル画像は以下の2枚を使います。左側がオリジナル画像(lenna.jpg)で、右側がPhotoshopでノイズ付与等の加工した画像(lenna_noised.jpg)になります。
* quality_sample.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> #include "opencv2/highgui.hpp" #include "opencv2/quality.hpp" int main(int argc, char **argv) { using namespace std; if(argc != 2) { cerr << "usage: ./quality_sample <image path>" << endl; exit(-1); } // SVMのモデルを2つ指定(SVMモデルと特徴ベクトルの正規化[-1,1]用データ) const string model_path = "./brisque_model_live.yml"; const string range_path = "./brisque_range_live.yml"; const cv::Mat input_image = cv::imread(argv[1]); // QualityBRISQUEのインスタンス生成 shared_ptr<cv::quality::QualityBase> quality(cv::quality::QualityBRISQUE::create(model_path, range_path)); // computeメソッドで画質評価 cv::Scalar score = quality->compute(input_image); cout << score[0] << endl; return 0; } |
学習済みのSVMモデル(実際は回帰モデルなのでSVR)は modules/quality/samples ディレクトリ以下にあります。学習にはLIVE DB R2と呼ばれるデータセットを使っているようです。これは、オリジナル画像に対してJPEG圧縮やブラー加工、ホワイトノイズ付与等の加工を施した画像のセットとなっており、教師データはDMOSスコアとなっています。
APIのインタフェースはまだ洗練されていないようには見えます。インスタンス生成用の cv::quality::QualityBase::create
メソッドは常に cv::Ptr
で返ってくるようですが、こういったファクトリメソッドは std::unique_ptr
で返して欲しいなと。もし共有したければ std::shared_ptr
に付け替えれば良いだけなので。C++11対応のOpenCV4.xにおいては cv::Ptr
や cv::String
を保守する動機付けは弱いでしょうから後々deprecated扱いになるのかもしれません。あと、スコアはfloat等のプリミティブ型ではなく cv::Scalar
で返されていますが、2019/05現在では1要素目しか値が入ってなかったです。doxygenのコメントにはチャンネル毎にスコア計算すると書いてありましたのでそのように改修されるのかもしれません。
1 2 3 4 5 6 7 8 9 10 11 |
// cvstd_wrapper.hpp // cv::Ptr は std::shared_ptr のエイリアステンプレート // 互換性のためインタフェース変換する程度の薄いラッパーメソッドがいくつか用意されている template <typename _Tp> using Ptr = std::shared_ptr<_Tp>; ... 省略 template<typename _Tp, typename ... A1> static inline Ptr<_Tp> makePtr(const A1&... a1) { return std::make_shared<_Tp>(a1...); } // cvstd.hpp // cv::String は std::string のエイリアス typedef std::string String; |
* 動作確認
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ cat CMakeLists.txt cmake_minimum_required(VERSION 3.5.1) set(project quality_sample) project(${project} CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS "-Wall -g") find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) add_executable(${project} ${project}.cpp) target_link_libraries(${project} ${OpenCV_LIBRARIES}) $ cmake . $ make $ ./quality_sample lenna.jpg 13.9547 $ ./quality_sample lenna_noised.jpg 53.7845 |
BRISQUEではスコアの値が大きいほど低画質の画像だということがわかります。OpenCVのBRISQUE実装ではスコアの範囲は0 – 100となっていました。SVRのハイパーパラメータ等は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ head -20 brisque_model_live.yml %YAML:1.0 --- opencv_ml_svm: format: 3 svmType: EPS_SVR kernel: type: RBF gamma: 5.0000000000000003e-02 C: 1024. p: 1.0000000000000001e-01 term_criteria: { epsilon:1.0000000000000000e-03 } var_count: 36 sv_total: 774 support_vectors: ... 省略 |
せっかくなのでPythonインタフェースも試します。使い方は簡単です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#!/usr/bin/env python # -*- coding: utf-8 -*- import cv2 import sys if __name__ == '__main__': if len(sys.argv) != 2: print('usage: ./quality_sample.py <image path>') sys.exit(-1) model_path = "./brisque_model_live.yml" range_path = "./brisque_range_live.yml" input_image = cv2.imread(sys.argv[1]) quality = cv2.quality.QualityBRISQUE_create(model_path, range_path) score = quality.compute([input_image]) print(score[0]) |
出力結果はC++版と同様なので省略します。いつものようにサンプルコードはGitHubに上げておきます。
次は以下の各画像でPSNRなど他の手法と比較してみました。cv::quality
モジュールにはPSNRやSSIMなどの実装も含まれているのでそれらを使います。ちなみにSSIMの実装を見てみましたが、以下の公式サンプルコードがベースになっていました。ガウシアンブラーをかける部分や各パラメータの値も同じでした。SSIMには任意パラメータがあるので、実装によって値がまちまちになって使いにくい面がありますが、BRISQUEでも同様に適用するモデルによって出力値は変わるので注意しておきます。
MSE: 144.2 PSNR: 26.5 SSIM: 0.90 BRISQUE: 18.6 |
MSE: 143.9 PSNR: 26.5 SSIM: 0.72 BRISQUE: 59.4 |
MSE: 142.0 PSNR: 26.6 SSIM: 0.70 BRISQUE: 76.4 |
The SSIM Index for Image Quality Assessmentより画像引用
比較用ソースコード: opencv-samples/quality/quality_comparison.py
念のため再度注意書きしておきますが、BRISQUEはNR IQAなのでオリジナル画像は不要です。上記のようなサンプルだとMSE/PSNRのスコアはどれもほぼ同じですが、見た目は全然異なっていますね。一方でSSIMとBRISQUEは視覚的な差異が定量的に判断できるので良い指標と言えそうです。SSIMは計算式の任意パラメータを開示すれば共通利用できますが、BRISQUEはモデル自体あるいは学習データを配布する必要があるので面倒かもしれません。
上記の例だと精度面での比較は不十分ですが、機械学習ベースのBRISQUEはデータ精査やハイパーパラメータの調整等でモデル改善していけば精度向上が期待できます。僕は cv::ml
モジュールを使って本格的に何かを学習させたことは無いんですが、cv::ml::SVM::trainAuto
を使うとグリッドサーチとかクロスバリデーションとか自動でやってくれるので便利に利用できそうです。
おわりに
元論文内ではBRISQUEの活用例が紹介されていますが、こういったNR IQAの手法は画質の劣化具合の測定に重用するというよりは、ノイズ除去などの画素補完や生成の為に活用すると良いようです。この分野だと学習データの鮮度は比較的重要ではなく、モデルの更新も頻繁には必要ないはずなので利用しやすいと思います。
深層学習ベースの手法が数多く提案・実用化された今では、2012年に提案されたBRISQUEの仕組みは特徴量設計の職人芸的な部分辺りに懐かしさを感じる面はありますね。現在だと既にCNNベースのNR IQAアルゴリズムがいくつか提案されているようですし、OpenCVへの最新手法の実装も期待できそうです。
今回作成したサンプルコード (C++/Python)
参考
- 1.A. Mittal, A. K. Moorthy and A. C. Bovik, “ No-Reference Image Quality Assessment in the Spatial Domain ”, IEEE Transactions on ImageProcessing, 2012 (to appear).
- Comparison of No-Reference Image Quality Assessment Machine Learning-based Algorithms on Compressed Images (PDF)
- Automatic Image Quality Assessment in Python – Towards Data Science
- No Reference Image and Video Quality Assessment
- The SSIM Index for Image Quality Assessment