C++ Image Processing (Part 3): Image Smoothing

Image Smoothing

Image smoothing (blurring the edges of an image through integration processes)

The process of suppressing noise and improving image quality is called image smoothing or denoising. This can be performed in both the spatial domain and the frequency domain.

Spatial filtering modifies the image by replacing the value of each pixel with a function of that pixel and its neighborhood. If the operation performed on the image pixels is linear, the filter is called a linear spatial filter. Otherwise, it is called a nonlinear spatial filter.

C++ Image Processing (Part 3): Image Smoothing

Linear filtering involves finding suitable methods to modify the frequency content of the image. In the spatial domain, this is achieved through convolution filtering. In the frequency domain, it is achieved using multiplicative filters.

1. Median Filtering

A nonlinear smoothing filtering method used to eliminate the influence of outliers such as salt-and-pepper noise. Median filtering sets the gray value of each pixel to the median of all pixel values within a certain neighborhood window. It is based on order statistics theory, and by selecting suitable points to replace the contaminated points, it can effectively preserve sharp edges in the image. Median filtering is particularly effective for removing noise caused by outliers but performs poorly with Gaussian noise.

OpenCV Call

/*
            // Box Filter
            Mat boxFilteredImage;
            boxFilter(image, boxFilteredImage, CV_32F, Size(5, 5));
            display_MatInQT(ui->label_1, boxFilteredImage);

            // Mean Filter
            Mat meanFilteredImage;
            blur(image, meanFilteredImage, Size(5, 5));
            display_MatInQT(ui->label_1, meanFilteredImage);

            // Median Filter
            Mat medianFilteredImage;
            medianBlur(image, medianFilteredImage, 5);
            display_MatInQT(ui->label_1, medianFilteredImage);

            // Gaussian Filter
            Mat gaussianFilteredImage;
            GaussianBlur(image, gaussianFilteredImage, Size(5, 5), 0);
            display_MatInQT(ui->label_1, gaussianFilteredImage);

            // Bilateral Filter
            Mat filteredImage;     // Filter diameter 15, color space standard deviation 50, spatial standard deviation 50
            bilateralFilter(image, filteredImage, 15, 50, 50);
            display_MatInQT(ui->label_1, filteredImage);
   */

Adding Salt-and-Pepper Noise

#include <iostream>  
#include <opencv2/opencv.hpp>  

// Add white or black salt-and-pepper noise  
void addSaltAndPepperNoise(cv::Mat&amp; image, double probability) {
    for (int i = 0; i < image.rows; ++i) {
        for (int j = 0; j < image.cols; ++j) {
            // Generate a random number (value returned by rand()) and convert it to a double.
            // Then, we divide it by RAND_MAX (the maximum value that the rand() function can return) to get a random number between 0 and 1.
            // If this random number is less than the noise probability we set (a float between 0 and 1), we decide to add noise at this position.
            if ((static_cast<double>(rand()) / RAND_MAX) < probability) {
                // Randomly set to 0 (black) or 255 (white)   rand() % 2 returns a random number of 0 or 1, when it returns 0, we set the pixel to 0, otherwise set it to 255.
                image.at<uchar>(i, j) = (rand() % 2 == 0) ? 0 : 255;
            }
        }
    }
}

int main() {
    // Read grayscale image  
    cv::Mat grayImage = cv::imread("cat.jpg", cv::IMREAD_GRAYSCALE);
    if (grayImage.empty()) {
        std::cerr << "Could not open or find the image" << std::endl;
        return-1;
    }

    // Display original image  
    cv::imshow("Original Image", grayImage);

    // Add salt-and-pepper noise  
    double noiseProbability = 0.05; // Noise probability, 5% of pixels affected by noise, can be adjusted as needed  
    addSaltAndPepperNoise(grayImage, noiseProbability);

    // Display noisy image  
    cv::imshow("Noisy Image", grayImage);

    // Apply median filter for denoising  
    int filterSize = 3; // Filter size, usually 3, 5, 7, etc. odd numbers  
    cv::Mat filteredImage;
    cv::medianBlur(grayImage, filteredImage, filterSize);

    // Display denoised image  
    cv::imshow("Filtered Image", filteredImage);

    // Wait for a key press, then close the window  
    cv::waitKey(0);
    return0;
}

C++ Image Processing (Part 3): Image SmoothingC++ Image Processing (Part 3): Image SmoothingC++ Image Processing (Part 3): Image Smoothing

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

#include <random>  // Random number header file
usingnamespace cv;
usingnamespacestd;
void Salt(Mat image, int n); // n: number of noise points to add

// Get the median of pixels within the window  
int getMedian(const std::vector<int>&amp; window) {
    std::vector<int> sortedWindow(window);
    std::nth_element(sortedWindow.begin(), sortedWindow.begin() + window.size() / 2, sortedWindow.end());
    return sortedWindow[window.size() / 2];
}

// Apply median filter  
void medianFilter(cv::Mat&amp; image, int filterSize) {
    int halfSize = filterSize / 2;
    cv::Mat filteredImage = image.clone();

    for (int i = halfSize; i < image.rows - halfSize; ++i) {
        for (int j = halfSize; j < image.cols - halfSize; ++j) {
            std::vector<int> window;

            for (int m = -halfSize; m <= halfSize; ++m) {
                for (int n = -halfSize; n <= halfSize; ++n) {
                    window.push_back(image.at<uchar>(i + m, j + n));
                }
            }

            int median = getMedian(window);
            filteredImage.at<uchar>(i, j) = median;
        }
    }

    image = filteredImage;
}

int main() {
    // Read image  
    cv::Mat image = cv::imread("cat.jpg", cv::IMREAD_GRAYSCALE);  // LenaNoise.png
    if (image.empty()) {
        std::cerr << "Could not open or find the image" << std::endl;
        return-1;
    }

    cv::imshow("origin", image);
    Salt(image, 5000); // Add 5000 noise points
    imshow("SaltAndPepperNoise", image);  // Display noisy image;

    // Apply median filter  
    medianFilter(image, 3);

    // Display processed image    Save cv::imwrite("output.jpg", image);
    cv::imshow("output", image);
    cv::waitKey(0);

    return0;
}

// Add white salt-and-pepper noise to the image
void Salt(Mat image, int n)
{
    // Random number generator
    default_random_engine generater;
    uniform_int_distribution<int>randomRow(0, image.rows - 1);
    uniform_int_distribution<int>randomCol(0, image.cols - 1);

    int i, j;
    for (int k = 0; k < n; k++)
    {
        i = randomCol(generater);
        j = randomRow(generater);
        if (image.channels() == 1)
        {
            image.at<uchar>(j, i) = 255;
        }
        elseif (image.channels() == 3)
        {
            image.at<Vec3b>(j, i)[0] = 255;
            image.at<Vec3b>(j, i)[1] = 255;
            image.at<Vec3b>(j, i)[2] = 255;
        }
    }
}
C++ Image Processing (Part 3): Image Smoothing

Principle of Median Filtering

/*
The principle of median filtering is as follows:
        The basic principle of the median filter is to replace the value of a pixel with the median of the pixels in its neighborhood.
1. Select an odd-sized filter window (e.g., 3x3, 5x5, etc.), ensuring that there is a pixel at the center of the window.
2. Slide the filter window over the image, sorting the pixels within the window.
3. Assign the median of the sorted pixel array to the center pixel of the window.
4. Repeat steps 2 and 3 until all pixels in the image have been processed.
*/
#include <iostream>  
#include <vector>  
#include <algorithm> // To use std::nth_element  

// Fill boundary so that padding does not count in median calculation

// Get the median of pixels within the window  
int getMedian(const std::vector<int>&amp; window) {
    std::vector<int> sortedWindow(window);
    // std::nth_element function rearranges elements so that the n-th element (counting from 0) is in the position it should appear in a fully sorted array, with all elements less than it in front and all greater behind it.
    std::nth_element(sortedWindow.begin(), sortedWindow.begin() + sortedWindow.size() / 2, sortedWindow.end());
    if (sortedWindow.size() % 2 == 0) {
        // If the window size is even, return the average of the two middle numbers  The median should not have decimals
        return (sortedWindow[sortedWindow.size() / 2 - 1] + sortedWindow[sortedWindow.size() / 2]) / 2;
    }
    else {
        // If the window size is odd, return the middle number  
        return sortedWindow[sortedWindow.size() / 2];
    }
}

// Apply median filter  
void medianFilter(std::vector<std::vector<int>>&amp; image, int filterSize) {
    int halfSize = filterSize / 2;
    int rows = image.size();
    int cols = image[0].size();

    // Create output image  
    std::vector<std::vector<int>> filteredImage(rows, std::vector<int>(cols, 0));

    // Traverse each pixel in the image  
    for (int i = halfSize; i < rows - halfSize; ++i) {
        for (int j = halfSize; j < cols - halfSize; ++j) {
            // Create window and fill pixel values  
            std::vector<int> window;
            for (int m = -halfSize; m <= halfSize; ++m) {
                for (int n = -halfSize; n <= halfSize; ++n) {
                    // Directly access pixel values by adding offsets m and n to the current indices i and j. Thus, boundary values for median are 0
                    window.push_back(image[i + m][j + n]);
                }
            }

            // Get median and set to output image  
            filteredImage[i][j] = getMedian(window);
        }
    }

    // Update original image  
    image = filteredImage;
}
C++ Image Processing (Part 3): Image Smoothing
// Apply median filter  
void medianFilter(std::vector<std::vector<int>>&amp; image, int filterSize) {
    if (filterSize % 2 == 0) {
        throwstd::invalid_argument("Filter size must be odd.");
    }

    int halfSize = filterSize / 2;
    int rows = image.size();
    int cols = image[0].size();

    // Create new image with boundary extension  
    int paddedRows = rows + 2 * halfSize;
    int paddedCols = cols + 2 * halfSize;
    std::vector<std::vector<int>> paddedImage(paddedRows, std::vector<int>(paddedCols, 0));

    // Create output image  
    std::vector<std::vector<int>> filteredImage(rows, std::vector<int>(cols, 0));

    // Traverse each pixel in the extended image  
    for (int i = halfSize; i < paddedRows - halfSize; ++i) {
        for (int j = halfSize; j < paddedCols - halfSize; ++j) {
            // Create window and fill pixel values  
            std::vector<int> window;
            for (int m = -halfSize; m <= halfSize; ++m) {
                for (int n = -halfSize; n <= halfSize; ++n) {
                    // Calculate the actual position of the pixel in the original image using originalRow and originalCol, then check if this position is within the image boundary.
                    // If so, add this pixel value to the window. This method ensures that only pixels located inside the image are included.
                    int originalRow = i - halfSize + m;
                    int originalCol = j - halfSize + n;
                    if (originalRow >= 0 &amp;&amp; originalRow < rows &amp;&amp; originalCol >= 0 &amp;&amp; originalCol < cols) {
                        window.push_back(image[originalRow][originalCol]);
                    }
                }
            }
            // Get median and set to output image  
            if (!window.empty()) {
                filteredImage[i - halfSize][j - halfSize] = getMedian(window);
            }
        }
    }
    // Update original image  
    image = filteredImage;
}

C++ Image Processing (Part 3): Image SmoothingPadding of 0 does not count in sorting, no decimals

int main() {
    // Example image (grayscale)  
    std::vector<std::vector<int>> image = {
        {1, 2, 3, 4, 5},
        {6, 7, 8, 9, 10},
        {11, 12, 13, 14, 15},
        {16, 17, 18, 19, 20},
        {21, 22, 23, 24, 25}
    };

    // Apply 3x3 median filter  
    medianFilter(image, 3);

    // Output filtered image  
    for (constauto&amp; row : image) {
        for (int pixel : row) {
            std::cout << pixel << " ";
        }
        std::cout << std::endl;
    }

    return0;
}

2. Mean Filtering

A simple smoothing filtering method that uses the average of the pixels within the filter window to replace the value of the center pixel. For each pixel within the filter window, the average of all pixels in its neighborhood is calculated, and the result is used as the value of the center pixel. Mean filtering can effectively smooth the image and remove high-frequency noise, but it will blur the image and lose details.

OpenCV Call

#include <opencv2/opencv.hpp>  
#include <iostream>  

int main() {
    cv::Mat src, dst;

    // Read image  
    src = cv::imread("cat.jpg", cv::IMREAD_GRAYSCALE);
    if (src.empty()) {
        std::cout << "Could not read the image!" << std::endl;
        return-1;
    }

    // Create target image  
    //dst.create(src.size(), src.type());

    // Apply 3x3 mean filter  
    cv::blur(src, dst, cv::Size(3, 3));

    // Display original image and processed image  
    cv::imshow("Original Image", src);
    cv::imshow("Averaging Filter", dst);

    // Wait for user key press, then close window  
    cv::waitKey(0);

    return0;
}

C++ Image Processing (Part 3): Image SmoothingC++ Image Processing (Part 3): Image Smoothing

Principle of Mean Filtering

// The basic idea is to replace the value of a pixel with the average of the surrounding pixels
#include <iostream>  
#include <vector>  
#include <cmath> // for round()   

// Fill boundary so that padding does not count in average calculation

// Calculate the average of pixels within the window  
float getAverage(const std::vector<int>&amp; window) {
    int sum = 0;
    for (int pixel : window) {
        sum += pixel;
    }
    returnstatic_cast<float>(sum) / window.size();
}

void meanFilter(std::vector<std::vector<int>>&amp; image, int filterSize) {
    if (filterSize % 2 == 0) {
        throwstd::invalid_argument("Filter size must be odd.");
    }

    int halfSize = filterSize / 2;
    int rows = image.size();
    int cols = image[0].size();

    // Create new image with boundary extension  Initialize the new image with extended boundary paddedImage, setting all pixel values to 0
    int paddedRows = rows + 2 * halfSize;
    int paddedCols = cols + 2 * halfSize;
    std::vector<std::vector<int>> paddedImage(paddedRows, std::vector<int>(paddedCols, 0));
    // Create output image  
    std::vector<std::vector<int>> filteredImage(rows, std::vector<int>(cols, 0));
    // Traverse each pixel in the extended image  
    for (int i = halfSize; i < paddedRows - halfSize; ++i) {
        for (int j = halfSize; j < paddedCols - halfSize; ++j) {
            // Create window and fill pixel values  
            std::vector<int> window;
            for (int m = -halfSize; m <= halfSize; ++m) {
                for (int n = -halfSize; n <= halfSize; ++n) {
                    // Check if the current pixel is within the original image range  
                    int originalRow = i - halfSize + m;
                    int originalCol = j - halfSize + n;
                    if (originalRow >= 0 &amp;&amp; originalRow < rows &amp;&amp; originalCol >= 0 &amp;&amp; originalCol < cols) {
                        window.push_back(image[originalRow][originalCol]);
                    }
                }
            }
            // Get average and set to output image, rounding  
            if (!window.empty()) { // Ensure window is not empty  
                filteredImage[i - halfSize][j - halfSize] = static_cast<int>(round(getAverage(window)));
            }
        }
    }
    // Update original image  
    image = filteredImage;
}

int main() {
    // Example: Create a simple image and a 3x3 mean filter  
    std::vector<std::vector<int>> image = {
        {10, 20, 30, 40, 50},
        {20, 30, 40, 50, 60},
        {30, 40, 50, 60, 70},
        {40, 50, 60, 70, 80},
        {50, 60, 70, 80, 90}
    };

    int filterSize = 3;
    meanFilter(image, filterSize);

    // Print filtered image  
    for (constauto&amp; row : image) {
        for (int val : row) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }

    return0;
}

C++ Image Processing (Part 3): Image SmoothingPadding of 0 does not count in average

3. Gaussian Filtering

A smoothing filtering method based on Gaussian distribution. It considers that the pixel values of neighboring pixels closer to the center pixel have greater weight. Gaussian filtering assigns a weight to each pixel in the neighborhood by calculating the Gaussian function, and then uses a weighted average to smooth the image. Gaussian filtering can better preserve image details and edge information while effectively removing noise.

Gaussian filtering is a linear smoothing filtering method widely used in the denoising process of image processing. Its main principle is to apply a Gaussian function to each pixel in the image to perform a weighted average of its neighboring pixel values.

The operation of the Gaussian filter can be described as: using a user-specified template (also known as a convolution kernel or mask) to scan each pixel in the image, replacing the value of the center pixel with the weighted average gray value of the pixels in the neighborhood determined by the template. The coefficients of this template are generated based on the Gaussian function, and its shape and size determine the degree of smoothing applied to the image.

The Gaussian function is a bell-shaped curve, mathematically expressed as: G(x, y) = 1 / (2πσ^2) * e^-((x^2 + y^2) / (2σ^2))

Where (x, y) are the coordinates of the pixel, σ is the standard deviation of the Gaussian function, and G(x, y) is the Gaussian weight.

This Gaussian function determines the coefficients of the template; the closer to the center of the template, the larger the coefficient; the farther away, the smaller the coefficient. Therefore, compared to the mean filter, the Gaussian filter has a smaller degree of blurring on the image.

OpenCV Call

#include <opencv2/opencv.hpp>  
#include <iostream>  
usingnamespace cv;
usingnamespacestd;

int main() {
    // Read image  
    Mat image = imread("cat.jpg");
    if (image.empty()) {
        std::cerr << "Unable to load image" << std::endl;
        return-1;
    }

    cvtColor(image, image, COLOR_BGR2GRAY);  // Convert to grayscale

    Mat gaussianFilteredImage;
    // Kernel needs to be odd  sigmaX and sigmaY parameters default to 0 (automatically calculated)  borderType parameter default value is cv::BORDER_DEFAULT, zero padding
    // sigmaX and sigmaY will be set to half the kernel size minus 1, then divided by 2. That is, if the kernel size is (5, 5), then sigmaX and sigmaY will be calculated as 1.40625 (because (5 - 1) / 2 / 2 = 1.40625)
    //GaussianBlur(image, gaussianFilteredImage, Size(29, 29), 0, 0, BORDER_DEFAULT);  // The larger the kernel, the larger the standard deviation, the more blurred
    GaussianBlur(image, gaussianFilteredImage, Size(5, 5), 0 ,0);

    // Display original image and filtered image  
    imshow("Original Image", image);
    imshow("Gaussian Blurred Image", gaussianFilteredImage);

    // Wait for key press  
    waitKey(0);

    return0;
}

C++ Image Processing (Part 3): Image SmoothingC++ Image Processing (Part 3): Image Smoothing

Generating Gaussian Distribution with rng.fill

#include<iostream>
#include<opencv2/opencv.hpp>
usingnamespace cv;
usingnamespacestd;

// Gaussian noise
void Gaussian_noise(Mat image) {

    imshow("orign", image);// Display original image
    Mat gray;
    cvtColor(image, gray, COLOR_BGR2GRAY);
    Mat image_noise = Mat::zeros(image.rows, image.cols, image.type());
    Mat gray_noise = Mat::zeros(gray.rows, gray.cols, gray.type());

    imshow("gray", gray);// Display grayscale image

    RNG rng;// Create an RNG class      
rng.fill(image_noise, RNG::NORMAL, 10, 20);// Generate three-channel Gaussian distributed random numbers  RNG::NORMAL may be an enumeration value indicating the type of random number distribution to generate, here it is normal distribution (Gaussian distribution) with mean 10 and standard deviation 20
    rng.fill(gray_noise, RNG::NORMAL, 15, 30);

    imshow("image_noise", image_noise);// Three-channel Gaussian noise
    imshow("gray_noise", gray_noise);// Single-channel Gaussian noise

    image = image + image_noise;// Add Gaussian noise to the color image
    gray = gray + gray_noise;// Add Gaussian noise to the grayscale image

    // Display image with added Gaussian noise
    imshow("image_Gaussian", image);
    imshow("gray_Gaussian", gray);

    waitKey(0);
}

int main() {

    Mat image;
    image = imread("cat.jpg");  // Read image;
    Gaussian_noise(image);

    return0;
}
C++ Image Processing (Part 3): Image Smoothing

Principle of Gaussian Filtering

C++ Image Processing (Part 3): Image Smoothing
// C++ implementation of Gaussian filtering
#include<iostream>
#include<opencv2/opencv.hpp>

usingnamespace cv;
usingnamespacestd;

// 1 Gaussian filtering
void Gaussfilter_ly(Mat input_image, Mat&amp; output_image, int Gauss_size, double Sigma)
{
    // Ensure Gaussian kernel size is an odd number greater than or equal to 3
    if (Gauss_size < 3) Gauss_size = 3;
    else Gauss_size = (int)(Gauss_size / 2) * 2 + 1;

    // Generate Gaussian convolution kernel
    double** Gausskernel = newdouble* [Gauss_size];
    for (int i = 0; i < Gauss_size; i++)
    {
        Gausskernel[i] = newdouble[Gauss_size];
    }
    int center = Gauss_size / 2;
    double sum = 0;

    for (int i = 0; i < Gauss_size; i++)
    {
        for (int j = 0; j < Gauss_size; j++)
        {
            Gausskernel[i][j] = exp(-((i - center) * (i - center) + (j - center) * (j - center)) / (2 * Sigma * Sigma));
            sum += Gausskernel[i][j];
        }
    }
    // Normalize Gaussian convolution kernel
    double sum1 = 1 / sum;
    for (int i = 0; i < Gauss_size; i++)
    {
        for (int j = 0; j < Gauss_size; j++)
        {
            Gausskernel[i][j] *= sum1;
        }
    }

    // Filtering
    Mat tem_image = input_image.clone();
    int rows = input_image.rows - center;
    int cols = input_image.cols - center;
    for (int i = center; i < rows; i++)
    {
        for (int j = center; j < cols; j++)
        {
            double sum = 0;
            for (int m = -center; m <= center; m++)
            {
                for (int n = -center; n <= center; n++)
                {
                    sum += Gausskernel[center + m][center + n] * input_image.at<uchar>(i + m, j + n);
                }
            }
            tem_image.at<uchar>(i, j) = static_cast<uchar>(sum);
        }
    }
    output_image = tem_image;

    // Free memory
    for (int i =0; i < Gauss_size; i++) delete[] Gausskernel[i];
    delete[] Gausskernel;
}

int main() {
    // Read image
    Mat image = imread("cat.jpg");
    if (image.empty()) {
        std::cerr << "Unable to load image" << std::endl;
        return-1;
    }

    cvtColor(image, image, COLOR_BGR2GRAY);  // Convert to grayscale

    Mat gaussianFilteredImage;
    int Gauss_size = 5;
    double Sigma = 1.5;
    Gaussfilter_ly(image, gaussianFilteredImage, Gauss_size , Sigma);

    // Display original image and filtered image
    imshow("Original Image", image);
    imshow("Gaussian Blurred Image", gaussianFilteredImage);

    // Wait for key press
    waitKey(0);

    return0;
}

C++ Image Processing (Part 3): Image SmoothingC++ Image Processing (Part 3): Image Smoothing

Leave a Comment