Cấu trúc dữ liệu ảnh trong OpenCV. Pixel là gì?

  Aug 9, 2020      2m
   

OpenCV - Tut 1.1: Cấu trúc dữ liệu ảnh trong OpenCV. Pixel là gì?

Cấu trúc dữ liệu ảnh trong OpenCV. Pixel là gì?

Cấu trúc dữ liệu ảnh trong OpenCV

Cấu trúc dữ liệu hình ảnh trong OpenCV được tổ chức dưới dạng một ma trận (số học) 3 chiều. Thứ tự các chiều của ma trận được sắp xếp theo chiều cao, chiều rộng và kênh màu (tiếng Anh: height, width, channel). Mỗi phần tử trong ma trận này có kiểu dữ liệu là số nguyên (0-255) hoặc số thực (0-1) mô tả giá trị của mức sáng (tiếng Anh: intensity).

Trong Xử Lý Ảnh, mức sáng của ảnh chạy trong miền giá trị 0-255. Màu đen "tuyệt đối" mang giá trị 0, màu trắng "tinh khôi" mang giá trị 255. Màu xám đậm sẽ có giá trị lệch về 0 nhiều, và màu xám nhạt lệch về phía 255. Do đó, đối với ảnh xám (hay còn gọi là ảnh "trắng đen", dĩ nhiên có màu xám tồn tại nha chớ hỏng phải nhị phân :D) ta chỉ cần một kênh màu để mô tả và lưu trữ.

Ảnh màu được biểu diễn bằng cách pha ba màu cơ bản là Đỏ (Red), Xanh Lá (Green) và Xanh Dương (Blue) - còn gọi là hệ màu RGB. Ở mỗi kênh màu đỏ hoặc xanh dương hoặc xanh lá được biểu diễn mức sáng trong miền giá trị 0-255. Một bộ giá trị 3 kênh RGB (trong lập trình gọi là tuple) biểu diễn một sắc màu. Như vậy, tổng cộng ta sẽ có tổ hợp của 255 x 255 x 255 = 16,581,375 - 16 triệu màu!~

Do đó, đối với ảnh màu ta cần 3 kênh màu để lưu trữ. Thứ tự kênh màu trong OpenCV được sắp theo BGR (Blue, Green, Red). Lưu ý: màu đen được biểu diễn theo 3 kênh sẽ là (0, 0, 0), màu trắng là (255, 255, 255), và các mức xám sẽ được biểu diễn là (K, K, K) với giá trị K nằm trong đoạn (0, 255).

RGB

OpenCV (phiên bản Python) sử dụng cấu trúc dữ liệu Numpy, do đó ảnh đọc lên bằng câu lệnh cv2.imread() sẽ trả ra một numpy.array (Numpy Array). Các thao tác ta thực hiện sẽ xử lý trên Numpy Array! Do đó, ta vô tư sử dụng các hàm có trong thư viện Numpy lên ma trận ảnh.

Ví dụ: một ảnh màu có kích thước 60 x 60 thì ma trận có hình dạng như thế nào, và có bao nhiêu phần tử?

Đáp: kích thước ma trận sẽ là 60 x 60 x 3. Số phần tử trong ma trận bằng 60 x 60 x 3 = 10,800. Ảnh xám có kích thước 60 x 60 pixel thì ma trận biểu diễn sẽ có kích thước 60 x 60 (x 1).

Để "thấm thía" (hiểu rõ) hơn về cấu trúc dữ liệu của OpenCV, ta thử load hình sau lên và in ra thử:

simple.png

simple.png

read_simple_image.py

import cv2
img = cv2.imread("simple.png")
print(img)
print("Blue layer (%d):" % img[:,:,0].size, img[:,:,0])
print("Green layer (%d):" % img[:,:,1].size, img[:,:,1])
print("Red layer (%d):" % img[:,:,2].size, img[:,:,2])
print("Image shape:", img.shape) # (60, 60, 3)

Thực thi script python trên bạn sẽ thấy trên màn hình in ra bộ ba các mức sáng cấu thành nên màu của các ô vuông nhỏ trong hình simple.png.

  • img: cấu trúc dữ liệu Numpy
  • img.shape: in ra kích thước ma trận 60x60x3
  • img[:,:,0]: lấy ra một ma trận 2D ở kênh Blue, kích thước 60x60
  • img[:,:,0].size: số phần tử trong ma trận 60x60=3600

Các màu sắc của ô vuông trong ảnh ví dụ mình cố ý tạo theo các mã màu sau (BGR):

simple_bgr.png

simple_bgr.png

Ảnh gốc được Minh tạo ra bởi đoạn script sau:

create_simple_image.py

import cv2
import numpy as np

data = np.zeros([60, 60, 3])
h, w, c = data.shape

color_map = [
    (255, 0, 0), # blue
    (0, 255, 0), # green
    (0, 0, 255), # red
    (0, 0, 0), # black
    (255, 255, 255), # white
    (100, 100, 100), # grey
    (120, 50, 10),
    (36, 167, 227),
    (240, 212, 0),
]

assert h == w
step = h // 3
i = 0
for row in range(3):
    for col in range(3):
        data[row*step:(row+1)*step, col*step:(col+1)*step, :] = color_map[i]
        i += 1

cv2.imwrite("simple.png", data)
print("Done writing image: simple.png")

Vòng lặp Minh sẽ lặp 9 lần (tương ứng 9 ô), mỗi ô có diện tích 20x20 và mình "fill" màu sắc với bộ ba mức sáng đọc từ danh sách "color_map" ra. Numpy cho phép ta đọc ra hoặc ghi vào một khối ma trận với các chỉ số trải dài liên tục. Cú pháp khác quen thuộc trong lập trình Python: data[Y_FROM:Y_TO, X_FROM:X_TO, C_FROM:C_TO].

Trục tọa độ ảnh. Pixel là gì?

  • Gốc tọa độ ảnh đặt ở góc phía trên bên trái.
  • Trục nằm dọc là trục y - tương ứng với chiều cao ảnh.
  • Trục nằm ngang là trục x - tương ứng với chiều rộng ảnh.
  • Trục z là trục chiều sâu (bạn tưởng tượng nó tương tự khối lập phương 3D đã từng học ở lớp 12 á) - tương ứng với kênh màu ảnh. Lưu ý: ảnh xám có thể KHÔNG CÓ trục z; hoặc có trục z nhưng một kênh; hoặc có trục z ba kênh nhưng màu sắc đều là xám => túm lại chỉ là các cách biểu diễn khác nhau của cùng một nội dung ảnh. Bạn tưởng tượng rằng kiểu như là: bạn đi siêu thị chọn một giỏ đựng hàng vừa đủ 4 món đồ bạn mua, HOẶC bạn chọn một giỏ đựng hàng to bự dư sức chứa cũng 4 món đồ bạn định mua đó => không có vấn đề gì cả nếu giỏ / sức chứa bạn to hơn nội dung bạn cần đựng trong nó :).
  • Một điểm trên ảnh ta gọi là một pixel. Pixel (tiếng Anh: picture element) là một điểm ảnh.

Ảnh minh họa tọa độ các điểm trong ảnh (ma trận 2D) có kích thước 7x11 (height x width):

image coordination

(nguồn ảnh: http://math.hws.edu/graphicsbook/c2/s1.html)

Ví dụ 1: Giả sử biến img chứa dữ liệu của ma trận 7x11, ta muốn:

  • Lấy giá trị mức sáng ở tọa độ gốc => img[0, 0]
  • Lấy giá trị mức sáng tại (x=3, y=5) => img[5, 3] => BỞI VÌ OpenCV lưu theo H x W (x C). Nếu ta dùng img[3, 5] là sai/nhầm nhé!
  • Lấy vùng ảnh y chạy từ 2->5 và x chạy từ 4->8 (tổng cộng 4*5=20 phần tử) => img[2:6, 4:9]

Ví dụ 2: Giả sử biến img chứa dữ liệu ảnh của một ảnh màu Full HD - độ phân giải ảnh 1080p (1920x1080 - width x height). Như vậy thì:

  • Đọc bằng OpenCV, img có "shape" như sau: 1080 x 1920 x 3 => 6,220,800 (phần tử trong ma trận)
  • Cắt một vùng ảnh từ ảnh gốc => img[y_from:y_to, x_from:x_to, :]
  • Cắt một vùng ảnh từ ảnh gốc VÀ chỉ lấy kênh màu XANH DƯƠNG (B) => img[y_from:y_to, x_from:x_to, 0]
  • Cắt một vùng ảnh từ ảnh gốc VÀ chỉ lấy kênh màu XANH LÁ (G) => img[y_from:y_to, x_from:x_to, 1]
  • Cắt một vùng ảnh từ ảnh gốc VÀ chỉ lấy kênh màu ĐỎ (R) => img[y_from:y_to, x_from:x_to, 2]
  • Lấy giá trị mức sáng trên kênh màu xanh dương (Blue) tại tọa độ x=555, y=777 => img[777, 555, 0]

y_from, y_to, x_from, x_to là các giá trị số nguyên nằm trong kích thước ảnh gốc (không bị lòi ra ngoài hoặc số âm).

Gốc tọa độ của ảnh không giống với gốc tọa độ trong toán học. Do đó, biểu diễn vị trí tọa độ pixel trong ảnh sẽ theo gốc tọa độ ảnh. Ta phải nắm rõ cách tổ chức này để lấy đúng giá trị pixel / vùng trên ảnh. Bài này là kiến thức nền tảng vô cùng quan trọng để bạn tiếp tục tìm hiểu thư viện OpenCV cho Xử Lý Ảnh.

Bài viết tiếp theo: Tut 2: Xử lý ảnh - OpenCV resize, crop và padding hình ảnh (code Python và C++)


Cảm ơn bạn đã theo dõi bài viết. Hãy kết nối với tớ nhé!


Danh sách bài viết series OpenCV: