Image Kernels & Filters
Kernel ảnh - Bộ lọc tích chập
Bạn có ảnh 10×10 pixel. Một ma trận 3×3 (gọi là 'kernel') trượt qua từng vị trí, nhân với vùng ảnh bên dưới rồi cộng lại. Kết quả bạn đoán là gì?
Hình minh họa
Chọn preset kernel
Nhấn vào tên preset để load ma trận chuẩn. Bạn cũng có thể tự chỉnh các ô trong ma trận bên dưới.
Edge Detection — Phát hiện MỌI cạnh theo mọi hướng. Tổng = 0 nên vùng đồng nhất ra 0 (đen), chỉ cạnh mới ra giá trị lớn.
Ma trận kernel (có thể chỉnh)
Kết quả mỗi pixel = (tổng tích / divisor) + offset, rồi clamp về [0, 255].
Walkthrough tại vị trí (4, 4)
Nhấn vào bất kỳ ô nào trong ảnh đầu vào để di chuyển "cửa sổ" 3×3.
Ảnh đầu vào (10×10)
Khung cam: vùng 3×3 được tích chập. Ô đỏ: tâm kernel.
Ảnh đầu ra (sau tích chập)
Ảnh cập nhật ngay khi bạn đổi kernel.
Walkthrough tích chập: 9 tích + 1 tổng
Mỗi ô = pixel_đầu_vào × kernel. Cộng 9 ô lại, chia divisor, cộng offset, clamp → 1 pixel đầu ra.
Kernel là bộ lọc có thể học. Trong xử lý ảnh truyền thống, kỹ sư ngồi thiết kế từng giá trị bằng tay: Sobel, Prewitt, Laplacian, Gaussian… Nhưng CNN hiện đại đi xa hơn — nó để backpropagation tự tìm những kernel tối ưu cho từng tác vụ cụ thể. Một lớp Conv trong ResNet có 64 kernel; mỗi kernel học phát hiện một đặc trưng khác nhau: cạnh dọc, cạnh ngang, đốm tròn, texture da, biên màu…
Điều đó có nghĩa là: thay vì lập trình mắt nhân tạo từng bộ lọc một, ta chỉ cần định nghĩa kiến trúc (có bao nhiêu lớp, mỗi lớp bao nhiêu kernel) rồi để dữ liệu tự dạy nó nhìn.
Kernel Gaussian Blur có các giá trị [[1,2,1],[2,4,2],[1,2,1]] và chia cho 16. Tại sao tâm lại có giá trị 4 (lớn nhất)?
Giải thích
Image Kernel (còn gọi là filter, mask, convolution matrix) là một ma trận nhỏ — thường 3×3, 5×5, hoặc 7×7 — thực hiện phép tích chập 2D với ảnh. Đây là thao tác cốt lõi của mạng nơ-ron tích chập (CNN) và mọi thuật toán xử lý ảnh truyền thống từ 1960s đến nay.
Công thức toán học của tích chập 2D rời rạc (với ảnh I và kernel K):
Kernel K trượt qua ảnh I. Tại mỗi vị trí (i, j): lấy vùng 3×3 của ảnh xung quanh điểm đó, nhân element-wise với K, rồi cộng 9 giá trị lại thành 1 pixel đầu ra.
Identity: [[0,0,0],[0,1,0],[0,0,0]]. Giữ nguyên ảnh — dùng để kiểm tra pipeline.
Edge Detection (Laplacian 8-neighbor): tâm +8, xung quanh -1. Tổng = 0 → vùng phẳng ra 0, chỉ cạnh ra giá trị lớn.
Sobel X/Y: phát hiện cạnh theo trục — cơ sở của edge detection Canny. Kết hợp √(Gx² + Gy²) cho edge magnitude tổng hợp.
Gaussian Blur: xấp xỉ phân phối chuẩn 2D. Làm mượt ảnh, giảm nhiễu, là bước tiền xử lý trước edge detection.
Sharpen (Unsharp Mask): khuếch đại sai biệt giữa pixel và lân cận, làm chi tiết rõ nét hơn.
Emboss: hiệu ứng chạm nổi — thấy ảnh như điêu khắc 3D bằng cách nhấn mạnh gradient theo một hướng.
Trong xử lý ảnh truyền thống (OpenCV, PIL, scikit-image), kernel được thiết kế bằng tay dựa trên công thức toán học cố định. Đây là cách làm đã ngự trị suốt 50+ năm.
Trong CNN hiện đại (AlexNet 2012, VGG, ResNet, EfficientNet, ConvNeXt…), kernel là parameter được học qua backpropagation. Khởi tạo ngẫu nhiên, sau đó SGD/Adam tự điều chỉnh từng giá trị để giảm loss. Mạng tự tìm bộ lọc tối ưu. ResNet-50 có hơn 23 triệu parameter kernel — không cách nào thiết kế thủ công!
Padding: thêm hàng/cột 0 quanh ảnh trước khi tích chập. Padding = 1 với kernel 3×3 giữ nguyên kích thước output. PyTorch: padding=1 hoặc padding='same'.
Stride: bước nhảy của kernel. Stride = 1 (mặc định): quét mọi vị trí. Stride = 2: bỏ qua 1 vị trí, output nhỏ hơn ~1/2.
Receptive field:vùng pixel gốc mà một neuron ở lớp sâu "nhìn" được. Càng xếp chồng nhiều lớp Conv, receptive field càng rộng — lớp sâu có thể "thấy" cả khuôn mặt, lớp đầu chỉ thấy vài pixel.
- Với blur để giảm nhiễu, thử Gaussian 5×5 sigma=1.0 — tốt hơn 3×3.
- Trước edge detection, luôn blur nhẹ trước để tránh nhận cạnh giả do nhiễu.
- Kernel sắc (sharpen) mạnh tay có thể gây halo artifact ở cạnh.
- Muốn phát hiện cạnh mạnh hơn Sobel? Thử Scharr hoặc Canny.
import cv2
import numpy as np
# ── Đọc ảnh grayscale ────────────────────────────────────────
img = cv2.imread("ho_guom.jpg", cv2.IMREAD_GRAYSCALE)
# ── Kernel phát hiện cạnh (Laplacian 8-neighbor) ────────────
edge_kernel = np.array([
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1],
], dtype=np.float32)
# ── Kernel Gaussian blur 5×5, sigma=1.0 ─────────────────────
gauss = cv2.getGaussianKernel(ksize=5, sigma=1.0)
blur_kernel = gauss @ gauss.T # 2D Gaussian = tích ngoài của 1D
# ── Kernel sharpen (Unsharp Mask đơn giản) ──────────────────
sharpen_kernel = np.array([
[ 0, -1, 0],
[-1, 5, -1],
[ 0, -1, 0],
], dtype=np.float32)
# ── Kernel emboss ───────────────────────────────────────────
emboss_kernel = np.array([
[-2, -1, 0],
[-1, 1, 1],
[ 0, 1, 2],
], dtype=np.float32)
# ── Áp dụng tích chập: filter2D(image, depth, kernel) ───────
# depth = -1 → cùng depth với ảnh gốc (uint8)
edges = cv2.filter2D(img, -1, edge_kernel)
blurred = cv2.filter2D(img, -1, blur_kernel)
sharp = cv2.filter2D(img, -1, sharpen_kernel)
emboss = cv2.filter2D(img, -1, emboss_kernel) + 128 # offset
# ── Sobel sẵn có trong OpenCV (chính xác hơn, hỗ trợ float) ─
sobel_x = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, dx=0, dy=1, ksize=3)
# Độ lớn gradient tổng hợp
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
magnitude = np.clip(magnitude, 0, 255).astype(np.uint8)
# ── Edge detection Canny (tổ hợp nhiều bước, rất mạnh) ──────
canny = cv2.Canny(img, threshold1=100, threshold2=200)
# ── Ghi ảnh ra file ─────────────────────────────────────────
cv2.imwrite("edges.jpg", edges)
cv2.imwrite("blurred.jpg", blurred)
cv2.imwrite("sharp.jpg", sharp)
cv2.imwrite("emboss.jpg", emboss)
cv2.imwrite("gradient.jpg", magnitude)
cv2.imwrite("canny.jpg", canny)Nếu bạn chỉ cần một kernel có sẵn, OpenCV cung cấp cv2.Sobel, cv2.Laplacian, cv2.GaussianBlur, cv2.medianBlur… Còn khi cần kernel tuỳ biến, dùng cv2.filter2D như trên.
import torch
import torch.nn as nn
import torch.nn.functional as F
# ── Một lớp Conv2D có 32 kernel 3×3, học qua backprop ──────
conv = nn.Conv2d(
in_channels=3, # ảnh RGB
out_channels=32, # 32 kernel → 32 feature map
kernel_size=3,
stride=1,
padding=1, # giữ kích thước (padding='same')
bias=True,
)
# Kernel weights: shape (32, 3, 3, 3)
# 32 = số kernel
# 3 = số kênh input (RGB)
# 3×3 = kích thước spatial
print(conv.weight.shape) # torch.Size([32, 3, 3, 3])
# ── Khởi tạo ngẫu nhiên He (phù hợp với ReLU) ──────────────
nn.init.kaiming_normal_(conv.weight, nonlinearity='relu')
# ── Forward pass: ảnh (batch=1, C=3, H=64, W=64) ───────────
x = torch.randn(1, 3, 64, 64)
y = conv(x)
print(y.shape) # torch.Size([1, 32, 64, 64])
# ── Training loop đơn giản ─────────────────────────────────
optimizer = torch.optim.Adam(conv.parameters(), lr=1e-3)
target = torch.randn(1, 32, 64, 64)
for step in range(100):
optimizer.zero_grad()
pred = conv(x)
loss = F.mse_loss(pred, target)
loss.backward() # ← BACKPROP điều chỉnh kernel weights
optimizer.step()
# Sau training, conv.weight đã thay đổi — mạng đã "học" ra
# những kernel tối ưu cho bài toán, KHÔNG CẦN thiết kế thủ công.Sự khác biệt cốt lõi: ở code đầu (OpenCV), bạn ghi rõ từng giá trị kernel. Ở code sau (PyTorch), bạn chỉ nói "tôi muốn 32 kernel 3×3" rồi để backprop tự tìm giá trị. Đây chính là cuộc cách mạng mà deep learning mang tới cho computer vision.
So sánh: Kernel thủ công vs Kernel học được
| Tiêu chí | Thủ công (OpenCV) | Học được (CNN) |
|---|---|---|
| Nguồn gốc giá trị | Kỹ sư thiết kế theo công thức | Backprop tự tìm từ dữ liệu |
| Cần dữ liệu? | Không | Có — nhiều càng tốt (thường 10K+ ảnh) |
| Tổng quát hoá | Cố định — không thích nghi | Rất tốt với task đã huấn luyện |
| Khả năng diễn giải | Dễ — nhìn giá trị là hiểu | Khó — phải dùng Grad-CAM, feature viz… |
| Hiệu năng trên task phức tạp | Thấp — không học được khái niệm cao cấp | Cao — học được mắt, mũi, bánh xe, khuôn mặt… |
| Chi phí tính toán | Rất rẻ — chỉ một phép filter | Tốn GPU khi training, inference vẫn nhẹ |
| Use case | Preprocessing, industrial vision đơn giản | Classification, detection, segmentation hiện đại |
Đi sâu: 1×1 Convolution — kernel kỳ lạ nhưng cực mạnh
Một kernel 1×1 có vẻ vô nghĩa — nó chỉ nhân pixel với một số. Nhưng trong CNN với nhiều kênh, 1×1 convolution thực sự là một phép combine các feature map: với input C kênh và C' kernel 1×1, mỗi kernel là một vector trọng số C chiều, thực hiện linear combination của C feature map input thành 1 feature map output.
Ứng dụng: Inception dùng 1×1 để giảm chiều kênh trước khi conv 3×3/5×5 đắt đỏ, ResNet-50 dùng bottleneck 1×1 → 3×3 → 1×1 để tiết kiệm tham số, MobileNet dùng 1×1 kết hợp với depthwise conv cho mobile inference.
Trực giác hình ảnh: tại sao tổng kernel quyết định hiệu ứng
Có một cách đơn giản để "đoán" một kernel làm gì chỉ từ tổng của nó:
- Tổng = 1:giữ độ sáng trung bình (Identity, Gaussian sau khi chia, Sharpen). Đây là kernel "bảo toàn năng lượng" — ảnh không sáng lên/tối đi tổng thể.
- Tổng > 1: làm ảnh sáng lên (hiếm dùng trực tiếp).
- Tổng < 1 nhưng > 0: làm ảnh tối đi.
- Tổng = 0: highlight sự thay đổi — vùng đồng nhất cho 0 (đen), chỉ cạnh/biên mới cho giá trị khác 0. Tất cả edge detector đều có tổng = 0.
- Kernel đối xứng: tạo hiệu ứng không thiên hướng (blur, sharpen).
- Kernel bất đối xứng: tạo hiệu ứng có hướng (Sobel X/Y, Emboss).
Thử nghiệm ngay ở phần playground phía trên: điền 9 số bất kỳ vào ma trận, quan sát ảnh output. Nếu bạn nhập [1,1,1; 1,1,1; 1,1,1] và divisor = 9, đó là box blur. Nhập [-1,0,1; -1,0,1; -1,0,1] thì ra một edge detector dọc thô sơ. Trực giác này rất hữu ích khi debug CNN hoặc khi thiết kế bộ lọc preprocessing.
Code: Separable Convolution trong thực tế
Nếu kernel 2D có rank = 1 (SVD cho đúng 1 giá trị kỳ dị khác 0), ta có thể tách thành tích ngoài của 2 vector 1D. Dưới đây là triển khai thuần Python + NumPy để minh hoạ tiết kiệm tính toán:
import numpy as np
from scipy.signal import convolve2d
# ── 1) Kernel Gaussian 5×5 (σ=1) ──────────────────────────
def gaussian_1d(k=5, sigma=1.0):
x = np.arange(k) - (k - 1) / 2
g = np.exp(-(x**2) / (2 * sigma**2))
return g / g.sum()
g1 = gaussian_1d(5, 1.0) # shape (5,)
G2d = np.outer(g1, g1) # shape (5,5), sum = 1
# ── 2) Kiểm tra rank bằng SVD: kernel separable nếu rank=1 ─
U, S, Vt = np.linalg.svd(G2d)
print("Giá trị kỳ dị:", S.round(4)) # chỉ S[0] khác 0 đáng kể
print("Tỉ lệ S[1]/S[0]:", S[1] / S[0]) # ~ 1e-16 — separable!
# ── 3) Cách "ngây thơ": conv2D trực tiếp O(k²) ────────────
img = np.random.rand(512, 512)
out_2d = convolve2d(img, G2d, mode="same", boundary="symm")
# ── 4) Cách tối ưu: 2 lần conv1D O(2k) ────────────────────
# kernel 1D dọc → ngang, kết quả TƯƠNG ĐƯƠNG cách 3)
out_sep_v = convolve2d(img, g1[:, None], mode="same", boundary="symm")
out_sep = convolve2d(out_sep_v, g1[None, :], mode="same", boundary="symm")
# ── 5) So sánh sai số ─────────────────────────────────────
diff = np.abs(out_2d - out_sep).max()
print(f"Max diff = {diff:.2e}") # ~ 1e-14, gần như = 0
# ── 6) Đếm FLOPs ──────────────────────────────────────────
# Naïve 2D: k² = 25 nhân/pixel → 25 · 512² = 6.55M
# Separable: 2k = 10 nhân/pixel → 2.62M (giảm 60%)
# Với kernel 11×11: 121 vs 22 → giảm 82%Trên GPU, lợi ích của separable conv đến từ việc hai bước 1D có dạng tensor nhỏ hơn, dễ tile vào shared memory. OpenCV tự nhận ra kernel symmetric separable và gọi sepFilter2D ở bên dưới khi bạn dùng GaussianBlur. Trên CPU, SIMD cũng khai thác 1D conv tốt hơn 2D.
Code: Depthwise Separable Convolution (MobileNet)
Depthwise-separable là một phát minh của MobileNet (2017): tách một conv chuẩn thành 2 bước — depthwise (mỗi kênh input có 1 kernel riêng) và pointwise (conv 1×1 để trộn kênh). Tổng chi phí giảm ~8–9× so với conv chuẩn.
import torch
import torch.nn as nn
# ── Conv chuẩn: 32 → 64 kênh, kernel 3×3 ──────────────────
# tham số: 32 × 64 × 3 × 3 = 18,432
standard_conv = nn.Conv2d(
in_channels=32, out_channels=64,
kernel_size=3, padding=1, bias=False,
)
print("Standard params:", sum(p.numel() for p in standard_conv.parameters()))
# ── Depthwise separable: 2 lớp thay cho 1 ─────────────────
class DepthwiseSeparable(nn.Module):
def __init__(self, in_ch, out_ch, k=3):
super().__init__()
# 1) Depthwise — mỗi kênh input 1 kernel riêng (groups = in_ch)
self.depthwise = nn.Conv2d(
in_ch, in_ch,
kernel_size=k, padding=k // 2,
groups=in_ch, # ← mấu chốt
bias=False,
)
# 2) Pointwise — conv 1×1 để trộn kênh từ in_ch → out_ch
self.pointwise = nn.Conv2d(
in_ch, out_ch,
kernel_size=1,
bias=False,
)
self.bn1 = nn.BatchNorm2d(in_ch)
self.bn2 = nn.BatchNorm2d(out_ch)
self.act = nn.ReLU6(inplace=True)
def forward(self, x):
x = self.act(self.bn1(self.depthwise(x)))
x = self.act(self.bn2(self.pointwise(x)))
return x
dsc = DepthwiseSeparable(32, 64)
print("DW-Sep params:", sum(p.numel() for p in dsc.parameters() if p.requires_grad))
# Tham số: depthwise 32·3·3 = 288 + pointwise 32·64·1·1 = 2048 = 2336
# So với conv chuẩn 18,432 → giảm ~7.9 lần! Accuracy giảm <1% trên ImageNet.
# Công thức chung: tiết kiệm ≈ 1/N + 1/k²
# N = số kênh output, k = kích thước kernel
# Với N=64, k=3: 1/64 + 1/9 = 0.127 → chỉ tốn 12.7% chi phí gốc.
x = torch.randn(1, 32, 56, 56)
print(standard_conv(x).shape) # [1, 64, 56, 56]
print(dsc(x).shape) # [1, 64, 56, 56] — cùng output shapeKể từ MobileNet, mọi mạng CNN hướng mobile (MobileNetV2/V3, EfficientNet, EfficientNet-Lite, ShuffleNet, GhostNet…) đều dựa trên depthwise-separable. ConvNeXt (2022) cũng dùng depthwise 7×7 để "đuổi kịp" ViT về receptive field rộng trong khi vẫn giữ tham số hợp lý.
Bạn áp dụng kernel Sobel X lên một bức ảnh chụp cầu thang (có nhiều cạnh dọc và ngang). Đoán kết quả bạn sẽ thấy?
Muốn đi sâu hơn? Xem convolution, sau đó CNN để hiểu cách nhiều lớp Conv xếp chồng học hierarchy đặc trưng.
- Image kernel là ma trận nhỏ (3×3, 5×5) trượt trên ảnh, thực hiện nhân element-wise rồi cộng — tạo ra pixel đầu ra mới.
- Các kernel kinh điển: Identity (giữ nguyên), Edge (phát hiện cạnh), Sobel X/Y (cạnh theo trục), Gaussian (làm mờ), Sharpen (làm sắc), Emboss (chạm nổi), Laplacian (đạo hàm 2).
- Tổng kernel quyết định: tổng = 1 → giữ độ sáng; tổng = 0 → highlight thay đổi; chia divisor để chuẩn hoá; cộng offset khi có giá trị âm.
- Padding giữ kích thước output, stride điều khiển độ phân giải output — cả hai đều có trong mọi framework deep learning.
- Xử lý ảnh truyền thống: kernel thiết kế thủ công (OpenCV). CNN hiện đại: kernel HỌC qua backpropagation — mạng tự tìm bộ lọc tối ưu cho tác vụ.
- Kernel 3×3 xếp chồng tạo receptive field lớn mà vẫn ít tham số — đó là lý do kiến trúc như ResNet, EfficientNet đều chủ yếu dùng 3×3.
Kiểm tra hiểu biết
Phép tích chập 2D (2D convolution) với kernel 3×3 làm gì ở MỖI vị trí?