Hiện thực trích đặc trưng Local Binary Patterns (LBP)
OpenCV - Tut 21: Local Binary Patterns (LBP)
Tut 21: Local Binary Patterns (LBP)
Hi mọi người, lại là Minh đây. Series bài viết sẽ được tiếp tục với một vài phương pháp rút trích đặc trưng khác. Ở Tut 21 này, ta sẽ cùng tìm hiểu về trích xuất đặc trưng Local Binary Patterns (LBP); vai trò của LBP sẽ tương tự như HOG (Tut 17: Xử lý ảnh - HOG - Histograms of Oriented Gradients) đó là vai trò nằm ở bước rút trích đặc trưng ảnh (feature extraction). Nào ta cùng tìm hiểu thôi :)
À mà quên, tiết mục quan trọng nhất là chia sẻ ảnh mẫu <3:
girl_xinh.jpg
Ghi chú: do ảnh này mình tìm kiếm và lấy trên mạng, nên không biết chính chủ để credit vào; ảnh này được sử dụng vào mục đích học tập / nghiên cứu (giáo dục), và mang tính chất phi thương mại!
Trích xuất đặc trưng LBP là gì?
Local Binary Patterns (hay còn viết tắt là LBP) là một phương pháp rút trích đặc trưng trong xử lý ảnh. Đặc trưng được rút trích sẽ tiếp tục được tiến hành chọn lọc (feature selection) thu gọn thành vector đặc trưng. Vector đặc trưng này sau đó có thể dùng để đưa vào mô hình học máy để học / phân loại.
Các bước tiến hành trích xuất đặc trưng theo phương pháp LBP:
- Duyệt lần lượt từng pixel trên ảnh (theo cột -> theo hàng), với pixel đang xét, ta áp dụng bước 2->4.
- Xét lần lượt 8 pixel lân cận (hàng xóm - neighbor) của pixel đang duyệt (trung tâm - center). Mỗi pixel hàng xóm sẽ ứng với một bit trong một chuỗi 8-bit. Chuỗi 8-bit này ban đầu sẽ bằng: 00000000. Chuỗi 8-bit này sẽ được cập nhật theo mô tả ở bước 3.
- Nếu mức sáng tại pixel hàng xóm >= mức sáng tại pixel trung tâm: vote bit ở vị trí tương ứng lên 1 trong chuỗi 8-bit đề cập ở bước 2.
- Sau khi hoàn tất bước 2 và 3, ta sẽ có mội chuỗi 8-bit (vd: 00101100) -> đổi giá trị nhị phân này sang thập phân để lưu trữ (vd: 00101100 nhị phân = 44 thập phân)
- Lặp hết toàn ảnh (bước 1->4), ta sẽ có kết quả đầu ra bằng kích thước với ảnh đầu vào. Mỗi giá trị trên ảnh đầu ra là đặc trưng LBP.
LBP ta có thể tạm hiểu (để nhớ như sau):
- Local: thể hiện tính chất cục bộ địa phương, đó là khi ở bước 2 ta xét pixel lân cận -> mỗi đặc trưng trong output sẽ mang đặc trưng đại diện cục bộ.
- Binary Patterns: các mẫu hình nhị phân -> cách nhị phân hóa mô tả ở bước 3, 4
Tổng quát hóa phương pháp tiếp cận LBP trên, ta sẽ có các tham số sau:
- P: Số pixel lận cận pixel trung tâm (vd: P=8).
- R: Bán kính của pixel lân cận mà ta sẽ xét - cách pixel trung tâm bao nhiêu pixel (vd: R=1 nghĩa là liền kề). Kiểu như khoảng cách của nhà bạn và nhà ông hàng xóm mà bạn "quan tâm" í :">.
- Thứ tự các pixel lân cận mã hóa vào chuỗi 8-bit sẽ theo chiều kim đồng hồ hay ngược chiều kim đồng hồ.
- Interpolation: do lấy pixel lân cận theo hình tròn, do đó tọa độ của các pixel lân cận khi tính toán ra sẽ là số thực -> quyết định lấy ra giá trị mức sáng của pixel đó theo cách nào: pixel gần nhất (nearest) hay có trọng số (bilinear). Interpolation này tương tự như khi bạn resize ảnh vậy. Tham khảo cv2.resize() và các cờ Interpolation trong OpenCV.
Nguồn tham khảo: Local binary patterns @ Wikipedia
Hiện thực LBP từ đầu
Trong các thư viện cung cấp API trích xuất đặc trưng LBP, mình tham khảo API của Scikit-Image: skimage.feature.local_binary_pattern. Ta tiến hành hiện thực thôi:
lbp.py
import math
import cv2
import numpy as np
from skimage.feature import local_binary_pattern # # pip install scikit-image
class LBP(object):
def __init__(self, radius=1, npoints=8, counter_clockwise=True, interpolation="bilinear"):
self.radius = radius
self.npoints = npoints
self.interpolation = interpolation
self.counter_clockwise= counter_clockwise
assert self.radius > 0 and self.npoints > 0
assert interpolation in ("bilinear", "nearest")
self.get_pixel_func = self._get_pixel_nearest if self.interpolation == "nearest" else self._get_pixel_bilinear
start_angle_radian = 0
angle_radian = 2*math.pi/npoints
circle_direction = 1 if counter_clockwise else -1
neighbor_positions = []
for pos in range(self.npoints):
# traverse on angles: 0, -1*angle_radian, -2*angle_radian, ...
delta_x = math.cos(start_angle_radian+circle_direction*pos*angle_radian) * self.radius
delta_y = -(math.sin(start_angle_radian+circle_direction*pos*angle_radian) * self.radius)
neighbor_positions.append((delta_x, delta_y))
neighbor_positions.reverse()
self.neighbor_positions = neighbor_positions # [(0.7071067811865474, 0.7071067811865477), (-1.8369701987210297e-16, 1.0), (-0.7071067811865477, 0.7071067811865475), (-1.0, -1.2246467991473532e-16), (-0.7071067811865475, -0.7071067811865476), (6.123233995736766e-17, -1.0), (0.7071067811865476, -0.7071067811865475), (1.0, -0.0)]
assert len(self.neighbor_positions) == npoints
pass
def _get_pixel_nearest(self, image, x, y, w, h):
xx = round(x)
yy = round(y)
if xx < 0 or yy < 0 or xx >= w or yy >= h:
return 0
else:
return image[yy, xx]
def _get_pixel_bilinear(self, image, x, y, w, h):
"""
x: float. Eg: 0.3
y: float. Eg: 0.7
"""
xmin, xmax = math.floor(x), math.ceil(x) # 0, 1
ymin, ymax = math.floor(y), math.ceil(y) # 0, 1
intensity_top_left = 0 if xmin<0 or ymin<0 or xmin>=w or ymin>=h else image[ymin, xmin]
intensity_top_right = 0 if xmax<0 or ymin<0 or xmax>=w or ymin>=h else image[ymin, xmax]
intensity_bottom_left = 0 if xmin<0 or ymax<0 or xmin>=w or ymax>=h else image[ymax, xmin]
intensity_bottom_right = 0 if xmax<0 or ymax<0 or xmax>=w or ymax>=h else image[ymax, xmax]
weight_x = x - xmin
weight_y = y - ymin
intensity_at_top = (1-weight_x) * intensity_top_left + weight_x * intensity_top_right
intensity_at_bottom= (1-weight_x) * intensity_bottom_left + weight_x * intensity_bottom_right
final_intensity = (1-weight_y) * intensity_at_top + weight_y * intensity_at_bottom
return final_intensity
def __call__(self, image):
assert len(image.shape) == 2
h, w = image.shape
result = np.zeros([h, w])
for y in range(h):
for x in range(w):
center_intensity = image[y, x]
binary_vector = [0] * self.npoints # [0, 0, 0, 0, 0, 0, 0, 0]
for npos in range(self.npoints):
new_x = x + self.neighbor_positions[npos][0]
new_y = y + self.neighbor_positions[npos][1]
neighbor_intensity = self.get_pixel_func(image, new_x, new_y, w, h)
if center_intensity <= neighbor_intensity:
binary_vector[npos] = 1
binary_str = "".join([str(e) for e in binary_vector]) # '00001001'
decimal_value = int(binary_str, 2) # convert binary string to decimal
result[y, x] = decimal_value
return result
def main():
pattern = np.array([
[234, 34, 67, 93, 165, 256, 96, 32],
[74, 32, 1, 56, 93, 20, 200, 93, ],
[72, 145, 83, 94, 145, 241, 176, 82],
[94, 83, 135, 185, 252, 187, 33, 58],
[99, 76, 92, 32, 56, 128, 194, 92],
[232, 155, 222, 94, 22, 185, 25, 65],
[87, 24, 43, 129, 32, 39, 74, 91],
[243, 97, 215, 36, 184, 92, 4, 9],
])
out_scikit = local_binary_pattern(image=pattern, P=8, R=1, method='default')
print("Scikit output:", out_scikit)
lbp = LBP()
out_our = lbp(pattern)
print("Our output:", out_our)
print("Same output:", (out_our == out_scikit).all())
if __name__ == "__main__":
main()
print('---------')
print('* Follow me @ ' + "\x1b[1;%dm" % (34) + ' https://www.facebook.com/kyznano/' + "\x1b[0m")
print('* Minh fanpage @ ' + "\x1b[1;%dm" % (34) + ' https://www.facebook.com/minhng.info/' + "\x1b[0m")
print('* Join GVGroup @ ' + "\x1b[1;%dm" % (34) + 'https://www.facebook.com/groups/ip.gvgroup/' + "\x1b[0m")
print('* Thank you ^^~')
Tadaaa, một nùi code quăng vào mặt (^___^). Mình sẽ giải thích một số điểm chính:
- Có npoints điểm lân cận và bán kính radius do người dùng đặc tả => ta tính được mỗi cung tròn thành phần có góc bao nhiêu (360 độ / npoints) => ước tính được tọa độ x, y của mỗi pixel lân cận @ delta_x, delta_y. Lưu ý rằng lúc tính delta_y ta phải có dấu trừ (-) ở đặc trước vì vì gốc tọa độ của ảnh nằm ở phía trên bên trái (top left), do đó trục y nó bị ngược so với gốc tọa độ trong toán học (nằm ở phía dưới bên trái - bottom left) mà ta thường tính sin / cos.
- neighbor_positions.reverse(): ta duyệt vòng tròn (các pixel lân cận) theo thứ tự điểm đầu tiên ở 0 độ, các điểm tiếp theo ngược kim đồng hồ. Ta muốn điểm lân cận xét đầu tiên sẽ nằm cuối chuỗi nhị phân (00000001) do đó ta đảo list lại.
- Hàm __call__() của class LBP ta sẽ hiện thực như giải thuật đó là duyệt lần lượt các pixel trong ảnh và rút trích đặc trưng.
- Hàm _get_pixel_bilinear hiện thực cách lấy mức sáng pixel ở vị trí tọa độ thực (vd: x=0.72, y=0.63) theo bilinear interpolation.
Nếu có thời gian mình sẽ viết 1 bài về các cách nội suy, hiện tại bạn tham khảo link tài liệu bên dưới để hiểu thêm nha.
Giải thích về Bilinear Interpolation: https://theailearner.com/2018/12/29/image-processing-bilinear-interpolation/
Kết quả thực thi script lbp.py, khớp chính xác so với hiện thực Scikit-Image :">~. Dĩ nhiên trong lúc hiện thực mình vào mày mò nhảy vào tận mã nguồn nó để tham khảo.
root@e7eba89aeeaf:/workspace/OPENCV/LBP# python lbp.py
Scikit output: [[ 0. 57. 1. 129. 1. 0. 240. 112.]
[134. 254. 255. 239. 238. 255. 0. 56.]
[197. 0. 241. 227. 225. 0. 20. 28.]
[ 66. 191. 1. 1. 0. 28. 255. 108.]
[192. 255. 238. 255. 175. 77. 0. 16.]
[ 0. 17. 0. 120. 255. 0. 255. 108.]
[198. 255. 239. 40. 251. 239. 9. 0.]
[ 0. 25. 0. 191. 0. 16. 63. 14.]]
Our output: [[ 0. 57. 1. 129. 1. 0. 240. 112.]
[134. 254. 255. 239. 238. 255. 0. 56.]
[197. 0. 241. 227. 225. 0. 20. 28.]
[ 66. 191. 1. 1. 0. 28. 255. 108.]
[192. 255. 238. 255. 175. 77. 0. 16.]
[ 0. 17. 0. 120. 255. 0. 255. 108.]
[198. 255. 239. 40. 251. 239. 9. 0.]
[ 0. 25. 0. 191. 0. 16. 63. 14.]]
Same output: True
Do Scikit-Image hiện thực đã được tối ưu nên tốc độ chạy cực nhanh so với mình tự hiện thực trên Python nha. Minh hiện thực lại từ đầu nhằm giải thích siêu chi tiết cho các bạn nắm rõ thôi, nếu đọc giải thích lý thuyết chưa hiểu bạn dễ dàng debug từng dọc code hiện thực của mình :).
Nghịch ngợm với LBP
LBP chỉ làm nhiệm vụ trích đặc trưng, nó chưa bao gồm tiền xử lý. Do đó, Minh sẽ thử áp dụng tiền xử lý làm mờ ảnh để các bạn xem thử kết quả ảnh in ra sẽ khác nhau như thế nào nhé. Ảnh mẫu lấy ở đầu bài nhe các ông.
lbp_blur.py
import math
import cv2
import numpy as np
from skimage.feature import local_binary_pattern # # pip install scikit-image
KERNEL_WIDTH = 7
KERNEL_HEIGHT = 7
SIGMA_X = 3
SIGMA_Y = 3
def main():
img = cv2.imread('girl_xinh.jpg', cv2.IMREAD_GRAYSCALE)
# LBP
out = local_binary_pattern(image=img, P=8, R=1, method='default')
cv2.imwrite('lbp.jpg', out)
print("Saved image @ lbp.jpg")
# Gaussian blur + LBP
blur_img = cv2.GaussianBlur(img, ksize=(KERNEL_WIDTH, KERNEL_HEIGHT), sigmaX=SIGMA_X, sigmaY=SIGMA_Y)
blur_out = local_binary_pattern(image=blur_img, P=8, R=1, method='default')
cv2.imwrite('blur.jpg', blur_img)
cv2.imwrite('blur_lbp.jpg', blur_out)
print("Saved image @ blur.jpg")
print("Saved image @ blur_lbp.jpg")
if __name__ == "__main__":
main()
print('---------')
print('* Follow me @ ' + "\x1b[1;%dm" % (34) + ' https://www.facebook.com/kyznano/' + "\x1b[0m")
print('* Minh fanpage @ ' + "\x1b[1;%dm" % (34) + ' https://www.facebook.com/minhng.info/' + "\x1b[0m")
print('* Join GVGroup @ ' + "\x1b[1;%dm" % (34) + 'https://www.facebook.com/groups/ip.gvgroup/' + "\x1b[0m")
print('* Thank you ^^~')
Các kết quả:
Bài viết tiếp theo: Tut 22: Xử lý ảnh - Trích đặc trưng Gabor filters
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