Giới thiệu
Toán tử hình thái học được ứng dụng nhiều trong các lĩnh vực liên quan đến xử lý hình ảnh như: xử lý ảnh y khoa, nhận dạng mẫu (nhận dạng kí tự), phát hiện lỗi trên sản phẩm công nghiệp (lỗ hổng, đứt khúc, nứt). Có 2 vấn đề khiến toán tử hình thái học được sử dụng nhiều so với các toán tử dựa trên kỹ thuật convolution, trong các lĩnh vực kể trên, là do:
- Tập trung vào xử lý hình dáng, cấu trúc đối tượng.
- Dựa trên lý thuyết tập hợp, nên là toán tử phi tuyến, vì vậy, thời gian xử lý nhanh hơn phép tích chập (dựa trên cấu trúc đại số).
Toán tử khung xương được xây dựng từ các toán tử cơ bản trong phép toán hình thái học trên ảnh nhị phân. Bao gồm toán tử co (erosion), giãn (dilation), đóng (closing), mở (opening). Nhiều toán tử khác nhau cũng được xây dựng từ các toán tử cơ bản này: trích biên (boundary extraction), lấp vùng - lỗ hổng (filling region), làm mảnh (thinning), làm dày (thickness), tái cấu trúc (reconstruction), ...
Bài viết này trình bày giải thuật thực hiện toán tử khung xương (skeleton) dựa trên các toán tử cơ bản của toán tử hình thái học trên ảnh nhị phân.
Ảnh gốc
Ảnh sau khi xử lý để lấy khung xương
Toán tử cơ bản
Trước khi đi vào tìm hiểu toán tử khung xương, cần nắm được nội dung của các toán tử cơ bản của binary morphology. Bao gồm: co (erosion), giãn (dilation), đóng (closing), mở (opening).
Cũng như convolution, ma trận kernel quyết định kết quả của phép toán, thì đối với binary morphology, phần tử cấu trúc là thành phần quyết định hình dáng, cấu trúc của đối tượng.
Kí hiệu:
SE
: phần tử cấu trúc.A
: tập các điểm thuộc đối tượng trong ảnh nhị phân (giá trị bằng 1).
Phần tử cấu trúc
SE là phần tử cấu trúc được biễu diễn trong không gian hai chiều, với anchor point của SE có tọa độ (0, 0). Định nghĩa các điểm p(x, y) = 1
thuộc SE.
Thư viện OpenCV hỗ trợ tạo phần tử cấu trúc sử dụng hàm:
Mat getStructuringElement (int shape, Size ksize, Point anchor=Point(-1,-1));
Trong đó:
shape
có thể nhận 1 trong các giá trị sau:CV_SHAPE_RECT
,CV_SHAPE_ELLIPSE
,CV_SHAPE_CROSS
.ksize
: kích thước phần tử cấu trúc.anchor
: điểm gốc với chỉ số dòng, chỉ số cột của SE. Nếu mặc định, thì điểm gốc có toạ độ:(ksize.x / 2, ksize.y / 2)
.
Trả về ma trận kích thước ksize
, gồm 2 giá trị là: 1 (thuộc SE), 0 (không thuộc SE).
Nói rõ hơn về anchor point, xét về tính cục bộ, anchor point sẽ nhận tọa độ là chỉ số dòng, cột của ma trận trả về từ hàm. Xét trong không gian hai chiều, anchor point của SE đặt ở gốc tọa độ.
Ví dụ: ksize
= (4, 4), anchor
= default
-(2, 2).
Tùy vào kích thước đối tượng và toán tử áp dụng, lựa chọn kích thước và hình dạng SE phù hợp. Xem ví dụ dưới đây, muốn xóa nhiễu ảnh vân tay (trái cùng) sử dụng toán tử erosion.
Kích thước phần tử cấu trúc (3, 3)
Kích thước phần tử cấu trúc (5, 5)
Binary Erosion
Toán tử có tác dụng làm mảnh đối tượng, xóa bỏ nhiễu và chi tiết thừa.
Minh họa
Ảnh gốc
Ảnh kết quả với:
ksize (3, 3);
anchor point = default;
shape = RECT;
Định nghĩa
Giải thích: Tịnh tiến SE theo vector w
bất kỳ, và tại đó SE là tập con của A, thì giữ lại w
đưa vào kết quả.
Theo suy nghĩ lập trình: duyệt qua tất cả các điểm trong A, tại mỗi điểm, đặt anchor point
của SE tại điểm đang xét, kiểm tra nếu tất cả các điểm trong SE (giá trị bằng 1) và các điểm tương ứng trên A bằng nhau, thì giữ lại điểm đang xét.
Mã nguồn
void Mathematics::doErosion (const Mat &srcImg, Mat &destImg, Point * kernelIndex, const Mat &kernel) { destImg.setTo(Scalar(0.0)); for (int idxRowImg = 0 ; idxRowImg < srcImg.size().height; idxRowImg++) { for (int idxColImg = 0; idxColImg < srcImg.size().width; idxColImg++) { if (match (srcImg, Point(idxColImg, idxRowImg), kernelIndex, kernel)) destImg.at<unsigned char>(idxRowImg, idxColImg) = 255; } } } bool Mathematics::match (const Mat &srcImg, Point2d pointAt, Point * kernelIndex, const Mat &kernel) { double idxRowImg, idxColImg; int idxKerIndex; idxKerIndex = -1; for (int idxRowKer = 0; idxRowKer < kernel.size().height; idxRowKer++) { for (int idxColKer = 0; idxColKer < kernel.size().width; idxColKer++) { idxKerIndex++; if (kernel.at<unsigned char>(idxRowKer, idxColKer) == 1) { idxRowImg = pointAt.y + kernelIndex[idxKerIndex].y; idxColImg = pointAt.x + kernelIndex[idxKerIndex].x; if (idxRowImg > srcImg.size().height - 1) continue; if (idxRowImg < 0) continue; if (idxColImg > srcImg.size().width - 1) continue; if (idxColImg < 0) continue; if (srcImg.at<unsigned char>(idxRowImg, idxColImg) != 255) return false; } } } return true; }
Dòng 16 - 48: Hàm kiểm tra các điểm thuộc về phần tử cấu trúc - kernel (value = 1) có là tập con của đối tượng hay không.
pointAt
: tọa độ (chỉ số dòng, chỉ số cột) của điểm đang xét.kernelIndex
: chỉ số truy cập nhanh đến kernel.kernel
: ma trận phần tử cấu trúc.
Hàm thực hiện duyệt ma trận kernel, với các điểm thuộc phần tử cấu trúc (value = 1
), kiểm tra tại điểm tương ứng trên ảnh, nếu giá trị là background (!255)
thì kết luận không phải tập con của đối tượng. Duyệt hết kernel, kết luận phần tử cấu trúc là tập con của đối tượng.
Hàm thư viện openCV:
void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue() )
Trong đó:
src
: ảnh nguồn.dst
: ảnh kết quả.kernel
: phần tử cấu trúc.anchor
: điểm gốc của phần tử cấu trúc.interations
: số lần thực hiện erosion liên tiếp cho ảnh nguồn.
Binary Dilation
Toán tử giãn nhị phân (binary dilation) có tác dụng làm tăng kích thước đối tượng, nối đứt đoạn, lấp lỗ hổng.
Minh họa
Ảnh gốc.
Ảnh kết quả với
ksize (3, 3);
anchor point = default;
shape = RECT;
Định nghĩa
Giải thích:
Tập đối xứng qua gốc tọa độ của SE. Tập này được tịnh tiến theo 1 vector w
bất kỳ, nếu tập này và A
có ít nhất 1 điểm giao, thì giữ lại w
, đưa vào kết quả.
Theo suy nghĩ lập trình: duyệt qua tất cả các điểm trong A
, tại mỗi điểm, đặt anchor point của SE tại điểm đang xét, kiểm tra nếu tồn tại 1 điểm trong SE (giá trị bằng 1) và điểm tương ứng trên A
bằng nhau, thì giữ lại điểm đang xét.
Mã nguồn
void Mathematics::doDilation(const Mat &srcImg, Mat &destImg, Point * kernelIndex, const Mat &kernel) { srcImg.copyTo(destImg); int idxKer, idxRowKer, idxColKer, m, n; int lenKer = kernel.size().height * kernel.size().width; for (int idxRowImg = 0 ; idxRowImg < srcImg.size().height; idxRowImg++) { for (int idxColImg = 0; idxColImg < srcImg.size().width; idxColImg++) { for (idxKer = 0; idxKer < lenKer; idxKer++) { idxRowKer = idxKer / kernel.size().width; idxColKer = idxKer % kernel.size().width; if (kernel.at<unsigned char>(idxRowKer, idxColKer) == 1) { m = idxRowKer + kernelIndex[idxKer].y; n = idxColKer + kernelIndex[idxKer].x; if (m > srcImg.size().height - 1) continue; if (m < 0) continue; if (n > srcImg.size().width - 1) continue; if (n < 0) continue; if (scrImg.at<unsigned char>(m, n) == 255) { destImg.at<unsigned char>(idxRowImg, idxColImg) = 255; break; } } } } } }
Hàm thư viện openCV:
void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue())
Trong đó:
src
: ảnh nguồn.dst
: ảnh kết quả.kernel
: phần tử cấu trúc.anchor
: điểm gốc của phần tử cấu trúc.interations
: số lần thực hiệndilation
liên tiếp cho ảnh nguồn.
Do chỉ sử dụng 1 cấu trúc phần tử duy nhất cho toàn bộ đối tượng trong ảnh, nên đối tượng sẽ giãn nở đều. Tuy nhiên, 1 số trường hợp như nhiễu, hoặc phần chi tiết người dùng không muốn bị giãn theo phép dilation
, thì toán tử giãn nở không linh hoạt, cứng nhắc.
Binary Opening
Bởi vì việc sử dụng riêng lẻ toán tử giãn nhị phân và co nhị phân gây cứng nhắc, không linh hoạt theo ý người sử dụng.
Việc kết hợp và tạo ra toán tử mở nhị phân giải quyết 1 số trường hợp như:
- Muốn giãn nở chi tiết quan trọng, nhưng loại bỏ chi tiết thừa hoặc nhiễu (giãn nhị phân không làm được điều này).
- Chỉ xoá nhiễu, nhưng kích thước đối tượng không thay đổi (co nhị phân không làm được điều này).
Toán tử mở giúp làm trơn biên đối tượng, loại bỏ eo hẹp (đối với những eo có kích thước nhỏ hơn phần tử cấu trúc), và loại bỏ thành phần lồi mỏng. Đồng thời, khắc phục nhược điểm của các toán tử đơn.
Định nghĩa
Mã nguồn
void Mathematics::doOpening(const Mat &srcImg, Mat &destImg, Point * kernelIndex, const Mat &kernel) { Mat temp = Mat(Size(srcImg.size()), CV_8UC1); doErosion(srcImg, temp, kernelIndex, kernel); doDilation(temp, destImg, kernelIndex, kernel); }
Binary Closing
Xoá nhiễu, làm mất khuyết điểm trong đối tượng. Lấp đầy lỗ hổng ở viền đối tượng. Sự kết hợp toán tử co và giãn khắc phục nhược điểm của việc sử dụng từng toán tử đơn.
Định nghĩa
Mã nguồn
void Mathematics::doClosing(const Mat &srcImg, Mat &destImg, Point * kernelIndex, const Mat &kernel) { Mat temp = Mat(Size(srcImg.size()), CV_8UC1); doDilation(srcImg, temp, kernelIndex, kernel); doErosion(temp, destImg, kernelIndex, kernel); }
Skeleton (toán tử khung xương)
Mục đích của toán tử rút xương là trích xuất hình dáng đặc trưng đại diện chung cho đối tượng.
Khái niệm skeleton được giới thiệu bởi H.Blum, là kết quả của Medial Axis Transform (MAT) hoặc Symmetry Axis Transform (SAT). MAT xác định số lượng điểm nằm trên biên của đối tượng mà gần nhất đến mỗi điểm thuộc đối tượng. 1 điểm thuộc về khung xương nếu như điểm đó được MAT xác định có số lượng điểm lớn hơn 1.
Ví dụ về minh hoạ cho điểm thuộc khung xương của đối tượng hình chữ nhật. Hai điểm A và B thuộc khung xương, tuy nhiên, điểm C không thuộc khung xương do chỉ có 1 điểm gần nhất nằm trên biên cạnh.
Toán tử rút xương có thể được biểu diễn bằng tập hữu hạn các phép co (erosion) và mở (opening).
Định nghĩa
Với:
- B là phần tử cấu trúc và là kết quả của phép thực hiện erosion k lần với B của A.
- K là lần thực hiện rút xương cuối cùng, mà bước tiếp theo sẽ cho kết quả rỗng.
Mã nguồn
void Mathematics::skeleton(const Mat &srcImg, Mat &destImg, Point * kernelIndex, const Mat &kernel) { Mat X_k = Mat(Size(srcImg.size()), CV_8UC1); srcImg.copyTo(X_k); Mat X_kSubOne = Mat::zeros(Size(srcImg.size()), CV_8UC1); Mat S_k = Mat(Size(srcImg.size()), CV_8UC1); Mat S_A = Mat::zeros(Size(srcImg.size()), CV_8UC1); Mat tempOpen = Mat(Size(srcImg.size()), CV_8UC1); doErosion(X_k, tempOpen, kernelIndex, kernel); while (sum(tempOpen)[0] != 0) { X_k.copyTo(X_kSubOne); doErosion(X_kSubOne, X_k, kernelIndex, kernel); doOpening(X_k, tempOpen, kernelIndex, kernel); tempOpen = 255 - tempOpen; bitwise_and(X_k, tempOpen, S_k); bitwise_or(S_A,S_k, S_A); doErosion(X_k, tempOpen, kernelIndex, kernel); } S_A.copyTo(destImg); }
Toán tử rút xương được áp dụng thành công trong những ứng dụng y học:
- Chẩn đoán được triệu chứng hẹp thanh quản.
- Chẩn đoán được triệu chứng phình động mạch chủ dưới thận.
- Phát hiện chứng đau đại tràng.