Search…

Lọc Ảnh Bằng Phép Tương Quan Và Tích Chập

20/09/20209 min read
Bài viết mô tả phép lọc ảnh sử dụng tương quan và tích chập trong lĩnh vực xử lý ảnh mà không cần sử dụng các thư viện như OpenCV, từ đó người đọc có thể hiểu chuyên sâu hơn về lĩnh vực khoa học này.

Xử lý ảnh và thị giác máy tính là một trong những nhánh của khoa học máy tính, có vô số ứng dụng cho đời sống con người như nhận diện khuôn mặt, đọc biển số xe, cảm biến chuyển động, … do đó khi đã đi vào ngành này, kiến thức về phép lọc ảnh bằng phép tính tương quan, tích chập là không thể thiếu.

Ngày nay, khi nói đến xử lý ảnh hay thị giác máy tính, người ta lập tức nghĩ đến các thư viện như OpenCV, thậm chí trong một số trường đại học, người ta chỉ chú tâm đến cách giải quyết vấn đề chứ không đề cập đến cốt lõi toán học bên dưới.

Bài viết này sẽ đem đến cho người đọc một cái nhìn chuyên sâu hơn về phép tính cơ bản của xử lý ảnh, thị giác máy tính, đó là lọc ảnh sử dụng phép tương quan (correlation) và phép tích chập (convolution).

Dẫn nhập

Nếu như bạn là một người hay dùng photoshop, bạn có thể từng nhìn thấy công cụ filter mà không để ý dùng thử hoặc dùng nhưng không biết mục đích chính của nó là gì.

photoshopimg

Bây giờ bạn hãy thử nhập vào như hình (một khối 3 x 3 toàn bộ là số 1 và scale là 9). Lúc này bạn có thể nhận thấy rằng hình ảnh của hình mờ hơn hình ảnh ban đầu. Vậy tại sao lại như vậy? Những con số này có ý nghĩa gì?

filterimg

Nguyên lý của việc làm mờ ảnh

Về bản, chất thực hiện làm mờ ảnh chính là tạo ra ảnh mới sao cho giá trị mức xám của mỗi pixel ở ảnh mới đúng bằng giá trị trung bình của điểm tương ứng và 8 điểm lân cận trên ảnh ban đầu. Nói cách khác, với mỗi điểm trên hình ban đầu, bạn tính giá trị trung bình của nó (tại hàng i cột j) với 8 điểm xung quanh rồi viết lại giá trị mức xám ở vị trí tương ứng (cũng tại hàng i cột j) lên ảnh mới, sau đó tương tự với các điểm tiếp theo.

averagesum

 

Vậy có nghĩa là ta đang tính trung bình cộng của 9 pixel (pixel tại điểm đó và 8 pixel lân cận), vậy phép tính đó cũng giống như nhân từng giá trị mức sáng của các pixel lân cận với 1/9  sau đó cộng lại với nhau. Vậy, nếu có một ma trận 3 x 3 với tất cả các con số trong ma trận đều là 1/9 , ta nhân từng phần tử của ma trận này với mức sáng của pixel tương ứng và cộng lại, ta sẽ có kết quả giống nhau (xem hình vẽ bên dưới).

kernelimg

Áp dụng tương tự cho mọi pixel trên ảnh ban đầu và lấy từng kết quả cho từng pixel của ảnh mới, ta sẽ được ảnh mới chính là ảnh mờ của ảnh ban đầu.

Đến đây, có thể bạn đã hiểu được ý nghĩa của công cụ filter của photoshop, các con số 1 chính là phần tử của ma trận 3x3 (bạn cũng có thể tạo ma trận 5x5 thậm chí lớn hơn nếu thích) và số 9 chính là chia tất cả cho 9 (để tạo ra các phần tử bằng 1/9).

Phép tương quan và tích chập

Phép tính trên gọi là phép tương quan (correlation) và ma trận 3x3 đó gọi là kernel. Một phiên bản khác của nó là phép tích chập (convolution) với khác biệt là kernel được xoay 180 độ (xem hình dưới), phép tích chập này rất thông dụng và được sử dụng rộng rãi trong xử lý ảnh.

1  2  3        9  8  7
4  5  6   ⟳   6  5  4
7  8  9        3  2  1

Một số vấn đề khi tính tương quan, tích chập

  • Khi nhân các phần tử tương ứng với nhau (giữa pixel, các điểm lân cận – các thành phần trong kernel), đối với các phần tử ở cạnh thì sẽ có một số pixel bị khuyết, lúc này, có nhiều cách giải quyết như bỏ qua, chèn thêm một (một số) hàng, cột mang giá trị 0 hoặc bằng giá trị gần nhất.
  • Các kernel thường có kích thước lẻ (3, 5, 7, …) để thuận lợi và có nhiều ý nghĩa hơn trong tính toán.
  • Do khi hiện thực, ta cần dùng đến 4 vòng for nên tốc độ tính toán sẽ rất chậm nếu hình ảnh quá lớn. Khi đó, ta nên sử dụng GPU để tính toán hoặc sử dụng phép biến đổi fourier để đưa về miền tầng số (dành cho bạn đọc muốn tìm hiểu sâu hơn – tìm hiểu với từ khóa “Fourier Transform image processing”).

Tách cạnh từ hình ảnh bằng phép convolution

Bây giờ, hãy thử sử dụng kernel bên dưới để thực hiện phép tính tích chập.

-1 0 1
-2 0 2
-1 0 1

Bạn có thể nhận thấy hình ảnh bây giờ chỉ còn giữ lại pixel của các cạnh. Vì sao kernel này giúp lấy được cạnh? Bởi vì nó khảo sát sự thay đổi mức xám đột ngột. Ví dụ với 1 ảnh chụp hình cửa sổ nằm trên một bức tường, ở trên bức tường thì mức xám của các pixel gần giống nhau, nên pixel bên trái và bên phải của pixel đang xét sẽ có mức xám gần bằng nhau. Khi nhân từng phần tử tương ứng lại và cộng lại, pixel bên trái sẽ triệt tiêu pixel bên phải (do hệ số trong ma trận là trái dấu), nên con số tính ra sẽ rất nhỏ (chứng tỏ pixel đang xét không phải là cạnh). Ngược lại nếu như có sự thay đổi mức xám đột ngột, thì con số tính được sẽ lớn (do sự mất cân bằng giữa pixel bên phải và bên trái), từ đó ta nhận diện được cạnh.

Tuy nhiên kernel trên chỉ có thể tìm được cạnh dọc (do chỉ xét các pixel bên trái và bên phải), ngoài ra có các kernel khác có thể tìm được cả 4 hướng:

0  1  0
1 -4 1
0 1 0

và thậm chí 8 hướng:

1  1  1
1 -8 1
1 1 1

Code hoàn chỉnh

/*
Owner: STDIO Training
Author: Phan Tan Phuc
Website: https://training.stdio.vn
Email: developer@stdio.vn
*/

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <string.h>

typedef unsigned char byte;

int _width;
int _height;
int _bitdepth;

typedef struct
{
    char            m_type[2];
    unsigned char    m_fSize[4];
    unsigned char    m_reserved1[2];
    unsigned char    m_reserved2[2];
    unsigned char    m_data_offset[4];
} BitMapHeader;

typedef struct
{
    unsigned char m_size[4];
    unsigned char m_width[4];
    unsigned char m_height[4];
    unsigned char m_planes[2];
    unsigned char m_bitDepth[2];
    unsigned char m_compression_type[16];
    unsigned char m_color_used[4];
    unsigned char m_color_important[4];
} BitMapInformation;

void readImage(const char * _filePath, BitMapHeader & _header, BitMapInformation & _information, unsigned char * &_data)
{
    FILE* _imageFile = fopen(_filePath, "rb");

    if (_imageFile == nullptr)
        return;

    fread(&_header, sizeof(BitMapHeader), sizeof(char), _imageFile);
    fread(&_information, sizeof(BitMapInformation), sizeof(char), _imageFile);

    _width = *(int*)_information.m_width;
    _height = *(int*)_information.m_height;
    _bitdepth = *(int*)_information.m_bitDepth;
    int data_offset = *(int*)_header.m_data_offset;

    int _rowSize = (_bitdepth * _width + 31) / 32 * 4;

    if (_data != nullptr)
    {
        delete[] _data;
        _data = nullptr;
    }
    _data = new unsigned char[_rowSize * _height];


    fseek(_imageFile, data_offset, SEEK_SET);
    fread(_data, _rowSize * _height, sizeof(char), _imageFile);

    fclose(_imageFile);
}

void corr(byte* data, int width, int height, int byte_per_pixel, float* kernel, int kernel_size)
{     int padding = kernel_size / 2;     int width_size = width * byte_per_pixel;     int height_size = height * byte_per_pixel;     int _rowSize = (byte_per_pixel * 8 * width + 31) / 32 * 4;     int img_size = width * height * byte_per_pixel * sizeof(byte);     byte* temp_img = new byte[_rowSize * height];     for (int r = padding; r < height - padding; ++r) {         for (int c = padding; c < width - padding; ++c) {             float sum_c1 = 0.0f;             float sum_c2 = 0.0f;             float sum_c3 = 0.0f;             for (int kr = -padding; kr <= padding; ++kr) {                 for (int kc = -padding; kc <= padding; ++kc) {                     int r_corr = r + kr;                     int c_corr = c + kc;                     sum_c1 += data[_rowSize * r_corr + c_corr * byte_per_pixel + 0] * kernel[kernel_size * (kr + padding) + (kc + padding)];                     sum_c2 += data[_rowSize * r_corr + c_corr * byte_per_pixel + 1] * kernel[kernel_size * (kr + padding) + (kc + padding)];                     sum_c3 += data[_rowSize * r_corr + c_corr * byte_per_pixel + 2] * kernel[kernel_size * (kr + padding) + (kc + padding)];                 }             }             if (sum_c1 > 255) sum_c1 = 255;             if (sum_c1 < 0) sum_c1 = 0;             if (sum_c2 > 255) sum_c2 = 255;             if (sum_c2 < 0) sum_c2 = 0;             if (sum_c3 > 255) sum_c3 = 255;             if (sum_c3 < 0) sum_c3 = 0;             temp_img[_rowSize * r + c * byte_per_pixel + 0] = (byte)sum_c1;             temp_img[_rowSize * r + c * byte_per_pixel + 1] = (byte)sum_c2;             temp_img[_rowSize * r + c * byte_per_pixel + 2] = (byte)sum_c3;         }     }     memcpy(data, temp_img, img_size);     delete[] temp_img; } void saveImage(const char * _filePath, BitMapHeader & _header, BitMapInformation & _information, const unsigned char * _data) {     int _width = *(int*)_information.m_width;     int _height = *(int*)_information.m_height;     int _bitdepth = *(int*)_information.m_bitDepth;     int _offset = *(int*)_header.m_data_offset;     FILE * _imageFile;     _imageFile = fopen(_filePath, "wb");     fwrite(&_header, sizeof(BitMapHeader), sizeof(char), _imageFile);     fwrite(&_information, sizeof(BitMapInformation), 1, _imageFile);     int _size = (_bitdepth * _width + 31) / 32 * 4;     fseek(_imageFile, _offset, SEEK_SET);     fwrite(_data, _size * _height, sizeof(char), _imageFile);     fclose(_imageFile); } int main() {     // initilize     byte * _dataImage = nullptr;     BitMapHeader        _header;     BitMapInformation    _infomation;     // Thay STDIO_Image bang duong dan den anh Bitmap phu hop     readImage("STDIO_Image.bmp", _header, _infomation, _dataImage);     float laplacekernel[] =         { -1, -1, -1,           -1, 8, -1,           -1, -1, -1 };     corr(_dataImage, _width, _height, _bitdepth / 8, laplacekernel, 3);     saveImage("STDIO_ImageNew.bmp", _header, _infomation, _dataImage);     delete[] _dataImage;     return 0; }

Project mẫu

STDIOTrainingImageFilter.zip

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024