Lọc ảnh (Image Filtering)

Lọc ảnh (làm mịn ảnh, làm mượt ảnh) là một bước rất quan trọng trong xử lý ảnh. Lọc ảnh thực tế có rất nhiều tác dụng như loại bỏ nhiễu, tìm biên đối tượng. Bài viết này sẽ giới thiệu nguyên tắc chung của lọc ảnh và một số phép lọc ảnh cơ bản.

A. Nguyên tắc chung của lọc ảnh

Nguyên tắc chung của các phương pháp lọc là cho ma trận ảnh nhân với một ma trận lọc (Kernel).  Ma trận lọc lọc (Kernel) còn có thể được gọi là cửa số chập (trong phép nhân chập), cửa sổ lọc, mặt nạ,… Trong bài viết này tôi sử dụng thuật ngữ ma trận lọc (Kernel).

Việc nhân ảnh với ma trận lọc giống như việc trượt ma trận lọc theo hàng trên ảnh và nhân với từng vùng của ảnh, cộng các kết quả lại tạo thành kết quả của điểm ảnh trung tâm.

Kết quả hình ảnh cho image filtering
Ma trận đầu vào I được nhân với ma trận lọc (phần xám ở hình trái) để tạo thành ma trận đầu ra O.

Trên thực tế, chúng ta sẽ thấy có 2 phép lọc ảnh là tương quan (correlation) và tích chập (convolution). Với phép tương quan, ma trận lọc sẽ đượt trượt đi và nhân với từng vùng của ảnh như trên. Tuy nhiên với phép tích chập, ma trận lọc sẽ được xoay 180 độ (theo cả chiều ngang và dọc) trước khi thực hiện nhân. 2 phép toán này là tương đương khi ma trận lọc đối xứng.

Với mỗi phép lọc ta có những ma trận lọc (Kernel) khác nhau, không có một quy định cụ thể nào cho việc xác định M. Kích thước ma trận M là một số lẻ. Ví dụ: 3×3, 5×5. 

Khi nhân các phần tử tương ứng với nhau (giữa pixel, các điểm lân cận – các thành phần trong kernel), đối với các phần tử ở cạnh thì sẽ có một số pixel bị khuyết, lúc này, có nhiều cách giải quyết như bỏ qua, chèn thêm một (một số) hàng, cột mang giá trị 0 hoặc bằng giá trị gần nhất, hoặc tạo một đối xứng gương ở cạnh ảnh.

Tổng Tpt các phẩn tử trong ma trận M thường là 1.

  • Tpt > 1: Ảnh sau khi thực hiện xong phép lọc số ảnh (Idst) có độ sáng lớn hơn so với ảnh ban đầu (Isrc). 
  • Tpt < 1: Ảnh sau khi thực hiện xong phép lọc số ảnh (Idst) có độ sáng nhỏ hơn so với ảnh ban đầu (Isrc).

Ví dụ

    \[ M = \begin{bmatrix}1/9 & 1/9 & 1/9 \\ 1/9 & 1/9 & 1/9 \\ 1/9 & 1/9 & 1/9 \end{bmatrix} \]

B. Một số bộ lọc làm mịn ảnh

1. Lọc trung bình (Normalized Box Filter)

Đây là bộ lọc đơn giản nhất. Nó được xây dựng dựa trên ý tưởng tính giá trị một điểm ảnh bằng trung bình cộng các điểm ảnh xung quanh nó.

Ma trận lọc của lọc trung bình có dạng:

    \[ K = \frac{1}{K_{width} \cdot K_{height}}  \begin{bmatrix} 1 & 1 & 1 & \ldots & 1 \\ 1 & 1 & 1 & \ldots & 1 \\ \cdot & \cdot & \cdot & \ldots & 1 \\ 1 & 1 & 1 & \ldots & 1 \end{bmatrix} \]

Cách lọc này thường được áp dụng cho làm trơn ảnh vẫn muốn giữ lại biên không bị mờ.

Code với Python – OpenCV: Đoạn code sau sẽ thực hiện lọc ảnh với ma trận lọc trung bình 5 x 5. Lưu ý: toàn bộ mã nguồn và hình ảnh dùng trong bài viết có thể được tải về tại liên kết trong mục Tham khảo.

    \[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \]

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# Load and blur image
img = cv.imread('rose_gauss.jpg')
img2 = cv.imread('rose_salt_and_pepper.jpg')
blur = cv.blur(img,(5,5))
blur2 = cv.blur(img2,(5,5))

# Convert color from bgr (OpenCV default) to rgb
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
blur_rgb = cv.cvtColor(blur, cv.COLOR_BGR2RGB)
img_rgb2 = cv.cvtColor(img2, cv.COLOR_BGR2RGB)
blur_rgb2 = cv.cvtColor(blur2, cv.COLOR_BGR2RGB)

# Display
plt.subplot(221),plt.imshow(img_rgb),plt.title('Gauss Noise')
plt.xticks([]), plt.yticks([])
plt.subplot(222),plt.imshow(blur_rgb),plt.title('Gauss Noise - Blurred')
plt.xticks([]), plt.yticks([])
plt.subplot(223),plt.imshow(img_rgb2),plt.title('Salt&Pepper Noise')
plt.xticks([]), plt.yticks([])
plt.subplot(224),plt.imshow(blur_rgb2),plt.title('Salt&Pepper Noise - Blurred')
plt.xticks([]), plt.yticks([])
plt.show()

Sau đây là kết quả sau khi chạy đoạn code trên:

Chú ý: bạn cần cài gói matplotlib (dùng pip) và tkinter để sử dụng với Python. Với Python 3.6, việc cài đặt tkinter có thể sử dụng:

sudo apt-get install python3.6-tk

Trên thực tế,  thay vì sử dụng hàm cv2.blur() thì bộ lọc trung bình có thể được sử dụng với hàm: cv2.filter2D() với một tham số là ma trận lọc.

kernel = np.ones((5,5),np.float32)/25
blur = cv.filter2D(img,-1,kernel)

Khi thay ma trận lọc trung bình bằng một ma trận khác, chúng ta có thể có những phép lọc khác như lọc thông thấp (low-pass filters(LPF)) hoặc lọc thông cao (high-pass filters(HPF) ).

2. Lọc Gauss (Gaussian Filter)

Bộ lọc Gauss được cho là bộ lọc hữu ích nhất, được thực hiện bằng cách nhân chập ảnh đầu vào với một ma trận lọc Gauss sau đó cộng chúng lại để tạo thành ảnh đầu ra.

Ý tưởng chung là giá  trị mỗi điểm ảnh sẽ phụ thuộc nhiều vào các điểm ảnh ở gần hơn là các điểm ảnh ở xa. Trọng số của sự phụ thuộc được lấy theo hàm Gauss (cũng được sử dụng trong quy luật phân phối chuẩn).

Dưới đây là biểu diễn ma trận lọc  Gauss:

Giả sử ảnh là một chiều. Điểm ảnh ở trung tâm sẽ có trọng số lớn nhất. Các điểm ảnh ở càng xa trung tâm sẽ có trọng số giảm dần khi khoảng cách từ chúng tới điểm trung tâm tăng lên. Như vậy điểm càng gần trung tâm sẽ càng đóng góp  nhiều hơn vào giá trị điểm trung tâm.


Chú ý: Trên thực tế, việc lọc ảnh dựa trên hàm Gauss 2 chiều (ngang và dọc). Phân phối chuẩn 2 chiều có thể biểu diễn dưới dạng:

    \[G_{0}(x, y) = A e^{ \dfrac{ -(x - \mu_{x})^{2} }{ 2\sigma^{2}_{x} } + \dfrac{ -(y - \mu_{y})^{2} }{ 2\sigma^{2}_{y} } }\]

Trong đó \mu là trung bình (đỉnh),  \sigma^{2} là phương sai của các biến số xy.

Tham số \mu quyết định tác dụng của bộ lọc Gauss lên ảnh. Độ lớn của ma trận lọc (kernel) cần được lựa chọn cho đủ rộng.


Code thực tế lọc Gauss với Python – OpenCV:   Trong OpenCV chúng ta sử dụng hàm sau để lọc Gauss: cv.GaussianBlur().

dst = cv.GaussianBlur(	src, ksize, sigmaX[, dst[, sigmaY[, borderType]]]	)

Bạn cũng có thể tạo một ma trận lọc Gauss và sử dụng với hàm cv.filter2D() phía trên bằng cách sử dụng: cv.getGaussianKernel().

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

# Load and blur image
img = cv.imread('rose_gauss.jpg')
img2 = cv.imread('rose_salt_and_pepper.jpg')
blur = cv.GaussianBlur(img,(5,5),0)
blur2 = cv.GaussianBlur(img2,(5,5),0)

# Convert color from bgr (OpenCV default) to rgb
img_rgb = cv.cvtColor(img, cv.COLOR_BGR2RGB)
blur_rgb = cv.cvtColor(blur, cv.COLOR_BGR2RGB)
img_rgb2 = cv.cvtColor(img2, cv.COLOR_BGR2RGB)
blur_rgb2 = cv.cvtColor(blur2, cv.COLOR_BGR2RGB)

# Display
plt.subplot(221),plt.imshow(img_rgb),plt.title('Gauss Noise')
plt.xticks([]), plt.yticks([])
plt.subplot(222),plt.imshow(blur_rgb),plt.title('Gauss Noise - Blurred')
plt.xticks([]), plt.yticks([])
plt.subplot(223),plt.imshow(img_rgb2),plt.title('Salt&Pepper Noise')
plt.xticks([]), plt.yticks([])
plt.subplot(224),plt.imshow(blur_rgb2),plt.title('Salt&Pepper Noise - Blurred')
plt.xticks([]), plt.yticks([])
plt.show()

Dưới đây là kết quả lọc sử dụng phép lọc Gauss. Các bạn có thể thấy kết quả lọc  (Gauss noise) rất tốt mà hình ảnh không bị nhòe nhiều như phép lọc trung bình như trên.

3. Lọc trung vị

Phép lọc trung vị cũng được thực hiện với các ma trận lọc. Tuy nhiên nó tính trung vị tất cả các giá trị điểm ảnh trong vùng ma trận lọc và sử dụng trung vị này cho giá trị điểm trung tâm. Một điều khá thú vị là với các cách lọc ở trên, giá trị điểm trung tâm được tính mới (có thể bằng hoặc khác với giá trị một điểm trong vùng ma trận lọc), còn với phép lọc trung vị, giá trị điểm trung tâm luôn được thay bằng một giá trị điểm ảnh trong bức ảnh đầu vào. Do vậy, phương pháp lọc này có khả năng loại bỏ nhiễu muối tiêu (salt-and-pepper noise )  khá tốt.

Có một điểm cũng cần được chú ý là phép lọc trung bình và lọc Gauss là phép lọc tuyến tính, nhưng phép lọc trung vị không phải là một phép lọc tuyến tính.

Để sử dụng lọc trung vị trong OpenCV, ta dùng hàm: cv.medianBlur().

Tương tự hai ví dụ ở trên, ta thay bước lọc thành:

blur = cv.medianBlur(img,5)

Kết quả thực hiện lọc trung vị với một số nhiễu:

Có thể thấy rõ, với việc lọc trung vị, nhiễu muối tiêu đã được loại bỏ tốt hơn nhiều so với lọc trung bình hay lọc Gauss.

4. Bộ lọc Bilateral (bộ lọc hai chiều)

cv.bilateralFilter() là một bộ lọc hiệu quả cao trong việc loạt bỏ nhiễu mà vẫn giữ lại được các đường viền (cạnh) trong ảnh.

Như chúng ta đã biết, bộ lọc Gauss quyết định giá trị một điểm ảnh bằng cách lấy trung bình theo hàm Gauss các giá trị điểm ảnh xung quanh điểm đó. Hàm trọng số Gauss chỉ phụ thuộc vào khoảng cách trong không gian so với điểm ảnh trung tâm, không quan tâm đến sự tương quan giữa mức xám của điểm trung tâm với các điểm xung quanh đó. Nó cũng không quan tâm rằng điểm ảnh trung tâm có nằm tại một đường biên trong ảnh không, vì thế làm nhòe luôn các đường biên trong ảnh.

Bộ lọc Bilateral cũng sử dụng một bộ lọc Gauss với khoảng cách đến điểm trung tâm, đảm bảo chỉ có các điểm ở gần tham gia vào giá trị của điểm ảnh trung tâm. Tuy vậy nó sử dụng thêm một hàm Gauss cho mức xám, đảm bảo chỉ các điểm ảnh có mức xám tương đồng với điểm ảnh trung tâm tham gia vào quá trình làm mịn. Vì thế bộ lọc Bilateral bảo toàn được các đường biên trong ảnh bởi vì điểm ảnh ở biên có sự thay đổi về mức xám rất rõ ràng. Hơn nữa, thay vì hoạt động trên các kênh màu một cách riêng rẽ như bộ lọc trung bình hay bộ lọc Gauss, bộ lọc Bilateral thi hành việc đo đạc màu sắc có chủ đích trong không gian màu CIE-Lab , làm mượt màu và bảo toàn các biên theo hướng phù hợp hơn với nhận thức con người.

Tuy vậy, bộ lọc Bilateral cũng có nhược điểm là chậm hơn các bộ lọc khác.

Sau đây là cách sử dụng bộ lọc Bilateral trong OpenCV:

blur = cv.bilateralFilter(img,9,75,75)

Kết quả: Hình 1 sử dụng phép lọc Bilateral, hình 2 sử dụng phéo lọc Gauss. Hãy thử chạy với các phép lọc khác nhé.

Như chúng ta có thể thấy, các texture được lọc rất tốt trong khi các đường biên trong ảnh vẫn được bảo toàn, không bị mờ đi.

Hình 1: Phép lọc Bilateral
Hình 2: Phép lọc Gauss

Các bạn có thể tìm hiểu thêm về bộ lọc Bilateral tại: http://people.csail.mit.edu/sparis/bf_course/.

C. Một số bộ lọc tìm biên ảnh

Ngoài việc làm mịn ảnh, một số bộ lọc còn có tác dụng tìm biên của ảnh.

1. Liên hệ giữa đạo hàm và biên ảnh

Xét vị dụ sau: Ta có một hình ảnh (1) với 2 biên đã được làm mờ. Hình (2) cho thấy mức xám tại đường quét màu đỏ của ảnh. Dễ dàng nhận thấy các đường biên ảnh chính là 2 vùng có sự thay đổi đột ngột về mức xám. Để xác định những sự thay đổi này, ta sử dụng đạo hàm của dải mức xám và tìm các cực trị (địa phương) trên đó. Có thể thấy rõ mối liên hệ giữa các cực trị địa phương của đạo hàm với các biên trong ảnh.

Liên hệ giữa đạo hàm và biên ảnh

2. Gradient của bức ảnh

Vậy là các biên của ảnh sẽ có quan hệ với đạo hàm theo chiều x và đạo hàm theo chiều y của mức xám. Gradient của ảnh là một đại lượng véc tơ hình thành từ 2 đạo hàm này và sẽ được sử dụng để lọc biên trong ảnh.

Công thức của Gradient là:

    \[\triangledown f = \begin{bmatrix} \frac{\partial f}{\partial x} , \frac{\partial f}{\partial y} \end{bmatrix}\]

3. Sobel và Scharr

Phép Sobel là sự kết hợp giữa làm mịn Gauss và phép vi phân, do vậy nó ít bị ảnh hưởng bởi nhiễu.

Việc kết hợp này không hẳn là việc lọc nhiễu bằng phép Gauss trước, rồi thực hiện Sobel để tìm biên mà phép Gauss và Sobel sẽ được kết hợp để tạo ra một ma trân lọc (kernel) rồi sau đó nhân chập ma trận này với ảnh. Hãy cùng xem tại sao có thể làm được như vậy:

Xét một hàm mức xám f , ma trận lọc Gauss h, ta có công thức:

    \[\frac{\partial}{\partial x}\left( h * f \right) =\left( \frac{\partial}{\partial x} h \right) * f\]

Như vậy, thay vì áp dụng bộ lọc Gauss lên ảnh (kích thước khá lớn) rồi áp dụng lọc Sobel để tìm biên, ta có thể áp dụng phép Sobel lên ma trận Gauss (kích thước nhỏ) rồi sau đó nhân chập ma trận thu được với ảnh để cho ra kết quả tương tự. Việc này sẽ giảm đáng kể chi phí tính toán. 

Trong OpenCV, bạn có thể chỉ định được hướng đạo hàm (theo chiều ngang hay chiều dọc). Bạn cũng có thể chỉ định kích thước ma trận lọc với tham số ksize. Nếu ksize = -1, bộ lọc Scharr 3×3 sẽ được sử dụng thay vì Sobel 3×3 để có kết quả tốt hơn.

4. Laplace

Laplace là một phép lọc giúp tìm đường biên cục bộ cho ảnh. Tư tưởng là lấy đạo hàm bậc hai của các điểm. Phép lọc này rất nhạy cảm với nhiễu do đạo hàm bậc 2 không ổn định.

Cài đặt Sobel và Laplace trong OpenCV:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('dave.jpg',0)
laplacian = cv.Laplacian(img,cv.CV_64F)
sobelx = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
sobely = cv.Sobel(img,cv.CV_64F,0,1,ksize=5)
plt.subplot(2,2,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,2),plt.imshow(laplacian,cmap = 'gray')
plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,3),plt.imshow(sobelx,cmap = 'gray')
plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,4),plt.imshow(sobely,cmap = 'gray')
plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
plt.show()

Kết quả:

Vấn đề quan trọng khi lập trình với Python – OpenCV:

Trong ví dụ cuối, kiểu dữ liệu đầu ra là cv.CV_8U hay np.uint8. Có một vấn đề với nó. Các chuyển dịch từ đen sang trắng (sự chuyển màu trên ảnh) có hệ số góc dương, các chuyển đổi từ trắng sang đen lại có hệ số góc âm. Do vậy, khi bạn chuyển dữ liệu sang np.uint8, các hệ số góc âm sẽ được chuyển thành 0. Do vậy bạn mất các cạnh ở chỗ màu sắc chuyển từ đen sang trắng.

Để nhận tất cả các đường biên, bạn phải chuyển kết quả sang kiểu cv.CV_16S, cv.CV_64F, hoặc một kiểu khác lưu trữ lớn hơn np.uint8, lấy giá trị tuyệt đối và chuyển lại về np.uint8.

Đoạn code dưới đây sẽ mô tả quá trình thực hiện. Ảnh đầu vào là một hình chữ nhật trắng trên nền đen. Ta thực hiện việc tìm cạnh theo chiều ngang (lấy các cạnh dọc). Nếu sử dụng kiểu dữ liệu np.uint8, cạnh bên phải bị mất (do cạnh đó được hình thành bởi sự chuyển dịch màu trắng -> đen). Để có cả 2 cạnh, ta phải làm như cách đã nêu trên.

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('box.png',0)
# Output dtype = cv.CV_8U
sobelx8u = cv.Sobel(img,cv.CV_8U,1,0,ksize=5)
# Output dtype = cv.CV_64F. Then take its absolute and convert to cv.CV_8U
sobelx64f = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)
plt.subplot(1,3,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray')
plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray')
plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
plt.show()

Tham khảo

Các thao tác cơ bản với ảnh trên OpenCV

Để hiểu rõ các thao tác xử lý ảnh và làm những điều phức tạp hơn, trước hết chúng ta cần thành thạo trong việc thao tác cơ bản với ảnh trong OpenCV. Trong bài viết này, tôi sẽ giới thiệu một số thao tác cơ bản như thao tác với ma trận ảnh, xử lí với mỗi điểm ảnh. Tôi sẽ sử dụng cấu trúc lưu ảnh cv::Mat được dùng mặc định từ OpenCV 2.0 để làm ví dụ. Ngôn ngữ lập trình được sử dụng là C++, ngôn ngữ có hiệu năng rất tốt, phù hợp với các dự án CV trong thực thế.

1. Cấu trúc ma trận ảnh cv::Mat trong OpenCV – tạo một ma trận ảnh mới

Ở phiên bản 1.0, OpenCV sử dụng cấu trúc IplImage để lưu ảnh. Tuy nhiên vì cấu trúc này được implement trên ngôn ngữ C nên mang nhiều bất lợi về ngôn ngữ như việc khai báo và giái phóng bộ nhớ hoàn toàn thủ công. Nếu không phải làm việc trong các điều kiện đặc biệt như các hệ thống nhúng chỉ hỗ trợ ngôn ngữ C, chúng ta nên lựa chọn một class mới được giới thiệu cho ngôn ngữ C++ – cv::Mat để lưu ảnh. Điều này sẽ giúp việc lập trình trở nên đơn giản hơn, tận dụng các cấu trúc mới.  Dưới đây tôi sẽ chỉ viết về cách thao tác với ma trận ảnh cv::Mat.

Trước hết chúng ta cần làm rõ, việc khai báo và giải phóng bộ nhớ của cv::Mat được thực hiện tự động. Việc giải phóng bộ nhớ sẽ được thực hiện ngay sau khi bạn không cần nó nữa.

Class cv::Mat được lưu trữ với 2 thành phần chính: header (chứa các thông tin về kích thước ma trận, phương pháp dùng để lưu trữ, địa chỉ lưu trữ ma trận ảnh… ) và một con trỏ trỏ đến phần ma trận lưu trữ các giá trị điểm ảnh (có kích thước dựa vào phương pháp dùng để lưu trữ ảnh).

Ảnh trong OpenCV được lưu trữ với nhiều không gian màu khác nhau. Trong mỗi không gian màu, giá trị của các điểm ảnh nằm trong một khoảng giá trị nhất định, do vậy OpenCV cũng dùng nhiều kiểu dữ liệu để lưu trữ các giá trị điểm ảnh. Các kiểu dữ liệu có thể kể đến là:

  • char : 8 bit – unsigned: để lưu các giá trị từ 0 -> 255; signed: lưu các giá trị -127 -> +127.
  • float (4 byte = 32 bit)
  • double (8 byte = 64 bit)

Tạo một ảnh mới

Có khoảng 20 constructor dùng để tạo ảnh cv::Mat. Để tạo một ma trận ảnh rỗng, chỉ cần sử dụng:

cv::Mat img;

Bạn cũng có thể sử dụng các constructor khác để tạo ảnh phù hợp với cấu trúc mình mong muốn, đống thời “đổ màu” cho tất cả các điểm ảnh.

Ví dụ, sử dụng constructor Mat (int rows, int cols, int type, const Scalar &s):

  • rows: số hàng của ảnh
  • cols: số cột của ảnh
  • type: loại ảnh, được viết dưới cấu trúc: CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char const *argv[])
{
    Mat M(2,2, CV_8UC3, Scalar(0,0,255));
    cout << "M = " << endl << " " << M << endl << endl;
    return 0;
}

Đoạn lệnh trên giúp chúng ta tạo một ảnh với kích thước 2 x 2. CV_8UC3 có nghĩa ảnh sử dụng kiểu dữ liệu unsigned char 8bit – mỗi điểm ảnh sẽ được biểu diễn bằng 1 bộ ba số unsigned char tạo thành ảnh 3 kênh. Mỗi điểm ảnh sẽ được set giá trị thành (0,0,255).

Để in ma trận ảnh M, lệnh cout được sử dụng trong câu lệnh:

cout << "M = " << endl << " " << M << endl << endl;

Kết quả nhận được sau khi chạy toàn bộ đoạn lệnh:

Các bạn có thể tham khảo thêm về cách tạo ảnh cv::Mat tại: https://docs.opencv.org/3.4.3/d3/d63/classcv_1_1Mat.html#af1d014cecd1510cdf580bf2ed7e5aafc.

2. Lưu và nạp ảnh đơn giản

Việc lưu và nạp ảnh trong OpenCV sẽ dùng 2 hàm là imread() (nạp ảnh) và imwrite() (ghi ảnh ra ổ cứng).

Đọc ảnh từ ổ cứng:

Việc đọc ảnh từ ổ cứng thực hiện với cú pháp:

Mat cv::imread	(	const String & 	filename,
	int 	flags = IMREAD_COLOR 
)		

Trong đó filename là đường dẫn đến file ảnh. Hiện tại OpenCV hỗ trợ rất nhiều định dạng ảnh. Bạn có thể xem tại đây.

flags là chế độ bạn muốn sử dụng để đọc ảnh. Ví dụ bạn có thể đọc ảnh đó như một ảnh màu RGB hoặc như một ảnh xám. Các chế độ đọc ảnh có thể xem tại đây.

Lưu ảnh lại vào ổ cứng:

Lưu ảnh với cú pháp:

bool cv::imwrite	(	const String & 	filename,
	InputArray 	img,
	const std::vector< int > & 	params = std::vector< int >() 
)	

Trong đó:

  • filename là đường dẫn đến file ảnh cần ghi ra.
  • img là hình ảnh của bạn, có thể là một ma trận cv::Mat
  • params là một vài thông số đặc biệt của một vài loại ảnh. Bạn có thể xem tại đây.

3. Thao tác với một điểm ảnh

Việc thao tác với 1 điểm ảnh trong ma trận cv::Mat có thể thực hiện theo 2 cách: Mat.at<Kiểu_dữ_liệu>(i,j) hoặc Mat.at<Kiểu_dữ_liệu>(Point(j, i)).

  • i là giá trị hàng trong ma trận (y)
  • j là giá trị cột trong ma trận (x)
  • Kiểu_dữ_liệu được chọn như sau:
Loại ma trận cv::MatKiểu_dữ_liệu
CV_8Uuchar
CV_8Sschar
CV_16Uushort
CV_16Sshort
CV_32Sint
CV_32Ffloat
CV_64Fdouble
Ảnh màu nhiều hơn 1 kênh màu Vec< kiểu_dữ_liệu_mỗi_phẩn_tử, Số_kênh_màu > 

OpenCV cũng đưa ra dạng rút gọn tên các kiểu dữ liệu Vec:

typedef Vec< uchar, 2 > 	cv::Vec2b
typedef Vec< uchar, 3 > 	cv::Vec3b
typedef Vec< uchar, 4 > 	cv::Vec4b
typedef Vec< short, 2 > 	cv::Vec2s
typedef Vec< short, 3 > 	cv::Vec3s
typedef Vec< short, 4 > 	cv::Vec4s
typedef Vec< ushort, 2 > 	cv::Vec2w
typedef Vec< ushort, 3 > 	cv::Vec3w
typedef Vec< ushort, 4 > 	cv::Vec4w
typedef Vec< int, 2 > 	cv::Vec2i
typedef Vec< int, 3 > 	cv::Vec3i
typedef Vec< int, 4 > 	cv::Vec4i
typedef Vec< int, 6 > 	cv::Vec6i
typedef Vec< int, 8 > 	cv::Vec8i
typedef Vec< float, 2 > 	cv::Vec2f
typedef Vec< float, 3 > 	cv::Vec3f
typedef Vec< float, 4 > 	cv::Vec4f
typedef Vec< float, 6 > 	cv::Vec6f
typedef Vec< double, 2 > 	cv::Vec2d
typedef Vec< double, 3 > 	cv::Vec3d
typedef Vec< double, 4 > 	cv::Vec4d
typedef Vec< double, 6 > 	cv::Vec6d

Sau đây là một vài lệnh mẫu để truy cập và xử lý điểm ảnh:

# Truy cập, sửa đổi điểm ảnh tại vị trí hàng 2 cột 3 trong ảnh xám gray_img kiểu dữ liệu uchar:
uchar value = gray_img.at<uchar>(2, 3);
gray_img.at<uchar>(2, 3) = 100; // gán giá trị mới: 100
# Hoặc 
value = gray_img.at<uchar>(Point(3, 2));
gray_img.at<uchar>(Point(3, 2)) = 100; // gán giá trị mới: 100
# Truy cập, sửa điểm ảnh trong ảnh màu BGR bgr_img tại vị trí hàng 2, cột 3.
Vec3b value = bgr_img.at<Vec3b>(2, 3);
# Gán kênh màu B (blue) thành giá trị 100
value[0] = 100;
# Đưa giá trị chỉnh sửa trở lại ma trận ảnh:
bgr_img.at<Vec3b>(2, 3) = value;

4. Sao chép ma trận ảnh

Tạo một tham chiếu đến ảnh

Dưới đây là 2 cách tạo tham chiếu đến ảnh:

Mat A, C;  // Tạo các header
A = imread(argv[1], IMREAD_COLOR); // Đọc ảnh và lưu vào bộ nhớ
Mat B(A);                                 // Copy tham chiếu đến ảnh A
C = A;                                    // Assignment operator

Trong ví dụ trên, 3 ma trận A, B, C sẽ tham chiếu đến cùng một vùng dữ liệu, chỉ có header của chúng là khác nhau. Khi thực hiện chỉnh sửa trên các ma trận B, C thì sự thay đổi cũng diễn ra trên A do chúng dùng chung một vùng dữ liệu. 

Hay hơn nữa, ta có thể tạo một ảnh với header khác để truy cập chỉ một vùng ảnh trên A. Việc này được thực hiện như sau:

Mat D (A, Rect(10, 10, 100, 100) ); // sử dụng một rectangle
Mat E = A(Range::all(), Range(1,3)); // sử dụng các biên là hàng và cột

Ảnh D sẽ là một tham chiếu đến cùng dữ liệu trong ảnh A thông qua một “cửa sổ” là hình chữ nhật có tọa độ (10, 10, 100, 100).

Ảnh E là một tham chiếu đến cùng dữ liệu trong ảnh A với biên của vùng tham chiếu được lựa chọn bằng các giá trị hàng và cột.

Các chỉnh sửa trên D, E sẽ làm thay đổi một phần ảnh (được tham chiếu bởi “cửa sổ”).

Sao chép ảnh gồm cả dữ liệu

Trong trường hợp bạn muốn có một sự sao chép thực sự, các thay đổi với ảnh đích không ảnh hưởng đến ảnh gốc, hãy sử dụng clone() hoặc copyTo():

Mat F = A.clone();
Mat G;
A.copyTo(G);

Các lệnh trên sẽ copy dữ liệu và header từ ảnh A sang ảnh F và ảnh G.

Tham khảo:

Ảnh số và các không gian màu trong xử lý ảnh

Ảnh số  (digital image) là một thành phần biểu diễn hình ảnh trong hầu hết các thiết bị điện tử hiện nay như máy ảnh, điện thoại, máy tính, các công cụ hỗ trợ có sử dụng ảnh. Để bắt đầu với xử lý ảnh, chúng ta phải nắm được các kiến thức cơ bản về ảnh số và cách biểu diễn ảnh số trong máy tính. Bài viết này sẽ khái quát sơ lược về ảnh số và các không gian màu để biểu diễn ảnh số.

1. Ảnh số là gì? Có những loại ảnh số nào?

Ảnh số thực tế là biểu diễn số học của hình ảnh trong máy tính, thường là biểu diễn nhị phân. Có thể phân ảnh số thành 2 loại: ảnh raster và ảnh vector.

Ảnh raster

Ảnh Raster là một tập hợp hữu hạn các giá trị số, gọi là điểm ảnh (pixel – picture element). Thông thường một hình ảnh được chia thành các hàng và cột chứa điểm ảnh. Điểm ảnh là thành phần bé nhất biểu diễn ảnh, có giá trị số biểu diễn màu sắc, độ sáng… của một thành phần trong bức ảnh.

Ảnh raster thường được thu từ camera, các máy chiếu, chụp, quét… và chính là đối tượng chính của xử lý ảnh và thị giác máy tính.

Ảnh vector

Ảnh vector là loại ảnh tạo thành từ các thành phần đơn giản của hình học như điểm, đường thẳng, hình khối… Thay vì được lưu lại thành các ma trận điểm ảnh như ảnh raster, ảnh vector được biểu diễn dưới dạng tọa độ của các thành phần trong ảnh.

Chính điều này đã tạo nên sự đặc biệt của ảnh vector, khiến nó có thể được kéo dãn, thu nhỏ tùy ý mà không bị vỡ, không xuất hiện răng cưa như ảnh raster. Dữ liệu trong ảnh vector nhỏ, do vậy thường tiết kiệm dung lượng lưu trữ hơn ảnh raster.

Tuy thế, màu sắc trong ảnh vector nhìn không thật, sắc độ ít tinh tế hơn ảnh raster.

Thông thường người ta sử dụng ảnh vector trong thiết kế các logo, banner, giao diện đồ họa… Loại ảnh này gần như không xuất hiện khi đề cập đến xử lý ảnh / thị giác máy tính.

Kết quả hình ảnh cho vector image
Ảnh vector

2. Các không gian màu biểu diễn ảnh

Các không gian màu là một mô hình toán học dùng để mô tả các màu sắc trong thực tế được biểu diễn dưới dạng số học. Ở đây mình sẽ chỉ đề cập đến một số không gian màu chủ yếu và thường sử dụng để biểu diễn ảnh raster.

2.1. Không gian màu RGB

Không gian màu RGB

RGB là không gian màu phổ biến dùng trong máy tính, máy ảnh, điện thoại và nhiều thiết bị kĩ thuật số khác. Không gian màu này khá gần với cách mắt người tổng hợp màu sắc. Nguyên lý cơ bản là sử dụng 3 màu sắc cơ bản R (red – đỏ), G (green – xanh lục) và B (blue – xanh lam) để biểu diễn tất cả các màu sắc.

Thông thường, trong mô hình 24 bit (không gian màu mặc định sử dụng bởi OpenCV – tuy nhiên OpenCV đảo 2 kênh R và B, trở thành BGR), mỗi kênh màu sẽ sử dụng 8bit để biểu diễn, tức là giá trị R, G, B nằm trong khoảng 0 – 255. Bộ 3 số này biểu diễn cho từng điểm ảnh, mỗi số biểu diễn cho cường độ của một màu. 

Với mô hình biểu diễn 24bit, số lượng màu tối đa sẽ là:

255\times255\times255 = 16581375

2.2. Không gian màu CMYK

Không gian màu CMYK

Không gian màu CMYK chỉ mô hình màu loại trừ, thường dùng trong in ấn. Mô hình màu này dựa trên cơ sở trộn các chất màu của các màu sau:

  • C=Cyan trong tiếng Anh có nghĩa là màu xanh lơ hay cánh chả
  • M=Magenta trong tiếng Anh có nghĩa là màu cánh sen hay hồng sẫm
  • Y=Yellow trong tiếng Anh có nghĩa là màu vàng
  • K=Key (trong tiếng Anh nên hiểu theo nghĩa là cái gì đó then chốt hay chủ yếu để ám chỉ màu đen mặc dù màu này có tên tiếng Anh là black do chữ B đã được sử dụng để chỉ màu xanh lam (blue) trong mô hình màu RGB

Hỗn hợp của các màu CMY lý tưởng là loại trừ (các màu này khi in cùng một chỗ trên nền trắng sẽ tạo ra màu đen). Nguyên lý làm việc của CMYK là trên cơ sở hấp thụ ánh sáng. Màu mà người ta nhìn thấy là từ phần của ánh sáng không bị hấp thụ. Trong CMYK hồng sẫm cộng với vàng sẽ cho màu đỏ, cánh sen cộng với xanh lơ cho màu xanh lam, xanh lơ cộng với vàng sinh ra màu xanh lá cây và tổ hợp của các màu xanh lơ, cánh sen và vàng tạo ra màu đen.

2.3. Không gian màu HSV (HSB)

Không gian màu HSV

Không gian màu HSV (còn gọi là HSB) là một cách tự nhiên hơn để mô tả màu sắc,  dựa trên 3 số liệu:

  • H: (Hue) Vùng màu
  • S: (Saturation) Độ bão hòa màu
  • B (hay V): (Bright hay Value) Độ sáng

2.4. Không gian màu CIE LAB

Không gian màu CIE Lab* là không gian màu có sự đồng đều trong dải màu sắc, do vậy phù hợp để so sánh sự khác biệt giữa màu sắc này với màu sắc khác. Các giá trị Lab mô tả tất cả những màu mà mắt một người bình thường có thể nhìn thấy được.

Lab được xem là một mô hình màu độc lập đối với thiết bị và thường được sử dụng như một cơ sở tham chiếu khi chuyển đổi một màu từ một không gian màu này sang một không gian màu khác.

Mô hình CIE Lab
Không gian màu Lab

Theo mô hình Lab, tất cả các màu có cùng một độ sáng sẽ nằm trên cùng một mặt phẳng có dạng hình tròn theo 2 trục a* và b. Màu có giá trị a dương thì ngả đỏ, màu có giá trị a* âm thì ngả lục. Tương tự b* dương thì ngả vàng và b* âm thì ngả lam. Còn độ sáng của màu thì thay đổi theo trục dọc.

2.5. Chuyển đổi giữa các không gian màu trong OpenCV

Việc chuyển đổi giữa các không gian màu trong OpenCV có thể sử dụng cvtColor() trong C++ hoặc cv2.cvtColor trong Python.

Ví dụ trong Python, để chuyển đổi ảnh frame từ hệ màu BGR (hệ màu mặc định trong OpenCV) sang hệ màu hsv, ta sử dụng lệnh như sau:

hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

Tương tự với C++:

cvtColor(frame, hsv, cv::COLOR_BGR2HSV);

Cài đặt OpenCV trên Ubuntu 18.04

Hướng dẫn này sẽ giúp các bạn cài đặt OpenCV trên hệ điều hành Ubuntu 18.04. Việc cài đặt và sử dụng OpenCV trên Ubuntu sẽ hết sức dễ dàng, ít nhất là theo cá nhân mình, nó dễ dàng hơn việc cài đặt trên Windows.

Về OpenCV, đó là một thư viện mã nguồn mở hàng đầu cho thị giác máy tính (computer vision), xử lý ảnh và máy học, và các tính năng tăng tốc GPU trong hoạt động thời gian thực.

Bước 1: Chuẩn bị môi trường:

Như tiêu đề bài viết, việc cài đặt sẽ được thực hiện trên Ubuntu 18.04 (bản Ubuntu mới nhất lúc mình viết bài này, sẽ được hỗ trợ đến tháng 4 năm 2023 nên các bạn có thể yên tâm sử dụng).

Môi trường mặc định khi cài đặt bản Ubuntu này, bạn sẽ có Python 3.6.5. Các bước tiếp theo mình sẽ hướng dẫn các bạn cài đặt và liên kết OpenCV cho bản Python này.

Dung lượng ổ cứng bạn cần có là khoảng 7GB để biên dịch và cài đặt. Sau khi cài đặt và dọn dẹp thì dung lượng của OpenCV và các gói phụ thuộc chưa đến 1GB. Các bạn có thể sử dụng ổ cứng rời trong quá trình biên dịch, tuy nhiên cần sửa lại các câu lệnh một chút.

Bước 2: Cập nhật các gói có sẵn và cài đặt các thành phần phụ thuộc:

Mở terminal (Ctrl + Alt + t) và gõ vào:

sudo apt update
sudo apt full-upgrade

Cài đặt các tool cho developer (dùng để biên dịch OpenCV):

sudo apt install build-essential cmake unzip pkg-config

Các gói hỗ trợ để làm việc với hình ảnh:

sudo apt-get install libjpeg-dev libpng-dev libtiff-dev

Các gói hỗ trợ làm việc với file video và các luồng video từ camera:

sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install libxvidcore-dev libx264-dev

Gói giao diện highgui của OpenCV hỗ trợ tạo một giao diện người dùng cơ bản để test code, các sản phẩm đang phát triển. Thành phần này phụ thuộc vào gói giao diện GTK. Lưu ý là nếu các bạn có ý định tạo những thành phần giao diện phức tạp sau khi hoàn thành các thử nghiệm trên giao diện cơ bản highgui của openCV, có thể bạn sẽ muốn sử dụng một thư viện đồ họa như QT chẳng hạn.

sudo apt-get install libgtk-3-dev

Cài đặt một số các thành phần cần có khác:

sudo apt-get install libatlas-base-dev gfortran
sudo apt-get install python3-dev
sudo apt-get install qt5-default libgtk2.0-dev libtbb-dev

Bước 3: Tải về OpenCV

Mình sẽ thực hiện tải về gói opencv 3.4.3 và phần mở rộng opencv_contrib 3.4.3. Các lệnh sẽ sử dụng có thể dùng được cho các bản OpenCV 3 mới hơn, chỉ cần thay tên phiên bản trong mã lệnh.

cd ~
wget -O opencv.zip https://github.com/opencv/opencv/archive/3.4.3.zip
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/3.4.3.zip

Giải nén các gói đã tải:

unzip opencv.zip
unzip opencv_contrib.zip
mv opencv-3.4.3 opencv
mv opencv_contrib-3.4.3 opencv_contrib

Bước 4: Cài đặt môi trường cho Python 3

Việc cài đặt môi trường thực tế là việc cài đặt một môi trường ảo (virtual environment) để cài đặt OpenCV trong đó. Việc này sẽ giúp cô lập các môi trường thực thi cho python, tránh nhiều xung đột về môi trường có thể xảy ra. Trừ khi bạn hiểu rõ những gì bạn đang làm, nếu không mình khuyên các bạn nên cài đặt theo cách này.

wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip install virtualenv virtualenvwrapper
sudo rm -rf ~/get-pip.py ~/.cache/pip

Thêm các đoạn mã tạo môi trường vào cuối file ~/.bashrc:

echo -e "\n# virtualenv and virtualenvwrapper" >> ~/.bashrc
echo "export WORKON_HOME=$HOME/.virtualenvs" >> ~/.bashrc
echo "export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3" >> ~/.bashrc
echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.bashrc
source ~/.bashrc

Tạo môi trường ảo để giữ OpenCV và các gói liên quan:

mkvirtualenv cv -p python3

Lệnh trên sẽ giúp các bạn tạo một môi trường ảo tên “cv”. Bạn có thể tạo dưới bất kỳ tên nào bạn muốn. Nhưng chú ý thay đổi các lệnh ở sau.

Để sử dụng môi trường ảo vừa tạo, gõ:

workon cv

Bạn sẽ thấy phần “(cv)” được thêm vào trước mỗi dòng lệnh như hình dưới.

Cài đặt NumPy – gói cần thiết để sử dụng OpenCV với Python:

pip install numpy

Bước 5: Cấu hình và cài đặt OpenCV

Sử dụng môi trường ảo vừa tạo bằng lệnh dưới. Hãy nhớ “workon cv” mỗi lúc bạn cần sử dụng môi trường chứa OpenCV này.

workon cv

Cấu hình để cài đặt OpenCV với cmake:

Hãy nhớ thay “cv” thành tên môi trường ảo vừa tạo.

cd ~/opencv/
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
	-D CMAKE_INSTALL_PREFIX=/usr/local \
	-D INSTALL_PYTHON_EXAMPLES=ON \
	-D INSTALL_C_EXAMPLES=OFF \
	-D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
	-D PYTHON_EXECUTABLE=~/.virtualenvs/cv/bin/python \
        -D WITH_TBB=ON \
        -D WITH_V4L=ON \
        -D WITH_QT=ON \
	-D BUILD_EXAMPLES=ON ..

Bước này có thể kéo dài vài phút. Ngay sau đó bạn sẽ nhận được màn hình tương tự như hình dưới:

Hãy để ý phần thông tin về Python3 được khoanh vùng màu đỏ. Nếu bạn không có thông tin chi tiết về Python như thế này, rất có thể bạn đã thiếu các thành phần sau:

  • Chưa cài gói python3-dev
  • Chưa cài pip
  • Chưa cài numpy
  • Chưa chuyển vào môi trường ảo vừa tạo (workon cv).

Hãy xem lại các cài đặt và chạy lại lệnh cấu hình.

Biên dịch (compile) OpenCV

Tiếp theo chúng ta sẽ biên dịch OpenCV với lệnh make. Vì máy của mình có 4 Core (Intel core i5 7200u) do vậy mình sẽ biên dịch với lệnh sau (bạn có thể thay đổi tham số j tương ứng với số core bạn có). Lệnh “nproc” sẽ trả về giá trị số core của bạn.

nproc # Sau khi chạy lệnh này, bạn sẽ có giá trị trả về là số core của bạn. Hãy thay nó vào lệnh dưới.
make -j4

Việc biên dịch OpenCV sẽ mất một thời gian khá lâu. Bạn nên đi làm việc khác để tiết kiệm thời gian.

Cài đặt OpenCV

Nếu việc biên dịch diễn ra suôn sẻ, bạn sẽ nhận được màn hình tương tự thế này:

Việc tiếp theo là cài đặt OpenCV

sudo make install
sudo sh -c 'echo "/usr/local/lib" >> /etc/ld.so.conf.d/opencv.conf'
sudo ldconfig

Bạn có thể kiểm tra lại việc cài đặt:

pkg-config --modversion opencv

Kết quả sẽ hiện ra phiên bản OpenCV bạn vừa cài (3.4.3) nếu việc cài đặt thành công.

Bước 6: Liên kết OpenCV vào các gói của Python3

Trước tiên hãy tìm đường dẫn đến nơi chứa gói cv2*.so vừa biên dịch:

find /usr/local/lib/ -type f -name "cv2*.so"

Tiếp theo bạn cần đổi tên gói này thành cv2.so và liên kết vào môi trường “cv”. Nhớ thay /usr/local/lib/python3.6/site-packages/ thành đường dẫn tương tứng bạn nhận được từ kết quả của lệnh trên.

ls /usr/local/lib/python3.6/site-packages/
cd /usr/local/lib/python3.6/site-packages/
sudo mv cv2.cpython-36m-x86_64-linux-gnu.so cv2.so
cd ~/.virtualenvs/cv/lib/python3.6/site-packages/
ln -s /usr/local/lib/python3.6/site-packages/cv2.so cv2.so

Kiểm tra lại cài đặt với python:

Bước 7: Clean up

Tổng quá trình biên dịch sẽ chiếm dụng của chúng ta khoảng 6-7GB bộ nhớ, dĩ nhiên là sau khi cài đặt OpenCV sẽ chiếm của bạn ít thôi. Vì thế sau khi cài đặt, kiểm tra, hãy xóa sạch “rác” tạo ra trong quá trình cài đặt nhé.

cd ~
rm opencv.zip opencv_contrib.zip
rm -rf opencv opencv_contrib

Tổng kết

Trên đây là các bước cài đặt OpenCV 3.4.3 trên Ubuntu 18.04. Nếu bạn gặp vấn đề gì / có thắc mắc gì trong quá trình cài đặt, hãy để lại comment bên dưới.