OpenCV还能实现这种效果? | 视频防抖技术
點擊上方“AI算法與圖像處理”,選擇加"星標"或“置頂”
重磅干貨,第一時間送達
這篇文章分享了一個視頻防抖的策略,這個方法同樣可以應用到其他領域,比如常見的關鍵點檢測,當使用視頻測試時,效果就沒有demo那么好,此時可以考慮本文的方法去優化。
分享這些demo并不一定所有人都會用到,但是在解決實際問題的時候,可以提供一個思路去解決問題。希望能給我一個三連,鼓勵一下哈
在這篇文章中,我們將學習如何使用OpenCV庫中的點特征匹配技術來實現一個簡單的視頻穩定器。我們將討論算法并且會分享代碼(python和C++版),以使用這種方法在OpenCV中設計一個簡單的穩定器。
視頻中低頻攝像機運動的例子
視頻防抖是指用于減少攝像機運動對最終視頻的影響的一系列方法。攝像機的運動可以是平移(比如沿著x、y、z方向上的運動)或旋轉(偏航、俯仰、翻滾)。
視頻防抖的應用
對視頻防抖的需求在許多領域都有。
這在消費者和專業攝像中是極其重要的。因此,存在許多不同的機械、光學和算法解決方案。即使在靜態圖像拍攝中,防抖技術也可以幫助拍攝長時間曝光的手持照片。
在內窺鏡和結腸鏡等醫療診斷應用中,需要對視頻進行穩定,以確定問題的確切位置和寬度。
同樣,在軍事應用中,無人機在偵察飛行中捕獲的視頻也需要進行穩定,以便定位、導航、目標跟蹤等。同樣的道理也適用于機器人。
視頻防抖的不同策略
視頻防抖的方法包括機械穩定方法、光學穩定方法和數字穩定方法。下面將簡要討論這些問題:
機械視頻穩定:機械圖像穩定系統使用由特殊傳感器如陀螺儀和加速度計檢測到的運動來移動圖像傳感器以補償攝像機的運動。
光學視頻穩定:在這種方法中,不是移動整個攝像機,而是通過鏡頭的移動部分來實現穩定。這種方法使用了一個可移動的鏡頭組合,當光通過相機的鏡頭系統時,可以可變地調整光的路徑長度。
數字視頻穩定:這種方法不需要特殊的傳感器來估計攝像機的運動。主要有三個步驟:1)運動估計2)運動平滑,3)圖像合成。第一步導出了兩個連續坐標系之間的變換參數。第二步過濾不需要的運動,在最后一步重建穩定的視頻。
在這篇文章中,我們將學習一個快速和魯棒性好的數字視頻穩定算法的實現。它是基于二維運動模型,其中我們應用歐幾里得(即相似性)變換包含平移、旋轉和縮放。
OpenCV Motion Models
正如你在上面的圖片中看到的,在歐幾里得運動模型中,圖像中的一個正方形可以轉換為任何其他位置、大小或旋轉不同的正方形。它比仿射變換和單應變換限制更嚴格,但對于運動穩定來說足夠了,因為攝像機在視頻連續幀之間的運動通常很小。
使用點特征匹配實現視頻防抖
該方法涉及跟蹤兩個連續幀之間的多個特征點。跟蹤特征允許我們估計幀之間的運動并對其進行補償。
下面的流程圖顯示了基本步驟。
我們來看看這些步驟。
第一步:設置輸入和輸出視頻
首先,讓我們完成讀取輸入視頻和寫入輸出視頻的設置。代碼中的注釋解釋每一行。
Python
# Import numpy and OpenCV import numpy as np import cv2# Read input video cap = cv2.VideoCapture('video.mp4') # Get frame count n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # Get width and height of video stream w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))# Define the codec for output video fourcc = cv2.VideoWriter_fourcc(*'MJPG')# Set up output video out = cv2.VideoWriter('video_out.mp4', fourcc, fps, (w, h))C++
第二步:讀取第一幀并將其轉換為灰度圖
對于視頻穩定,我們需要捕捉視頻的兩幀,估計幀之間的運動,最后校正運動。
Python
# Read first frame _, prev = cap.read() # Convert frame to grayscale prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)C++
// Define variable for storing framesMat curr, curr_gray;Mat prev, prev_gray;// Read first framecap >> prev;// Convert frame to grayscalecvtColor(prev, prev_gray, COLOR_BGR2GRAY);
第三步:尋找幀之間的移動
這是算法中最關鍵的部分。我們將遍歷所有的幀,并找到當前幀和前一幀之間的移動。沒有必要知道每一個像素的運動。歐幾里得運動模型要求我們知道兩個坐標系中兩個點的運動。但是在實際應用中,找到50-100個點的運動,然后用它們來穩健地估計運動模型是一個好方法。
3.1 可用于跟蹤的優質特征
現在的問題是我們應該選擇哪些點進行跟蹤。請記住,跟蹤算法使用一個小補丁圍繞一個點來跟蹤它。這樣的跟蹤算法受到孔徑問題的困擾,如下面的視頻所述
因此,光滑的區域不利于跟蹤,而有很多角的紋理區域則比較好。幸運的是,OpenCV有一個快速的特征檢測器,可以檢測最適合跟蹤的特性。它被稱為goodFeaturesToTrack)
3.2 Lucas-Kanade光流
一旦我們在前一幀中找到好的特征,我們就可以使用Lucas-Kanade光流算法在下一幀中跟蹤它們。
它是利用OpenCV中的calcOpticalFlowPyrLK函數實現的。在calcOpticalFlowPyrLK這個名字中,LK代表Lucas-Kanade,而Pyr代表金字塔。計算機視覺中的圖像金字塔是用來處理不同尺度(分辨率)的圖像的。
由于各種原因,calcOpticalFlowPyrLK可能無法計算出所有點的運動。例如,當前幀的特征點可能會被下一幀的另一個對象遮擋。幸運的是,您將在下面的代碼中看到,calcOpticalFlowPyrLK中的狀態標志可以用來過濾掉這些值。
3.3 估計運動
回顧一下,在3.1步驟中,我們在前一幀中找到了一些好的特征。在步驟3.2中,我們使用光流來跟蹤特征。換句話說,我們已經找到了特征在當前幀中的位置,并且我們已經知道了特征在前一幀中的位置。所以我們可以使用這兩組點來找到映射前一個坐標系到當前坐標系的剛性(歐幾里德)變換。這是使用函數estimateRigidTransform完成的。
一旦我們估計了運動,我們可以把它分解成x和y的平移和旋轉(角度)。我們將這些值存儲在一個數組中,這樣就可以平穩地更改它們。
下面的代碼將完成步驟3.1到3.3。請務必閱讀代碼中的注釋以進行后續操作。
Python
# Pre-define transformation-store array transforms = np.zeros((n_frames-1, 3), np.float32) for i in range(n_frames-2):# Detect feature points in previous frameprev_pts = cv2.goodFeaturesToTrack(prev_gray,maxCorners=200,qualityLevel=0.01,minDistance=30,blockSize=3)# Read next framesuccess, curr = cap.read() if not success: break # Convert to grayscalecurr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY) # Calculate optical flow (i.e. track feature points)curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None) # Sanity checkassert prev_pts.shape == curr_pts.shape # Filter only valid pointsidx = np.where(status==1)[0]prev_pts = prev_pts[idx]curr_pts = curr_pts[idx]#Find transformation matrixm = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False) #will only work with OpenCV-3 or less# Extract traslationdx = m[0,2]dy = m[1,2]# Extract rotation angleda = np.arctan2(m[1,0], m[0,0])# Store transformationtransforms[i] = [dx,dy,da]# Move to next frameprev_gray = curr_grayprint("Frame: " + str(i) + "/" + str(n_frames) + " - Tracked points : " + str(len(prev_pts)))C++
在c++實現中,我們首先定義一些類來幫助我們存儲估計的運動向量。下面的TransformParam類存儲了運動信息(dx -運動在x中,dy -運動在y中,da -角度變化),并提供了一個方法getTransform來將該運動轉換為變換矩陣。
struct TransformParam {TransformParam() {}TransformParam(double _dx, double _dy, double _da) {dx = _dx;dy = _dy;da = _da;}double dx;double dy;double da; // anglevoid getTransform(Mat &T) {// Reconstruct transformation matrix accordingly to new valuesT.at<double>(0,0) = cos(da);T.at<double>(0,1) = -sin(da);T.at<double>(1,0) = sin(da);T.at<double>(1,1) = cos(da);T.at<double>(0,2) = dx;T.at<double>(1,2) = dy;} };在下面的代碼中,我們循環視頻幀并執行步驟3.1到3.3。
第四步:計算幀之間的平滑運動
在前面的步驟中,我們估計幀之間的運動并將它們存儲在一個數組中。我們現在需要通過疊加上一步估計的微分運動來找到運動軌跡。
步驟4.1:軌跡計算
在這一步,我們將增加運動之間的幀來計算軌跡。我們的最終目標是平滑這條軌跡。
Python 在Python中,可以很容易地使用numpy中的cumsum(累計和)來實現。
# Compute trajectory using cumulative sum of transformations trajectory = np.cumsum(transforms, axis=0C++
在c++中,我們定義了一個名為Trajectory的結構體來存儲轉換參數的累積和。
struct Trajectory {Trajectory() {}Trajectory(double _x, double _y, double _a) {x = _x;y = _y;a = _a;}double x;double y;double a; // angle };C我們還定義了一個函數cumsum,它接受一個TransformParams 向量,并通過執行微分運動dx、dy和da(角度)的累積和返回軌跡。
步驟4.2:計算平滑軌跡
在上一步中,我們計算了運動軌跡。所以我們有三條曲線來顯示運動(x, y,和角度)如何隨時間變化。
在這一步,我們將展示如何平滑這三條曲線。
平滑任何曲線最簡單的方法是使用移動平均濾波器(moving average filter)。顧名思義,移動平均過濾器將函數在某一點上的值替換為由窗口定義的其相鄰函數的平均值。讓我們看一個例子。
假設我們在數組c中存儲了一條曲線,那么曲線上的點是c[0]…c[n-1]。設f是我們通過寬度為5的移動平均濾波器過濾c得到的平滑曲線。
該曲線的k^{th}元素使用
如您所見,平滑曲線的值是噪聲曲線在一個小窗口上的平均值。下圖顯示了左邊的噪點曲線的例子,使用右邊的尺度為5 濾波器進行平滑。
Python
在Python實現中,我們定義了一個移動平均濾波器,它接受任何曲線(即1-D的數字)作為輸入,并返回曲線的平滑版本。
def movingAverage(curve, radius): window_size = 2 * radius + 1# Define the filter f = np.ones(window_size)/window_size # Add padding to the boundaries curve_pad = np.lib.pad(curve, (radius, radius), 'edge') # Apply convolution curve_smoothed = np.convolve(curve_pad, f, mode='same') # Remove padding curve_smoothed = curve_smoothed[radius:-radius]# return smoothed curvereturn curve_smoothed我們還定義了一個函數,它接受軌跡并對這三個部分進行平滑處理。
這是最后去使用
# Compute trajectory using cumulative sum of transformations trajectory = np.cumsum(transforms, axis=0)C++
在c++版本中,我們定義了一個名為smooth的函數,用于計算平滑移動平均軌跡。
vector <Trajectory> smooth(vector <Trajectory>& trajectory, int radius) {vector <Trajectory> smoothed_trajectory; for(size_t i=0; i < trajectory.size(); i++) {double sum_x = 0;double sum_y = 0;double sum_a = 0;int count = 0;for(int j=-radius; j <= radius; j++) { if(i+j >= 0 && i+j < trajectory.size()) {sum_x += trajectory[i+j].x;sum_y += trajectory[i+j].y;sum_a += trajectory[i+j].a;count++;}}double avg_a = sum_a / count;double avg_x = sum_x / count;double avg_y = sum_y / count;smoothed_trajectory.push_back(Trajectory(avg_x, avg_y, avg_a));}return smoothed_trajectory; }
我們在主函數中使用它
步驟4.3:計算平滑變換
到目前為止,我們已經得到了一個平滑的軌跡。在這一步,我們將使用平滑的軌跡來獲得平滑的變換,可以應用到視頻的幀來穩定它。
這是通過找到平滑軌跡和原始軌跡之間的差異,并將這些差異加回到原始的變換中來完成的。
Python
# Calculate difference in smoothed_trajectory and trajectory difference = smoothed_trajectory - trajectory# Calculate newer transformation array transforms_smooth = transforms + differenceC++
vector <TransformParam> transforms_smooth;
for(size_t i=0; i < transforms.size(); i++)
{
// Calculate difference in smoothed_trajectory and trajectory
double diff_x = smoothed_trajectory[i].x - trajectory[i].x;
double diff_y = smoothed_trajectory[i].y - trajectory[i].y;
double diff_a = smoothed_trajectory[i].a - trajectory[i].a;
// Calculate newer transformation array
double dx = transforms[i].dx + diff_x;
double dy = transforms[i].dy + diff_y;
double da = transforms[i].da + diff_a;
transforms_smooth.push_back(TransformParam(dx, dy, da));
}
第五步:將平滑的攝像機運動應用到幀中
差不多做完了?,F在我們所需要做的就是循環幀并應用我們剛剛計算的變換。
如果我們有一個指定為(x, y, \theta),的運動,對應的變換矩陣是
請閱讀代碼中的注釋以進行后續操作。
Python
# Reset stream to first frame cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # Write n_frames-1 transformed frames for i in range(n_frames-2):# Read next framesuccess, frame = cap.read() if not success:break# Extract transformations from the new transformation arraydx = transforms_smooth[i,0]dy = transforms_smooth[i,1]da = transforms_smooth[i,2]# Reconstruct transformation matrix accordingly to new valuesm = np.zeros((2,3), np.float32)m[0,0] = np.cos(da)m[0,1] = -np.sin(da)m[1,0] = np.sin(da)m[1,1] = np.cos(da)m[0,2] = dxm[1,2] = dy# Apply affine wrapping to the given frameframe_stabilized = cv2.warpAffine(frame, m, (w,h))# Fix border artifactsframe_stabilized = fixBorder(frame_stabilized) # Write the frame to the fileframe_out = cv2.hconcat([frame, frame_stabilized])# If the image is too big, resize it.if(frame_out.shape[1] > 1920): frame_out = cv2.resize(frame_out, (frame_out.shape[1]/2, frame_out.shape[0]/2));cv2.imshow("Before and After", frame_out)cv2.waitKey(10)out.write(frame_out)C++
cap.set(CV_CAP_PROP_POS_FRAMES, 1); Mat T(2,3,CV_64F); Mat frame, frame_stabilized, frame_out; for( int i = 0; i < n_frames-1; i++) { bool success = cap.read(frame); if(!success) break; // Extract transform from translation and rotation angle. transforms_smooth[i].getTransform(T); // Apply affine wrapping to the given frame warpAffine(frame, frame_stabilized, T, frame.size()); // Scale image to remove black border artifact fixBorder(frame_stabilized); // Now draw the original and stabilised side by side for coolness hconcat(frame, frame_stabilized, frame_out); // If the image is too big, resize it. if(frame_out.cols > 1920) {resize(frame_out, frame_out, Size(frame_out.cols/2, frame_out.rows/2));}imshow("Before and After", frame_out);out.write(frame_out);waitKey(10); }
步驟5.1:修復邊界偽影
當我們穩定一個視頻,我們可能會看到一些黑色的邊界偽影。這是意料之中的,因為為了穩定視頻,幀可能不得不縮小大小。
我們可以通過將視頻的中心縮小一小部分(例如4%)來緩解這個問題。
下面的fixBorder函數顯示了實現。我們使用getRotationMatrix2D,因為它在不移動圖像中心的情況下縮放和旋轉圖像。我們所需要做的就是調用這個函數時,旋轉為0,縮放為1.04(也就是提升4%)。
Python
def fixBorder(frame):s = frame.shape# Scale the image 4% without moving the centerT = cv2.getRotationMatrix2D((s[1]/2, s[0]/2), 0, 1.04)frame = cv2.warpAffine(frame, T, (s[1], s[0]))return frame
C++
結果:
我們分享的視頻防抖代碼的結果如上所示。我們的目標是顯著減少運動,但不是完全消除它。
我們留給讀者去思考如何修改代碼來完全消除幀之間的移動。如果你試圖消除所有的相機運動,會有什么副作用?
目前的方法只適用于固定長度的視頻,而不適用于實時feed。我們不得不對這個方法進行大量修改,以獲得實時視頻輸出,這超出了本文的范圍,但這是可以實現的,更多的信息可以在這里找到。
https://abhitronix.github.io/2018/11/30/humanoid-AEAM-3/
優點和缺點
優點
這種方法對低頻運動(較慢的振動)具有良好的穩定性。這種方法內存消耗低,因此非常適合嵌入式設備(如樹莓派)。這種方法對視頻縮放抖動有很好的效果。
缺點
這種方法對高頻擾動的抵抗效果很差。如果有一個嚴重的運動模糊,特征跟蹤將失敗,結果將不是最佳的。這種方法也不適用于滾動快門失真。
References:
Example video and Code reference from Nghia Ho’s?post
http://nghiaho.com/uploads/code/videostab.cpp
Various References, data, and image from my?website
https://abhitronix.github.io/
https://www.learnopencv.com/video-stabilization-using-point-feature-matching-in-opencv/
資源傳送門(素材&代碼獲取)
覺得不錯,請點個在看
總結
以上是生活随笔為你收集整理的OpenCV还能实现这种效果? | 视频防抖技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数制的介绍
- 下一篇: 字幕编辑修改脚本-python方式