私はopenCVでいくつかのコードを書いていて、非常に大きな行列配列(単一チャネルのグレースケール、浮動小数点)の中央値を見つけたいと思っています。
配列の並べ替え(std :: sortを使用)や中央のエントリの選択など、いくつかの方法を試しましたが、MATLABの中央値関数と比較すると非常に遅くなります。正確には、MATLABで0.25秒かかるのに、openCVでは19秒以上かかります。
私の入力画像は元々、寸法が3840x2748(約10.5メガピクセル)の12ビットのグレースケール画像で、浮動小数点数(CV_32FC1)に変換され、すべての値が範囲[0,1]にマップされ、コードのある時点でI以下を呼び出して中央値を要求します。
double myMedianValue = medianMat(Input);
ここで、関数medianMatは次のとおりです。
double medianMat(cv::Mat Input){
Input = Input.reshape(0,1); // spread Input Mat to single row
std::vector<double> vecFromMat;
Input.copyTo(vecFromMat); // Copy Input Mat to vector vecFromMat
std::sort( vecFromMat.begin(), vecFromMat.end() ); // sort vecFromMat
if (vecFromMat.size()%2==0) {return (vecFromMat[vecFromMat.size()/2-1]+vecFromMat[vecFromMat.size()/2])/2;} // in case of even-numbered matrix
return vecFromMat[(vecFromMat.size()-1)/2]; // odd-number of elements in matrix
}
関数medinaMatだけでなく、さまざまなパーツの時間も計測しました-予想どおり、ボトルネックが発生しています:
std::sort( vecFromMat.begin(), vecFromMat.end() ); // sort vecFromMat
ここに誰かが効率的な解決策を持っていますか?
ありがとう!
[〜#〜] edit [〜#〜] Adi Shavitの回答で提供されたstd :: nth_elementを使用してみました。
関数medianMatは次のようになります。
double medianMat(cv::Mat Input){
Input = Input.reshape(0,1); // spread Input Mat to single row
std::vector<double> vecFromMat;
Input.copyTo(vecFromMat); // Copy Input Mat to vector vecFromMat
std::nth_element(vecFromMat.begin(), vecFromMat.begin() + vecFromMat.size() / 2, vecFromMat.end());
return vecFromMat[vecFromMat.size() / 2];}
ランタイムが19秒以上から3.5秒に低下しました。これは、中央値関数を使用してMatlabで0.25秒にまだほど遠いです...
OK。
私は実際に質問を投稿する前にこれを試しました、そしていくつかの愚かな間違いのために私は解決策としてそれを失格にしました...とにかくここにあります:
基本的に、2 ^ 12 = 4096ビンを使用して元の入力の値のヒストグラムを作成し、CDFを計算して正規化して、0から1にマップし、CDFで0.5以上の最小のインデックスを見つけます。次に、このインデックスを12 ^ 2で割り、要求された中央値を見つけます。これは0.11秒で実行され(これはデバッグモードであり、大幅な最適化は行われません)、Matlabで必要な時間の半分未満になります。
これが関数です(nVals = 4096の場合、12ビットの値に対応します)。
double medianMat(cv::Mat Input, int nVals){
// COMPUTE HISTOGRAM OF SINGLE CHANNEL MATRIX
float range[] = { 0, nVals };
const float* histRange = { range };
bool uniform = true; bool accumulate = false;
cv::Mat hist;
calcHist(&Input, 1, 0, cv::Mat(), hist, 1, &nVals, &histRange, uniform, accumulate);
// COMPUTE CUMULATIVE DISTRIBUTION FUNCTION (CDF)
cv::Mat cdf;
hist.copyTo(cdf);
for (int i = 1; i <= nVals-1; i++){
cdf.at<float>(i) += cdf.at<float>(i - 1);
}
cdf /= Input.total();
// COMPUTE MEDIAN
double medianVal;
for (int i = 0; i <= nVals-1; i++){
if (cdf.at<float>(i) >= 0.5) { medianVal = i; break; }
}
return medianVal/nVals; }
中央の要素を並べ替えて取得することは、中央値を見つける最も効率的な方法ではありません。 O(n log n)操作が必要です。
C++では std::nth_element()
を使用し、中間のイテレータを使用する必要があります。これはO(n)操作です:
_
nth_element
_は部分ソートアルゴリズムであり、次のような_[first, last)
_の要素を再配置します。
nth
が指す要素は、その位置で発生するあらゆる要素に変更されますif _[first, last)
_がソートされた場合。- この新しいn番目の要素の前のすべての要素は、新しいn番目の要素の後の要素以下です。
また、元のデータは12ビット整数です。実装は、Matlabとの比較を困難にするいくつかのことを行います。
vector<double>
_への追加のコピーがありますOpenCVのデフォルトのように、イメージがメモリ内で連続していると仮定すると、_CV_16C1
_を使用し、reshape()
の後にデータ配列を直接操作する必要があります
非常に高速な別のオプションは、画像のヒストグラムを作成することです。これは、画像に対する単一パスです。次に、ヒストグラムを操作して、各側のピクセルの半分に対応するビンを見つけます。これは、binsに対する多くても1回のパスです。
OpenCVドキュメントには severaltutorialson ヒストグラムの作成方法があります。ヒストグラムを取得したら、パス3840x2748/2が得られるまでビン値を累積します。このビンはあなたの中央値です。
元のデータから見つける方がおそらく高速です。
元のデータには12ビット値があるため、4096の異なる値しかありません。それは素敵で小さなテーブルです! 1回のパスですべてのデータを調べ、各値の数を数えます。これはO(n)演算です。次に中央値を見つけるのは簡単で、カウントsize/2
テーブルの両端のアイテム。