計算機視覺之目標跟蹤——論文Learning to Track at 100 FPS with Deep Regression Networks
論文解讀
本文采用深度學習迴歸模型GOTURN(Generic Object Tracking Using Regression Networks)解決單目標跟蹤問題。單目標跟蹤問題的難點在於物體的平移、旋轉、大小變化、視角變化、明暗變化、變形以及遮擋等情況。
作者一直在強調的是本模型可以離線訓練,這樣可以從大量的訓練資料中學到資料的分佈;而且在使用的時候由於只有正向的一個推理過程,在GPU上的速度可以達到100fps(其他基於神經網路的跟蹤器速度在0.8fps-15fps之間,效能最好的神經網路跟蹤器的速遞為1fps。);另外大量的訓練資料也使得模型的泛化效能比較好,可以跟蹤其他沒有見過的物體。需要補充一點,速度的提升的原因除了離線訓練還有模型本身的原因,本文提出的模型是迴歸模型,網路只需正向跑一次,而其他深度跟蹤模型本身是分類模型,需要對多個候選patches進行打分,以得分最高者作為目標。
模型:
輸入:
GOTURN是按幀處理視訊的,根據上一張圖中目標的位置來判斷當前圖片的目標位置,本質上還是在處理圖片。假設上一張圖片目標以
那麼問題來了,不同圖片上的目標的BBox大小形狀不一,導致截圖不一樣,但是神經網路的輸入要求固定大小,怎麼辦呢?對截圖進行reshape處理,縮放到一樣大小。
為什麼要在當前圖片中以
輸出:
跟蹤目標在搜尋區域中的相對位置,左上角及右下角的位置
模型結構:
五層卷積層:CaffeNet結構的前五層
三層4096全連線層:使用ReLU非線性函式,層之間加上dropout層
訓練
損失函式為L1距離。
motion model
作者針對視訊中物體的運動情況進行統計,發現物體由上一幀運動到下一幀的過程是有規律的。假設上一張圖片的BBox的中心點為
其中
對BBox的大小變化建立以下模型:
其中
訓練資料
訓練資料既有視訊,還由影象。使用大量圖片資料進行訓練可以增加模型的泛化能力,追蹤更多種類的目標。
那對於一張標明目標的圖片,怎麼產生兩張圖片呢?通過檢視作者的實現發現,深度迴歸網路的兩個輸入是一樣的,都是對這幅圖片的填充完全截圖。而對於用random crop生成的人造資料,是對圖片的BBox進行變換,然後再根據新BBox在圖片上進行截圖,得到人造當前截圖。
對於視訊的上一圖片和當前圖片,除了前面介紹的真實截圖輸入,我們也對當前圖片進行random crop生成資料(上一張圖片的截圖保持不變)。具體來說,根據Laplace分佈隨機取樣位移
實現
作者在Github上上傳了實現原始碼https://github.com/davheld/GOTURN。在測試集上跑了一下,跟蹤速度非常快,但是會出現目標錯誤、跟蹤範圍過大的情況,而且目標錯誤出現了多次。
這裡簡單地閱讀一下原始碼以便後面可以對程式碼做些修改。
說明
首先對解讀原始碼過程中用到的詞語做一下說明:
截圖、完全截圖、填充完全截圖、填充完全變換截圖:在截圖的時候,可能會出現截圖範圍超過圖片的情況。這時我們把在圖片內部的部分稱為截圖;整個截圖(包括超出部分)稱為完全截圖;超出部分用黑色填充稱為填充完全截圖;對目標BBox做變換後再截圖稱為填充完全變換截圖。
原始碼解讀
src/helper資料夾
bounding_box.cpp:
全域性變數:
kContextFactor = 2
:為目標填充多少上下文,BBox大小的相對值kScaleFactor = 10
:基於神經網路的輸出值範圍,縮放BBox的座標use_coordinates_output
:決定在初始化BoundingBox時傳入的浮點數向量代表的座標含義。為true時,神經網路輸出(x1, y1, x2, y2);為false時,輸出(center_x, center_y, width, height)
類BoundingBox:
成員變數:
x1_, y1_, x2_, y2_
:BBox的座標scale_factor_
:輸入神經網路前縮放BBox的座標
成員函式:
double compute_output_width() const
:計算截圖的寬,為kContextFactor * bbox_width。
引數:無
返回:
output_weight
:截圖的寬double edge_spacing_x() const
:當截圖的左邊界超過圖片的左邊界時,超出的長度。如果不超出,則返回0。void Recenter(const BoundingBox& search_location,
:計算BBox相對搜尋區域(填充完全變換截圖)的位置。
const double edge_spacing_x, const double edge_spacing_y,
BoundingBox* bbox_gt_recentered) constvoid Scale(const cv::Mat& image, BoundingBox* bbox_scaled) const
:根據圖片的大小規範化BBox,即不再以畫素點長度作為座標值而是以和圖片之間的比例。先縮放BBox的座標使其位於[0,1]之間,然後再乘以scale_factor_,使其到[0,scale_factor_]之間。
引數:
image
:
bbox_scaled
:就是呼叫此函式的BoundingBox物件,現在儲存的座標值是在[0, scale_factor_]之間,scale_factor_在建立BoundingBox物件時初始化為kScaleFactor。
返回:無void Shift(const cv::Mat& image,
:根據Laplace分佈得到BBox附近的一個BBox儲存在bbox_rand中
const double lambda_scale_frac,
const double lambda_shift_frac,
const double min_scale, const double max_scale,
const bool shift_motion_model,
BoundingBox* bbox_rand) const
image_proc.cpp:
函式:
void CropPadImage(const BoundingBox& bbox_tight, const cv::Mat& image, cv::Mat* pad_image)
:在BBox周圍截圖。
引數:
bbox_tight
:BBox
image
:整幅圖片
pad_image
:儲存截圖void CropPadImage(const BoundingBox& bbox_tight, const cv::Mat& image, cv::Mat* pad_image,
:基於BBox截圖,並增加一些padding。根據BBox進行截圖後,因為考慮到完全截圖可能會超過圖片,所以截圖和完全截圖不一定一樣大小,作者又恢復了完整截圖,只不過對於超過圖片的部分用黑色進行填充。
BoundingBox* pad_image_location, double* edge_spacing_x, double* edge_spacing_y)
引數:
bbox_tight
:BBox
image
:圖片
pad_image
:儲存被黑色填充的target_pad完全截圖
pad_image_location
:儲存截圖的BBox
edge_spacing_x
:截圖的左邊界超出圖片的時候,超出的長度;不超過的時候為0
edge_spacing_y
:截圖的上邊界超出圖片的時候,超出的長度;不超出的時候為0
返回:void ComputeCropPadImageLocation(const BoundingBox& bbox_tight, const cv::Mat& image, BoundingBox* pad_image_location)
:計算截圖的位置,以BBox的中心為中心,大小為(output_weight, output_height)。如果截圖超過圖片,那麼只考慮圖片內的截圖。
引數:
bbox_tight
:BBox,圖片中目標的BBox
image
:整張圖片
pad_image_location
:儲存截圖的BBox
返回:
src/train資料夾
example_generator.cpp:
類ExampleGenerator:處理兩張圖片,生成訓練資料,包括真實樣本和生成樣本
成員變數:
lambda_shift_
:BBox的平移隨機變數服從的分佈的引數lambda_scale_
:BBox的縮放隨機變數服從的分佈的引數min_scale_
:max_scale_
:限制BBox放大、縮小比例image_curr_
:當前的訓練圖片bbox_curr_gt_
:當前圖片中的目標的BBoxbbox_prev_gt_
:上一張圖片中的目標的BBoxtarget_pad_
:根據上一張圖片的目標擷取的用黑色填充的完全截圖video_index_
:frame_index_
:當前樣本生成自哪一個視訊和哪一個幀,這兩個引數只在儲存圖片的額時候才會用到
成員函式:
建構函式:
ExampleGenerator(const double lambda_shift,
const double lambda_scale,
const double min_scale,
const double max_scale)
引數
lambda_shift
:paramater of Laplace distribution of shift
lambda_scale
:paramater of Laplace distribution of rescale
min_scale
:
max_scale
:constraint on change of size
返回:無void Reset(const BoundingBox& bbox_prev,
:根據輸入的引數設定ExampleGenerator物件對應的成員變數值
const BoundingBox& bbox_curr,
const cv::Mat& image_prev,
const cv::Mat& image_curr)
引數:
bbox_prev
:上一張圖片的BBox
bbox_curr
:當前圖片的BBox
image_prev
:上一張圖片
image_curr
:當前圖片
返回:無void get_default_bb_params(BBParams* default_params) const
:把ExampleGenerator物件的lambda引數和scale引數儲存至defaut_params。void MakeTrainingExampleBBShift(const bool visualize_example,
:生成人造資料。對當前BBox進行平移和縮放(Laplace分佈),然後對變換BBox在當前圖片上進行填充完全截圖(稱為搜尋區域),並計算新BBox此時相對搜尋區域的位置得到新的bbox_gt_recentered,對這個BBox的座標縮放到[0, kScaleFactor]之間。
const BBParams& bbparams,
cv::Mat* rand_search_region,
cv::Mat* target_pad,
BoundingBox* bbox_gt_scaled) const
引數:
visualize_example
:是否視覺化
bbparams
:獲取ExampleGenerator物件的變換引數
rand_search_region
:填充完全轉換截圖
target_pad
:填充的完全截圖
bbox_gt_scaled
:BBox相對於搜尋區域的位置(經過規範化的,座標取值在[0, kScaleFactor]之間)
返回:無void MakeTrainingExampleBBShift(cv::Mat* image_rand_focus,
:平移和縮放
cv::Mat* target_pad,
BoundingBox* bbox_gt_scaled) const
引數:
image_rand_focus
:儲存填充完全轉換截圖
target_pad
:儲存上一張圖片的目標BBox的完全填充截圖
bbox_gt_scaled
:儲存BBox相對於搜尋區域的位置(經過規範化的,座標取值在[0, kScaleFactor]之間)
返回:無void MakeTrainingExamples(const int num_examples,
: 根據當前圖片生成num_examples個人造資料。根上一張圖片的截圖保持不變,對當前圖片的目標進行變換並截圖:[targets,images]就是神經網路的輸入。
std::vector<cv::Mat>* images,
std::vector<cv::Mat>* targets,
std::vector<BoundingBox>* bboxes_gt_scaled)
引數:
num_examples
:合成樣本數
images
:儲存得到的填充完全變換截圖的向量
targets
:儲存填充完全截圖的向量
bboxes_gt_scaled
:儲存BBox在搜尋區域(填充完全變換截圖)中的相對位置,座標在[0, kScaleFactor]之間
返回:無void MakeTrueExample(cv::Mat* curr_search_region,
:根據當前圖片和上一圖片得到的截圖(真實資料)
cv::Mat* target_pad,
BoundingBox* bbox_gt_scaled) const
引數:
curr_search_region
:當前圖片目標的填充完全截圖
target_pad
:上一圖片目標的填充完全截圖
bbox_gt_scaled
:ground truth
返回:
tracker_trainer.cpp:
變數:
const int kBatchSize = 50
:batch大小const int kGeneratedExamplesPerImage = 10
:對每張圖片生成多少個人工樣本
類TrackerTrainer:
成員變數:
std::vector<cv::Mat> image_batch_
:當前訓練batch的資料。這個是當前圖片的截圖std::vector<cv::Mat> targets_batch_
:上一張圖片的截圖std::vector<BoundingBox> bboxes_gt_scaled_batch_
:輸出的ground truthExampleGenerator* example_generator_
:用來生成真實訓練資料以及人工資料RegressorTrainBase* regressor_train_
:神經網路int num_batches_
:已經訓練的batch總數
成員函式:
void Train(const cv::Mat& image_prev, const cv::Mat& image_curr,
:根據提供的當前圖片、上一圖片以及對應的BBox引數得到一個真實樣本以及kGeneratedExamplesPerImage個人工樣本。用這些樣本去補TrackerTrainer的batch資料。如果這些樣本數加上TrackerTrainer的資料超過了kBatchSize,那麼填滿batch後剩下的資料留作下一個batch用,並且由於TrackerTrainer的資料達到一個batch開始訓練,然後清空batch,並把由當前圖片得到的資料中沒有使用的部分再新增到空batch中;如果沒有達到kBatchSize個數據,那麼新增到batch中去等待再一個數據。
const BoundingBox& bbox_prev, const BoundingBox& bbox_curr)
引數:
image_prev
:
image_curr
:
bbox_prev
:
bbox_curr
:
返回:virtual void MakeTrainingExamples(std::vector<cv::Mat>* images,
:根據傳遞給train函式的引數設定成員變數example_generator_,然後呼叫example_generator_的MakeTrueExample()函式以及MakeTrainingExamples()得到真實樣本以及生成樣本
std::vector<cv::Mat>* targets,
std::vector<BoundingBox>* bboxes_gt_scaled)
引數:
images
:儲存根據當前圖片目標生成的填充完全截圖以及填充完全變換截圖
targets
:儲存上一圖片的目標填充完全截圖
bboxes_gt_scaled
:儲存ground truth
返回:virtual void ProcessBtch()
:根據TrackerTrainer的batch資料訓練網路
引數:
返回:
train.cpp
變數:
kNumBatches = 500000
:batch的數目
函式:
void train_image(const LoaderImagenetDet& image_loader,
:任意選擇一個圖片,並任意選擇它的一個Annotation,通過image_loader載入響應的圖片資料和BBox進行訓練
const std::vector<std::vector<Annotation> >& images,
TrackerTrainer* tracker_trainer)
引數:
image_loader
:用於載入圖片資料和BBox
images
:所有圖片的所有Annotation
tracker_trainer
:訓練器
返回:void train_video(const std::vector<Video>& videos, TrackerTrainer* tracker_trainer)
:任意選擇一個視訊,然後選擇該視訊中任意一個Annotation,以其對應的幀作為上一張圖片,以下一個Annotation對一個的幀作為當前圖片,載入兩個圖片和兩個BBox進行訓練
引數:
videos
:所有視訊
tracker_trainer
:訓練器
返回:main函式
:需要資料檔案引數、網路檔案引數、以及目標BBox變換所需要的四個引數,用於載入圖片資料集和視訊資料集;建立神經網路;建立ExampleGenerator來生成訓練樣本。定義TrackerTrainer物件,然後隨機訓練一張圖片,隨機訓練一個視訊。問題
怎麼保證神經網路的輸入是一樣大小的?不同目標的BBox的大小不一樣,所以截圖也不一樣;在人工資料中,對BBox進行的變換是不定的,對應的截圖大小也不一樣。
在Regressor::Estimate()函式中會對資料reshape到神經網路需要的大小。