Hiện thực trích đặc trưng Local Binary Patterns (LBP)

  Aug 29, 2020      2m
   

OpenCV - Tut 21: Local Binary Patterns (LBP)

Hiện thực trích đặc trưng 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

girl_xinh_3.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:

  1. 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.
  2. 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.
  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.
  4. 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)
  5. 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.

local binary patterns

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:

  • 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ả:

LBP_EXP.jpg

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é!


Danh sách bài viết series OpenCV: