Chắc các bạn đã từng đọc sách trinh thám, điệp viên có nói đến kỹ thuật viết mật mã lên những quyển sách bằng loại mực không thể nhìn thấy bằng mặt thường. Để đọc được điệp viên phải quét một loại hoá chất lên sau đó soi đèn tia cực tím mới có thể đọc được mật mã. Hay như kho báu được chôn ở một mảnh đất mà đúng ngày giờ trong năm, khi bóng trăng rằm chiếu qua khe núi rọi đúng vào nơi chôn kho báu.

Trong điệp vụ hay chuyến tìm khó báu cần phải có hoá chất, tác động vật lý, chọn đúng thời điểm, đúng không gian, bí mật sẽ được mở ra. Còn hôm nay AI, Deep Learning, hay xử lý ảnh, xử lý tín hiệu số hôm nay cần có những kỹ thuật, hàm biến đổi, thuật toán để biến dữ liệu thành thông tin có ích.

Tiếp nối bài trước Convolution - Tích chập giải thích bằng code thực tế, bài này tôi sử dụng kỹ thuật convolution để xử lý ảnh: mờ (blur), sắc nét (sharp), nhận dạng cạnh (edge detect). Code minh hoạ viết bằng Python. Những bài viết của tôi ít có công thức toán học nhưng rất nhiều code ví dụ từ dễ đến phức tạp, giá trị nghiên cứu học thuật ít bởi những thuật toán này cũng rất cổ xưa rồi, chắc đã có cách đây đến 30 -40 năm. Trong sản phẩm thực tế, các bạn nên dùng hàm sẵn có của OpenCV hay thay Numpy bằng Cupy.
Toàn bộ code ví dụ nằm trong thư mục này https://github.com/TechMaster/CythonOpenCV/tree/master/Convolution tuy nhiên bạn cần clone cả git repo để lấy cả thư mục ảnh. Trong git repo này một số code tôi viết, một số code là của tác giả khác đó.

Bước 1: Chạy thử code bản cuối xem thế nào

Chạy thử file ConvoluteFilterMatplot.py ra nhiều kết quả bộ lọc khác nhau như thế này. Chú ý code chạy rất chậm vì phải lọc tuần tự 14 kiểu rồi xuất ra màn hình. Tôi sẽ thử Cupy để tăng tốc ở thí nghiệm bài sau. Lý thuyết convolution bài trước đã trình bày, code bài này dùng lại thư viện
convolute_lib.py 
Ở cuối bài, tôi share lại code ConvoluteFilterMatplot.py . Bạn có thể thay hàm cnn.convolve_np4(img, filter[1]) bằng cv2.filter2D(img, -1, filter[1]). Tốc độ tăng nhanh rất nhiều.

Bước 2: Giải thích một số phương pháp lọc qua ma trận kernel

identity = np.array((
    [0, 0, 0],
    [0, 1, 0],
    [0, 0, 0]))

Dot product của ma trận đầu vào với ma trận identity (đúng ra phải gọi là center one matrix để không nhầm ma trận toàn 1 trên đường chéo chính) được giá trị là phần tử nằm chính giữa ma trận đầu vào. Như vậy các điểm ảnh xung quanh đều nhân hệ số 0.0 và không tác động gì điểm ảnh chính giữa. Do đó với kernel indentiy, ma trận ra bằng ma trận đầu vào

boxblur = (1.0 / 9) * np.array(
    [[1, 1, 1],
     [1, 1, 1],
     [1, 1, 1]])

Cách làm mờ này có phong cách chủ nghĩa xã hội không tưởng, điểm chính giữa mới sẽ bằng tổng các điểm ảnh xung quanh và chính nó chia trung bình. Điểm nào sáng sẽ cho giảm xuống, điểm nào tối hơn sẽ được nâng lên. Kết quả mờ nhiều hay ít tuỳ thuộc kích thước ma trận kernel. Small Blur ma trận 7x7, Large Blur ma trận 21x21.

gaussian = (1.0 / 16) * np.array(
    [[1, 2, 1],
     [2, 4, 2],
     [1, 2, 1]])

Gaussian là cách mờ tự nhiên hơn, điểm chính giữa x hệ số 4, điểm trên - dưới - trái - phải x hệ số 2, điểm ở góc chéo x hệ số 1

sharpen = np.array((
    [0, -2, 0],
    [-2, 10, -2],
    [0, -2, 0]))

Trong sharpen, kernel, ta bắt đầu thấy các hệ số giá trị âm xen kẽ hệ số dương mục đích để đào sâu sự khác biệt điểm ảnh chính giữa với các điểm ảnh xung quanh. Kết quả đầu ra là mặt con mèo rất tương phản trắng đen.

emboss = np.array(
    [[-2, -1,  0],
     [-1,  1,  1],
     [ 0,  1,  2]])

Emboss là kỹ thuật dập nổi hoa văn hãy chữ trên bìa giấy. Bản chất kỹ thuật này là tạo tương phản chiều cao. Còn trong ma trận emboss, có 2 cụm hệ số đối lập âm - dương qua điểm chính giữa.

Edge Detection - Tìm cạnh, viền

Cạnh, viền trong một bức ảnh là nơi phân biệt rõ 2 miền điểm ảnh sáng vs tối hoặc màu A với màu B. Để đơn giản chúng ta số hoá được thành 2 mức High vs Low.

High = 100, Low = 10, khác biệt khá lớn
High =100, Low= 98, khác biệt nhỏ
Tạm gọi khác biệt là d = abs(High - Low)
Nếu hệ số trong kernel matrix là (2, -2) hoặc (-2, 2), chúng tính tổng kết quả sau khi nhân

  High = 100, Low = 10 -> d = 90 High = 100, Low = 98 -> d = 2
(2, -2) (2 * 100, -2 * 10) -> d = 180 (2*100, -2 *98)-> d = 4
(-2, 2) (-2*100, 2*10) -> d = 180 (-2*100, 2*98) -> d = 4
  khác biệt lớn từ 90 -> 180 Khác biệt tăng ít 2 -> 4

Khi dot product với kernel SobelLeft hay Right , nếu 2 điểm ảnh ở trái và phải điểm chính giữa khác nhau nhiều, thì điểm ảnh chính giữa sẽ thay đổi rất mạnh ~ sáng lên giúp đường biên giới nổi bật. Các vùng có mức đồng đều, cùng sáng hay cùng tối, khi dot product giá trị điểm ảnh nếu có tăng sẽ vẫn thấp ở đường biên giữa 2 miền sáng tối.

Kỹ thuật xử lý ảnh nào hữu ích cho máy học, deep learning?

Xử lý ảnh cho máy học khác rất nhiều xử lý ảnh kiểu Photoshop. Đặc điểm các thuật toán xử lý ảnh cho máy học:

  • Làm nổi bật các tính chất mà máy cần tìm: đường viền, khu vực cần quan tâm: mắt, mũi, mồm, súng, dao trong ảnh trích xuất từ scanner hải quan, khối u trong cơ thể.
  • Làm sao giảm được kích thước ảnh xuống để giảm khối lượng tính toán
  • Hiệu quả với ảnh này, như lại kém hiệu quả với ảnh kia do đó lập trình viên AI cần cảm quan đánh giá chọn thuật toán nào cho vào pipe line xử lý để tối ưu độ chính xác và tốc độ xử lý.

Cá nhân mình ghi chú vài tên thuật toán để chính mình mày mò tìm tiếp:

  • Contour detection: nhận dạng đường bao
  • Clustering: phân cụm
  • Noise remove: khử nhiễu
  • Transform: xoay nghiêng, phóng to thu nhỏ, co kéo để đối tượng cần quan sát hiện lên rõ nhất

Còn một thuật toán edge detection rất nổi tiếng nữa là Canny Edge Detection thì đã có bài viết chi tiết ở đây Canny Edge Detection Step by Step in Python — Computer Vision

Mã nguồn tham khảo

from pathlib import Path
import matplotlib.pyplot as plt
import cv2
import numpy as np

import convolute_lib as cnn

img_path = str(Path(__file__).parent.parent / 'Images/cat_bw_s.jpg')

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)

identity = np.array((
    [0, 0, 0],
    [0, 1, 0],
    [0, 0, 0]))

edge = np.array((
    [0,  1,  0],
    [1, -4,  1],
    [0,  1,  0]))

boxblur = (1.0 / 9) * np.array(
    [[1, 1, 1],
     [1, 1, 1],
     [1, 1, 1]])

gaussian = (1.0 / 16) * np.array(
    [[1, 2, 1],
     [2, 4, 2],
     [1, 2, 1]])

emboss = np.array(
    [[-2, -1,  0],
     [-1,  1,  1],
     [ 0,  1,  2]])

square = np.array(
    [[ 0,  2,  0],
     [-2, -1,  2],
     [ 0, -2,  0]])

# construct average blurring kernels used to smooth an image
smallBlur = np.ones((7, 7), dtype="float") * (1.0 / (7 * 7))
largeBlur = np.ones((21, 21), dtype="float") * (1.0 / (21 * 21))

# construct a sharpening filter
sharpen = np.array((
    [0, -2, 0],
    [-2, 10, -2],
    [0, -2, 0]))

laplacian = (1.0 / 16) * np.array(
    [[ 0,  0, -1,  0,  0],
     [ 0, -1, -2, -1,  0],
     [-1, -2, 16, -2, -1],
     [ 0, -1, -2, -1,  0],
     [ 0,  0, -1,  0,  0]])

sobelLeft = np.array((
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]))

sobelRight = np.array((
    [1, 0, -1],
    [2, 0, -2],
    [1, 0, -1]))

sobelTop = np.array((
    [-1, -2, -1],
    [ 0,  0,  0],
    [ 1,  2,  1]))

sobelBottom = np.array((
    [ 1,  2,  1],
    [ 0,  0,  0],
    [-1, -2, -1]))

filters = [
    ("Identity", identity),
    ("Edge", edge),
    ("Box Blur", boxblur),
    ("Square", square),
    ("Gaussian", gaussian),
    ("Emboss", emboss),
    ("Small blur", smallBlur),
    ("Large blur", largeBlur),
    ("Sharpen", sharpen),
    ("Laplacian", laplacian),
    ('Sobel Left', sobelLeft),
    ('Sobel Right', sobelRight),
    ('Sobel Top', sobelTop),
    ('Sobel Bottom', sobelBottom)
]

fig = plt.figure(figsize=(12, 8))
fig.subplots_adjust(hspace=0.3, wspace=0.1)

for i, filter in enumerate(filters):
    axes = fig.add_subplot(3, 5, i+1)
    axes.set(title=filter[0])
    axes.grid(False)
    axes.set_xticks([])
    axes.set_yticks([])
    img_out = cnn.convolve_np4(img, filter[1])
    # img_out = cv2.filter2D(img, -1, filter[1]) Hàm của OpenCV chạy tối ưu hơn
    axes.imshow(img_out, cmap='gray', vmin=0, vmax=255)
plt.show()