天天看點

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

機器學習與計算機視覺入門項目——視訊投籃檢測(二)

一、手工特征與CNN特征

在上一次的部落格中,介紹了計算機視覺和機器學習的關系、籃球進球檢測的基本問題和資料集的制作。這次的我們主要介紹如何從原始圖像中提取有用的圖像特征,以便應用于之後的分類器。

如下圖所示,我們現在要做的是Feature Extraction。在前DeepLearning時代,圖像特征都是由人設計的,對于不同的圖像如人臉、行人,都有各自所适合的特征,也就是所謂的Hand-crafted Feature。比如常用Haar特征來描述人臉,常用HOG特征來描述行人。

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

是以,我們可以拿先前中醫看病的例子來類比前深度學習時代的工程師是怎樣解決計算機視覺問題的。假設有這樣一位精通醫術的老中醫在藥店坐堂,前來看病的人病情各不一樣。老中醫需要根據每位病人的具體情況開出藥方,然後抓藥,以特定的方式熬制湯藥并且服用才能治療病情。比如,有位病人患了“臉盲症”,老中醫開出藥方:準備“人臉資料集”一副,取“光照歸一化”一兩,再稱“Haar特征”三錢,送入“Adaboost級聯分類器”慢火熬制,連續服用若幹代,臉盲症可愈。

從這裡我們可以看到,解決CV問題是非常依賴人工經驗的,他需要工程師對手頭上的“工具”,也就是“特征”和“分類器”的使用條件以及性能都有充分的了解,必要時甚至需要針對這類問題設計新的圖像特征。是以拿中醫診病來類比前深度學習時代的CV工程問題一點也不為過。

在卷積神經網絡開始大顯神威之後,feature extraction這一步基本都交給CNN來做了。利用多個卷積層的級聯,依靠資料驅動(data-driven)的學習方式,CNN可以很好地學習到資料集的特定的資料分布,多數情況下,比人類手工設計的特征好使多了。

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

在籃球進球檢測這個簡單的任務的開始階段,我們采用HOG特征足以應對了。

二、HOG特征簡介

HOG百度百科上總結了HOG特征的基本思想:

把樣本圖像分割為若幹個像素的單元,把梯度方向平均劃分為多個區間,在每個單元裡面對所有像素的梯度方向在各個方向區間進行直方圖統計,得到一個多元的特征向量,每相鄰的單元構成一個區間,把一個區間内的特征向量聯起來得到多元的特征向量,用區間對樣本圖像進行掃描,掃描步長為一個單元。最後将所有塊的特征串聯起來,就得到了人體的特征。

原版論文在此。我在實習時,王老師要求把文章的每一處地方都推導一遍,然後自己寫代碼實作HOG算法。是以我主要是參考了上面這篇CVPR2005的最早的文章。另外,知乎上有一篇文章講的也比較清楚,在這裡貼出來圖像學習-HOG特征。

網上講HOG特征的文章不在少數,隻要認真看過兩三篇,基本可以了解HOG特征是怎麼提取的了。是以在這裡就不再贅述了。如果希望進一步挖掘HOG特征的潛力,可以看這裡。

三、HOG特征的Python實作

學習一個算法最好的辦法就是親自實作一遍。隻有親手經曆一遍這樣的過程,才能體會到算法實作過程中出現了什麼樣的問題,才能對算法的性能和算法的擅長領域做到心中有數。下面是我實作的HOG算法的Python代碼,當然也參考了前輩的CSDN博文。

class Hog_descriptor():
    '''
    提取灰階圖檔的HOG特征,畫HOG特征圖
    '''
    def __init__(self, img, cell_size=, bin_size=):
        # 輸入的圖檔資料應是np.array格式的uint8資料,0-255
        self.img = np.sqrt(img / float(np.max(img)))# 伽馬校正
        self.cell_size = cell_size # 計算梯度直方圖的最小單元的尺寸
        self.bin_size = bin_size # 梯度直方圖中的條數
        self.angle_unit =  / self.bin_size # 角度增加的步長
        height, width = self.img.shape

        # 将圖像尺寸resize成cell_size的整數倍,友善之後運算
        self.width = int(np.ceil(width/cell_size)*cell_size)
        self.height = int(np.ceil(height/cell_size)*cell_size)
        img = cv2.resize(self.img,(width,height),interpolation=cv2.INTER_CUBIC)
        self.img = img

    def global_gradient(self): 
        # 在原圖上求梯度,函數傳回與原圖尺寸相同
        # 1,0在x方向求梯度,0,1在y方向求梯度,sobel濾波器尺寸=3
        gradient_values_x = cv2.Sobel(self.img, cv2.CV_64F, , , ksize=)
        gradient_values_y = cv2.Sobel(self.img, cv2.CV_64F, , , ksize=)
        # 梯度幅值=sqrt(gx^2+gy^2)
        gradient_magnitude = np.sqrt(np.square(gradient_values_x)+np.square(gradient_values_y))
        # 梯度相角=arctan(gy/gx)
        gradient_angle = cv2.phase(gradient_values_x, gradient_values_y, angleInDegrees=True)
        return gradient_magnitude, gradient_angle

    def extract(self):
        # 1.提特征向量 2.畫特征圖
        # 對原圖每一點求梯度幅值和相角
        gradient_magnitude, gradient_angle = self.global_gradient()
        gradient_magnitude = abs(gradient_magnitude)
        # 記錄每一個cell的hog向量(1*bin_size維)的矩陣
        cell_histogram_vector = np.zeros((int(self.height / self.cell_size), 
                                          int(self.width / self.cell_size), self.bin_size))
        # 對每個cell,計算hog向量
        for i in range(cell_histogram_vector.shape[]):
            for j in range(cell_histogram_vector.shape[]):
                # 從整張圖的梯度資訊中摳出該cell的梯度資訊
                cell_magnitude = gradient_magnitude[i * self.cell_size:(i + ) * self.cell_size,
                                 j * self.cell_size:(j + ) * self.cell_size]
                cell_angle = gradient_angle[i * self.cell_size:(i + ) * self.cell_size,
                             j * self.cell_size:(j + ) * self.cell_size]
                # 将每一個cell的直方圖向量寫入總的存儲矩陣
                cell_histogram_vector[i][j] = self.cell_gradient(cell_magnitude, cell_angle)
        # 可視化所有cell的hog向量
        hog_image = self.render_gradient(np.zeros([self.height*, self.width*]), cell_histogram_vector)
        # 将block以stride=cell_size為步長滑動,将提取的36維hog向量拼接起來構成描述全圖的特征向量
        hog_vector = []
        for i in range(cell_histogram_vector.shape[] - ):
            for j in range(cell_histogram_vector.shape[] - ):
                # 在一個block内,拼接4個hog向量,并對其歸一化,消除光照不均的影響
                block_vector = []
                block_vector.extend(cell_histogram_vector[i][j])
                block_vector.extend(cell_histogram_vector[i][j + ])
                block_vector.extend(cell_histogram_vector[i + ][j])
                block_vector.extend(cell_histogram_vector[i + ][j + ])
                mag = lambda vector: math.sqrt(sum(i **  for i in vector))
                magnitude = mag(block_vector)# lambda定義匿名函數,求L2範,vector是自變量,冒号後是函數體
                if magnitude != :# 用L2範歸一化block直方圖向量
                    normalize = lambda block_vector, magnitude: [element / magnitude for element in block_vector]
                    block_vector = normalize(block_vector, magnitude)
                hog_vector.extend(block_vector)
        return hog_vector, hog_image



    def cell_gradient(self, cell_magnitude, cell_angle):# 計算一個cell的直方圖向量
        orientation_centers = [] * self.bin_size# bin_size維的清單,預設是9維
        for i in range(cell_magnitude.shape[]):
            for j in range(cell_magnitude.shape[]):
                gradient_strength = cell_magnitude[i][j]# 周遊cell中的每一個梯度幅值
                gradient_angle = cell_angle[i][j]# 取出每個幅值對應的角度
                min_angle, max_angle, mod = self.get_closest_bins(gradient_angle)
                orientation_centers[min_angle] += (gradient_strength * ( - (mod / self.angle_unit)))
                orientation_centers[max_angle] += (gradient_strength * (mod / self.angle_unit))
        return orientation_centers

    def get_closest_bins(self, gradient_angle):
        '''
        若有角度330°,bin=9,angle_unit=40°,則330°應落在320°到0°之間,其幅值應按比例加到320和0對應的bin上
        min_angle = 8,max_angle = (8+1)%9 = 0
        mod=10°,離320°更近,配置設定到320對應的bin應占總幅值的3/4
        離0°遠,幅值配置設定1/4
        '''
        idx = int(gradient_angle / self.angle_unit)% self.bin_size
        mod = gradient_angle % self.angle_unit
        return idx, (idx + ) % self.bin_size, mod

    def render_gradient(self, image, cell_histogram):# 畫圖
        # 為了使HOG圖像更為清晰,特征圖的尺寸擴大為原來的五倍。
        cell_width = int(self.cell_size) /  * 
        max_magnitude = np.array(cell_histogram).max()
        for x_height in range(cell_histogram.shape[]): # x_height代表img高度,y_width代表img寬度
            for y_width in range(cell_histogram.shape[]):
                cell_hist = cell_histogram[x_height][y_width] # 取一個cell代表的histogram
                cell_hist /= max_magnitude # 幅值歸一化
                angle = 
                # 找cell的中心
                x_center = *(x_height)*self.cell_size+int(self.cell_size*)/
                y_center = *(y_width)*self.cell_size+int(self.cell_size*)/
                angle_gap = self.angle_unit
                for magnitude in cell_hist:
                    angle_radian = math.radians(angle)
                    x1 = int(x_center + cell_width* * math.cos(angle_radian))
                    y1 = int(y_center - cell_width* * math.sin(angle_radian))
                    x2 = int(x_center - cell_width* * math.cos(angle_radian))
                    y2 = int(y_center + cell_width* * math.sin(angle_radian))                    
                    cv2.line(image, (y1, x1), (y2, x2), int( * math.sqrt(magnitude)))
                    angle += angle_gap
        return image

    def hog_imshow_save(self,hog_img_name = 'hog_img.png'):
        # 儲存特征圖
        vector, image = self.extract()
        plt.imshow(image, cmap=plt.cm.gray)
        plt.show()
        cv2.imwrite(hog_img_name,image)
           

這是調用Hog_descriptor類提取圖像特征并展示特征圖的example。

# example of hog class    
img = cv2.imread('test.png', cv2.IMREAD_GRAYSCALE) # 讀入灰階圖
hog = Hog_descriptor(img, cell_size=, bin_size=)
hog.hog_imshow_save('D:/hog_img.png')
           

效果如下。

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)
機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

第二張圖是第一張圖的尺寸放大五倍後的效果,可以看到,條狀亮線是每一個cell特征可視化的結果,很好地反映了原圖的邊緣資訊。

四、一個小問題

HOG特征描述的是圖像的邊緣資訊,因為邊緣處梯度的幅值才比較大。那麼針對我們的籃球進球檢測問題來說,如果由于攝像頭的視線原因,籃球恰好出現在籃網前面,就像下圖這樣:

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

它和正常進球的樣本差別就在于幾根比較稀疏的籃網線在籃球的前面或者後面,此時,我們的HOG特征還能準确地描述兩者在紋理、邊緣、輪廓上的不同嗎?考慮到一張圖檔的尺寸大約在60*50,是比較小的,沒有辦法把籃網的輪廓精細地描繪出來。下面的hog特征圖就是上面這個迷惑性很強的負樣本的可視化圖。

機器學習與計算機視覺入門項目——視訊投籃檢測(二)機器學習與計算機視覺入門項目——視訊投籃檢測(二)

我認為在HOG特征這個層次上,實際上是無法厘清這兩者的差別的,所幸的是,實時的進球檢測效果看起來還不錯,至少模型沒有對這樣的假陽性樣本出現誤判。在随後的使用的MLP和CNN做檢測時,對這類樣本的魯棒性要更好一些。

五、制作整個資料集的HOG特征集

在第一部分我們提取了視訊中的籃筐位置并儲存為小圖檔。為了之後存取的友善,将其儲存成pkl檔案形式。

img_dir = 'D:/dataset/'
filelist = os.listdir(img_dir)
# 先讀一張圖檔,獲得尺寸資訊
img = cv2.imread(img_dir + filelist[])
height,width = np.shape(img)
# 開辟空矩陣
img_to_save = np.zeros((np.shape(filelist)[],height,width),np.uint8)
for i in range(,len(filelist)):
    tmp = cv2.imread(img_dir+filelist[i])
    img_to_save[i] = tmp
# 存成pkl檔案
img_output = open('img.pkl','wb')
pickle.dump(img_to_save,img_output)
img_output.close()
           

現在對

img.pkl

的每一張圖檔都提取hog特征

# 讀資料集
img_input = open('img.pkl','rb')
img_matrix = pickle.load(img_input)
img_input.close()

hog_vector = []
for i in range(,np.shape(img_matrix)[]):
    hog = Hog_descriptor(img_matrix[i], cell_size=, bin_size=)
    temp,_ = hog.extract()
    hog_vector.append(temp)
    if i %  == :
    print(i)
hog_output = open('hog.pkl','wb')
pickle.dump(hog_vector,hog_output)
hog_output.close()
           

繼續閱讀