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:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.