Xử lý ảnh - Phát hiện cạnh Canny (Canny Edge Detection)
OpenCV - tut 11: Hiện thực Canny Edge
Môi trường "hành sự"
- Linux (bài viết sử dụng Ubuntu 16.04)
- OpenCV (bài viết sử dụng OpenCV 3.4.1)
- Python (bài viết sử dụng Python 3.5.5)
- Ảnh mẫu để xử lý: girl_11.jpg
Bạn có thể download ảnh mẫu về:
girl_11.jpg (Nguồn: Lụm trên mạng)
Giải thuật phát hiện cạnh Canny - Canny Edge Detection
Trong hình ảnh, thường tồn tại các thành phần như: vùng trơn, góc / cạnh và nhiễu. Cạnh trong ảnh mang đặc trưng quan trọng, thường là thuộc đối tượng trong ảnh (object). Do đó, để phát hiện cạnh trong ảnh, giải thuật Canny là một trong những giải thuật phổ biến / nổi tiếng nhất trong Xử lý ảnh.
Giải thuật phát hiện cạnh Canny gồm 4 bước chính sau:
- Giảm nhiễu: Làm mờ ảnh, giảm nhiễu dùng bộ lọc Gaussian kích thước 5x5. Kích thước 5x5 thường hoạt động tốt cho giải thuật Canny. Dĩ nhiên bạn cũng có thể thay đổi kích thước của bộ lọc làm mờ cho phù hợp. Tham khảo bài viết: Xử lý ảnh - Làm mờ ảnh (blur)
- Tính Gradient và hướng gradient: ta dùng bộ lọc Sobel X và Sobel Y (3x3) để tính được ảnh đạo hàm Gx và Gy. Tham khảo bài viết giải thích về gradient: Gradient của ảnh là gì?. Sau đó, ta tiếp tục tính ảnh Gradient và góc của Gradient theo công thức. Ảnh đạo hàm Gx và Gy là ma trận (ví dụ: 640x640), thì kết quả tính ảnh đạo hàm Edge Gradient cũng là một ma trận (640x640), mỗi pixel trên ma trận này thể hiện độ lớn của biến đổi mức sáng ở vị trí tương ứng trên ảnh gốc. Tương tự, ma trận Angle cũng có cùng kích thước (640x640), mỗi pixel trên Angle thể hiện góc, hay nói cách khác là hướng của cạnh. Ví dụ dễ hiểu, nếu góc gradient là 0 độ, thì cạnh của ta trên ảnh sẽ là một đường thẳng đứng (tức tạo góc 90 độ so với trục hoành) (vuông góc hướng gradient). Khi tính toán, giá trị hướng gradient sẽ nằm trong đoạn [-180, 180] độ, ta không giữ nguyên các góc này mà gom các giá trị này về 4 bin đại diện cho 4 hướng: hướng ngang (0 độ), hướng chéo bên phải (45 độ), hướng dọc (90 độ) và hướng chéo trái (135 độ).
- Non-maximum Suppression (viết tắt NMS): loại bỏ các pixel ở vị trí không phải cực đại toàn cục. Ở bước này, ta dùng một filter 3x3 lần lượt chạy qua các pixel trên ảnh gradient. Trong quá trình lọc, ta xem xét xem độ lớn gradient của pixel trung tâm có phải là cực đại (lớn nhất trong cục bộ - local maximum) so với các gradient ở các pixel xung quanh. Nếu là cực đại, ta sẽ ghi nhận sẽ giữ pixel đó lại. Còn nếu pixel tại đó không phải là cực đại lân cận, ta sẽ set độ lớn gradient của nó về zero. Ta chỉ so sánh pixel trung tâm với 2 pixel lân cận theo hướng gradient. Ví dụ: nếu hướng gradient đang là 0 độ, ta sẽ so pixel trung tâm với pixel liền trái và liền phải nó. Trường hợp khác nếu hướng gradient là 45 độ, ta sẽ so sánh với 2 pixel hàng xóm là góc trên bên phải và góc dưới bên trái của pixel trung tâm. Tương tự cho 2 trường hợp hướng gradient còn lại. Kết thúc bước này ta được một mặt nạ nhị phân (ảnh nhị phân - tham khảo bài viết để hiểu ảnh nhị phân nhé). Tham khảo hình dưới:
- Lọc ngưỡng: ta sẽ xét các pixel dương trên mặt nạ nhị phân kết quả của bước trước. Nếu giá trị gradient vượt ngưỡng max_val thì pixel đó chắc chắn là cạnh. Các pixel có độ lớn gradient nhỏ hơn ngưỡng min_val sẽ bị loại bỏ. Còn các pixel nằm trong khoảng 2 ngưỡng trên sẽ được xem xét rằng nó có nằm liên kề với những pixel được cho là "chắc chắn là cạnh" hay không. Nếu liền kề thì ta giữ, còn không liền kề bất cứ pixel cạnh nào thì ta loại. Sau bước này ta có thể áp dụng thêm bước hậu xử lý loại bỏ nhiễu (tức những pixel cạnh rời rạc hay cạnh ngắn) nếu muốn. Ảnh minh họa về ngưỡng lọc:
Lý thuyết giải thuật Canny trong bài viết này tham khảo chính tại:
- https://docs.opencv.org/master/da/d22/tutorial_py_canny.html
- https://en.wikipedia.org/wiki/Canny_edge_detector
Canny Edge trong OpenCV
Trong OpenCV, để dùng giải thuật Canny, ta đơn giản chỉ cần 1 lệnh cv2.Canny
. Tài liệu cho tham số gọi hàm tham khảo tại đây: cv2.Canny.
Chính vì quá dễ dùng nên có thể mình chỉ cần gọi hàm là xong, nhưng nếu gọi hàm đã có kết quả thì bản thân mình sẽ không hiểu rõ được quá trình của giải thuật Canny trên. Vì vậy mình quyết định nỗ lực hiện thực lại giải thuật Canny bằng OpenCV:
canny.py
import cv2
import numpy as np
def scale_to_0_255(img):
min_val = np.min(img)
max_val = np.max(img)
new_img = (img - min_val) / (max_val - min_val) # 0-1
new_img *= 255
return new_img
def my_canny(img, min_val, max_val, sobel_size=3, is_L2_gradient=False):
"""
Try to implement Canny algorithm in OpenCV tutorial @ https://docs.opencv.org/master/da/d22/tutorial_py_canny.html
"""
#2. Noise Reduction
smooth_img = cv2.GaussianBlur(img, ksize=(5, 5), sigmaX=1, sigmaY=1)
#3. Finding Intensity Gradient of the Image
Gx = cv2.Sobel(smooth_img, cv2.CV_64F, 1, 0, ksize=sobel_size)
Gy = cv2.Sobel(smooth_img, cv2.CV_64F, 0, 1, ksize=sobel_size)
if is_L2_gradient:
edge_gradient = np.sqrt(Gx*Gx + Gy*Gy)
else:
edge_gradient = np.abs(Gx) + np.abs(Gy)
angle = np.arctan2(Gy, Gx) * 180 / np.pi
# round angle to 4 directions
angle = np.abs(angle)
angle[angle <= 22.5] = 0
angle[angle >= 157.5] = 0
angle[(angle > 22.5) * (angle < 67.5)] = 45
angle[(angle >= 67.5) * (angle <= 112.5)] = 90
angle[(angle > 112.5) * (angle <= 157.5)] = 135
#4. Non-maximum Suppression
keep_mask = np.zeros(smooth_img.shape, np.uint8)
for y in range(1, edge_gradient.shape[0]-1):
for x in range(1, edge_gradient.shape[1]-1):
area_grad_intensity = edge_gradient[y-1:y+2, x-1:x+2] # 3x3 area
area_angle = angle[y-1:y+2, x-1:x+2] # 3x3 area
current_angle = area_angle[1,1]
current_grad_intensity = area_grad_intensity[1,1]
if current_angle == 0:
if current_grad_intensity > max(area_grad_intensity[1,0], area_grad_intensity[1,2]):
keep_mask[y,x] = 255
else:
edge_gradient[y,x] = 0
elif current_angle == 45:
if current_grad_intensity > max(area_grad_intensity[2,0], area_grad_intensity[0,2]):
keep_mask[y,x] = 255
else:
edge_gradient[y,x] = 0
elif current_angle == 90:
if current_grad_intensity > max(area_grad_intensity[0,1], area_grad_intensity[2,1]):
keep_mask[y,x] = 255
else:
edge_gradient[y,x] = 0
elif current_angle == 135:
if current_grad_intensity > max(area_grad_intensity[0,0], area_grad_intensity[2,2]):
keep_mask[y,x] = 255
else:
edge_gradient[y,x] = 0
#5. Hysteresis Thresholding
canny_mask = np.zeros(smooth_img.shape, np.uint8)
canny_mask[(keep_mask>0) * (edge_gradient>min_val)] = 255
return scale_to_0_255(canny_mask)
img = cv2.imread('girl_11.jpg', 0)
my_canny = my_canny(img, min_val=100, max_val=200)
edges = cv2.Canny(img, 100, 200)
cv2.imwrite('my_canny.jpg', my_canny)
cv2.imwrite('edges.jpg', edges)
Kết quả tự hiện thực của mình mới chỉ xấp xỉ kết quả khi gọi hàm canny trong OpenCV, các điểm khác biệt sau:
- Hiện thực của OpenCV có thể sẽ có một số chi tiết hiện thực khác mình (hàm my_canny).
- Ở bước lọc ngưỡng, hiện thực của mình chỉ dùng min_val để lọc, mình chưa hiện thực việc xét liền kề nên dùng min_val luôn cho gọn :D.
- Mình dùng vòng lặp for ở bước NMS do đó sẽ chạy chậm hơn hàm của OpenCV rất nhiều, và OpenCV chắc chắn cũng đã tối ưu các bước nên chạy nhanh hơn hàm tự viết rất nhiều!
Vì vậy, hàm tự hiện thực của mình chỉ mang tính chất tham khảo học hỏi thôi nha. Bạn nào pro có thể hiện thực thêm và bình luận chia sẻ nhé.
Cảm ơn bạn đã theo dõi bài viết. Hãy kết nối với tớ nhé!
- Minh: https://www.facebook.com/minhng.info
- Khám phá xử lý ảnh - GVGroup: https://www.facebook.com/groups/ip.gvgroup
Khám phá xử lý ảnh - GVGroup
Danh sách bài viết series OpenCV:
- Hashtag #OpenCV
- Tut 1: Xử lý ảnh - OpenCV đọc ghi hình ảnh (code Python và C++)
- Tut 1.1: Xử lý ảnh - Cấu trúc dữ liệu ảnh trong OpenCV. Pixel là gì?
- Tut 1.2: Xử lý ảnh - Chuyển đổi ảnh OpenCV sang Pillow và ngược lại
- Tut 2: Xử lý ảnh - OpenCV resize, crop và padding hình ảnh (code Python và C++)
- Tut 3: Xử lý ảnh - OpenCV biến đổi mức sáng hình ảnh (code Python)
- Tut 4: Xử lý ảnh - OpenCV vùng quan tâm (ROI) là gì? (code Python)
- Tut 4.1: Xử lý ảnh - OpenCV: vẽ văn bản, đường thẳng, mũi tên, hình chữ nhật, hình tròn, ellipse, đa giác
- Tut 4.2: Xử lý ảnh - Pha trộn ảnh trong OpenCV (blending)
- Tut 5: Xử lý ảnh - OpenCV ảnh nhị phân
- Tut 6: Xử lý ảnh - OpenCV cân bằng sáng (histogram equalization)
- Tut 7: Xử lý ảnh - OpenCV kỹ thuật cửa sổ trượt (sliding window)
- Tut 8: Xử lý ảnh - Convolution là gì?
- Tut 9: Xử lý ảnh - Làm mờ ảnh (blur)
- Tut 10: Xử lý ảnh - Gradient của ảnh là gì?
- Tut 11: Xử lý ảnh - Phát hiện cạnh Canny (Canny Edge Detection)
- Tut 12: Xử lý ảnh - Phát hiện đường thẳng bằng Hough Transform (Hough Line)
- Tut 13: Xử lý ảnh - Hiện thực phát hiện đoạn thẳng dùng Hough Transform (Hough Line)
- Tut 14: Xử lý ảnh - Giải thuật phân vùng Region Growing trên ảnh màu
- Tut 15: Xử lý ảnh - Giải thuật Background Subtraction trên ảnh màu
- Tut 16: Xử lý ảnh - Frame Subtraction để phát hiện chuyển động trong video
- Tut 17: Xử lý ảnh - HOG - Histograms of Oriented Gradients
- Tut 18: Xử lý ảnh - HOG - Huấn luyện mô hình phân loại người
- Tut 19: Xử lý ảnh - HOG - Phát hiện người
- Tut 20: Xử lý ảnh - Tổng hợp kinh nghiệm xử lý ảnh (End)
- Tut 21: Xử lý ảnh - Hiện thực trích đặc trưng Local Binary Patterns (LBP)
- Tut 22: Xử lý ảnh - Trích đặc trưng Gabor filters