0
0
Lập trình
NM

Khôi Phục Hình Ảnh từ Biến Đổi Fourier 2D

Đăng vào 4 tháng trước

• 6 phút đọc

Khôi Phục Hình Ảnh từ Biến Đổi Fourier 2D

Biến đổi Fourier 2D (FFT) là một công cụ mạnh mẽ trong xử lý hình ảnh, thường được sử dụng cho các tác vụ như khử nhiễu. Nó hoạt động bằng cách phân tích một hình ảnh thành các thành phần tần số cơ bản - về cơ bản là một tập hợp các sóng sine đơn giản.

Tôi luôn hiểu phần phân tích, nhưng điều này khiến tôi đặt ra câu hỏi: Chúng ta có thể đảo ngược quá trình này không? Có thể khôi phục hoàn hảo hình ảnh gốc chỉ bằng cách cộng tất cả các thành phần tần số trở lại với nhau? Để trả lời câu hỏi này, tôi đã xây dựng một ứng dụng để kiểm chứng.

Mục Lục

Tổng Quan

Ứng dụng này bao gồm hai phần chính: một mô-đun FFT và một GUI. Phần đầu tiên phân tích hình ảnh gốc, và phần còn lại hình dung toàn bộ quá trình.

Phần FFT

Đầu tiên, chúng ta thực hiện một phép dịch FFT để di chuyển thành phần tần số bằng không đến trung tâm của hình ảnh. Hàm Python dưới đây sau đó lấy dữ liệu đã dịch này và sắp xếp tất cả các thành phần tần số. Nó thực hiện điều này bằng cách tính toán khoảng cách của từng thành phần từ trung tâm, tương ứng với tần số của nó (từ thấp đến cao).

Quan trọng là, chúng ta cũng lưu trữ tọa độ (x, y) gốc cho mỗi thành phần. Chúng ta sẽ cần những thông tin này để đặt mọi thứ trở lại đúng vị trí trong giai đoạn tái tổng hợp.

python Copy
fft_result = fft2(img)

def get_sorted_freq_components(self, fft_shifted: np.ndarray) -> List[Dict]:
    h, w = fft_shifted.shape
    center_x, center_y = h // 2, w // 2

    freq_components = []
    for y in range(h):
        for x in range(w):
            distance = np.sqrt((y - center_y)**2 + (x - center_x)**2)
            freq_components.append({
                "distance": distance,
                "value": fft_shifted[y, x],
                "y": y,
                "x": x
            })

    freq_components.sort(key = lambda item: item["distance"])
    return freq_components

Giao Diện Đồ Họa: Hình Ảnh Khôi Phục

Tính năng chính của GUI là một màn hình cập nhật theo thời gian thực khi chúng ta tái tạo hình ảnh. Ý tưởng rất đơn giản: trong một vòng lặp, chúng ta thêm một thành phần tần số tại một thời điểm (từ thấp đến cao) và cập nhật hình ảnh với kết quả, tạo ra một hoạt hình.

Khó Khăn Đầu Tiên: Màn Hình Đen

Tuy nhiên, lần thử đầu tiên của tôi không hoạt động như mong đợi. Khi tôi truyền mảng NumPy từ quá trình FFT ngược trực tiếp vào QImage của PySide6, tất cả những gì tôi nhận được là một màn hình đen. Hình ảnh đơn giản không hiển thị chính xác.

python Copy
def _create_scaled_pixmap(self, img: np.ndarray, frame: QFrame) -> QPixmap:
    h, w = img.shape
    bytes_per_line = w
    q_image = QImage(img, w, h, bytes_per_line, QImage.Format.Format_Grayscale8)
    pixmap = QPixmap.fromImage(q_image.copy())

    return pixmap.scaled(
        frame.size(),
        Qt.AspectRatioMode.KeepAspectRatio,
        Qt.TransformationMode.SmoothTransformation
    )

Khoảnh Khắc "Aha!": Mismatch Dữ Liệu

Sau một số lần gỡ lỗi, tôi nhận ra vấn đề là sự không tương thích về kiểu dữ liệu. QImage với định dạng Format_Grayscale8 mong đợi một đầu vào rất cụ thể: một mảng NumPy của các số nguyên không dấu 8-bit (uint8) với giá trị trong khoảng 0-255.

Mảng của tôi, là kết quả của FFT ngược, là một mảng số thực với thang đo hoàn toàn khác (ví dụ: từ -50.0 đến 3000.0). QImage không biết cách diễn giải các giá trị số thực này thành các pixel grayscale, dẫn đến màn hình đen.

Giải Pháp: Chuẩn Hóa Là Chìa Khóa

Để giải quyết vấn đề này, tôi đã phải thêm một bước xử lý trước. Trước khi tạo QImage, hàm giờ đây kiểm tra xem mảng đầu vào có đúng kiểu uint8 không. Nếu không, nó chuẩn hóa mảng - quy mô các giá trị của nó về khoảng 0-255 - và sau đó chuyển đổi kiểu dữ liệu của nó.

Điều này đảm bảo rằng dữ liệu luôn ở định dạng mà QImage có thể hiểu và hiển thị chính xác.

python Copy
def _create_scaled_pixmap(self, img: np.ndarray, frame: QFrame) -> QPixmap:
    if img.dtype != np.uint8:
        # Chuẩn hóa hình ảnh float về khoảng 0-255 và chuyển đổi sang uint8
        min_val, max_val = np.min(img), np.max(img)
        if min_val == max_val:
            img_norm = np.zeros_like(img)
        else:
            img_norm = (img - min_val) / (max_val - min_val)
        img = (255 * img_norm).astype(np.uint8)

    if not img.flags['C_CONTIGUOUS']:
        img = np.ascontiguousarray(img)

    h, w = img.shape
    bytes_per_line = w
    q_image = QImage(img, w, h, bytes_per_line, QImage.Format.Format_Grayscale8)
    pixmap = QPixmap.fromImage(q_image.copy())

    return pixmap.scaled(
        frame.size(),
        Qt.AspectRatioMode.KeepAspectRatio,
        Qt.TransformationMode.SmoothTransformation
    )

Kết Luận

Và đó là tất cả! Bằng cách xây dựng ứng dụng đơn giản này, chúng ta không chỉ hình dung quá trình thú vị của Biến đổi Fourier mà còn học được một bài học quý giá trong việc gỡ lỗi. Bài học lớn nhất đối với tôi là nhận ra tầm quan trọng của kiểu dữ liệu khi chuyển các mảng NumPy cho các khung GUI như PySide6. Khoảnh khắc "màn hình đen" đã dạy tôi rằng chuẩn hóa không chỉ là một khái niệm lý thuyết, mà là một nhu cầu thực tiễn.

Kết Quả Cuối Cùng trong Hành Động

Dưới đây là hình ảnh cuối cùng của ứng dụng của chúng ta, khôi phục thành công một hình ảnh từ một biển tần số, từng sóng một.

Xem Mã Nguồn Trên GitHub!

Tôi đã đăng toàn bộ mã nguồn cho ứng dụng này trên kho GitHub của mình. Hãy thoải mái sao chép, chạy thử và thử nghiệm với những hình ảnh của riêng bạn!

➡️ Kho GitHub của tôi

Nếu bạn thấy bài viết này hoặc dự án hữu ích, hãy cân nhắc để lại một ngôi sao ⭐️ trên kho. Điều đó sẽ làm tôi rất vui!

Cảm Ơn Bạn Đã Đọc!

Những khái niệm toán học nào khác mà bạn nghĩ sẽ thú vị để hình dung trong một ứng dụng như thế này? Hãy cho tôi biết ý tưởng của bạn trong phần bình luận dưới đây!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào