? ? ? 這是opencv系列的最后一節,主要學習視頻序列,上一節介紹了讀取、處理和存儲視頻的工具,本文將介紹幾種跟蹤圖像序列中運動物體的算法。可見運動或表觀運動,是物體以不同的速度在不同的方向上移動,或者是因為相機在移動(或者兩者都有)。在很多應用程序中,跟蹤表觀運動都是極其重要的。它可用來追蹤運動中的物體,以測定它們的速度、判斷它們的目的地。對于手持攝像機拍攝的視頻,可以用這種方法消除抖動或減小抖動幅度,使視頻更加平穩。運動估值還可用于視頻編碼,用以壓縮視頻,便于傳輸和存儲。
目錄
1. 跟蹤視頻中的特征點
2. 估算光流
3. 跟蹤視頻中的物體
1. 跟蹤視頻中的特征點
? ? ? ? ?被跟蹤的運動可以是稀疏的(圖像的少數位置上有運動,稱為稀疏運動),也可以是稠密的(圖像的每個像素都有運動,稱為稠密運動)。在啟動跟蹤過程時,首先要在最初的幀中檢測特征點,然后在下一幀中跟蹤這些特征點,如果想找到特征點在下一幀的新位置,就必須在它原來位置的周圍進行搜索。這個功能由函數cv::calcOpticalFlowPyrLK 實現。在函數中輸入兩個連續的幀和第一幅圖像中特征點的向量,將返回新的特征點位置的向量。
? ? ? 要逐幀地跟蹤特征點,就必須在后續幀中定位特征點的新位置。假設每個幀中特征點的強度
值是不變的,這個過程就是尋找如下的位移(u, v):
? ? ? 其中It 和It+1 分別是當前幀和下一個瞬間的幀。強度值不變的假設普遍適用于相鄰圖像上的
微小位移。我們可使用泰勒展開式得到近似方程式(包含圖像導數):
? ? ? ?根據第二個方程式,可以得到另一個方程式(根據強度值不變的假設,去掉了兩個表示強度
值的項)
? ? ? ? 這就是基本的光流約束方程,也稱作亮度恒定方程,Lukas-Kanade 特征跟蹤算法使用了這個約束方程。除此之外,該算法還做了一個假設,即特征點鄰域中所有點的位移量是相等的。因此,我們可以將光流約束應用到所有位移量為(u, v)的點(u 和v 還是未知的)。這樣就得到了更多的方程式,數量超過未知數的個數(兩個),因此可以在均方意義下解出這個方程組。
? ? ? ?在實際應用中,我們采用迭代的方法來求解。為了使搜索更高效且適應更大的位移量,OpenCV? 還提供了在不同分辨率下進行計算的方法:默認的圖像等級數量為3,窗口大小為15,還可以設定一個終止條件,符合這個條件時就停止迭代搜索。
? ? ? cv::calcOpticalFlowPyrLK 函數的第六個參數是剩余均方誤差,用于評定跟蹤的質量。第五個參數包含二值標志,表示是否成功跟蹤了對應的點。
// 1. 特征點檢測方法
void detectFeaturePoints() {// 檢測特征點cv::goodFeaturesToTrack(gray, // 圖像features, // 輸出檢測到的特征點max_count, // 特征點的最大數量qlevel, // 質量等級minDist); // 特征點之間的最小差距
}// 2 法根據應用程序定義的條件剔除部分被跟蹤的特征點。這里剔除靜止的特征點(還有不能被cv::calcOpticalFlowPyrLK 函數跟蹤的特征點)。我們假定靜止的點屬于背景部分,可以忽略:
// 判斷需要保留的特征點
bool acceptTrackedPoint(int i) {return status[i] &&// 如果特征點已經移動(abs(points[0][i].x-points[1][i].x)+(abs(points[0][i].y-points[1][i].y))>2);
}// 3 處理被跟蹤的特征點,具體做法是在當前幀畫直線,連接特征點和它們的初始位置(即第一次檢測到它們的位置):
// 處理當前跟蹤的特征點
void handleTrackedPoints(cv:: Mat &frame, cv:: Mat &output) {// 遍歷所有特征點for (int i= 0; i < points[1].size(); i++ ) {// 畫線和圓cv::line(output, initial[i], // 初始位置points[1][i], // 新位置cv::Scalar(255,255,255));cv::circle(output, points[1][i], 3,cv::Scalar(255,255,255),-1);
}
}
2. 估算光流
? ? ? ? 通常關注視頻序列中運動的部分,即場景中不同元素的三維運動在成像平面上的投影。三維運動向量的投影圖被稱作運動場。但是在只有一個相機傳感器的情況下,是不可能直接測量三維運動的,我們只能觀察到幀與幀之間運動的亮度模式,亮度模式上的表觀運動被稱作光流。通常認為運動場和光流是等同的,但其實不一定:典型的例子是觀察均勻的物體;例如相機在白色的墻壁前移動時就不產生光流。
? ? ? 估算光流其實就是量化圖像序列中亮度模式的表觀運動。首先來看視頻中某個時刻的一幀畫
面。觀察當前幀的某個像素(x, y),我們要知道它在下一幀會移動到哪個位置。也就是說,這個點
的坐標在隨著時間變化(表示為(x(t), y(t))),而我們要估算出這個點的速度(dx/dt, dy/dt),對應的幀中獲取這個點在t 時刻的亮度,表示為I(x(t), y(t),t),根據圖像亮度恒定的假設:
? ? ? 這個約束條件可以用基于光流的拉普拉斯算子的公式表示:
? ? ? ?現在要做的就是找到光流場,使亮度恒定公式的偏差和光流向量的拉普拉斯算子都達到最
小值,估算稠密光流的方法有很多,可以使用cv::Algorithm的子類cv::DualTVL1OpticalFlow。
? ? ? ? 所得結果是二維向量(cv::Point)組成的圖像,每個二維向量表示一個像素在兩個幀之
間的變化值。要展示結果,就必須顯示這些向量。為此我們創建了一個函數,用來創建光流場的圖像映射。為控制向量的可見性,需要使用兩個參數:步長(間隔一定像素)和縮放因子,
// 1. 創建光流算法
cv::Ptr<cv::DualTVL1OpticalFlow> tvl1 = cv::createOptFlow_DualTVL1();
這個實例已經可以使用了,所以只需調用計算兩個幀之間的光流場的方法即可:
cv::Mat oflow; // 二維光流向量的圖像
// 計算frame1 和frame2 之間的光流
tvl1->calc(frame1, frame2, oflow);// 2. 繪制光流向量圖
void drawOpticalFlow(const cv::Mat& oflow, // 光流
cv::Mat& flowImage, // 繪制的圖像
int stride, // 顯示向量的步長
float scale, // 放大因子
const cv::Scalar& color) // 顯示向量的顏色
{
// 必要時創建圖像
if (flowImage.size() != oflow.size()) {
flowImage.create(oflow.size(), CV_8UC3);
flowImage = cv::Vec3i(255,255,255);
}
// 對所有向量,以stride 作為步長
for (int y = 0; y < oflow.rows; y += stride)
for (int x = 0; x < oflow.cols; x += stride) {
// 獲取向量
cv::Point2f vector = oflow.at< cv::Point2f>(y, x);
// 畫線條
cv::line(flowImage, cv::Point(x,y),
cv::Point(static_cast<int>(x + scale*vector.x + 0.5),
static_cast<int>(y + scale*vector.y + 0.5)),
color);
// 畫頂端圓圈
cv::circle(flowImage,
cv::Point(static_cast<int>(x + scale*vector.x + 0.5),
static_cast<int>(y + scale*vector.y + 0.5)),
1, color, -1);
}
}
? ? ? 前面使用的方法被稱作雙DV L1 方法,由兩部分組成。第一部分使用光滑約束,使光流梯度的絕對值(不是平方值)最小化;選用絕對值可以削弱平滑度帶來的影響,尤其是對于不連續的區域,運動物體和背景部分的光流向量的差別很大。第二部分使用一階泰勒近似,使亮度恒定約束公式線性化。?
3. 跟蹤視頻中的物體
? ? ?在很多應用程序中,更希望能夠跟蹤視頻中一個特定的運動物體。為此要先標識出該物體,然后在很長的圖像序列中對它進行跟蹤。這是一個很有挑戰性的課題,因為隨著物體在場景中的運動,物體的圖像會因視角和光照改變、非剛體運動、被遮擋等原因而不斷變化。
import cv2 # type: ignore
import numpy as np
# Illustration of the Median Tracker principle
image1 = cv2.imread("E:/CODE/images/goose/goose130.bmp", 0)
image_show = cv2.imread("E:/CODE/images/goose/goose130.bmp")
#define a regular grid of points
grid = []
x,y,width,height = 290, 100, 65, 40
for i in range(10):for j in range(10):p = (x+i*width/10,y+j*height/10)grid.append(p)
grid = np.array(grid, dtype=np.float32)
#track in next image
image2 = cv2.imread("E:/CODE/images/goose/goose131.bmp",0)
lk_params = []lk_params = dict(winSize=(10, 10),maxLevel=2,criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# ShiTomasi corner detection的參數
feature_params = dict(maxCorners=300,qualityLevel=0.3,minDistance=7,blockSize=7)p0 = cv2.goodFeaturesToTrack(image1, mask=None, **feature_params)
#grid 從22*2 維度調整為22*1*2
grid = grid.reshape(-1,1,2)
kp2, st, err =cv2.calcOpticalFlowPyrLK(image1, image2, grid, None, **lk_params)
#good_new = kp2[st == 1]
for i in grid:cv2.circle(image_show, (int(i[0][0]),int(i[0][1])), 1, (255, 255, 255), 3)for i in kp2:cv2.circle(image_show, (int(i[0][0]),int(i[0][1])), 1, (255, 0, 255), 3)cv2.imshow("Tracked points", image_show)cv2.waitKey()
? ? ? ?開始跟蹤前,要先在一個幀中標識出物體,然后從這個位置開始跟蹤。標識物體的方法就是指定一個包含該物體的矩形(YOLO),而跟蹤模塊的任務就是在后續的幀中重新識別出這個物體。OpenCV 中的物體跟蹤框架類cv::Tracker 包含兩個主方法,一個是init 方法,用于定義初始目標矩形;另一個是update 方法,輸出新的幀中對應的矩形。中值流量跟蹤算法的基礎是特征點跟蹤。它先在被跟蹤物體上定義一個點陣。你也可以改為檢測物體的興趣點,例如采用第8 章介紹的FAST 算子檢測興趣點。但是使用預定位置的點有很多好處:它不需要計算興趣點,因而節約了時間;它可以確保用于跟蹤的點的數量足夠多,還能確保這些點分布在整個物體上。默認情況下,中值流量法采用10×10 的點陣。