본문 바로가기
Quality control (Univ. Study)/Digital Image Processing

DIP 실습 - Median / Bilateral / Canny edge detection

by 생각하는 이상훈 2024. 4. 12.
728x90

Median filter

과제: salt_pepper2.png에 대해서 3x3, 5x5 Median 필터를 적용해보고 결과를 분석할 것

#include <opencv2/opencv.hpp>
#include <vector>
#include <algorithm>

using namespace cv;
using namespace std;

// 중간값 필터 함수
Mat MedianFilter(const Mat& src, int kernelSize) {
    Mat dst = src.clone();  // 출력 이미지 초기화
    int pad = kernelSize / 2;  // 패딩 크기 계산

    // 이미지의 각 픽셀에 대해 중간값 필터 적용
    for (int i = pad; i < src.rows - pad; i++) {
        for (int j = pad; j < src.cols - pad; j++) {
            vector<uchar> neighbors;  // 주변 픽셀 값을 저장할 벡터

            // 커널 내의 모든 픽셀 값을 neighbors 벡터에 추가
            for (int k = -pad; k <= pad; k++) {
                for (int l = -pad; l <= pad; l++) {
                    neighbors.push_back(src.at<uchar>(i + k, j + l));
                }
            }

            // neighbors 벡터를 정렬하여 중간값을 찾음
            sort(neighbors.begin(), neighbors.end());
            dst.at<uchar>(i, j) = neighbors[neighbors.size() / 2];  // 중간값을 출력 이미지에 설정
        }
    }

    return dst;  // 필터링된 이미지 반환
}

int main() {
    // 이미지 불러오기
    Mat srcImg = imread("salt_pepper2.png", 0);
    if (srcImg.empty()) {
        cout << "Image not found!" << endl;
        return -1;
    }

    // 3x3 중간값 필터 적용
    Mat median3x3 = MedianFilter(srcImg, 3);

    // 5x5 중간값 필터 적용
    Mat median5x5 = MedianFilter(srcImg, 5);

    // 각 이미지에 텍스트 추가
    putText(srcImg, "Original Image", Point(20, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2);
    putText(median3x3, "3x3 Median", Point(20, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2);
    putText(median5x5, "5x5 Median", Point(20, 30), FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 255, 255), 2);

    // 이미지 연결
    Mat combined;
    hconcat(srcImg, median3x3, combined);
    hconcat(combined, median5x5, combined);

    // 연결된 이미지 표시
    imshow("Combined Image", combined);
    waitKey(0);

    return 0;
}

중간값 필터는 주변 픽셀 값들을 수집하여 그 값들의 중간값을 픽셀의 새로운 값으로 설정하는 방식으로 작동한다. 이 방법은 salt and pepper 노이즈와 같이 극단적인 값들이 포함된 노이즈를 제거하는 데 매우 효과적이다.
3x3 중간값 필터를 적용한 결과, 노이즈는 상당 부분 제거되었지만, 일부 노이즈가 여전히 남아 있음을 관찰할 수 있었다. 반면, 5x5 중간값 필터를 사용했을 때는 노이즈가 더욱 효과적으로 제거되었지만, 이는 이미지의 세부 사항이 더 많이 흐려지는 결과를 초래했다. 이는 중간값 필터의 커널 크기가 클수록 더 많은 주변 픽셀을 고려하기 때문에 발생하는 현상이다. 따라서 필터의 크기를 선택할 때는 노이즈 제거 효과와 이미지 세부 정보 보존 사이의 trade-off를 고려해서 적절하게 선택해야 한다.
추가로 가우시안 필터는 솔트 앤 페퍼 노이즈와 같이 극단적인 값을 가진 노이즈에 대해서는 중간값 필터만큼 효과적이지 않다. 가우시안 필터는 거리에 따라 다른 가중치를 적용하기 때문에 극단적인 값을 어느 정도 희석시키기는 하지만 완전히 제거하지는 못하기 때문이다.
이 실험을 통해 중간값 필터가 솔트 앤 페퍼 노이즈와 같은 특정 유형의 노이즈에 대해 매우 효과적인 노이즈 제거 방법임을 확인할 수 있었다. 또한, 필터의 크기가 더 클수록 노이즈 제거는 개선되지만, 이미지 세부 정보가 더 손실될 수 있음을 확인했다.


Bilateral filter

과제: rock.png에 대해서 Bilateral 필터를 적용해볼 것 (아래 table을 참고하여 기존 gaussian 필터와의 차이를 분석해볼 것)

#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

#define USE_OPENCV false;

using namespace cv;
using namespace std;

void myBilateral(const Mat& src_img, Mat& dst_img, int diameter, double sig_r, double sig_s);

double gaussian(float x, double sigma) {
	return exp(-(pow(x, 2)) / (2 * pow(sigma, 2))) / (2 * CV_PI * pow(sigma, 2));
}

float distance(int x, int y, int i, int j) {
	return float(sqrt(pow(x - i, 2) + pow(y - j, 2)));
}

void bilateral(const Mat& src_img, Mat& dst_img, int c, int r, int diameter, double sig_r, double sig_s) {
	int radius = diameter / 2;

	double gr, gs, wei;
	double tmp = 0.0;
	double sum = 0.0;

	// bilateral filter calculation
	for (int kc = -radius; kc <= radius; kc++) {
		for (int kr = -radius; kr <= radius; kr++) {
			// range calc
			double intensity_diff = src_img.at<uchar>(c + kc, r + kr) - src_img.at<uchar>(c, r);
			gr = gaussian((float)src_img.at<uchar>(c + kc, r + kr) - (float)src_img.at<uchar>(c, r), sig_r);

			// spatial calc
			gs = gaussian(distance(c, r, c + kc, r + kr), sig_s);

			wei = gr * gs;
			tmp += src_img.at<uchar>(c + kc, r + kr) * wei;
			sum += wei;
		}
	}

	dst_img.at<double>(c, r) = tmp / sum; // normalization
}


void myBilateral(const Mat& src_img, Mat& dst_img, int diameter, double sig_r, double sig_s) {
	dst_img = Mat::zeros(src_img.size(), CV_8UC1);
	Mat guide_img = Mat::zeros(src_img.size(), CV_64F);
	int wh = src_img.cols; int hg = src_img.rows;
	int radius = diameter / 2;

	// 픽셀 연산 (가장자리 제외)
	for (int c = radius + 1; c < hg - radius; c++) {
		for (int r = radius + 1; r < wh - radius; r++) {
			bilateral(src_img, guide_img, c, r, diameter, sig_r, sig_s);
			// 화소별 Bilateral 계산 수행
		}
	}
	guide_img.convertTo(dst_img, CV_8UC1); // Mat 타입 변환
}



int myKernelConv7x7(uchar* arr, int kernel[][7], int x, int y, int width, int height) {
	int sum = 0;
	int sumKernel = 0;

	// 커널의 크기를 7로 변경하였습니다.
	for (int j = -3; j <= 3; j++) {
		for (int i = -3; i <= 3; i++) {
			if ((y + j) >= 0 && (y + j) < height && (x + i) >= 0 && (x + i) < width) {
				// 7x7 커널을 사용하기 때문에 offset도 조정해야 합니다.
				sum += arr[(y + j) * width + (x + i)] * kernel[j + 3][i + 3];
				sumKernel += kernel[j + 3][i + 3];
			}
		}
	}

	if (sumKernel != 0) { return sum / sumKernel; } // 합이 1로 정규화되도록 해 영상의 밝기변화 방지
	else { return sum; }
}


void hw2() {
	// 이미지 로드
	Mat src_img = imread("rock.png", IMREAD_GRAYSCALE);
	if (src_img.empty()) {
		cout << "No image data \n";
		return; // 이미지 로드에 실패한 경우 함수를 종료합니다.
	}

	int width = src_img.cols;
	int height = src_img.rows;
	int kernel[7][7] = {
			{1, 1, 2, 2, 2, 1, 1},
			{1, 2, 2, 4, 2, 2, 1},
			{2, 2, 4, 8, 4, 2, 2},
			{2, 4, 8, 16, 8, 4, 2},
			{2, 2, 4, 8, 4, 2, 2},
			{1, 2, 2, 4, 2, 2, 1},
			{1, 1, 2, 2, 2, 1, 1}
	};

	Mat gaus_img(src_img.size(), CV_8UC1);
	uchar* srcData = src_img.data;
	uchar* gaus_Data = gaus_img.data;

	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			gaus_Data[y * width + x] = myKernelConv7x7(srcData, kernel, x, y, width, height);
		}
	}

	int inf = 100000000000000.0;

	// Bilateral 필터링 수행
	Mat bil_r25, bil_r_inf;
	myBilateral(src_img, bil_r25, 5, 25.0, 50.0);
	myBilateral(src_img, bil_r_inf, 5, inf, 50.0); // 이론상 무한대에 해당하는 매우 큰 sigma_r 값 사용

	// 텍스트를 추가할 위치와 스타일 설정
	int fontFace = FONT_HERSHEY_SIMPLEX;
	double fontScale = 1;
	Scalar color(255, 255, 255); // 흰색 텍스트
	int thickness = 2;

	// 각 이미지에 텍스트 추가
	putText(src_img, "Source", Point(10, src_img.rows - 20), fontFace, fontScale, color, thickness);
	putText(bil_r25, "Bilateral r=25", Point(10, bil_r25.rows - 20), fontFace, fontScale, color, thickness);
	putText(bil_r_inf, "Bilateral r=inf", Point(10, bil_r_inf.rows - 20), fontFace, fontScale, color, thickness);
	putText(gaus_img, "Gaussian", Point(10, gaus_img.rows - 20), fontFace, fontScale, color, thickness);

	// 이미지를 수평으로 연결
	vector<Mat> array_to_hconcat;
	array_to_hconcat.push_back(src_img);
	array_to_hconcat.push_back(bil_r25);
	array_to_hconcat.push_back(bil_r_inf);
	array_to_hconcat.push_back(gaus_img);
	Mat result_img;
	hconcat(array_to_hconcat, result_img); // 이미지를 하나로 연결

	// 결과 출력
	imshow("Filter Comparison", result_img);
	waitKey(0);
	destroyAllWindows(); // 모든 윈도우 닫기
}



//----------------------------------------------------------------------------------------------------------------------------------------------------------------
int main() {

	hw2();

	return 0;
}

Bilateral 필터는 이미지의 엣지를 보존하면서 노이즈를 제거하는 데 효과적인 비선형 필터다. 이 필터는 각 픽셀에 대해 주변 픽셀의 가중 평균을 계산하지만, 가중치는 두 가지 요소, 즉 공간적 근접성과 밝기의 유사성을 기반으로 결정된다. 즉, Bilateral 필터는 픽셀 간의 거리뿐만 아니라 그들의 강도 차이도 고려하여 smoothing을 수행한다. 

반면에, Gaussian 필터는 공간적 근접성만을 고려하는 선형 필터다. 픽셀 주변의 값들에 Gaussian 함수를 적용한 가중치를 곱하고 이를 모두 합하여 평균을 낸 것이 해당 픽셀의 새로운 값이 된다. Gaussian 필터는 엣지를 보존하지 않기 때문에 이미지가 흐릿해질 수 있다. `myBilateral` 함수는 `bilateral` 함수를 호출하여 각 픽셀에 대한 Bilateral 필터링을 수행한다. 이 과정에서 각 픽셀과 그 주변 픽셀 간의 강도 차이(`intensity_diff`)와 거리(`distance`)를 이용해 가중치를 계산한다. 이 필터는 `sig_r`과 `sig_s` 두 개의 시그마 값을 사용한다. 

기존의 `myKernelConv3x3` 함수를 더 변형하여 극적인 필터링 효과를 확인할 수 있는 `myKernelConv7x7` 함수를 구현했다. 이렇게 구현된 Gaussian 필터는 주어진 커널을 사용하여 이미지에 컨볼루션을 적용한다. 이 커널은 Gaussian 분포를 근사화한 것으로, 주변 픽셀에 대한 가중치를 제공한다. 

Bilateral 필터에서 `sig_r` 값을 무한대로 설정하면 모든 픽셀 강도 차이에 대한 가중치가 1이 되어 Gaussian 필터와 유사한 효과를 나타내게 된다. 즉, 이 경우 Bilateral 필터는 공간적 근접성만을 고려하는 필터로 변모하게 되며, 이는 Gaussian 필터의 동작 방식과 일치한 것을 확인할 수 있었다. 

결론적으로, Bilateral 필터는 Gaussian 필터보다 더 복잡한 구조를 가지고 있으며, 이를 통해 엣지를 유지하면서 이미지의 노이즈를 효과적으로 제거할 수 있다. 파라미터의 조정을 통해, Bilateral 필터는 Gaussian 필터처럼 동작할 수도 있다. 이는 Bilateral 필터가 보다 유연하게 이미지 처리 작업에 적용될 수 있음을 의미한다.


Canny edge detection

과제: OpenCV Canny edge detection 함수의 파라미터를 조절해 여러 결과를 도출하고 파라미터에 따라서 처리시간이 달라지는 이유를 정확히 서술할 것

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <chrono>

using namespace cv;
using namespace std;
using namespace std::chrono;

int main() {
    Mat img = imread("gear.jpg", 0); // gear.jpg를 그레이스케일로 불러옴

    if (img.empty()) {
        cout << "Image not found!" << endl;
        return -1;
    }

    // 원본 이미지 크기를 절반으로 줄임
    resize(img, img, Size(img.cols / 2, img.rows / 2));

    // 개별 이미지 변수 선언
    Mat imgCanny1, imgCanny2, imgCanny3, imgCanny4, imgCanny5, imgCanny6, imgCanny7, imgCanny8;
    Mat imgCanny9, imgCanny10, imgCanny11, imgCanny12, imgCanny13, imgCanny14, imgCanny15, imgCanny16;

    double lowThresholds[2] = { 10, 100 };
    double highThresholds[2] = { 150, 300 };
    int apertureSizes[2] = { 3, 7 };
    bool L2gradients[2] = { false, true };

    int count = 1;
    int caseNumber = 1;

    // 파라미터 조합에 대한 모든 경우의 수 탐색
    for (double lowThreshold : lowThresholds) {
        for (double highThreshold : highThresholds) {
            for (int apertureSize : apertureSizes) {
                for (bool L2gradient : L2gradients) {
                    Mat imgCanny;

                    auto start = high_resolution_clock::now(); // 각 처리 시작 시간 측정

                    Canny(img, imgCanny, lowThreshold, highThreshold, apertureSize, L2gradient);

                    auto stop = high_resolution_clock::now(); // 처리 종료 시간 측정
                    auto duration = duration_cast<nanoseconds>(stop - start);

                    // 파라미터 정보와 처리 시간 출력
                    cout << "Case " << caseNumber << " (Low: " << lowThreshold
                        << ", High: " << highThreshold << ", Aperture: " << apertureSize
                        << ", L2: " << L2gradient << "): "
                        << duration.count() << " ns" << endl;

                    caseNumber++;

                    // 파라미터 정보를 이미지에 텍스트로 추가
                    stringstream ss;
                    ss << "Low: " << lowThreshold << " High: " << highThreshold
                        << " Aperture: " << apertureSize << " L2: " << (L2gradient ? "True" : "False");
                    putText(imgCanny, ss.str(), Point(10, 15), FONT_HERSHEY_SIMPLEX, 0.4, Scalar(255, 255, 255), 2);



                    // 개별 변수에 이미지 할당
                    switch (count) {
                    case 1: imgCanny1 = imgCanny; break;
                    case 2: imgCanny2 = imgCanny; break;
                    case 3: imgCanny3 = imgCanny; break;
                    case 4: imgCanny4 = imgCanny; break;
                    case 5: imgCanny5 = imgCanny; break;
                    case 6: imgCanny6 = imgCanny; break;
                    case 7: imgCanny7 = imgCanny; break;
                    case 8: imgCanny8 = imgCanny; break;
                    case 9: imgCanny9 = imgCanny; break;
                    case 10: imgCanny10 = imgCanny; break;
                    case 11: imgCanny11 = imgCanny; break;
                    case 12: imgCanny12 = imgCanny; break;
                    case 13: imgCanny13 = imgCanny; break;
                    case 14: imgCanny14 = imgCanny; break;
                    case 15: imgCanny15 = imgCanny; break;
                    case 16: imgCanny16 = imgCanny; break;
                    }
                    count++;
                }
            }
        }
    }

    // 가로로 이미지 이어붙이기
    Mat horizontal1, horizontal2, horizontal3, horizontal4;
    hconcat(vector<Mat>{imgCanny1, imgCanny2, imgCanny3, imgCanny4}, horizontal1);
    hconcat(vector<Mat>{imgCanny5, imgCanny6, imgCanny7, imgCanny8}, horizontal2);
    hconcat(vector<Mat>{imgCanny9, imgCanny10, imgCanny11, imgCanny12}, horizontal3);
    hconcat(vector<Mat>{imgCanny13, imgCanny14, imgCanny15, imgCanny16}, horizontal4);

    // 세로로 이미지 이어붙이기
    Mat vertical;
    vconcat(vector<Mat>{horizontal1, horizontal2, horizontal3, horizontal4}, vertical);

    // 결과 보여주기
    imshow("Canny Edge Detection Results", vertical);
    waitKey(0);
    destroyAllWindows();

    return 0;
}

OpenCV에서 사용되는 Canny 엣지 감지 기법은 여러 파라미터를 통해 조절된다. 이 파라미터들은 다음과 같다.


- `T_lower`는 낮은 임계값을 의미하며, 지정된 값보다 낮은 그래디언트를 가진 픽셀은 엣지로 간주되지 않는다.
- `T_upper`는 높은 임계값을 나타내며, 이 값보다 높은 그래디언트를 보유한 픽셀은 엣지로 판단된다.
- `aperture_size`는 Sobel 연산을 위한 커널의 크기를 정하는데 사용되며, 기본값은 3이다.
- `L2Gradient`는 그래디언트의 크기를 계산하는 방식을 결정하며, 기본적으로는 `False`로 설정되어 L1 노름을 사용한다.


그래디언트 크기 계산은 `L2Gradient`의 값에 따라 달라진다. `True`일 경우, L2 norm을 사용하여 계산하며, `False`일 경우 L1 norm을 활용한다.
L2 norm을 이용하면 제곱근 연산이 들어가서 계산과정이 길어져서 실행시간이 더 길어질 것으로 예상을 하였으나 위의 결과에서 볼 수 있듯이 오히려 실행시간이 더 짧아졌다. 이는 아래와 같은 이유로 추정해볼 수 있었다.
 최근의 하드웨어들은 제곱근 계산과 같은 부동소수점 연산이 매우 빠르게 실행될 수 있다. 따라서, L2 gradient의 추가 계산 부담이 최적화를 통해 상쇄되었을 수 있다. 또한 OpenCV는 내부적으로 매우 최적화되어 있으며, 특정 조건에서는 L2 gradient 계산이 더 효율적으로 구현될 수 있다. 마지막으로 처리하는 이미지의 특징 등이 실행 시간에 영향을 미칠 수 있다. 다양한 연구에서 딥러닝 모델을 설계할 때 L2 정규화를 이용하는 것을 보면 L2정규화가 실행시간 단축에 도움된다는 방증이 될 수도 있다고 생각한다.

정리해보자면 아래와 같다.


`T_lower`: 높을수록 더 많은 픽셀이 엣지에서 배제되어 처리 시간이 단축된다.
`T_upper`: 높을수록 더 적은 수의 엣지가 탐지되어, 이로 인해 처리 시간이 감소한다.
`aperture_size`: 클수록 Sobel 연산이 더 많은 픽셀을 처리해야 하므로 계산 복잡도가 증가하고, 결과적으로 처리 시간이 길어진다.
`L2Gradient`: 제곱근 계산이 추가되었으나 위에서 살펴본 다양한 이유로 실행시간이 오히려 단축된다.


728x90