YOLO3 + Python3.6 深度學習篇(中)- Transfer Learning 遷移學習

上一篇文章鏈接:


繼上一次內容中,我們調用了 Google “圖片下載” 的 API 接口通過簡易爬蟲的方法抓下了一卡車的圖片信息,在整個環節中可以算是完成了第一步:蒐集數據。但是我們需要通過一個機制,一個電腦看得懂的方法,告訴電腦圖片裏面的東西分別是什麼,而這個機制就是使用 XML 文件去記錄圖片內容信息,從圖片名稱,物件座標位置,物件名稱,圖片像素點... 等等信息完整記錄於此。

之所以使用 XML 作爲記錄標記圖片信息的方式原因在於它是一個除了 JSON 之外,極爲明確且通用的一種記錄方式,並且從學術論文和相關單位研究中也都可以看到研究人員使用此類檔案作爲他們標記圖片的方式,與他們的模型良好的整合在一起。隨着計算機發展的時間到現在,XML 的配套函數包也都已經健全,只要簡單地引用相關函數功能,就可以輕鬆建立一個 XML 文件。


PASCAL Visual Object Classes

實戰中,一個完整的 XML 文件內容長這樣的 [點擊] ,在 YOLO 的官網中也有相關的數據庫 [點擊] 介紹並提供下載,而他們統一遵循着一個名爲 VOC 的數據訓練集(官網 [點擊] ) 所使用的文檔規則,蒐集文章的時候讀到一篇說明比較清楚的內容 [點擊] 供大家參考。

由於這個訓練集是單位發起的一個挑戰賽,每個參賽單位都以訓練集的統一格式作爲他們模型輸入端口的資料展開訓練,所以很快的這類的文檔也逐漸沉澱成爲所謂的 “業內標準”。

訓練集裏面的圖片又一個共通性,其大小被約束在一個範圍區間內,並且在一開始訓練的第一步就是重新整合數據集裏面的圖片大小,做到 “大一統” 的目的後,纔來開始談訓練過程和結果。


Data Labeling 標記數據

於實戰中使用到的 Python module 有:(模塊引用名: 使用文檔)

p.s. 更多記錄會逐一完備並添加鏈接於此,更爲深入的介紹個人使用每個包的心得與常見功能


The codes are written below in practice:

# 用來操控電腦相關文件屬性與檔案路徑的模塊
import os
# 用來操控圖片,導入攝像頭,處理色彩超級模塊
import cv2
# 這是一個 matplotlib 下面的類,主要功能是製圖
import matplotlib.pyplot as plt
# 這是一個 matplotlib 下面的類,提供格式小工具,這邊使用的是方形選取
from matplotlib.widgets import RectangleSelector
# 一個用來處理 xml 文本的包,這邊用來重新排布文本段落,使之更可視化
from lxml import etree
# 同上,只是它的引用名太長了,給個短的名字方便
import xml.etree.cElementTree as ET


# 人工輸入一個我們目標的文件夾路徑,讓代碼從此展開執行的旅程
folder_path = input("Enter the directory of the targeted folder: ")
# 一開始要全選出來的東西的名字也需要在這裏 ”初始化“
Label_name = input("The object name being specified: ")
# 設置一個 list 容器,分別用來容納之後經過鼠標產出的數據
TL_corner = []
BR_corner = []
Labels_list = []


# 定義一個 “事件“ 名爲 mouse_click 的函數,在下面與 “RectangleSelector" 相連接
def mouse_click(press, release):
    # 當需要對這些變量在函數裏面被修改的時候,用上 global 會比較精確且保險
    global TL_corner
    global BR_corner
    global Labels_list
    
    # 如果第一個參數按下去的 .button 按鈕是鼠標左鍵的話
    if press.button == 1:
        # 就把這個框框左上與右下的值分別貼到兩個空的 list 中做保存,並且把一開始我們取好名字的 Label 也存入 list 中
        TL_corner.append((int(press.xdata), int(press.ydata)))
        BR_corner.append((int(release.xdata), int(release.ydata)))
        Labels_list.append(Label_name)

    # 如果第二個參數放開鼠標的 .button 按鈕是左鍵的話
    elif release.button == 3:
        # 就把最近一次存進去 list 裏面的元素給刪了,並且打印字串告知
        del TL_corner[-1]
        del BR_corner[-1]
        del Labels_list[-1]
        print('-- Latest bounding box has been removed --')


# 定義一個函數,用來在中途改變我們要標記的物體
def change_label(event):
    # 當需要對這些變量在函數裏面被修改的時候,用上 global 會比較精確且保險
    global Label_name
    # 如果按下的按鈕是滑鼠中間的滾輪(如果你的滑鼠沒有滾輪那就 GG 了)
    if event.button == 2:
        # 繼續讓方框選擇的功能開啓
        selectImg_RS.set_active(True)
        # 重新輸入定義標籤名稱
        Label_name = input('The other object name being specified: ')

    # 即便鼠標按下的功能不是滾輪,也還是要確保方框選擇功能是被開啓的狀態
    elif event.button != 2:
        selectImg_RS.set_active(True)


'''
這只是個用來學習和測試的代碼部分,之所以留下來就是爲了深刻告訴自己:
在 matplotlib.widgets 這個模塊中的 RectangleSelector 和諸多 plt.connect(‘event name’, function)
的不同之處,當時用 RectangleSelector 的時候,他所鏈接到的自定義 function 就可以有兩個 arguments,
他們分別表示按下和放開方框的時候鼠標的座標軸位置,而 connect 的 event 就不同。
def mouse_press(press):
    global TL_corner
    if press.button == 1:
        TL_corner.append((int(press.xdata), int(press.ydata)))
    elif press.button == 3:
        print('-- Release button to remove your latest bounding box --')
    else:
        print('-- Please use mosue left click to select an area --')


def mouse_release(release):
    global BR_corner
    if release.button == 1:
        BR_corner.append((int(release.xdata), int(release.ydata)))
    elif release.button == 3:
        del TL_corner[-1]
        del BR_corner[-1]
    else:
        print('-- Please use mosue left click to select an area --')
拿這兩個 function 做舉例他們分別連接到的是 ‘button_press_event’ 和 ‘button_release_event’,
只容許他們在定義函數的時候有一個 argument 表示按下或是放開的瞬間鼠標座標點的位置。
而那個 argument 自帶的 .xdata | .ydata | .button 屬性也是在 .connect 鏈接起來後自己產生的 attribute,
如果沒有 connect,那是沒有 .xdata 這類功能的。
'''


# 定義一個 xml 文件生成函數,需要方框的兩個頂點座標信息,圖片放置位置,與最一開始的手動輸入的目標文件位置
def xml_maker(TL_corner, BR_corner, file_path, folder_path):
    # os.path 生成的 object 有一個 .name 功能打印改路徑的最後一個文件名稱
    target_img = file_path.name
    # 告知 xml 文件最後面應該存在哪個資料夾,os.path.split() 可以把最後一個文件名和前面路徑分開成爲一個 tuple 裏面的兩個不同元素
    xml_save_dir = os.path.join(os.path.split(folder_path)[0],
                                os.path.split(folder_path)[1] + "_xml")
    # 如果沒有這個文件夾名字的話,創造一個該路徑下的文件夾
    if not os.path.isdir(xml_save_dir):
        os.mkdir(xml_save_dir)

    # 開始編輯 xml 文件內容,最外層的 Tag 叫 annotation
    main_tag = ET.Element('annotation')
    # main_tag 下面有許多子 tags,分別他們的內容要裝的是對應到的文件夾名稱,對應圖片名稱
    ET.SubElement(main_tag, 'folder').text = os.path.split(folder_path)[1]
    ET.SubElement(main_tag, 'filename').text = target_img
    ET.SubElement(main_tag, 'segmented').text = str(0)

    # 同理上面編輯步驟,把圖片的尺寸資料記錄於此
    size_tag = ET.SubElement(main_tag, 'size')
    ET.SubElement(size_tag, 'width').text = str(width)
    ET.SubElement(size_tag, 'height').text = str(height)
    ET.SubElement(size_tag, 'depth').text = str(depth)

    # 由於 object 可能有很多個,甚至很多個 objects 要記錄,這邊需要迭代,把三個 list 容器重新 zip 在一起會方便許多
    for La, TL, BR in zip(Labels_list, TL_corner, BR_corner):
        # 同理上面編輯步驟,把 object 對應的名字等信息記錄於此
        object_tag = ET.SubElement(main_tag, 'object')
        ET.SubElement(object_tag, 'name').text = La
        ET.SubElement(object_tag, 'pose').text = 'Unspecified'
        ET.SubElement(object_tag, 'truncated').text = str(0)
        ET.SubElement(object_tag, 'difficult').text = str(0)         

        # 同理上面編輯步驟,把方框起來的座標記錄於此
        bndbox_tag = ET.SubElement(object_tag, 'bndbox')
        ET.SubElement(bndbox_tag, 'xmin').text = str(TL[0])
        ET.SubElement(bndbox_tag, 'ymin').text = str(TL[1])
        ET.SubElement(bndbox_tag, 'xmax').text = str(BR[0])
        ET.SubElement(bndbox_tag, 'ymax').text = str(BR[1])

    # 爲了讓 xml 排布能夠漂亮,pretty_print=True 前面的 root 必須是對應的 object,所以做了一個轉換過去然後又變回來的過程
    xml_str = ET.tostring(main_tag)
    root = etree.fromstring(xml_str)
    xml_str = etree.tostring(root, pretty_print=True)
    # 重新命名文件夾並重新整合儲存路徑,修改意味着先要拆開
    # os.path.splitext 可以良好的把文件名和檔名分成兩個元素放在一個 tuple 裏面
    save_path = os.path.join(xml_save_dir,
                             str(os.path.splitext(target_img)[0] + '.xml'))
    # 儲存文件於該位置
    with open(save_path, 'wb') as xml_file:
        xml_file.write(xml_str)


# 定義一個函數,當對一張圖片的事情做好了之後,跳到下一張圖片時候需要做的事情
def next_image(release):
    global TL_corner
    global BR_corner
    global Labels_list
    # 如果按下的按鈕是 Space 鍵,且方框選取功能是開着的
    if release.key in [' '] and selectImg_RS.active:
        # 那就呼叫剛定義好的生成 xml 函數
        xml_maker(TL_corner, BR_corner, file_path, folder_path)
        # 爲了給自己方便看存了什麼,在內容還沒背歸零最前先打印出來給我看看
        print(TL_corner, BR_corner, Labels_list)
        # 歸零,並關掉該窗口
        TL_corner = []
        BR_corner = []
        Labels_list = []
        plt.close()

    # 如果按的不是 Space 鍵,則打印下面句子
    else:
        print('-- Press "space" to jump to the next pic --')


# 只有當前 .py 文件呼叫的函數可以被執行,如果是 import 進來的文本里面有函數執行指令,該函數就會被擋住不執行
if __name__ == '__main__':
    # 遍歷每個一開始輸入進去的路徑裏面的文件路徑
    for file_path in os.scandir(folder_path):
        # 如果裏面有些文件不符合預期,讓程序報錯了,用此跳開進到下一個文件
        try:
            # 習慣的畫圖手法,可以一次創造 figure 和 axis 兩個 objects 並且還同時描繪了幾個子窗口,非常方便
            fig, ax = plt.subplots(1)
            # 使用 opencv 讀取圖片信息,找出其長寬深度值
            image = cv2.imread(file_path.path, -1)
            height, width, depth = image.shape
            # 並且由於在 matplotlib 顯示圖片是 RGB 格式,和 opencv 的BGR 順序不同,需要轉制
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            # 在 matpolotlib 基礎上秀出圖片內容
            ax.imshow(image)

            # 把 widgets 裏面的 RectangleSelector 跟 mouse_click 做關聯,給他一個名字原因純粹是太長了,要開要關不方便
            selectImg_RS = RectangleSelector(
                ax, mouse_click, drawtype='box',
                useblit=True, minspanx=5, minspany=5,
                spancoords='pixels', interactive=True)
            # 一樣把其他上面設定好的函數與圖片關聯
            plt.connect('button_press_event', change_label)
            plt.connect('key_release_event', next_image)
            # plt.connect('button_press_event', mouse_press)
            # plt.connect('button_release_event', mouse_release)
            
            # 這裏把在圖片上面做的事情 show 出來
            plt.show()

        # 如果報錯,則直接跳到下一個循環中
        except:
            continue

這次的代碼相對上次來說,是比較多的,做的事情也更爲複雜和多元,主要代碼任務排布順序如下:

  1. 找到裝載很多張圖片檔案的目標資料夾
  2. 把這些資料夾裏面的照片一次一張的方式顯示出來
  3. 在圖片上面拉上方框選取我們肉眼判定的目標區域
  4. 如果區域拉錯了,可以使用滑鼠右鍵拉方框刪除最近一次拉的方框座標點記錄
  5. 按下滑鼠中間滾輪可以重新定義方框的名字
  6. 按下鍵盤上 “空白鍵” 儲存那些拉好的方框座標位置到 XML 文件中
  7. 重複步驟完成所有圖片的標記工作

p.s. 一般圖片標記量會在百來張圖片的範圍,要是希望圖像識別能夠更爲準確,那麼就需要甚至上千張的量去訓練機器學習的模型。

標記環節也也因此成爲這個項目中最耗費人力時間的部分,可能需要花兩三個小時毫不費力的做着同樣的動作,如圖:


而標記出來的 XML 內容如下代碼:

<annotation>
  <folder>motorcycle</folder>
  <filename>000026.png</filename>
  <segmented>0</segmented>
  <size>
    <width>338</width>
    <height>149</height>
    <depth>3</depth>
  </size>
  <object>
    <name>human</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>111.93951612903226</xmin>
      <ymin>10.352419354838723</ymin>
      <xmax>166.11491935483872</xmax>
      <ymax>118.02177419354837</ymax>
    </bndbox>
  </object>
  <object>
    <name>wheel</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>173.27016129032256</xmin>
      <ymin>82.58629032258065</ymin>
      <xmax>227.10483870967744</xmax>
      <ymax>144.25766129032257</ymax>
    </bndbox>
  </object>
  <object>
    <name>wheel</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>47.88306451612903</xmin>
      <ymin>73.7274193548387</ymin>
      <xmax>113.98387096774195</xmax>
      <ymax>138.46532258064514</ymax>
    </bndbox>
  </object>
</annotation>

細心的讀者們可能會發現到這樣的 XML 文件內容和上面鏈接裏面展現出來的內容有三點不同之處:

  1. <source> 和 <owner> 兩個 Tags 沒有出現在上面結果中,原因在於訓練過程中兩個 Tags 的存在與否並不影響結果,因此標註圖片生成 XML 的時候就沒把它們放入其內容中去。
  2. 一張圖不只有可以標註一個 object,而是可以有很多個 objects 並行,只要他們彼此的標籤在同一個標籤樹的 “深度” 即可,並且分別做好各自方框座標單位在 object “支” 裏面的保存動作,訓練起來就會順利不出錯!
  3. <bndbox> 裏面的數值是浮點數,於樣本的整數不一樣,還不知道數字形式是否會影響到訓練結果


重新細究代碼

標記數據的過程不外乎就是把圖片信息用手工的方式標記好,並記錄在 XML 文件當中,中間牽涉到一些比較繁瑣的細節。

  • 初步是:文件存放位置,存放的文件夾名字,文件的名字,讀取圖片的名字,圖片存放的位置,圖片的尺寸
  • 接着是:鼠標與圖片的交互,鍵盤與流程的交互,選取圖片區域的描述
  • 最後是:如何歸檔於 XML 文件,如何重新拼接文件名和資料夾名,讓文件成功落入指定位置

寫代碼的時候需要的不止是清晰的思路,很多時候人類在直覺上視爲理所當然的事情,卻需要在計算機上面拆分到極細的步驟讓它去順利執行。而就此項目而言,精簡的步驟說明如下:

  1. 手動輸入找到文件目標圖片存放的文件夾位置,並用 os module 遍歷這個文件夾裏面的所有東西出來一個一個準備處理。
  2. 用 matplotlib 建立一個含有座標軸的視窗,並用 cv2 module 把圖片轉換成 np.array 的數據形式,經過矩陣轉制一下(因爲顏色顯示順序兩個包不同),把圖片結果在建立好的座標軸上面呈現。
  3. 開始在圖片上面動手腳,畫方框標註座標位置等,但是這之前我們要先創建屬於自己需要的工具,就是函數的意思,因此上面一堆的 def ... 開始定義按下鼠標,按下鍵盤分別代表什麼意思,定義好之後把這些功能與圖片在對的步驟用 plt.connect() 做觸發鏈接。
  4. 接着把小工具在圖片上做的事情 show() 出來,讓整個過程更可視化一些。
  5. 最後觸發鍵盤按鈕,把 “圖片上動手腳的記錄” 存到 XML 文件之後,跳到下一張圖片繼續處理,直到所有圖片都被處理完畢。


點擊此處通往訓練環節  [ YOLO... (下)... ]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章