Bỏ qua

Kiến thức nền tảng về Ảnh

Kiến thức nền tảng về ảnh giải thích cách ảnh số được biểu diễn, hình thành và tiền xử lý trước khi được đưa vào bất kỳ mô hình nào. File này đề cập đến pixel, không gian màu (RGB, HSV, YCbCr, LAB), mô hình camera lỗ kim (pinhole), phép tích chập, phát hiện biên (Sobel, Canny), biểu đồ histogram, và bộ mô tả đặc trưng (SIFT, ORB) — bộ công cụ thị giác cấp thấp.

  • Một ảnh số là một lưới 2D gồm các con số. Mỗi ô trong lưới là một pixel (phần tử ảnh), và giá trị của nó biểu diễn cường độ sáng hoặc màu sắc. Ảnh xám (grayscale) là một ma trận 2D duy nhất, mỗi pixel chứa giá trị độ sáng, thường từ 0 (đen) đến 255 (trắng) đối với ảnh 8-bit.

  • Ảnh màu mở rộng điều này thành ba kênh. Trong không gian màu RGB, mỗi pixel lưu ba giá trị: cường độ đỏ, lục và lam.

  • Ảnh màu là một tensor (ma trận) 3D với kích thước (height, width, 3). Việc pha trộn ba kênh này ở các cường độ khác nhau tạo ra toàn bộ quang phổ màu sắc nhìn thấy được.

Ảnh màu được phân tách thành các kênh đỏ, lục và lam, mỗi kênh được hiển thị dưới dạng bản đồ cường độ xám

  • Độ sâu bit (bit depth) quyết định số lượng mức cường độ riêng biệt mà mỗi kênh có thể biểu diễn.

  • Ảnh 8-bit có \(2^8 = 256\) mức mỗi kênh, cho \(256^3 \approx 16.7\) triệu màu. Ảnh 16-bit có 65,536 mức mỗi kênh, được dùng trong chụp ảnh y tế và HDR khi cần phân biệt cường độ tinh tế.

  • RGB thuận tiện cho màn hình hiển thị, nhưng các không gian màu khác phù hợp hơn cho các tác vụ khác nhau.

  • HSV (Hue - Sắc màu, Saturation - Độ bão hoà, Value - Giá trị độ sáng) tách thông tin màu sắc khỏi độ sáng. Hue là màu thuần khiết (0-360 độ trên vòng tròn màu), saturation là độ rực rỡ của màu (0 = xám, 1 = màu thuần), và value là độ sáng. HSV hữu ích cho phân đoạn dựa trên màu sắc vì bạn có thể đặt ngưỡng chỉ dựa trên hue, bất kể điều kiện ánh sáng. Phát hiện "vật thể đỏ" dễ dàng hơn nhiều trong HSV so với RGB.

  • YCbCr tách độ chói (Y, độ sáng cảm nhận được) khỏi sắc độ (Cb, Cr, tín hiệu chênh lệch màu). Đây là không gian màu được dùng trong nén JPEG và codec video. Mắt người nhạy cảm với độ sáng hơn màu sắc, nên sắc độ có thể được lưu ở độ phân giải thấp hơn (chroma subsampling) mà ít bị mất cảm nhận.

  • LAB (CIELAB) được thiết kế sao cho khoảng cách số giữa hai màu tương ứng với sự khác biệt cảm nhận. Các bước đều nhau trong không gian LAB trông giống nhau đối với người quan sát. Kênh L là độ sáng (lightness), A đi từ xanh lục đến đỏ, và B đi từ xanh lam đến vàng. LAB được dùng khi cần so sánh màu đồng nhất về mặt cảm nhận.

  • Sự hình thành ảnh mô tả cách một cảnh 3D trở thành ảnh 2D. Mô hình đơn giản nhất là camera lỗ kim (pinhole camera): ánh sáng từ cảnh xuyên qua một lỗ nhỏ và chiếu lên mặt phẳng cảm biến phía sau. Một điểm \((X, Y, Z)\) trong toạ độ thế giới chiếu lên toạ độ pixel \((u, v)\):

\[ \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \frac{1}{Z} \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} X \\ Y \\ Z \end{bmatrix} \]
  • Ma trận 3x3 là ma trận nội thông số (intrinsic matrix) \(K\). Nó mã hoá các thuộc tính nội tại của camera: tiêu cự \(f_x, f_y\) (mức độ hội tụ ánh sáng của thấu kính) và điểm chính \((c_x, c_y)\) (nơi trục quang học gặp cảm biến, thường gần tâm ảnh). Các giá trị này cố định cho một tổ hợp camera và thấu kính nhất định.

Mô hình camera lỗ kim: điểm 3D chiếu qua tâm quang học lên mặt phẳng ảnh, với tiêu cự và điểm chính được ghi chú

  • Tham số ngoại (extrinsic parameters) mô tả vị trí của camera trong thế giới thực: ma trận xoay \(R\) (3x3, từ chương 02) và vector tịnh tiến \(t\) (3x1). Cùng nhau, chúng biến đổi toạ độ thế giới thành toạ độ camera. Phép chiếu đầy đủ là:
\[ \mathbf{p} = K [R \mid t] \mathbf{P} \]
  • trong đó \(\mathbf{P} = [X, Y, Z, 1]^T\) là điểm 3D trong toạ độ thuần nhất và \(\mathbf{p} = [u, v, 1]^T\) là pixel đã chiếu. Ma trận \([R \mid t]\) có kích thước 3x4, xếp ma trận xoay và tịnh tiến cạnh nhau. Đây là đại số tuyến tính từ chương 02.

  • Thấu kính thực tế gây ra méo (distortion).

    • Méo xuyên tâm (radial distortion) làm cong các đường thẳng: méo hình thùng (barrel) làm ảnh phình ra ngoài; méo hình đệm ghim (pincushion) làm ảnh co vào trong.
    • Méo tiếp tuyến (tangential distortion) xảy ra khi thấu kính không song song hoàn hảo với cảm biến.
  • Hiệu chỉnh camera (camera calibration) ước lượng cả tham số nội và hệ số méo từ ảnh của một mẫu đã biết (như bàn cờ), sau đó hiệu chỉnh (undistort) ảnh.

  • Lọc không gian (spatial filtering) là nền tảng của xử lý ảnh cổ điển. Một bộ lọc (filter) hay kernel là một ma trận nhỏ (thường 3x3 hoặc 5x5) trượt qua ảnh. Tại mỗi vị trí, các giá trị filter được nhân element-wise với patch ảnh chồng lấp và cộng lại để tạo ra một pixel đầu ra. Đây là phép tích chập 2D, cùng một phép toán vận hành các CNN (file 02), nhưng ở đây trọng số filter được thiết kế thủ công thay vì học được.

\[ (\text{image} * K)[i,j] = \sum_{m} \sum_{n} \text{image}[i+m, j+n] \cdot K[m, n] \]
  • Đây là mở rộng 2D của tích chập 1D từ chương 06. Bộ lọc quyết định phép toán phát hiện ra điều gì: các filter khác nhau phát hiện các đặc trưng khác nhau.

  • Làm mờ (blurring) làm mượt ảnh bằng cách lấy trung bình các pixel lân cận. Bộ lọc hộp (box filter) gán trọng số bằng nhau cho tất cả các pixel lân cận.

  • Bộ lọc Gaussian gán trọng số cho các láng giềng bằng Gaussian 2D (chương 05), cho trọng số cao hơn cho pixel gần và thấp hơn cho pixel xa. Làm mờ Gaussian là phép làm mượt phổ biến nhất và được tham số hoá bởi \(\sigma\): \(\sigma\) càng lớn thì càng làm mượt nhiều.

  • Lọc trung vị (median filtering) thay thế mỗi pixel bằng trung vị của vùng lân cận thay vì trung bình có trọng số. Nó đặc biệt hiệu quả trong việc loại bỏ nhiễu muối-tiêu (salt-and-pepper noise — các pixel đen và trắng ngẫu nhiên) trong khi vẫn giữ được biên, vì trung vị rất bền vững với các điểm ngoại lai (như đã thảo luận trong chương 04).

  • Phát hiện biên (edge detection) xác định các ranh giới nơi cường độ pixel thay đổi đột ngột. Biên mang hầu hết thông tin cấu trúc trong ảnh; bạn có thể nhận ra vật thể chỉ từ các đường biên của chúng.

  • Toán tử Sobel sử dụng hai bộ lọc 3x3 để ước lượng gradient theo chiều ngang và chiều dọc:

\[ G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}, \quad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} \]
  • Tích chập ảnh với \(G_x\) cho gradient ngang (phản ứng mạnh tại các biên dọc), và \(G_y\) cho gradient dọc (phản ứng mạnh tại biên ngang).

  • Độ lớn gradient \(\sqrt{G_x^2 + G_y^2}\) và hướng \(\arctan(G_y / G_x)\) cùng mô tả độ mạnh biên và hướng tại mỗi pixel. Đây là analogue trong miền ảnh của gradient từ chương 03.

Ảnh gốc, gradient ngang Sobel, gradient dọc Sobel, và độ lớn biên kết hợp

  • Bộ phát hiện biên Canny là tiêu chuẩn vàng cho phát hiện biên. Nó áp dụng bốn bước:

    1. Làm mượt ảnh với bộ lọc Gaussian để giảm nhiễu
    2. Tính độ lớn và hướng gradient (dùng Sobel)
    3. Không có cực đại giả (non-maximum suppression): làm mỏng biên bằng cách chỉ giữ lại các pixel là cực đại địa phương dọc theo hướng gradient
    4. Phân ngưỡng trễ (hysteresis thresholding): dùng hai ngưỡng (cao và thấp). Pixel trên ngưỡng cao chắc chắn là biên. Pixel giữa hai ngưỡng chỉ là biên nếu chúng được kết nối với một biên chắc chắn. Pixel dưới ngưỡng thấp bị loại bỏ.
  • Hai ngưỡng trong Canny làm nó bền vững hơn so với một ngưỡng đơn: biên mạnh luôn được giữ, và biên yếu chỉ được giữ nếu chúng là một phần của cấu trúc biên liên tục.

  • Phân tích miền tần số (frequency domain) tiết lộ các mẫu khó thấy trong miền không gian. Biến đổi Fourier 2D (mở rộng phiên bản 1D từ chương 03) phân tích ảnh thành tổng các mẫu hình sin 2D ở các tần số và hướng khác nhau:

\[ F(u, v) = \sum_{x=0}^{M-1} \sum_{y=0}^{N-1} f(x, y) \cdot e^{-j2\pi(ux/M + vy/N)} \]
  • Tần số thấp tương ứng với các vùng mượt, biến đổi chậm (bầu trời, bức tường). Tần số cao tương ứng với các chuyển đổi đột ngột (biên, kết cấu, nhiễu). Phổ biên độ (magnitude spectrum) cho thấy năng lượng tại mỗi tần số, và phổ pha (phase spectrum) mã hoá sự sắp xếp không gian.

  • Lọc thông thấp (low-pass filtering) loại bỏ tần số cao, làm mượt ảnh (tương đương với làm mờ Gaussian trong miền không gian). Lọc thông cao (high-pass filtering) loại bỏ tần số thấp, làm nổi bật biên và chi tiết nhỏ. Lọc thông dải (band-pass filtering) chỉ giữ một dải tần số, hữu ích cho phân tích kết cấu (texture).

  • Trong thực tế, lọc trong miền tần số có thể nhanh hơn tích chập không gian đối với các bộ lọc lớn, vì tích chập trong miền không gian tương đương với phép nhân element-wise trong miền tần số (định lý tích chập - convolution theorem). Điều này kết nối trực tiếp với các tính chất của biến đổi Fourier từ chương 03.

  • Biểu đồ histogram tóm tắt phân bố cường độ pixel. Một histogram đếm số lượng pixel có mỗi giá trị cường độ (0-255 cho ảnh 8-bit). Nó là phân bố tần số từ chương 04 áp dụng lên giá trị pixel.

Ảnh với histogram cường độ: ảnh tối có histogram lệch trái, ảnh sáng có histogram lệch phải

  • Ảnh tối có histogram tập trung bên trái (giá trị thấp). Ảnh sáng có histogram tập trung bên phải. Ảnh có độ tương phản thấp có histogram hẹp. Ảnh có độ tương phản cao có histogram rộng, trải dài.

  • Cân bằng histogram (histogram equalisation) kéo dãn histogram để trải toàn bộ dải cường độ, cải thiện độ tương phản. Ý tưởng là tìm một ánh xạ làm cho hàm phân bố tích luỹ (CDF) của cường độ pixel xấp xỉ tuyến tính. Đây là ứng dụng trực tiếp của khái niệm CDF từ chương 04.

  • Phương pháp Otsu tự động tìm ngưỡng tốt nhất để tách ảnh thành tiền cảnh và hậu cảnh. Nó thử mọi ngưỡng có thể và chọn ngưỡng tối thiểu hoá phương sai nội lớp (within-class variance) (hoặc tương đương, tối đa hoá phương sai liên lớp - between-class variance). Đây là cùng khái niệm phương sai từ chương 04, áp dụng lên các quần thể cường độ pixel.

  • Trích xuất đặc trưng (feature extraction) xác định các điểm hoặc vùng đặc biệt trong ảnh có thể dùng để so khớp, nhận dạng, và tái tạo 3D. Đặc trưng tốt cần lặp lại được (có thể tìm lại trong một góc nhìn khác), phân biệt được (có thể phân biệt với các đặc trưng khác), và hiệu quả để tính toán.

  • Phát hiện góc (corner detection) tìm các điểm nơi cường độ ảnh thay đổi đáng kể theo nhiều hướng. Vùng mượt có ít thay đổi theo bất kỳ hướng nào. Biên có thay đổi theo một hướng. Góc có thay đổi theo ít nhất hai hướng, làm nó duy nhất cục bộ và do đó là một mốc đáng tin cậy.

  • Bộ phát hiện góc Harris phân tích tensor cấu trúc (structure tensor) (còn gọi là ma trận mô-men bậc hai) tại mỗi pixel:

\[ M = \sum_{(x,y) \in W} w(x,y) \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix} \]
  • trong đó \(I_x\)\(I_y\) là gradient ảnh (tính bằng Sobel), \(W\) là một cửa sổ địa phương, và \(w\) là hàm trọng số Gaussian. Các trị riêng (eigenvalues) của \(M\) (từ chương 02) cho bạn biết loại đặc trưng:

    • Cả hai trị riêng nhỏ: vùng phẳng (không có đặc trưng)
    • Một lớn, một nhỏ: biên
    • Cả hai lớn: góc
  • Thay vì tính trị riêng một cách tường minh, Harris dùng hàm phản hồi góc (corner response): \(R = \det(M) - k \cdot (\text{trace}(M))^2\), trong đó \(\det(M) = \lambda_1 \lambda_2\)\(\text{trace}(M) = \lambda_1 + \lambda_2\) (cả hai từ chương 02). \(R\) dương lớn chỉ ra góc. Hằng số \(k\) thường là 0.04-0.06.

  • Bộ phát hiện Shi-Tomasi đơn giản hoá thành \(R = \min(\lambda_1, \lambda_2)\), kiểm tra trực tiếp rằng trị riêng nhỏ hơn đủ lớn. Cách này ổn định hơn trong thực tế.

  • Phát hiện blob tìm các vùng khác biệt so với xung quanh. Không giống góc (là đặc trưng điểm), blob có kích thước đặc trưng.

  • SIFT (Scale-Invariant Feature Transform, Lowe, 2004) phát hiện blob ở nhiều tỷ lệ và xây dựng bộ mô tả bất biến với phép xoay, tỷ lệ, và một phần bất biến với thay đổi ánh sáng. Nó hoạt động bằng cách:

    1. Xây dựng không gian tỷ lệ (scale space) (xem bên dưới) sử dụng làm mờ Gaussian với \(\sigma\) tăng dần
    2. Tìm cực trị trong Hiệu Gaussian (Difference of Gaussians - DoG) qua các tỷ lệ
    3. Tinh chỉnh vị trí keypoint và loại bỏ các điểm có độ tương phản thấp và phản hồi biên
    4. Gán một hướng chủ đạo dựa trên hướng gradient địa phương
    5. Xây dựng bộ mô tả 128 chiều từ các histogram gradient trong một patch 16x16 xung quanh keypoint
  • SURF (Speeded-Up Robust Features) xấp xỉ SIFT bằng các bộ lọc hộp và ảnh tích phân để tính toán nhanh hơn. ORB (Oriented FAST and Rotated BRIEF) là một giải pháp thay thế nhanh, mã nguồn mở, kết hợp bộ phát hiện góc FAST với bộ mô tả nhị phân BRIEF, thêm tính bất biến xoay.

  • Bộ mô tả HOG (Histogram of Oriented Gradients) chia ảnh thành các ô nhỏ, tính một histogram của hướng gradient trong mỗi ô, và chuẩn hoá qua các khối ô. HOG nắm bắt phân bố của các hướng biên, rất giàu thông tin cho hình dạng vật thể. Trước học sâu, HOG + SVM (chương 06) là phương pháp chủ đạo cho phát hiện người đi bộ và nhận dạng vật thể.

  • Kim tự tháp ảnh (image pyramids) biểu diễn ảnh ở nhiều độ phân giải.

    • Kim tự tháp Gaussian được xây bằng cách lặp lại việc làm mờ và giảm mẫu (giảm một nửa độ phân giải). Mỗi mức là một phiên bản thô hơn của ảnh gốc.
    • Kim tự tháp Laplacian lưu hiệu số giữa các mức Gaussian liên tiếp, nắm bắt chi tiết bị mất ở mỗi bước giảm mẫu. Kim tự tháp Laplacian có thể đảo ngược: bạn có thể tái tạo lại ảnh gốc từ nó.

Kim tự tháp Gaussian: ảnh gốc ở độ phân giải đầy đủ, sau đó các phiên bản nhỏ dần với độ phân giải giảm một nửa mỗi mức

  • Không gian tỷ lệ (scale space) hình thức hoá ý tưởng rằng các vật thể tồn tại ở các tỷ lệ khác nhau. Một cái cây là một blob lớn; một chiếc lá trên cây đó là một blob nhỏ. Để phát hiện cả hai, bạn cần tìm kiếm qua các tỷ lệ. Không gian tỷ lệ của một ảnh là họ các ảnh được tạo ra bằng tích chập với Gaussian ở \(\sigma\) tăng dần:
\[ L(x, y, \sigma) = G(x, y, \sigma) * I(x, y) \]
  • trong đó \(G\) là Gaussian 2D với độ lệch chuẩn \(\sigma\). Các đặc trưng tồn tại qua nhiều tỷ lệ có nhiều khả năng là cấu trúc có ý nghĩa hơn là nhiễu. Không gian tỷ lệ là nền tảng lý thuyết của SIFT và của xử lý đa tỷ lệ được dùng trong thị giác máy tính hiện đại, bao gồm các mạng kim tự tháp đặc trưng (feature pyramid networks) trong phát hiện đối tượng (file 03).

Bài tập lập trình (dùng CoLab hoặc notebook)

  1. Tải ảnh, chuyển đổi sang các không gian màu khác nhau (RGB, HSV, LAB), và trực quan hoá các kênh riêng lẻ. Quan sát cách thông tin màu sắc được phân bố khác nhau giữa các không gian.

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    from PIL import Image
    import numpy as np
    
    # Create a synthetic test image with distinct colours
    H, W = 128, 256
    img = np.zeros((H, W, 3), dtype=np.uint8)
    img[:, :64] = [255, 50, 50]     # red
    img[:, 64:128] = [50, 255, 50]  # green
    img[:, 128:192] = [50, 50, 255] # blue
    img[:, 192:] = [255, 255, 50]   # yellow
    
    # Add a brightness gradient
    for y in range(H):
        scale = 0.3 + 0.7 * y / H
        img[y] = (img[y] * scale).astype(np.uint8)
    
    img_jnp = jnp.array(img, dtype=jnp.float32) / 255.0
    
    # Manual RGB to HSV conversion
    def rgb_to_hsv(rgb):
        r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
        maxc = jnp.max(rgb, axis=-1)
        minc = jnp.min(rgb, axis=-1)
        diff = maxc - minc + 1e-7
    
        # Hue
        h = jnp.where(maxc == minc, 0.0,
            jnp.where(maxc == r, 60 * ((g - b) / diff % 6),
            jnp.where(maxc == g, 60 * ((b - r) / diff + 2),
                                  60 * ((r - g) / diff + 4))))
        s = jnp.where(maxc < 1e-7, 0.0, diff / maxc)
        v = maxc
        return jnp.stack([h / 360, s, v], axis=-1)
    
    hsv = rgb_to_hsv(img_jnp)
    
    fig, axes = plt.subplots(2, 3, figsize=(14, 8))
    for i, (ch, name) in enumerate(zip([img_jnp[...,0], img_jnp[...,1], img_jnp[...,2]],
                                         ['Red', 'Green', 'Blue'])):
        axes[0, i].imshow(ch, cmap='gray', vmin=0, vmax=1)
        axes[0, i].set_title(f'RGB: {name}'); axes[0, i].axis('off')
    
    for i, (ch, name) in enumerate(zip([hsv[...,0], hsv[...,1], hsv[...,2]],
                                         ['Hue', 'Saturation', 'Value'])):
        axes[1, i].imshow(ch, cmap='gray', vmin=0, vmax=1)
        axes[1, i].set_title(f'HSV: {name}'); axes[1, i].axis('off')
    
    plt.suptitle('RGB vs HSV Channels')
    plt.tight_layout(); plt.show()
    

  2. Cài đặt phát hiện biên Sobel và làm mờ Gaussian từ đầu bằng tích chập 2D. Áp dụng chúng lên một ảnh và so sánh kết quả.

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    def conv2d(image, kernel):
        """2D convolution (valid mode) from scratch."""
        H, W = image.shape
        kH, kW = kernel.shape
        out_h, out_w = H - kH + 1, W - kW + 1
        output = jnp.zeros((out_h, out_w))
        for i in range(out_h):
            for j in range(out_w):
                patch = image[i:i+kH, j:j+kW]
                output = output.at[i, j].set(jnp.sum(patch * kernel))
        return output
    
    # Create a test image: white rectangle on dark background
    img = jnp.zeros((64, 64))
    img = img.at[15:50, 20:45].set(1.0)
    # Add some noise
    key = jax.random.PRNGKey(42)
    img = img + jax.random.normal(key, img.shape) * 0.05
    
    # Sobel filters
    sobel_x = jnp.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=jnp.float32)
    sobel_y = jnp.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=jnp.float32)
    
    # Gaussian blur kernel (5x5, sigma=1)
    ax = jnp.arange(-2, 3, dtype=jnp.float32)
    xx, yy = jnp.meshgrid(ax, ax)
    gaussian = jnp.exp(-(xx**2 + yy**2) / (2 * 1.0**2))
    gaussian = gaussian / gaussian.sum()
    
    # Apply filters
    gx = conv2d(img, sobel_x)
    gy = conv2d(img, sobel_y)
    edges = jnp.sqrt(gx**2 + gy**2)
    blurred = conv2d(img, gaussian)
    
    fig, axes = plt.subplots(1, 4, figsize=(16, 4))
    for ax, data, title in zip(axes,
        [img, edges, blurred, gx],
        ['Original', 'Edge Magnitude', 'Gaussian Blur', 'Horizontal Gradient']):
        ax.imshow(data, cmap='gray')
        ax.set_title(title); ax.axis('off')
    plt.tight_layout(); plt.show()
    

  3. Cài đặt cân bằng histogram từ đầu và áp dụng lên ảnh xám có độ tương phản thấp. So sánh histogram trước và sau.

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # Create a low-contrast image (values clustered in a narrow range)
    key = __import__('jax').random.PRNGKey(42)
    img = __import__('jax').random.uniform(key, (128, 128)) * 0.3 + 0.3  # values in [0.3, 0.6]
    
    def histogram_equalise(img, n_bins=256):
        """Histogram equalisation for a grayscale image."""
        # Quantise to bins
        bins = jnp.linspace(0, 1, n_bins + 1)
        hist = jnp.histogram(img, bins=bins)[0]
    
        # Compute CDF
        cdf = jnp.cumsum(hist)
        cdf_normalised = (cdf - cdf.min()) / (cdf.max() - cdf.min())
    
        # Map each pixel through the CDF
        indices = jnp.clip((img * n_bins).astype(jnp.int32), 0, n_bins - 1)
        equalised = cdf_normalised[indices]
        return equalised
    
    eq_img = histogram_equalise(img)
    
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=1)
    axes[0, 0].set_title('Original (Low Contrast)'); axes[0, 0].axis('off')
    axes[0, 1].imshow(eq_img, cmap='gray', vmin=0, vmax=1)
    axes[0, 1].set_title('After Histogram Equalisation'); axes[0, 1].axis('off')
    
    axes[1, 0].hist(img.ravel(), bins=64, color='#3498db', alpha=0.8)
    axes[1, 0].set_title('Histogram Before'); axes[1, 0].set_xlim(0, 1)
    axes[1, 1].hist(eq_img.ravel(), bins=64, color='#e74c3c', alpha=0.8)
    axes[1, 1].set_title('Histogram After'); axes[1, 1].set_xlim(0, 1)
    
    plt.tight_layout(); plt.show()
    

  4. Cài đặt bộ phát hiện góc Harris từ đầu. Phát hiện các góc trong một ảnh đơn giản và trực quan hoá chúng.

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    def harris_corners(img, k=0.05, threshold=0.01):
        """Harris corner detection from scratch."""
        # Compute gradients with Sobel
        sobel_x = jnp.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=jnp.float32)
        sobel_y = jnp.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=jnp.float32)
    
        # Pad image for valid convolution to preserve size
        img_pad = jnp.pad(img, 1, mode='edge')
        H, W = img.shape
    
        Ix = jnp.zeros_like(img)
        Iy = jnp.zeros_like(img)
        for i in range(H):
            for j in range(W):
                patch = img_pad[i:i+3, j:j+3]
                Ix = Ix.at[i, j].set(jnp.sum(patch * sobel_x))
                Iy = Iy.at[i, j].set(jnp.sum(patch * sobel_y))
    
        # Structure tensor components
        Ixx = Ix * Ix
        Iyy = Iy * Iy
        Ixy = Ix * Iy
    
        # Gaussian smoothing of structure tensor (approximate with window sum)
        w = 3  # window half-size
        R = jnp.zeros_like(img)
        pad_xx = jnp.pad(Ixx, w, mode='constant')
        pad_yy = jnp.pad(Iyy, w, mode='constant')
        pad_xy = jnp.pad(Ixy, w, mode='constant')
    
        for i in range(H):
            for j in range(W):
                sxx = jnp.sum(pad_xx[i:i+2*w+1, j:j+2*w+1])
                syy = jnp.sum(pad_yy[i:i+2*w+1, j:j+2*w+1])
                sxy = jnp.sum(pad_xy[i:i+2*w+1, j:j+2*w+1])
                det = sxx * syy - sxy * sxy
                trace = sxx + syy
                R = R.at[i, j].set(det - k * trace * trace)
    
        # Threshold
        corners = R > threshold * R.max()
        return R, corners
    
    # Test image: checkerboard pattern (lots of corners)
    block = 16
    n = 4
    checker = jnp.zeros((block * n, block * n))
    for i in range(n):
        for j in range(n):
            if (i + j) % 2 == 0:
                checker = checker.at[i*block:(i+1)*block, j*block:(j+1)*block].set(1.0)
    
    R, corners = harris_corners(checker)
    cy, cx = jnp.where(corners)
    
    fig, axes = plt.subplots(1, 3, figsize=(14, 4))
    axes[0].imshow(checker, cmap='gray')
    axes[0].set_title('Checkerboard'); axes[0].axis('off')
    axes[1].imshow(R, cmap='hot')
    axes[1].set_title('Harris Response'); axes[1].axis('off')
    axes[2].imshow(checker, cmap='gray')
    axes[2].scatter(cx, cy, c='#e74c3c', s=15, marker='x')
    axes[2].set_title(f'Detected Corners ({len(cx)})'); axes[2].axis('off')
    plt.tight_layout(); plt.show()