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.
Nội dung bài viế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.
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ụng | Cá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ải | Vị 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 ảnh | cv2.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ớn | OpenCV 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.getOptimalDFTSizecho 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).


