Học AI & Lập trình

Biến đổi Fourier: lọc vân sọc tuần hoàn để lộ vết lỗi bề mặt

Dùng FFT tìm đỉnh tần số của hoa văn lặp lại trên vải, lưới, kim loại, xóa bằng notch filter rồi phát hiện vết bẩn mờ. Code NumPy và OpenCV có kết quả kiểm chứng.

Biến đổi Fourier: lọc vân sọc tuần hoàn để lộ vết lỗi bề mặt
Nội dung bài viết
  1. Miền tần số: hiểu trong một phút
  2. 1. Ảnh thử: vân sọc và một vết bẩn mờ
  3. 2. Xem phổ và tìm đỉnh tần số
  4. 3. Lọc chặn dải (notch filter)
  5. 4. Phát hiện vết lỗi trên ảnh đã lọc
  6. Ứng dụng khác của FFT trong Machine Vision
  7. Lưu ý khi dùng với ảnh thật

Kiểm tra lỗi trên bề mặt vải, lưới lọc, tấm kim loại phay xước đều, màn hình LCD... có một khó khăn chung: bề mặt có hoa văn lặp lại rất rõ, còn vết lỗi lại mờ. Phân ngưỡng hay phát hiện cạnh thông thường sẽ bắt toàn bộ hoa văn thay vì lỗi. Công cụ kinh điển cho tình huống này là biến đổi Fourier (FFT): chuyển ảnh sang miền tần số, nơi hoa văn tuần hoàn chỉ là vài điểm sáng, xóa các điểm đó đi, rồi chuyển ngược lại. Hoa văn biến mất, lỗi hiện ra.

Bài dùng NumPy (np.fft) và OpenCV. Nên đọc trước: ảnh là mảng số và lọc ảnh và phát hiện cạnh.

Miền tần số: hiểu trong một phút

Mọi ảnh đều có thể viết thành tổng của rất nhiều "sóng" sáng tối với chu kỳ và hướng khác nhau. FFT cho biết mỗi sóng đóng góp bao nhiêu:

  • Tâm phổ (sau fftshift): tần số thấp, tức độ sáng trung bình và các thay đổi chậm.
  • Xa tâm: tần số cao, tức chi tiết nhỏ, cạnh sắc, nhiễu.
  • Hoa văn lặp đều: tập trung thành các đỉnh sáng, nằm đối xứng qua tâm. Khoảng cách từ tâm cho biết chu kỳ: với ảnh rộng W pixel, đỉnh cách tâm k điểm theo phương ngang ứng với chu kỳ W/k pixel.
  • Vết lỗi đơn lẻ: không lặp lại, nên năng lượng của nó trải mỏng ra nhiều tần số, không tạo đỉnh.

Chính sự khác biệt này cho phép tách hoa văn khỏi lỗi.

1. Ảnh thử: vân sọc và một vết bẩn mờ

import cv2
import numpy as np

H, W = 256, 256
yy, xx = np.mgrid[0:H, 0:W]

# Bề mặt vải/lưới: vân sọc tuần hoàn chu kỳ 8 px theo phương ngang
anh = 128 + 40 * np.sin(2 * np.pi * xx / 8)
# Lỗi: một vết bẩn tối, mờ, kích thước khoảng 12 px
anh -= 35 * np.exp(-((xx - 170) ** 2 + (yy - 90) ** 2) / (2 * 6 ** 2))
anh = np.clip(anh + np.random.default_rng(0).normal(0, 3, anh.shape), 0, 255).astype(np.uint8)

_, nhi_phan = cv2.threshold(anh, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
print("Phân ngưỡng thẳng: tỉ lệ pixel bị coi là tối =", round(np.count_nonzero(nhi_phan) / nhi_phan.size, 2))
Phân ngưỡng thẳng: tỉ lệ pixel bị coi là tối = 0.38

Phân ngưỡng Otsu coi 38% ảnh là "tối", tức là bắt toàn bộ các vạch tối của vân sọc. Vết bẩn chìm hoàn toàn trong hoa văn.

2. Xem phổ và tìm đỉnh tần số

F = np.fft.fftshift(np.fft.fft2(anh.astype(np.float32)))
pho = np.log1p(np.abs(F))                       # phổ biên độ, thang log để dễ nhìn
tam_y, tam_x = H // 2, W // 2

# Tìm các đỉnh nổi bật, bỏ qua vùng tần số thấp quanh tâm
pho_tim = pho.copy()
cv2.circle(pho_tim, (tam_x, tam_y), 10, 0, -1)
for _ in range(2):
    y, x = np.unravel_index(np.argmax(pho_tim), pho_tim.shape)
    print(f"Đỉnh tại (x={x}, y={y}): lệch tâm ({x - tam_x}, {y - tam_y}) -> chu kỳ {W / abs(x - tam_x):.0f} px")
    cv2.circle(pho_tim, (int(x), int(y)), 5, 0, -1)
Đỉnh tại (x=96, y=128): lệch tâm (-32, 0) -> chu kỳ 8 px
Đỉnh tại (x=160, y=128): lệch tâm (32, 0) -> chu kỳ 8 px

Hai đỉnh đối xứng cách tâm 32 điểm theo phương ngang: 256/32 = 8 px, đúng chu kỳ vân sọc ta tạo. Trong thực tế, bạn có thể hiển thị pho (chuẩn hóa về 0–255) để nhìn tận mắt các đỉnh, rồi xác định vị trí một lần trên ảnh mẫu OK. Vì hoa văn sản phẩm cố định, vị trí đỉnh cũng cố định từ ảnh này sang ảnh khác.

Vì sao phải che vùng tâm?Tâm phổ luôn là điểm sáng nhất (độ sáng trung bình của ảnh). Che một vòng nhỏ quanh tâm trước khi tìm đỉnh để không chọn nhầm nó.

3. Lọc chặn dải (notch filter)

Tạo mặt nạ bằng 1 ở mọi nơi, bằng 0 tại các đỉnh, nhân với phổ rồi biến đổi ngược. Bờ mặt nạ được làm mềm bằng Gaussian: cắt đột ngột trong miền tần số gây ra gợn vòng (ringing) trong ảnh kết quả.

def loc_chan_dai(F, cac_dinh, ban_kinh=4):
    """Notch filter: xóa (làm mềm) vùng quanh các đỉnh tần số."""
    mat_na = np.ones(F.shape, np.float32)
    for (x, y) in cac_dinh:
        cv2.circle(mat_na, (x, y), ban_kinh, 0, -1)
    mat_na = cv2.GaussianBlur(mat_na, (9, 9), 0)        # bờ mềm, tránh gợn vòng (ringing)
    return F * mat_na


F_loc = loc_chan_dai(F, [(tam_x - 32, tam_y), (tam_x + 32, tam_y)])
anh_loc = np.real(np.fft.ifft2(np.fft.ifftshift(F_loc)))
print(f"Độ lệch chuẩn: trước lọc {anh.std():.1f}, sau lọc {anh_loc.std():.1f}")
Độ lệch chuẩn: trước lọc 28.5, sau lọc 3.7

Độ lệch chuẩn của ảnh giảm từ 28,5 xuống 3,7: gần như toàn bộ biến thiên của vân sọc đã bị loại, chỉ còn lại nhiễu cảm biến và vết bẩn. Lưu ý phải xóa cả hai đỉnh đối xứng; phổ của ảnh thực luôn đối xứng qua tâm, xóa một bên sẽ làm ảnh kết quả có phần ảo khác 0.

4. Phát hiện vết lỗi trên ảnh đã lọc

Ảnh đã sạch hoa văn nên chỉ cần so mỗi pixel với độ sáng nền cục bộ (ước lượng bằng Gaussian blur rất rộng) là đủ:

nen = cv2.GaussianBlur(anh_loc, (0, 0), 25)             # độ sáng nền thay đổi chậm
chenh = nen - anh_loc                                    # dương ở chỗ tối hơn nền
mat_na = (chenh > 15).astype(np.uint8) * 255
n, _, stats, tam = cv2.connectedComponentsWithStats(mat_na)
for i in range(1, n):
    if stats[i, cv2.CC_STAT_AREA] >= 20:
        print(f"Vết lỗi tại ({tam[i][0]:.0f}, {tam[i][1]:.0f}), diện tích {stats[i, cv2.CC_STAT_AREA]} px")
Vết lỗi tại (170, 90), diện tích 165 px

Đúng vị trí (170, 90) của vết bẩn ta tạo. Để thấy tác dụng của bước lọc, chạy cùng cách dò trên ảnh chưa lọc:

# So sánh: cùng cách dò nhưng trên ảnh CHƯA lọc
chenh_goc = cv2.GaussianBlur(anh.astype(np.float32), (0, 0), 25) - anh
n_goc = cv2.connectedComponents((chenh_goc > 15).astype(np.uint8))[0] - 1
print("Ảnh chưa lọc: số vùng 'lỗi' =", n_goc)
Ảnh chưa lọc: số vùng 'lỗi' = 32

Không lọc, mỗi vạch tối của vân sọc đều thành một "lỗi": 32 vạch trên ảnh rộng 256 px chu kỳ 8 px. Thuật toán vô dụng.

Ứng dụng khác của FFT trong Machine Vision

Ứng dụngCách dùng
Nhiễu sọc do điện (nhiễu đường dây, dây cáp camera chạy cạnh biến tần)Notch filter như trên. Nhưng hãy sửa tận gốc: đi lại dây, nối đất, dùng cáp chống nhiễu
Đo chu kỳ, bước lưới, mật độ sợi vảiVị trí đỉnh phổ cho chu kỳ trung bình chính xác hơn đo từng vạch
Đo độ lệch giữa hai ảnhcv2.phaseCorrelate, dùng trong bài so sánh với ảnh mẫu
Đánh giá ảnh có nét không (lấy nét tự động)Ảnh nét có nhiều năng lượng ở tần số cao hơn ảnh mờ
Template matching ảnh lớnOpenCV dùng DFT bên trong matchTemplate khi mẫu đủ lớn để tăng tốc

Lưu ý khi dùng với ảnh thật

  • Hiệu ứng biên: FFT coi ảnh như lặp lại vô hạn, nên mép trái và mép phải "nối" vào nhau tạo một vạch giả trong phổ. Nhân ảnh với cửa sổ (ví dụ cv2.createHanningWindow) trước FFT khi cần phân tích phổ chính xác.
  • Hoa văn không đều (vải co giãn, lưới cong): đỉnh phổ bị nhòe, cần notch rộng hơn, hoặc chia ảnh thành từng ô nhỏ để xử lý.
  • Lỗi cũng tuần hoàn (ví dụ một sợi bị đứt chạy suốt chiều dài vải): lỗi có thể nằm trong chính tần số bạn xóa. Kiểm tra kỹ trên ảnh lỗi thật trước khi triển khai.
  • Tốc độ: FFT nhanh nhất khi kích thước ảnh là tích của các số nhỏ (2, 3, 5). cv2.getOptimalDFTSize cho biết kích thước nên đệm thêm.

Bài tập

Thêm vân sọc thứ hai theo phương dọc, chu kỳ 16 px: anh += 20 * np.sin(2 * np.pi * yy / 16) (trước bước clip). Dự đoán vị trí hai đỉnh mới trong phổ, sửa vòng lặp tìm đỉnh để tìm 4 đỉnh, rồi lọc cả hai hoa văn.

Đáp án dự đoán

Chu kỳ 16 px trên ảnh cao 256 px ứng với 256/16 = 16 điểm cách tâm theo phương dọc: đỉnh tại (x=128, y=112) và (x=128, y=144).

Nguồn tham khảo: OpenCV-Python Tutorials: Fourier Transform, NumPy: Discrete Fourier Transform (numpy.fft).

Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.