1. 程式人生 > 其它 >Python計算機視覺-OpenCV中的光流

Python計算機視覺-OpenCV中的光流

尊重原創版權: https://www.gewuweb.com/hot/8227.html

Python計算機視覺-OpenCV中的光流

在這篇文章中,我們將學習在視訊或幀序列中計算光流的各種演算法。我們將討論稀疏和密集光流演算法的相關理論和在OpenCV中的實現。

1.什麼是光流?

光流是一個視訊中兩個連續幀之間的逐畫素運動估計任務。基本上,光流任務意味著計算畫素的移動向量作為物體在兩個相鄰影象之間的位移差。光流的主要思想是估計物體運動或攝像機運動引起的物體的位移向量。

2.理論基礎

讓我們假設我們有一個灰度影象。我們定義函式I ( x , y , t ) ,其中x,y為畫素座標,t為幀數。函式I ( x , y , t )
定義了t幀處的畫素強度。

首先,我們假設物件的位移不會改變物件的畫素強度,這意味著I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t
)。在我們的例子中,Δ t = 1 。主要關注的是找到運動向量( Δ x , Δ y )。讓我們看看圖形表示:

使用泰勒級數展開,我們可以重寫I ( x , y , t ) − I ( x + Δ x , y + Δ y , t + Δ t ) = 0為I x ′ u

  • I y ′ v = − I t ′,其中u = d x d t , v = d y d t , I x ′ , I y ′
    是影象梯度。重要的是,我們假設高階泰勒級數的部分可以忽略,所以這是一個函式近似,只用一階泰勒展開式。幀I 1 和I 2 之間的畫素運動差可表示為I 1 −
    I 2 ≈ I x ′ u + I y ′ v + I t ′ 。現在,我們有兩個變數u 和v
    ,只有一個方程,所以我們現在不能解這個方程,但是我們可以使用一些技巧,這些技巧會在接下來的演算法中被揭示。

3.光流的應用

光流可以應用於許多對目標運動資訊至關重要的領域。光流通常在視訊編輯壓縮,穩定,慢動作等被發現。此外,光流在動作識別任務和實時跟蹤系統中也有應用。

4.稀疏和密集光流

光流有兩種型別,第一種稱為稀疏光流。它計算特定物件集合的運動向量(例如,影象上檢測到的角)。因此,需要對影象進行預處理以提取特徵,這是光流計算的基礎。OpenCV提供了一些演算法實現來解決稀疏光流任務:(1)Pyramid
Lucas-Kanade(2)Sparse RLOF

僅使用稀疏特徵集意味著我們將不會有不包含在其中的畫素的運動資訊。使用密集光流演算法可以消除這一限制,該演算法假定為影象中的每個畫素計算一個運動向量。OpenCV中已經實現了一些密集光流演算法:

(1)Dense Pyramid Lucas-Kanade

(2)Farneback

(3)PCAFlow

(4)SimpleFlow

(5)RLOF

(6)DeepFlow

(7)DualTVL1

在這篇文章中,我們將看看其中一些演算法的理論方面以及它們在OpenCV中的使用。

5.稀疏光流

Lucas-Kanade演算法

Lucas-
Kanade方法是計算稀疏特徵集光流的常用方法。該方法的主要思想是基於區域性運動不變的假設,即附近畫素具有相同的位移方向。這個假設有助於求出二元方程的近似解。

Lucas-Kanade演算法改進

由於演算法的侷限性,光流演算法確實會受到突然移動的影響。實踐中常用的方法是使用多重縮放技巧。我們需要建立一個所謂的影象金字塔,其中每一張影象都將比前一張影象大一些(例如,比例因子是2)。在固定尺寸視窗中,小尺寸影象上的突然移動比大尺寸影象上更明顯。在小影象中建立的位移向量將用於下一個更大的金字塔階段,以獲得更好的結果。

如前所述,密集光流演算法計算稀疏特徵集的運動向量,所以這裡常用的方法是使用Shi-
Tomasi角點檢測器。該演算法用於尋找影象中的角點,然後計算連續兩幀之間的角點運動向量。

使用OpenCV實現Lucas-Kanade

OpenCV實現了基於Shi-Tomasi的Pyramid Lucas & Kanade演算法來計算光流。讓我們看看基於官方文件的OpenCV演算法。

(1)Python

# lucas_kanade.py
import cv2
import numpy as np
def lucas_kanade_method(video_path):
cap = cv2.VideoCapture(video_path)
# ShiTomasi角點檢測的引數
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7)
# lucas kanade光流演算法的引數
lk_params = dict(
winSize=(15, 15),
maxLevel=2,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03),
)
# 建立一些隨機的顏色
color = np.random.randint(0, 255, (100, 3))
# 取第一幀並在其中找到角點
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
# 建立用於繪圖的掩模影象
mask = np.zeros_like(old_frame)
while True:
ret, frame = cap.read()
if not ret:
break
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 計算光流
# calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts[, status[, err[, \\
# winSize[, maxLevel[, criteria[, flags[, minEigThreshold]]]]]]]) -> nextPts, status, err
p1, st, err = cv2.calcOpticalFlowPyrLK(
old_gray, frame_gray, p0, None, **lk_params
)
# 選擇比較好的點
good_new = p1[st == 1]
good_old = p0[st == 1]
# 畫出軌跡
for i, (new, old) in enumerate(zip(good_new, good_old)):
a, b = new.ravel()
c, d = old.ravel()
mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
frame = cv2.circle(frame, (a, b), 5, color[i].tolist(), -1)
img = cv2.add(frame, mask)
cv2.imshow("frame", img)
k = cv2.waitKey(25) & 0xFF
if k == 27:
break
if k == ord("c"):
mask = np.zeros_like(old_frame)
# 現在更新之前的幀和之前的點
old_gray = frame_gray.copy()
p0 = good_new.reshape(-1, 1, 2)
if __name__ == "__main__":
video_path = "videos//people.mp4"
lucas_kanade_method(video_path)
# python lucas_kanade.py

(2)C++

// lucas_kanade.cpp
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/video.hpp>
#include <opencv2/optflow.hpp>
#include <sys/stat.h>
using namespace cv;
using namespace std;
int lucas_kanade(const string& filename, bool save)
{
VideoCapture capture(filename);
if (!capture.isOpened()){
//開啟視訊輸入錯誤
cerr << "Unable to open file!" << endl;
return 0;
}
// 建立一些隨機的顏色
vector<Scalar> colors;
RNG rng;
for(int i = 0; i < 100; i++)
{
int r = rng.uniform(0, 256);
int g = rng.uniform(0, 256);
int b = rng.uniform(0, 256);
colors.push_back(Scalar(r,g,b));
}
Mat old_frame, old_gray;
vector<Point2f> p0, p1;
// 取第一幀並在其中找到角點
capture >> old_frame;
cvtColor(old_frame, old_gray, COLOR_BGR2GRAY);
goodFeaturesToTrack(old_gray, p0, 100, 0.3, 7, Mat(), 7, false, 0.04);
// 建立用於繪圖的掩模影象
Mat mask = Mat::zeros(old_frame.size(), old_frame.type());
int counter = 0;
while(true){
Mat frame, frame_gray;
capture >> frame;
if (frame.empty())
break;
cvtColor(frame, frame_gray, COLOR_BGR2GRAY);
// 計算光流
vector<uchar> status;
vector<float> err;
TermCriteria criteria = TermCriteria((TermCriteria::COUNT) + (TermCriteria::EPS), 10, 0.03);
calcOpticalFlowPyrLK(old_gray, frame_gray, p0, p1, status, err, Size(15,15), 2, criteria);
vector<Point2f> good_new;
for(uint i = 0; i < p0.size(); i++)
{
// 選擇比較好的點
if(status[i] == 1) {
good_new.push_back(p1[i]);
// 畫出軌跡
line(mask,p1[i], p0[i], colors[i], 2);
circle(frame, p1[i], 5, colors[i], -1);
}
}
Mat img;
add(frame, mask, img);
if (save) {
string save_path = "./optical_flow_frames/frame_" + to_string(counter) + ".jpg";
imwrite(save_path, img);
}
imshow("flow", img);
int keyboard = waitKey(25);
if (keyboard == 'q' || keyboard == 27)
break;
// 建立用於繪圖的掩模影象
old_gray = frame_gray.clone();
p0 = good_new;
counter++;
}
}
int main(int argc, char** argv)
{
const string keys =
"{ h help | | print this help message }"
"{ @video | | path to image file }"
"{ @method | | method to OF calcualtion }"
"{ save | | save video frames }";
CommandLineParser parser(argc, argv, keys);
string filename = samples::findFile(parser.get<string>("@video"));
if (!parser.check())
{
parser.printErrors();
return 0;
}
string method = parser.get<string>("@method");
printf("%s %s", method.c_str(), "method is now working!");
bool save = false;
if (parser.has("save")){
save = true;
mkdir("optical_flow_frames", 0777);
}
bool to_gray = true;
if (method == "lucaskanade")
{
lucas_kanade(filename, save);
}
return 0;
}
//./OpticalFlow ../videos/car.mp4 lucaskanade

程式碼解析

首先,我們需要載入我們的視訊,並從第一幀得到Shi-Tomasi演算法的特徵。此外,這裡還需要一些演算法和視覺化的預處理。

之後,我們可以開始我們的演示。這是一個迴圈過程,我們讀取一個新的視訊幀,並在迴圈中計算Shi-Tomasi特徵和光流。計算出的光流顯示為彩色曲線。

簡而言之,這個指令碼取兩個連續的幀,並使用cv2.goodFeaturesToTrack()函式查詢第一個幀的角點。然後根據角點位置資訊,利用Lucas-
Kanade演算法計算光流。這是一個迴圈的過程,對每一對連續的影象做同樣的事情。

6.稠密光流

在本節中,我們將看一看稠密光流演算法,它可以計算影象中每個畫素的運動向量。

Farneback演算法

該方法的主要思想是用一個多項式逼近每個畫素的一些鄰域:I ( x ) ∼ x T ⁣ A x + b T ⁣ x + c 。一般來說,在Lucas-
Kanade方法中,由於只有一階泰勒展開式,我們使用了I = b T x + c
的線性逼近。現在,我們要提高二階近似的精度。在這裡,這個想法導致觀察由物體位移引起的近似多項式的差異。我們的目標是用多項式近似計算I 2 ( x ) = I
1 ( x − d )方程中的位移d 。

RLOF演算法(Robust Local Optical Flow algorithm)

這項工作的主要觀點是,強度不變性假設並不能完全反映真實世界的行為。也有陰影、反射、天氣狀況、移動的光源,簡而言之,不同的亮度。

RLOF演算法基於Gennert和Negahdaripour在1995年提出的光照模型:I ( x , y , t ) + m ⋅ I ( x , y , t
) + c = I ( x + u , y + v , t + 1 ),其中m , c
m,cm,c為光照模型引數。與之前的演算法一樣,有一個區域性運動恆常性假設,並輔以光照恆常性。數學上,這意味著向量[ d m c ]
對於每個區域性影象區域都是常數。

基於OpenCV的實現

由於OpenCV密集光流演算法具有相同的使用模式,我們建立了封裝函式以方便和避免程式碼重複。

(1)Python

# dense_optical_flow.py
import cv2
import numpy as np
def dense_optical_flow(method, video_path, params=[], to_gray=False):
# 讀取視訊
cap = cv2.VideoCapture(video_path)
# 讀取第一幀
ret, old_frame = cap.read()
# 建立HSV並使Value為常量
hsv = np.zeros_like(old_frame)
hsv[..., 1] = 255
# 精確方法的預處理
if to_gray:
old_frame = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
while True:
# 讀取下一幀
ret, new_frame = cap.read()
frame_copy = new_frame
if not ret:
break
# 精確方法的預處理
if to_gray:
new_frame = cv2.cvtColor(new_frame, cv2.COLOR_BGR2GRAY)
# 計算光流
flow = method(old_frame, new_frame, None, *params)
# 編碼:將演算法的輸出轉換為極座標
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
# 使用色相和飽和度來編碼光流
hsv[..., 0] = ang * 180 / np.pi / 2
hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
# 轉換HSV影象為BGR
bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
cv2.imshow("frame", frame_copy)
cv2.imshow("optical flow", bgr)
k = cv2.waitKey(25) & 0xFF
if k == 27:
break
old_frame = new_frame
def main():
parser = ArgumentParser()
parser.add_argument(
"--algorithm",
choices=["farneback", "lucaskanade_dense", "rlof"],
required=True,
help="Optical flow algorithm to use",
)
parser.add_argument(
"--video_path", default="videos/people.mp4", help="Path to the video",
)
args = parser.parse_args()
video_path = args.video_path
if args.algorithm == "lucaskanade_dense":
method = cv2.optflow.calcOpticalFlowSparseToDense
dense_optical_flow(method, video_path, to_gray=True)
elif args.algorithm == "farneback":
# OpenCV Farneback演算法需要一個單通道的輸入影象,因此我們將BRG影象轉換為灰度。
method = cv2.calcOpticalFlowFarneback
params = [0.5, 3, 15, 3, 5, 1.2, 0] # Farneback的演算法引數
dense_optical_flow(method, video_path, params, to_gray=True)
elif args.algorithm == "rlof":
# 與Farneback演算法相比,RLOF演算法需要3通道影象,所以這裡沒有預處理。
method = cv2.optflow.calcOpticalFlowDenseRLOF
dense_optical_flow(method, video_path)
if __name__ == "__main__":
main()
# python dense_optical_flow.py

(2)C++

// dense_optical_flow.cpp
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/optflow.hpp>
#include <sys/stat.h>
using namespace cv;
using namespace std;
template <typename Method, typename... Args>
void dense_optical_flow(string filename, bool save, Method method, bool to_gray, Args&&... args)
{
VideoCapture capture(samples::findFile(filename));
if (!capture.isOpened()) {
//開啟視訊錯誤
cerr << "Unable to open file!" << endl;
}
Mat frame1, prvs;
capture >> frame1;
if (to_gray)
cvtColor(frame1, prvs, COLOR_BGR2GRAY);
else
prvs = frame1;
int counter = 0;
while (true) {
Mat frame2, next;
capture >> frame2;
if (frame2.empty())
break;
if (to_gray)
cvtColor(frame2, next, COLOR_BGR2GRAY);
else
next = frame2;
Mat flow(prvs.size(), CV_32FC2);
method(prvs, next, flow, std::forward<Args>(args)...);
// 視覺化
Mat flow_parts[2];
split(flow, flow_parts);
Mat magnitude, angle, magn_norm;
cartToPolar(flow_parts[0], flow_parts[1], magnitude, angle, true);
normalize(magnitude, magn_norm, 0.0f, 1.0f, NORM_MINMAX);
angle *= ((1.f / 360.f) * (180.f / 255.f));
//構建hsv影象
Mat _hsv[3], hsv, hsv8, bgr;
_hsv[0] = angle;
_hsv[1] = Mat::ones(angle.size(), CV_32F);
_hsv[2] = magn_norm;
merge(_hsv, 3, hsv);
hsv.convertTo(hsv8, CV_8U, 255.0);
cvtColor(hsv8, bgr, COLOR_HSV2BGR);
if (save) {
string save_path = "./optical_flow_frames/frame_" + to_string(counter) + ".jpg";
imwrite(save_path, bgr);
}
imshow("frame", frame2);
imshow("flow", bgr);
int keyboard = waitKey(30);
if (keyboard == 'q' || keyboard == 27)
break;
prvs = next;
counter++;
}
}
int main(int argc, char** argv)
{
const string keys =
"{ h help | | print this help message }"
"{ @video | | path to image file }"
"{ @method | | method to OF calcualtion }"
"{ save | | save video frames }";
CommandLineParser parser(argc, argv, keys);
string filename = samples::findFile(parser.get<string>("@video"));
if (!parser.check())
{
parser.printErrors();
return 0;
}
string method = parser.get<string>("@method");
printf("%s %s", method.c_str(), "method is now working!");
bool save = false;
if (parser.has("save")){
save = true;
mkdir("optical_flow_frames", 0777);
}
bool to_gray = true;
if (method == "lucaskanade_dense"){
dense_optical_flow(filename, save, optflow::calcOpticalFlowSparseToDense, to_gray, 8, 128, 0.05f, true, 500.0f, 1.5f);
}
else if (method == "farneback"){
dense_optical_flow(filename, save, calcOpticalFlowFarneback, to_gray, 0.5, 3, 15, 3, 5, 1.2, 0);
}
else if (method == "rlof"){
to_gray = false;
dense_optical_flow(
filename, save, optflow::calcOpticalFlowDenseRLOF, to_gray,
Ptr<cv::optflow::RLOFOpticalFlowParameter>(), 1.f, Size(6,6),
cv::optflow::InterpolationType::INTERP_EPIC, 128, 0.05f, 999.0f, 15, 100, true, 500.0f, 1.5f, false
);
}
return 0;
}

程式碼解析

首先,我們需要讀取第一個視訊幀,並在必要時進行影象預處理。演示的主要部分是一個迴圈,我們在其中為每對新的連續影象計算光流。之後,我們將結果編碼為 HSV
格式以進行視覺化。因此,method()函式讀取兩個連續的幀作為輸入。在某些情況下,需要影象灰度化,所以to_gray引數應該設定為True。在得到演算法輸出後,我們對其進行編碼,使用HSV顏色格式進行適當的視覺化。

總結

在這篇文章中,我們考慮了光流任務,這是我們需要物體運動資訊時不可缺少的任務。我們看了一些經典的演算法,它們的理論思想,以及OpenCV庫的實際使用。實際上,光流估計並不侷限於演算法方法,基於深度學習的新方法提高了光流估計的質量。

參考目錄

https://learnopencv.com/optical-flow-in-opencv/

更多內容參考: https://www.gewuweb.com/sitemap.html