Xử Lý Ảnh Nâng Cao với OpenCV: Biến Đổi Hough, Remap và Affine

Giới Thiệu

OpenCV là một thư viện mạnh mẽ cho thị giác máy tính, cung cấp nhiều công cụ để phân tích, biến đổi và hiểu hình ảnh. Bài viết này sẽ đi sâu vào ba kỹ thuật xử lý ảnh cơ bản nhưng quan trọng: Biến đổi Hough để phát hiện các hình dạng hình học, kỹ thuật Remap cho các biến đổi hình học tùy chỉnh và Biến đổi Affine cho các phép co giãn, xoay và dịch chuyển.

Phát Hiện Đường Bằng Biến Đổi Hough

Biến đổi Hough là một kỹ thuật được sử dụng rộng rãi để phát hiện các hình dạng hình học như đường thẳng, đường tròn trong hình ảnh. Phương pháp này đặc biệt hiệu quả khi các hình dạng bị gián đoạn hoặc bị nhiễu. OpenCV cung cấp hàm HoughLinesP (Probabilistic Hough Line Transform) để phát hiện các đoạn thẳng một cách hiệu quả hơn so với HoughLines truyền thống.

Ví dụ dưới đây minh họa cách sử dụng HoughLinesP để phát hiện các đoạn thẳng trong một hình ảnh. Chương trình sẽ thực hiện phát hiện cạnh bằng Canny, sau đó áp dụng Biến đổi Hough xác suất và hiển thị các đường được phát hiện. Người dùng có thể điều chỉnh ngưỡng phát hiện thông qua một thanh trượt.


#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
#include <vector> // Explicitly include vector

// Khai báo các biến toàn cục cho hình ảnh và ngưỡng
cv::Mat inputImage, outputDisplayImage, edgeDetectedImage;
int lineDetectionThreshold = 100; // Ngưỡng cho HoughLinesP

// Khai báo các hàm tiền xử lý
static void onTrackbarChange(int, void* userData);
static void showProgramInstructions();

int main() {
    showProgramInstructions();

    // Đọc ảnh đầu vào từ thư mục dự án
    inputImage = cv::imread("input.jpg");
    if (inputImage.empty()) {
        std::cerr << "Lỗi: Không thể đọc ảnh. Đảm bảo có tệp 'input.jpg' trong thư mục chương trình." << std::endl;
        return -1;
    }

    cv::imshow("Ảnh Gốc", inputImage);

    // Tạo cửa sổ và thanh trượt để điều chỉnh ngưỡng Hough
    cv::namedWindow("Kết Quả Phát Hiện Đường", cv::WINDOW_AUTOSIZE);
    cv::createTrackbar("Ngưỡng Hough", "Kết Quả Phát Hiện Đường", &lineDetectionThreshold, 200, onTrackbarChange);

    // Thực hiện phát hiện cạnh Canny
    cv::Canny(inputImage, edgeDetectedImage, 50, 150, 3);
    // Chuyển đổi ảnh cạnh sang định dạng BGR để có thể vẽ các đường màu
    cv::cvtColor(edgeDetectedImage, outputDisplayImage, cv::COLOR_GRAY2BGR);

    // Gọi hàm callback lần đầu để hiển thị kết quả ban đầu
    onTrackbarChange(lineDetectionThreshold, nullptr);

    cv::waitKey(0); // Chờ cho đến khi người dùng nhấn phím
    return 0;
}

// Hàm callback khi thanh trượt thay đổi
static void onTrackbarChange(int, void* userData) {
    cv::Mat currentDisplayImage = outputDisplayImage.clone(); // Tạo bản sao để vẽ đường
    cv::Mat currentEdgeImage = edgeDetectedImage.clone();     // Sử dụng ảnh cạnh

    std::vector<cv::Vec4i> currentDetectedLines;
    // Áp dụng Biến đổi Hough xác suất
    // Các tham số: ảnh cạnh, vector lưu trữ đường, khoảng cách phân giải rho, góc phân giải theta,
    // ngưỡng, độ dài đường tối thiểu, khoảng cách tối đa giữa các đoạn để coi là một đường
    cv::HoughLinesP(currentEdgeImage, currentDetectedLines, 1, CV_PI / 180, lineDetectionThreshold + 1, 40, 15);

    // Vẽ các đường được phát hiện lên ảnh hiển thị
    for (size_t i = 0; i < currentDetectedLines.size(); ++i) {
        cv::Vec4i lineCoords = currentDetectedLines[i];
        cv::line(currentDisplayImage, cv::Point(lineCoords[0], lineCoords[1]),
                 cv::Point(lineCoords[2], lineCoords[3]), cv::Scalar(0, 255, 0), 2, cv::LINE_AA); // Vẽ đường màu xanh lá, độ dày 2
    }
    cv::imshow("Kết Quả Phát Hiện Đường", currentDisplayImage);
}

// Hàm hiển thị hướng dẫn sử dụng
static void showProgramInstructions() {
    std::cout << "Phiên bản OpenCV hiện tại: " << CV_VERSION << std::endl;
    std::cout << "Chương trình ví dụ phát hiện đường bằng Biến đổi Hough P." << std::endl;
    std::cout << "Vui lòng điều chỉnh thanh trượt để quan sát hiệu ứng phát hiện đường." << std::endl;
}

Biến Đổi Hình Ảnh Tùy Chỉnh với Remap

Kỹ thuật Remap (ánh xạ lại) trong OpenCV cho phép thực hiện các biến đổi hình học rất linh hoạt bằng cách xác định vị trí nguồn cho mỗi pixel trong ảnh đích. Điều này có nghĩa là bạn tạo ra hai ma trận (map_x và map_y) để lưu trữ tọa độ X và Y của pixel tương ứng trong ảnh gốc cho mỗi vị trí pixel trong ảnh kết quả. Khi hàm remap được gọi, nó sẽ lấy giá trị pixel từ vị trí đã cho trong ảnh gốc và đặt vào ảnh đích.

Đoạn mã sau đây minh họa cách sử dụng remap để tạo ra các hiệu ứng biến đổi hình ảnh khác nhau như phóng to vùng trung tâm, lật ảnh theo chiều dọc, lật ảnh theo chiều ngang hoặc xoay ảnh 180 độ, dựa trên phím người dùng nhấn.


#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp> // Cần cho remap
#include <iostream>

// Khai báo các biến toàn cục
cv::Mat sourceImage, remappedImage;
cv::Mat xMap, yMap; // Ma trận ánh xạ

#define REMAP_WINDOW_NAME "Cửa Sổ Biến Đổi Ảnh (Remap)"

// Khai báo các hàm tiền xử lý
int generateMapping(int keyPressed);
static void displayRemapHelp();

int main() {
    displayRemapHelp();

    // Đọc ảnh nguồn
    sourceImage = cv::imread("input.jpg", cv::IMREAD_COLOR);
    if (sourceImage.empty()) {
        std::cerr << "Lỗi: Không thể đọc ảnh. Đảm bảo có tệp 'input.jpg' trong thư mục chương trình." << std::endl;
        return -1;
    }
    cv::imshow("Ảnh Gốc", sourceImage);

    // Khởi tạo ảnh đích và các ma trận ánh xạ
    remappedImage.create(sourceImage.size(), sourceImage.type());
    xMap.create(sourceImage.size(), CV_32FC1);
    yMap.create(sourceImage.size(), CV_32FC1);

    cv::namedWindow(REMAP_WINDOW_NAME, cv::WINDOW_AUTOSIZE);
    cv::imshow(REMAP_WINDOW_NAME, sourceImage); // Hiển thị ảnh gốc ban đầu

    // Vòng lặp để xử lý đầu vào từ người dùng cho việc ánh xạ lại
    while (true) {
        int key = cv::waitKey(0); // Chờ vô thời hạn cho một lần nhấn phím

        if ((key & 255) == 27) { // Phím ESC để thoát
            std::cout << "Thoát chương trình Remap..." << std::endl;
            break;
        }

        if (key == '1' || key == '2' || key == '3' || key == '4') {
            generateMapping(key); // Cập nhật ánh xạ dựa trên phím nhấn
            // Thực hiện ánh xạ lại
            cv::remap(sourceImage, remappedImage, xMap, yMap, cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0));
            cv::imshow(REMAP_WINDOW_NAME, remappedImage); // Hiển thị ảnh đã ánh xạ lại
        } else {
            std::cout << "Phím không hợp lệ. Vui lòng nhấn 1, 2, 3, 4 hoặc ESC." << std::endl;
        }
    }
    return 0;
}

// Hàm tạo ma trận ánh xạ dựa trên phím đã nhấn
int generateMapping(int keyPressed) {
    // Lặp qua từng pixel để xác định vị trí nguồn mới
    for (int row = 0; row < sourceImage.rows; ++row) {
        for (int col = 0; col < sourceImage.cols; ++col) {
            switch (keyPressed) {
                case '1': // Phóng to vùng trung tâm
                    if (col > sourceImage.cols * 0.2 && col < sourceImage.cols * 0.8 &&
                        row > sourceImage.rows * 0.2 && row < sourceImage.rows * 0.8) {
                        xMap.at<float>(row, col) = static_cast<float>(1.5 * (col - sourceImage.cols * 0.2) + 0.5);
                        yMap.at<float>(row, col) = static_cast<float>(1.5 * (row - sourceImage.rows * 0.2) + 0.5);
                    } else { // Các pixel bên ngoài vùng được phóng to sẽ có màu đen
                        xMap.at<float>(row, col) = -1; // -1 hoặc 0 thường dẫn đến màu viền
                        yMap.at<float>(row, col) = -1;
                    }
                    break;
                case '2': // Lật theo chiều dọc
                    xMap.at<float>(row, col) = static_cast<float>(col);
                    yMap.at<float>(row, col) = static_cast<float>(sourceImage.rows - 1 - row);
                    break;
                case '3': // Lật theo chiều ngang
                    xMap.at<float>(row, col) = static_cast<float>(sourceImage.cols - 1 - col);
                    yMap.at<float>(row, col) = static_cast<float>(row);
                    break;
                case '4': // Lật cả chiều ngang và dọc (xoay 180 độ)
                    xMap.at<float>(row, col) = static_cast<float>(sourceImage.cols - 1 - col);
                    yMap.at<float>(row, col) = static_cast<float>(sourceImage.rows - 1 - row);
                    break;
            }
        }
    }
    return 1;
}

// Hàm hiển thị hướng dẫn sử dụng cho remap
static void displayRemapHelp() {
    std::cout << "Phiên bản OpenCV hiện tại: " << CV_VERSION << std::endl;
    std::cout << "Chương trình ví dụ biến đổi ảnh bằng kỹ thuật Remap." << std::endl;
    std::cout << "Hướng dẫn sử dụng: \n"
                 "  Nhấn phím [ESC] - Thoát chương trình\n"
                 "  Nhấn phím [1]   - Phóng to vùng trung tâm\n"
                 "  Nhấn phím [2]   - Lật ảnh theo chiều dọc\n"
                 "  Nhấn phím [3]   - Lật ảnh theo chiều ngang\n"
                 "  Nhấn phím [4]   - Xoay ảnh 180 độ\n";
}

Biến Đổi Affine

Biến đổi Affine là một phép biến đổi hình học tuyến tính bảo toàn các đường thẳng và tỷ lệ khoảng cách giữa các điểm trên đường thẳng. Nó cho phép thực hiện các phép co giãn (scaling), xoay (rotation), cắt xiên (shearing) và dịch chuyển (translation). Trong OpenCV, chúng ta thường sử dụng hàm getAffineTransform để tính toán ma trận biến đổi từ ba cặp điểm tương ứng (ba điểm trong ảnh gốc và ba điểm tương ứng trong ảnh đích), và sau đó dùng warpAffine để áp dụng ma trận này lên toàn bộ hình ảnh.

Ngoài ra, hàm getRotationMatrix2D cũng cung cấp một cách tiện lợi để tạo ma trận cho các phép xoay và co giãn quanh một điểm cụ thể.

Ví dụ sau đây minh họa một phép biến đổi affine hai bước: đầu tiên là một phép biến đổi từ ba điểm tùy ý, sau đó là một phép xoay và co giãn thêm vào ảnh đã biến đổi.


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

#define ORIGINAL_WINDOW "Ảnh Gốc"
#define WARPED_WINDOW "Ảnh Biến Đổi Affine"
#define ROTATED_WARPED_WINDOW "Ảnh Biến Đổi & Xoay"

static void displayAffineInstructions();

int main() {
    displayAffineInstructions();

    // Định nghĩa ba điểm nguồn và ba điểm đích cho biến đổi affine
    cv::Point2f sourceTrianglePts[3];
    cv::Point2f destTrianglePts[3];

    // Các ma trận biến đổi
    cv::Mat rotationMatrix(2, 3, CV_32FC1);
    cv::Mat affineTransformMatrix(2, 3, CV_32FC1);

    // Các biến ảnh
    cv::Mat originalImg, warpedImg, rotatedAndWarpedImg;

    // Đọc ảnh gốc
    originalImg = cv::imread("input.jpg", cv::IMREAD_COLOR);
    if (originalImg.empty()) {
        std::cerr << "Lỗi: Không thể đọc ảnh. Đảm bảo có tệp 'input.jpg' trong thư mục chương trình." << std::endl;
        return -1;
    }

    // Khởi tạo ảnh đích với cùng kích thước và kiểu với ảnh nguồn, được lấp đầy bằng số không (màu đen)
    warpedImg = cv::Mat::zeros(originalImg.rows, originalImg.cols, originalImg.type());

    // Định nghĩa 3 điểm nguồn (các góc của một tam giác)
    sourceTrianglePts[0] = cv::Point2f(0, 0);
    sourceTrianglePts[1] = cv::Point2f(static_cast<float>(originalImg.cols - 1), 0);
    sourceTrianglePts[2] = cv::Point2f(0, static_cast<float>(originalImg.rows - 1));

    // Định nghĩa 3 điểm đích tương ứng
    destTrianglePts[0] = cv::Point2f(static_cast<float>(originalImg.cols * 0.1), static_cast<float>(originalImg.rows * 0.2));
    destTrianglePts[1] = cv::Point2f(static_cast<float>(originalImg.cols * 0.7), static_cast<float>(originalImg.rows * 0.3));
    destTrianglePts[2] = cv::Point2f(static_cast<float>(originalImg.cols * 0.2), static_cast<float>(originalImg.rows * 0.7));

    // Tính toán ma trận biến đổi affine từ 3 cặp điểm
    affineTransformMatrix = cv::getAffineTransform(sourceTrianglePts, destTrianglePts);

    // Áp dụng biến đổi affine
    cv::warpAffine(originalImg, warpedImg, affineTransformMatrix, warpedImg.size());

    // Tính toán ma trận xoay: xoay quanh tâm ảnh -45 độ (chiều kim đồng hồ), tỷ lệ co giãn 0.7
    cv::Point2f center = cv::Point2f(static_cast<float>(warpedImg.cols / 2), static_cast<float>(warpedImg.rows / 2));
    double rotationAngle = -45.0; // Xoay 45 độ theo chiều kim đồng hồ
    double scaleFactor = 0.7;    // Co giãn xuống

    rotationMatrix = cv::getRotationMatrix2D(center, rotationAngle, scaleFactor);
    cv::warpAffine(warpedImg, rotatedAndWarpedImg, rotationMatrix, warpedImg.size());

    // Hiển thị các kết quả
    cv::imshow(ORIGINAL_WINDOW, originalImg);
    cv::imshow(WARPED_WINDOW, warpedImg);
    cv::imshow(ROTATED_WARPED_WINDOW, rotatedAndWarpedImg);

    cv::waitKey(0); // Chờ cho đến khi người dùng nhấn phím
    return 0;
}

// Hàm hiển thị hướng dẫn sử dụng cho biến đổi affine
static void displayAffineInstructions() {
    std::cout << "Phiên bản OpenCV hiện tại: " << CV_VERSION << std::endl;
    std::cout << "Chương trình minh họa biến đổi Affine tổng hợp." << std::endl;
}

Thẻ: opencv C++ computer vision Hough Transform Image Processing

Đăng vào ngày 24 tháng 10 lúc 09:01