Xử lí ảnh: thuật toán cân bằng histogram ảnh

Lý thuyết

1. Khái niệm.

Histogram là biểu đồ tần xuất thống kê số lần xuất hiện các mức sáng trong ảnh. Dưới đây là ảnh minh họa. Nhìn vào biểu đồ (chưa cần quan tâm tới đường màu đỏ), dựa vào các cột gía trị có thể dễ dàng thấy được rằng: có khoảng 2000 điểm ảnh mang giá trị 150, giá trị pixel chủ yếu trong khoảng [150, 200] nên độ tương phản không cao, không rõ nét.

Ảnh gốc

Cân bằng histogram (histogram equalization) là sự điều chỉnh histogram về trạng thái cân bằng, giá trị các điểm ảnh không bị co cụm tại một khoảng nhỏ mà được "kéo dãn" ra. Cân bằng histogram là một phương pháp tiền/hậu xử lí ảnh rất mạnh mẽ. Đặc biệt trong nhiều bài toán mình từng làm trong lĩnh vực compute vision, phương pháp tiền xử lí ảnh này cho chất lượng dữ liệu rất cao, cải thiện chất lượng model deep learning rất nhiều.

2. Công thức.

Mình nhận ra các bài viết về histogram equalization thường sơ sài chỉ có code, lại có những bài hơi nặng về công thức toán, gây khó hiểu cho những bạn mới. Trong bài này mình sẽ diễn giải rõ hơn về thuật toán. Mình sẽ làm việc với ảnh H1.

Gọi hàm biến đổi ta cần xác định là K(i)K(i) với iϵ[0,255]i \epsilon [0,255] . Với hình H1, ta cần xác định K sao cho K(150)0K(150) \approx 0, tức pixel có giá trị 150 được thay thế bằng giá trị 0.

B1: Thống kê số lượng pixel cho từng mức sáng, ta được histogram H(i)H(i)

B2: Tính "hàm tích lũy" HH' cho từng mức sáng theo công thức:

H(i)H'(i) = j=0iH(i)\sum_{j=0}^{i} H(i).

Trong đó H(i)H(i) chính là tổng số pixel có giá trị i\leqslant i . Trên hình H1, đường màu đỏ chính là đường minh họa HH' (hàm đồng biến). (Note: Giá trị H'(i) trên hình đã được thay đổi để dễ dàng show trên cùng 1 biểu đồ).

Giả sử H'(140) = 100, H'(150) = 200, H'(160) = 5000. Thấy H(150)H(140)H(150) - H(140) = 100, trong khi H(150)H(140)H(150) - H(140) = 4800 (tức có tận 4800 pixel nằm trong khoảng [150,160][150,160] ). Ta cần 1 hàm biến đổi sao cho K(150)K(140)K(150) - K(140) << K(160)K(150)K(160) - K(150) ... (<< là nhỏ hơn nhiều).

B3: Hàm biến đổi K tại một mức sáng i được tính như sau:

K(i)=H(i)min(H)max(H)min(H)255K(i) = \dfrac{H'(i) - min(H')}{max(H') - min(H')}* 255

Với hàm biến đổi K, với H'(i) - H'(j) nhỏ thì K(i) - K(j) nhỏ, và ngược lại. Nói cách khác, các khoảng sáng có nhiều pixel thì được giãn ra, những khoảng sáng có ít pixel thì được co lại.

Thực hành

Để hiểu rõ hơn về thuật toán, mình sẽ hướng dẫn code thuật toán hoàn toàn từ đầu.

1. Ảnh xám

Cân bằng histogram là cân bằng lại mức cường độ sáng, tức chỉ là 1 trong 3 chanel của hệ màu HSV. Vậy nên trước hết mình sẽ code với ảnh xám (gray)

Import thư viện và ảnh:

import numpy
import cv2
import matplotlib.pyplot as plt

img = cv2.imread("img.png", 0)

Hàm tính histogram của một ảnh

def compute_hist(img):
    hist = np.zeros((256,), np.uint8)
    h, w = img.shape[:2]
    for i in range(h):
        for j in range(w):
            hist[img[i][j]] += 1
    return hist

Hàm cân bằng histogram

def equal_hist(hist):
    cumulator = np.zeros_like(hist, np.float64)
    for i in range(len(cumulator)):
        cumulator[i] = hist[:i].sum()
    print(cumulator)
    new_hist = (cumulator - cumulator.min())/(cumulator.max() - cumulator.min()) * 255
    new_hist = np.uint8(new_hist)
    return new_hist

Thử chạy:

hist = compute_hist(img).ravel()
new_hist = equal_hist(hist)

h, w = img.shape[:2]
for i in range(h):
   for j in range(w):
       img[i,j] = new_hist[img[i,j]]
       
fig = plt.figure()
ax = plt.subplot(121)
plt.imshow(img, cmap='gray')

plt.subplot(122)
plt.plot(new_hist)
plt.show()

Và tất nhiên, kết quả mình thu được là hình H2 trên đầu bài viết.

Ngoài ra, với các bạn có thể dùng hàm của cv2 với cú pháp:

img = cv2.equalizeHist(img)

2. Ảnh màu

Mặc định ảnh màu là hệ RGB hoặc BGR, muốn cân bằng sáng ta cần biến đổi về hệ màu HSV. Hệ màu HSV bao gồm 3 chanel:

  • H-HUE: giá trị màu
  • S-SATURATION: độ bảo hòa.
  • V- VALUE: độ sáng của màu sắc.

Ta sẽ áp dụng cân bằng histogram chỉ trên độ sáng V của ảnh

img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0])

# convert the YUV image back to RGB format
img_output = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR)

Đây là kết quả demo, có thể thấy rõ màu sắc, hình ảnh sau khi cân bằng đã rõ nét hơn rất nhiều:

Cảm ơn các bạn đã đọc bài viết :p