聚焦Python和OpenCV的影像處理,3D場景重建,物件檢測和跟蹤 - 物件分類和定位

國外課棧發表於2021-01-02

Python OpenCV

聚焦Python和OpenCV的影像處理,3D場景重建,物件檢測和跟蹤

準備

除了安裝OpenCV,您還需要安裝TensorFlow。

可以從此連結下載Oxford-IIIT-Pet資料集,以及我們的資料集準備指令碼,該指令碼將自動為您下載。

構思App

最終的應用將由模組組成,這些模組用於準備資料集,訓練模型以及使用相機輸入來推斷模型。 這將需要以下元件:

  • main.py:這是用於實時啟動應用程式和定位(寵物頭)的主要指令碼。
  • data.py:這是一個下載並準備資料集進行訓練的模組。
  • classification.py:這是訓練分類器網路的指令碼。
  • localization.py:這是用於訓練和儲存本地化網路的指令碼。

在準備好資料集進行訓練之後,我們將執行以下操作以完成我們的應用程式:

  1. 我們將首先使用遷移學習來訓練分類網路。
  2. 接下來,我們將再次使用轉移學習來訓練物件定位網路。
  3. 建立並訓練了本地化網路之後,我們將執行main.py指令碼以實時對頭部進行本地化。

讓我們從學習如何準備將執行我們的應用程式的推理指令碼開始。 該指令碼將連線到您的攝像機,並使用我們將建立的定位本地化模型在視訊流的每一幀中找到頭部位置,然後實時顯示結果。

準備推理指令碼

我們的推理指令碼非常簡單。 它將首先準備繪圖功能,然後載入模型並將其連線到相機。 然後,它將迴圈播放視訊流中的幀。 在迴圈中,對於流的每一幀,它將使用匯入的模型進行推理,並使用繪圖功能顯示結果。 讓我們使用以下步驟建立一個完整的指令碼:

  1. 首先,匯入所需模組:

    import numpy as np
    import cv2
    import tensorflow.keras as K
    

    在這段程式碼中,除了匯入NumPy和OpenCV,我們還匯入了Keras。 我們將使用Keras在此指令碼中進行預測; 此外,我們將使用它來建立和訓練我們的模型。

  2. 然後,我們定義一個函式以在框架上繪製定位邊界框:

    def draw_box(frame: np.ndarray, box: np.ndarray) -> np.ndarray:
     h, w = frame.shape[0:2]
     pts = (box.reshape((2, 2)) * np.array([w, h])).astype(np.int)
     cv2.rectangle(frame, tuple(pts[0]), tuple(pts[1]), (0, 255, 0),
    2)
     return frame
    

    前面的draw_box函式將邊框和邊界框的兩個角的標準化座標接受為四個數字的陣列。 該函式首先將盒子的一維陣列整形為二維陣列,其中第一個索引表示點,第二個索引表示x和y座標。 然後,它通過將標準化座標與影像寬度和高度組成的陣列相乘,將標準化座標轉換為影像座標,並將結果轉換為同一行中的整數值。 最後,它使用cv2.rectangle函式繪製綠色邊框,並返回幀。

  3. 然後,匯入準備的模型並連線到攝像機:

    model = K.models.load_model("localization.h5")
    cap = cv2.VideoCapture(0)
    

    模型將儲存在一個二進位制檔案中,該檔案是使用Keras的便捷功能匯入的。

  4. 之後,我們遍歷相機中的幀,將每個幀調整為標準尺寸(即我們將建立的模型的預設影像尺寸),然後將幀轉換為RGB(紅色,綠色,藍色)顏色 空間,因為我們將在RGB影像上訓練模型:

    for _, frame in iter(cap.read, (False, None)):
     input = cv2.resize(frame, (224, 224))
     input = cv2.cvtColor(input, cv2.COLOR_BGR2RGB)
    
  5. 在同一迴圈中,我們對影像進行歸一化,並在模型接受批次影像時將其新增到框架的形狀中。 然後,將結果傳遞給模型以進行推斷:

    box, = model.predict(input[None] / 255)
    
  6. 我們通過使用先前定義的函式繪製預測的邊界框來繼續迴圈,顯示結果,然後設定終止條件:

    cv2.imshow("res", frame)
     if(cv2.waitKey(1) == 27):
     break
    

準備資料集

我們將使用Oxford-IIIT-Pet資料集。 將資料集的準備工作封裝在單獨的data.py指令碼中是個好主意,然後可以在本文中使用它。 與其他任何指令碼一樣,首先,我們必須匯入所有必需的模組,如以下程式碼片段所示:

import glob
import os
from itertools import count
from collections import defaultdict, namedtuple
import cv2
import numpy as np
import tensorflow as tf
import xml.etree.ElementTree as ET

為了準備我們的資料集以供使用,我們將首先下載資料集並將其解析到記憶體中。 然後,從解析的資料中,我們將建立一個TensorFlow資料集,該資料集使我們能夠以方便的方式使用資料集以及在後臺準備資料,以便資料的準備不會中斷神經網路訓練處理。

首先,我們首先從官方網站下載資料集,然後將其解析為方便的格式。 在此階段,我們將省去佔用大量記憶體的影像。 我們通過以下步驟介紹此過程:

  1. 定義我們要儲存寵物資料集的位置,並在Keras中使用便捷的get_file函式下載它:

    DATASET_DIR = "dataset"
    for type in ("annotations", "images"):
     tf.keras.utils.get_file(
     type,
    f"https://www.robots.ox.ac.uk/~vgg/data/pets/data/{type}.tar.gz",
     untar=True,
     cache_dir=".",
     cache_subdir=DATASET_DIR)
    

    由於我們的資料集位於存檔中,因此我們還通過傳遞untar = True提取了它。 我們還將cache_dir指向當前目錄。 儲存檔案後,隨後執行get_file函式將不執行任何操作。

    資料集的重量超過半GB,並且在第一次執行時,您將需要具有良好頻寬的穩定Internet連線。

  2. 下載並提取資料集後,讓我們為資料集和批註資料夾定義常量,並將要調整影像大小的影像大小設定為:

    IMAGE_SIZE = 224
    IMAGE_ROOT = os.path.join(DATASET_DIR,"images")
    XML_ROOT = os.path.join(DATASET_DIR,"annotations")
    

    224通常是在其上訓練影像分類網路的預設大小。 因此,最好保持該大小以提高準確性。

  3. 該資料集的註釋包含有關XML格式影像的資訊。 在解析XML之前,讓我們首先定義我們想要擁有的資料:

    Data = namedtuple("Data","image,box,size,type,breed")
    

    namedtuple是Python中標準元組的擴充套件,並允許您通過其名稱引用元組的元素。 我們定義的名稱與我們感興趣的資料元素相對應。即,影像本身(影像),寵物的頭部邊框(框),影像大小,型別(貓或狗)和品種(有37個品種)。

  4. 品種和型別是註釋中的字串; 我們想要的是與品種相對應的數字。 為此,我們定義了兩個字典:

    types = defaultdict(count().__next__ )
    breeds = defaultdict(count().__next__ )
    

    defaultdict是一個字典,它返回未定義鍵的預設值。 在這裡,它將根據要求返回從零開始的下一個數字。

  5. 接下來,我們定義一個函式,給定XML檔案的路徑,該函式將返回我們的資料例項:

    def parse_xml(path: str) -> Data:
    

    先前定義的功能包括以下步驟:

    1. 開啟XML檔案並進行解析:

      with open(path) as f:
       xml_string = f.read()
      root = ET.fromstring(xml_string)
      

      使用ElementTree模組解析XML檔案的內容,該模組以易於瀏覽的格式表示XML。

    2. 然後,獲取相應影像的名稱並提取品種的名稱:

      img_name = root.find("./filename").text
      breed_name = img_name[:img_name.rindex("_")]
      
    3. 之後,使用先前定義的breeds將品種轉換為數字,為每個未定義的鍵分配下一個數字:

      breed_id = breeds[breed_name]
      
    4. 同樣,獲取型別的ID:

      type_id = types[root.find("./object/name").text]
      
    5. 然後,提取邊界框並將其標準化:

      box =np.array([int(root.find(f"./object/bndbox/{tag}").text)
      for tag in "xmin,ymin,xmax,ymax".split(",")])
      	size = np.array([int(root.find(f"./size/{tag}").text)
      for tag in "width,height".split(",")])
      	normed_box = (box.reshape((2, 2)) / size).reshape((4))
      

      返回Data的例項:

      return Data(img_name,normed_box,size,type_id,breed_id)
      
    6. 現在我們已經下載了資料集並準備了一個解析器,讓我們繼續解析資料集:

      xml_paths = glob.glob(os.path.join(XML_ROOT,"xmls","*.xml"))
      xml_paths.sort()
      parsed = np.array([parse_xml(path) for path in xml_paths])
      

      我們還對路徑進行了排序,以便它們在不同的執行時環境中以相同的順序出現。

    解析完資料集後,我們可能希望列印出可用的品種和型別進行說明:

    print(f"{len(types)} TYPES:", *types.keys(), sep=", ")
    print(f"{len(breeds)} BREEDS:", *breeds.keys(), sep=", ")
    

    先前的程式碼段輸出兩種型別,即貓和狗,以及它們的品種:

    2 TYPES:, cat, dog
    37 BREEDS:, Abyssinian, Bengal, Birman, Bombay, British_Shorthair,
    Egyptian_Mau, Maine_Coon, Persian, Ragdoll, Russian_Blue, Siamese, Sphynx,
    american_bulldog, american_pit_bull_terrier, basset_hound, beagle, boxer,
    chihuahua, english_cocker_spaniel, english_setter, german_shorthaired,
    great_pyrenees, havanese, japanese_chin, keeshond, leonberger,
    miniature_pinscher, newfoundland, pomeranian, pug, saint_bernard, samoyed,
    scottish_terrier, shiba_inu, staffordshire_bull_terrier, wheaten_terrier,
    yorkshire_terrier
    

    我們將不得不在訓練集和測試集上拆分資料集。 為了進行良好的分割,我們應該從資料集中隨機選擇資料元素,以便在訓練和測試集中按比例分配品種。

    現在,我們可以混合資料集,這樣我們以後就不必擔心它了,如下所示:

    np.random.seed(1)
    np.random.shuffle(parsed)
    

    先前的程式碼首先設定一個隨機種子,每次執行程式碼時都需要獲得相同的結果。 seed方法接受一個引數,該引數是指定隨機序列的數字。

    設定種子方法後,使用隨機數的函式中將具有相同的隨機數序列。 這樣的數字稱為偽隨機數。 這意味著,儘管它們看起來是隨機的,但它們是預定義的。 在我們的例子中,我們使用shuffle方法,該方法混合瞭解析陣列中元素的順序。

    現在我們已經將資料集解析為方便的NumPy陣列,讓我們繼續並從中建立TensorFlow資料集。

建立TensorFlow資料庫

我們將使用TensorFlow資料集介面卡來訓練我們的模型。 當然,我們可以從資料集中建立一個NumPy陣列,但可以想象將所有影像保留在記憶體中需要多少記憶體。

相反,資料集介面卡可讓您在需要時將資料載入到記憶體中。 而且,資料是在後臺載入和準備的,因此不會成為我們培訓過程中的瓶頸。 我們將解析後的陣列轉換如下:

ds = tuple(np.array(list(i)) for i in np.transpose(parsed))
ds_slices = tf.data.Dataset.from_tensor_slices(ds)

從前面的程式碼段中,from_tensor_slices建立了Dataset,其元素是給定tensor的切片。 在我們的例子中,tensor是標籤的NumPy陣列(盒子,品種,影像位置等)。

在後臺,它與Python zip函式的概念相似。 首先,我們準備了相應的輸入。 現在讓我們從資料集中列印一個元素以檢視其外觀:

for el in ds_slices.take(1):
 print(el)

這給出以下輸出:

(<tf.Tensor: id=14, shape=(), dtype=string,
numpy=b'american_pit_bull_terrier_157.jpg'>, <tf.Tensor: id=15, shape=(4,),
dtype=float64, numpy=array([0.07490637, 0.07 , 0.58426966, 0.44333333])>,
<tf.Tensor: id=16, shape=(2,), dtype=int64, numpy=array([267, 300])>,
<tf.Tensor: id=17, shape=(), dtype=int64, numpy=1>, <tf.Tensor: id=18,
shape=(), dtype=int64, numpy=13>)

TensorFlow-tensor包含我們已經從單個XML檔案中解析的所有資訊。 給定資料集,我們可以檢查所有邊界框是否正確:

for el in ds_slices:
 b = el[1].numpy()
 if(np.any((b>1) |(b<0)) or np.any(b[2:]-b[:2] < 0)):
	 print(f"Invalid box found {b} image: {el[0].numpy()}")

當我們將框標準化後,它們應該在[0,1]的範圍內。 此外,我們確保框的第一個點的座標小於或等於第二個點的座標。

現在,我們定義一個函式,該函式將轉換我們的資料元素,以便可以將其輸入到神經網路中:

def prepare(image,box,size,type,breed):
 image = tf.io.read_file(IMAGE_ROOT+"/"+image)
 image = tf.image.decode_png(image,channels=3)
 image = tf.image.resize(image,(IMAGE_SIZE,IMAGE_SIZE))
 image /= 255
 return
Data(image,box,size,tf.one_hot(type,len(types)),tf.one_hot(breed,len(breeds
)))

詳情參閱http://viadean.com/opencv_python_object.html

相關文章