0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[画像処理] アルゴリズムから画像処理を知ろう -フィルタ処理編-

Posted at

はじめに

前回の画像処理の続編になります。
今回はフィルタ処理を説明していきます。前回より面白いんじゃないかと。

動作環境

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はフィルタ$$

フィルタのことをカーネル、オペレータとも言う

演算イメージ

フィルタ処理の演算.png

上記の演算を全ての画素に対して行い、新しい画素値を求めます。
要は畳み込み演算をしています。
イメージでは3×3のフィルタとしていますが、5×5や9×9などもあります。
その場合の計算方法は全く同じです。ただ単に範囲が広くなるだけです。

演算はシンプルでわかりやすいのですが、1点注意しないといけないことがあります。
境界(端っこの画素)の扱いです。

境界処理.png

この場合どう処理するかですが、いくつかの手段があります。

境界処理の手段
方法 内容 メリット デメリット
リピート 端の画素を繰り返し使用 計算が簡単 若干ボヤけるかも
ミラー 端の画素を反射して使用 境界が自然に見える 計算が増える
ゼロパディング 境界を0にする 計算が簡単 端が黒くなるかも
なにもしない 端は処理しない 計算が簡単 斜線が出るかも
非対称フィルタ(L字/T字) 有効な画素のみ使用する形に変える 境界が自然に見える 計算が複雑

手軽に実装したいため、今回はリピートにします。

画像処理

先ほどの説明でフィルタ処理のイメージはできたかと思いますので、実際に処理していきましょう。

平滑化フィルタ

効果:ピンぼけしたような画像になる
フィルタ:以下画像
平滑化フィルタ.png

ソースコード
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()関数でガードしています

結果

元画像
元画像.png

処理後 ※3×3は効果がわかりにくいため5×5にしています
平滑化フィルタ.png

加重平均フィルタ

効果:元の色(中心画素)をできるだけ残して平滑化を行う
フィルタ:以下画像
加重平均フィルタイメージ.png

ソースコード
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次元配列)を使用し、積和演算を行います

結果

元画像
元画像.png

処理後
加重平均フィルタ.png

先鋭化フィルタ

効果:画像の境界部分(色の変化が激しい場所)を鮮明にする
   そのためノイズがある場合はそこも強調されてしまう
フィルタ:以下画像
先鋭化フィルタイメージ.png

ソースコード
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);
        }
    }
}
ソースコードの説明(ポイントだけ)

・フィルタが変わっただけなため特になし

結果

元画像
元画像.png

処理後(ザラザラとした画像になる)
先鋭化フィルタ.png

エッジ検出フィルタ

ここからはフィルタを使用して画像のエッジ検出をやっていきます。

まず、簡単に画像のエッジについて説明します。
エッジとは一言で言うと、物体の輪郭部分(色の変化が激しい)のことです。

エッジのイメージ.png

この写真の例だと、オレンジ色で線を書いている部分が帽子のエッジになります。
他にも目のエッジや顔のエッジ、体のエッジなど全ての物体にエッジは存在しています。

例えば漫画とかも全てエッジで表現していますよね。
特に色もついていないですが、線だけで物体を区切って表現していますよね。
でもそれだけで何かを瞬時に判断できるためエッジはすごく重要な情報になります。

では具体的にどうやってエッジを検出するかですが、微分をします。
微分と言いつつ、単なる差分のことです。

フィルタ:縦方向のフィルタと横方向のフィルタを使用します
エッジ検出フィルタイメージ.png

上記フィルタは中心画素に対して縦方向と横方向で差分をとっています。
なぜ縦/横の2つのフィルタを使用するのかと言うと、エッジには向きがあります。

エッジ検出縦横イメージ.png

※もちろん縦方向のみ、横方向のみで使用することも可能です

ソースコード
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種類になったが、今まで通りそれぞれの積和を求める
・なぜ平方根をしているかですが、縦方向と横方向の変化量を組み合わせて全体の変化の
 大きさを求めているからです。ここではピタゴラスの定理を使用しています。
 以下の様に考えればわかりやすいかと思います

エッジ検出の平方根の説明.png

結果

元画像
元画像.png

処理後
エッジ検出.png

ソーベルフィルタ

効果:画質のノイズを抑えつつ、エッジの検出を行う(水平、垂直方向)
フィルタ:以下画像
ソーベルフィルタイメージ.png

ソースコード
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);
        }
    }
}
ソースコードの説明(ポイントだけ)

・フィルタが変わっただけなため特になし

結果

元画像
元画像.png

処理後
ソーベルフィルタ.png

プレウィットフィルタ

効果:ソーベルフィルタをほぼ同じだが、各方向のエッジを均等に検出する
フィルタ:以下画像
プレウィットフィルタイメージ.png

ソースコード
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);
        }
    }
}
ソースコードの説明(ポイントだけ)

・フィルタが変わっただけなため特になし

結果

元画像
元画像.png

処理後
プレウィットフィルタ.png

ロバーツフィルタ

効果:エッジの検出を行う(特に斜め方向の検出)
フィルタ:以下画像
ロバーツフィルタイメージ.png

ソースコード
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);
        }
    }
}
ソースコードの説明(ポイントだけ)

・フィルタが変わっただけなため特になし

結果

元画像
元画像.png

処理後
ロバーツフィルタ.png

エンボスフィルタ(おまけ)

効果:エッジを浮き彫りにしたような画像を作る

ソースコード
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);
        }
    }
}
ソースコードの説明(ポイントだけ)

・フィルタが変わっただけなため特になし

結果

元画像
元画像.png

処理後
エンボスフィルタ.png

最後に

以上でフィルタ処理編を終わります。

また時間があれば勉強して次もなにか書きますね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?