はじめに
前回の画像処理の続編になります。
今回はフィルタ処理を説明していきます。前回より面白いんじゃないかと。
動作環境
PC : M4 Macbook Pro(Sequoia 15.1)
コンパイラ : g++
c++ : 17
OpenCV : 4.11.0_*
エディタ:VScode
ソースコード
フィルタ処理とは
フィルタ処理とは入力画像の画素値だけではなく、その周辺の画素値も利用して出力値を求める処理を指します。
$$P_o=\sum_{i=1}^{n} F_iP_i $$
$$P_iは入力画素、P_oは出力画素、F_iはフィルタ$$
フィルタのことをカーネル、オペレータとも言う
演算イメージ
上記の演算を全ての画素に対して行い、新しい画素値を求めます。
要は畳み込み演算をしています。
イメージでは3×3のフィルタとしていますが、5×5や9×9などもあります。
その場合の計算方法は全く同じです。ただ単に範囲が広くなるだけです。
演算はシンプルでわかりやすいのですが、1点注意しないといけないことがあります。
境界(端っこの画素)の扱いです。
この場合どう処理するかですが、いくつかの手段があります。
方法 | 内容 | メリット | デメリット |
---|---|---|---|
リピート | 端の画素を繰り返し使用 | 計算が簡単 | 若干ボヤけるかも |
ミラー | 端の画素を反射して使用 | 境界が自然に見える | 計算が増える |
ゼロパディング | 境界を0にする | 計算が簡単 | 端が黒くなるかも |
なにもしない | 端は処理しない | 計算が簡単 | 斜線が出るかも |
非対称フィルタ(L字/T字) | 有効な画素のみ使用する形に変える | 境界が自然に見える | 計算が複雑 |
手軽に実装したいため、今回はリピートにします。
画像処理
先ほどの説明でフィルタ処理のイメージはできたかと思いますので、実際に処理していきましょう。
平滑化フィルタ
ソースコード
void ImageProcessor::equalizationFilter(Mat inImg, int32_t height, int32_t width, int32_t filterCoeff, Mat outImg)
{
int32_t filterSize = (2 * filterCoeff + 1) * (2 * filterCoeff + 1);
int32_t redSum, greenSum, blueSum;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
redSum = greenSum = blueSum = 0;
// fiterCoeff x filterCoeffのフィルタ処理
for (int32_t yy = -filterCoeff; yy <= filterCoeff; yy++) {
for (int32_t xx = -filterCoeff; xx <= filterCoeff; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy, 0, height - 1);
int32_t replicateX = std::clamp(x + xx, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値の総和
redSum += pix[RED];
greenSum += pix[GREEN];
blueSum += pix[BLUE];
}
}
// 画素値の平均化
uint8_t red = static_cast<uint8_t>(std::clamp(redSum / filterSize, 0, 255));
uint8_t green = static_cast<uint8_t>(std::clamp(greenSum / filterSize, 0, 255));
uint8_t blue = static_cast<uint8_t>(std::clamp(blueSum / filterSize, 0, 255));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue, green, red);
}
}
}
ソースコードの説明(ポイントだけ)
・今回はフィルタサイズを任意にユーザー側で変えられるようにしています
・フィルタ係数は全て1になるため、フィルタを使用せずに純粋に画素値を総和してフィルタサイズで割っています
・範囲外対策としてclamp()関数でガードしています
結果
加重平均フィルタ
効果:元の色(中心画素)をできるだけ残して平滑化を行う
フィルタ:以下画像
ソースコード
void ImageProcessor::weightedAverageFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter[3][3] = {
{1, 2, 1},
{2, 8, 2},
{1, 2, 1}
}; // 加重平均フィルタ
int32_t filterSum = std::reduce(&filter[0][0], &filter[0][0] + 3 * 3);
int32_t redSum, greenSum, blueSum;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
redSum = greenSum = blueSum = 0;
// 3x3のフィルタ処理(-1から1までの範囲)
for (int32_t yy = -1; yy <= 1; yy++) {
for (int32_t xx = -1; xx <= 1; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy, 0, height - 1);
int32_t replicateX = std::clamp(x + xx, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値とフィルタの積和
redSum += filter[yy + 1][xx + 1] * pix[RED];
greenSum += filter[yy + 1][xx + 1] * pix[GREEN];
blueSum += filter[yy + 1][xx + 1] * pix[BLUE];
}
}
// 画素値の正規化、0~255に収める
uint8_t red = static_cast<uint8_t>(std::clamp(redSum / filterSum, 0, 255));
uint8_t green = static_cast<uint8_t>(std::clamp(greenSum / filterSum, 0, 255));
uint8_t blue = static_cast<uint8_t>(std::clamp(blueSum / filterSum, 0, 255));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue, green, red);
}
}
}
ソースコードの説明(ポイントだけ)
・加重平均フィルタ(2次元配列)を使用し、積和演算を行います
結果
先鋭化フィルタ
効果:画像の境界部分(色の変化が激しい場所)を鮮明にする
そのためノイズがある場合はそこも強調されてしまう
フィルタ:以下画像
ソースコード
void ImageProcessor::sharpeningFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter[3][3] = {
{0, -1, 0 },
{-1, 5, -1},
{0, -1, 0 }
}; // 先鋭化フィルタ(4近傍)
int32_t redSum, greenSum, blueSum;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
redSum = greenSum = blueSum = 0;
// 3x3のフィルタ処理(-1から1までの範囲)
for (int32_t yy = -1; yy <= 1; yy++) {
for (int32_t xx = -1; xx <= 1; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy, 0, height - 1);
int32_t replicateX = std::clamp(x + xx, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値とフィルタの積和
redSum += filter[yy + 1][xx + 1] * pix[RED];
greenSum += filter[yy + 1][xx + 1] * pix[GREEN];
blueSum += filter[yy + 1][xx + 1] * pix[BLUE];
}
}
// 画像の輝度値を0~255に収める
uint8_t red = static_cast<uint8_t>(std::clamp(redSum, 0, 255));
uint8_t green = static_cast<uint8_t>(std::clamp(greenSum, 0, 255));
uint8_t blue = static_cast<uint8_t>(std::clamp(blueSum, 0, 255));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue, green, red);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが変わっただけなため特になし
結果
エッジ検出フィルタ
ここからはフィルタを使用して画像のエッジ検出をやっていきます。
まず、簡単に画像のエッジについて説明します。
エッジとは一言で言うと、物体の輪郭部分(色の変化が激しい)のことです。
この写真の例だと、オレンジ色で線を書いている部分が帽子のエッジになります。
他にも目のエッジや顔のエッジ、体のエッジなど全ての物体にエッジは存在しています。
例えば漫画とかも全てエッジで表現していますよね。
特に色もついていないですが、線だけで物体を区切って表現していますよね。
でもそれだけで何かを瞬時に判断できるためエッジはすごく重要な情報になります。
では具体的にどうやってエッジを検出するかですが、微分をします。
微分と言いつつ、単なる差分のことです。
上記フィルタは中心画素に対して縦方向と横方向で差分をとっています。
なぜ縦/横の2つのフィルタを使用するのかと言うと、エッジには向きがあります。
※もちろん縦方向のみ、横方向のみで使用することも可能です
ソースコード
void ImageProcessor::edgeDetectionFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter1[3][3] = {
{0, 0, 0 },
{1, 0, -1},
{0, 0, 0 }
}; // 横フィルタ
int32_t filter2[3][3] = {
{0, 1, 0},
{0, 0, 0},
{0, -1, 0}
}; // 縦フィルタ
int32_t rrx, ggx, bbx;
int32_t rry, ggy, bby;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
rrx = ggx = bbx = 0;
rry = ggy = bby = 0;
// 3x3のフィルタ処理
for (int32_t yy = 0; yy < FILTER_SIZE; yy++) {
for (int32_t xx = 0; xx < FILTER_SIZE; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy - 1, 0, height - 1);
int32_t replicateX = std::clamp(x + xx - 1, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値と横フィルタの積和
rrx += pix[RED] * filter1[yy][xx];
ggx += pix[GREEN] * filter1[yy][xx];
bbx += pix[BLUE] * filter1[yy][xx];
// 画素値と縦フィルタの積和
rry += pix[RED] * filter2[yy][xx];
ggy += pix[GREEN] * filter2[yy][xx];
bby += pix[BLUE] * filter2[yy][xx];
}
}
// 画素値の平方根
double red = sqrt(static_cast<double>(rrx * rrx + rry * rry));
double green = sqrt(static_cast<double>(ggx * ggx + ggy * ggy));
double blue = sqrt(static_cast<double>(bbx * bbx + bby * bby));
// 画像の輝度値を0~255に収める
uint8_t red8 = static_cast<uint8_t>(std::clamp(red, 0., 255.));
uint8_t green8 = static_cast<uint8_t>(std::clamp(green, 0., 255.));
uint8_t blue8 = static_cast<uint8_t>(std::clamp(blue, 0., 255.));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue8, green8, red8);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが2種類になったが、今まで通りそれぞれの積和を求める
・なぜ平方根をしているかですが、縦方向と横方向の変化量を組み合わせて全体の変化の
大きさを求めているからです。ここではピタゴラスの定理を使用しています。
以下の様に考えればわかりやすいかと思います
結果
ソーベルフィルタ
効果:画質のノイズを抑えつつ、エッジの検出を行う(水平、垂直方向)
フィルタ:以下画像
ソースコード
void ImageProcessor::sobelFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter1[3][3] = {
{1, 2, 1 },
{0, 0, 0 },
{-1, -2, -1}
}; // 横方向のSobelフィルタ
int32_t filter2[3][3] = {
{1, 0, -1},
{2, 0, -2},
{1, 0, -1}
}; // 縦方向のSobelフィルタ
int32_t rrx, ggx, bbx;
int32_t rry, ggy, bby;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
rrx = ggx = bbx = 0;
rry = ggy = bby = 0;
// 3x3のフィルタ処理
for (int32_t yy = 0; yy < FILTER_SIZE; yy++) {
for (int32_t xx = 0; xx < FILTER_SIZE; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy - 1, 0, height - 1);
int32_t replicateX = std::clamp(x + xx - 1, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値と横フィルタの積和
rrx += pix[RED] * filter1[yy][xx];
ggx += pix[GREEN] * filter1[yy][xx];
bbx += pix[BLUE] * filter1[yy][xx];
// 画素値と縦フィルタの積和
rry += pix[RED] * filter2[yy][xx];
ggy += pix[GREEN] * filter2[yy][xx];
bby += pix[BLUE] * filter2[yy][xx];
}
}
// 画素値の平方根
double red = sqrt(static_cast<double>(rrx * rrx + rry * rry));
double green = sqrt(static_cast<double>(ggx * ggx + ggy * ggy));
double blue = sqrt(static_cast<double>(bbx * bbx + bby * bby));
// 画像の輝度値を0~255に収める
uint8_t red8 = static_cast<uint8_t>(std::clamp(red, 0., 255.));
uint8_t green8 = static_cast<uint8_t>(std::clamp(green, 0., 255.));
uint8_t blue8 = static_cast<uint8_t>(std::clamp(blue, 0., 255.));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue8, green8, red8);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが変わっただけなため特になし
結果
プレウィットフィルタ
効果:ソーベルフィルタをほぼ同じだが、各方向のエッジを均等に検出する
フィルタ:以下画像
ソースコード
void ImageProcessor::prewittFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter1[3][3] = {
{1, 1, 1 },
{0, 0, 0 },
{-1, -1, -1}
}; // 横方向のPrewittフィルタ
int32_t filter2[3][3] = {
{1, 0, -1},
{1, 0, -1},
{1, 0, -1}
}; // 縦方向のPrewittフィルタ
int32_t rrx, ggx, bbx;
int32_t rry, ggy, bby;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
rrx = ggx = bbx = 0;
rry = ggy = bby = 0;
// 3x3のフィルタ処理
for (int32_t yy = 0; yy < FILTER_SIZE; yy++) {
for (int32_t xx = 0; xx < FILTER_SIZE; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy - 1, 0, height - 1);
int32_t replicateX = std::clamp(x + xx - 1, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値と横フィルタの積和
rrx += pix[RED] * filter1[yy][xx];
ggx += pix[GREEN] * filter1[yy][xx];
bbx += pix[BLUE] * filter1[yy][xx];
// 画素値と縦フィルタの積和
rry += pix[RED] * filter2[yy][xx];
ggy += pix[GREEN] * filter2[yy][xx];
bby += pix[BLUE] * filter2[yy][xx];
}
}
// 画素値の平方根
double red = sqrt(static_cast<double>(rrx * rrx + rry * rry));
double green = sqrt(static_cast<double>(ggx * ggx + ggy * ggy));
double blue = sqrt(static_cast<double>(bbx * bbx + bby * bby));
// 画像の輝度値を0~255に収める
uint8_t red8 = static_cast<uint8_t>(std::clamp(red, 0., 255.));
uint8_t green8 = static_cast<uint8_t>(std::clamp(green, 0., 255.));
uint8_t blue8 = static_cast<uint8_t>(std::clamp(blue, 0., 255.));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue8, green8, red8);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが変わっただけなため特になし
結果
ロバーツフィルタ
効果:エッジの検出を行う(特に斜め方向の検出)
フィルタ:以下画像
ソースコード
void ImageProcessor::robertsFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter1[3][3] = {
{0, 0, 0 },
{0, 1, 0 },
{0, 0, -1}
}; // 横方向のRobertsフィルタ
int32_t filter2[3][3] = {
{0, 0, 0},
{0, 0, 1},
{0, -1, 0}
}; // 縦方向のRobertsフィルタ
int32_t rrx, ggx, bbx;
int32_t rry, ggy, bby;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
rrx = ggx = bbx = 0;
rry = ggy = bby = 0;
// 3x3のフィルタ処理
for (int32_t yy = 0; yy < FILTER_SIZE; yy++) {
for (int32_t xx = 0; xx < FILTER_SIZE; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy - 1, 0, height - 1);
int32_t replicateX = std::clamp(x + xx - 1, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値と横フィルタの積和
rrx += pix[RED] * filter1[yy][xx];
ggx += pix[GREEN] * filter1[yy][xx];
bbx += pix[BLUE] * filter1[yy][xx];
// 画素値と縦フィルタの積和
rry += pix[RED] * filter2[yy][xx];
ggy += pix[GREEN] * filter2[yy][xx];
bby += pix[BLUE] * filter2[yy][xx];
}
}
// 画素値の平方根
double red = sqrt(static_cast<double>(rrx * rrx + rry * rry));
double green = sqrt(static_cast<double>(ggx * ggx + ggy * ggy));
double blue = sqrt(static_cast<double>(bbx * bbx + bby * bby));
// 画像の輝度値を0~255に収める
uint8_t red8 = static_cast<uint8_t>(std::clamp(red, 0., 255.));
uint8_t green8 = static_cast<uint8_t>(std::clamp(green, 0., 255.));
uint8_t blue8 = static_cast<uint8_t>(std::clamp(blue, 0., 255.));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue8, green8, red8);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが変わっただけなため特になし
結果
エンボスフィルタ(おまけ)
効果:エッジを浮き彫りにしたような画像を作る
ソースコード
void ImageProcessor::embossingFilter(Mat inImg, int32_t height, int32_t width, Mat outImg)
{
int32_t filter[3][3] = {
{0, 0, 0},
{-3, 0, 3},
{0, 0, 0}
}; // エンボスフィルタ (数値を大きくするとエンボスの強さが増す)
int32_t redSum, greenSum, blueSum;
for (int32_t y = 0; y < height; y++) {
for (int32_t x = 0; x < width; x++) {
redSum = greenSum = blueSum = 0;
// 3x3のフィルタ処理(-1から1までの範囲)
for (int32_t yy = -1; yy <= 1; yy++) {
for (int32_t xx = -1; xx <= 1; xx++) {
// 画像の端の処理 (リピート)
int32_t replicateY = std::clamp(y + yy, 0, height - 1);
int32_t replicateX = std::clamp(x + xx, 0, width - 1);
// 画素取得
Vec3b &pix = inImg.at<Vec3b>(replicateY, replicateX);
// 画素値とフィルタの積和
redSum += filter[yy + 1][xx + 1] * pix[RED];
greenSum += filter[yy + 1][xx + 1] * pix[GREEN];
blueSum += filter[yy + 1][xx + 1] * pix[BLUE];
}
}
// 画像の輝度値を0~255に収める
// 128は中間の明るさを示し、6はフィルタの係数の絶対値、それで割ることで画像のコントラストを調整
uint8_t red = static_cast<uint8_t>(std::clamp(redSum / 6 + 128, 0, 255));
uint8_t green = static_cast<uint8_t>(std::clamp(greenSum / 6 + 128, 0, 255));
uint8_t blue = static_cast<uint8_t>(std::clamp(blueSum / 6 + 128, 0, 255));
// 画素の書き込み
outImg.at<Vec3b>(y, x) = Vec3b(blue, green, red);
}
}
}
ソースコードの説明(ポイントだけ)
・フィルタが変わっただけなため特になし
結果
最後に
以上でフィルタ処理編を終わります。
また時間があれば勉強して次もなにか書きますね。