[計算機視覺]100行python實現攝像機偏移、抖動告警
背景
在實際專案中,利用深度學習在檢測道路車輛並分析車輛行為時,需要按照事先規定的方法繪製檢測區(包含道路方向、車道區域等)。由於各種原因(人為、天氣),獲取視訊資料的攝像角度容易偏移原來設定的位置,造成檢測區域和實際畫面不匹配,系統容易產生誤檢誤報等錯誤資料。因此需要在攝像機位置偏移第一時間告訴系統檢測模組停止工作,直到攝像機歸位後再進行檢測。攝像機角度偏移告警屬於‘視訊診斷’中的一類,本文利用提取圖片特徵點實現攝像機偏移告警,demo全部python程式碼不足200行。
前面有幾篇部落格文字太少,發不了首頁:
這裡是,需要的朋友可以點一波關注或者收藏一下。
影象特徵點
對於任何一張二維圖片,從畫素級別上看,都存在一些我們肉眼看不到的比較獨特的畫素單元(可以理解為畫素塊),就像我們每個人的臉都會與眾不同一樣,我們稱這些具有特點的畫素區域為“影象特徵點”。已經有非常成熟的演算法來提取圖片的特徵點:
(1)Harris:用於檢測角點;
(2)SIFT:用於檢測斑點;
(3)SURF:用於檢測斑點;
(4)FAST:用於檢測角點;
(5)BRIEF:用於檢測斑點;
(6)ORB:表示帶方向的FAST演算法與具有旋轉不變性的BRIEF演算法;
詳細演算法原理上網搜一下(我也不是很清楚:)),OpenCV中包含以上幾種演算法實現。
角點:
影象中涉及到拐角的區域,比如物體有輪廓,影象中的物體有邊緣區分。
斑點:
一塊有特別規律的畫素區域。
方向、尺寸不變性:
指特徵點不會受圖片尺寸、旋轉而改變,比如同一張圖,你縮小一倍旋轉90度後,特徵點還是一樣的。
影象匹配
提取兩張圖片的特徵點,然後將這些特徵點進行匹配關聯。如果匹配程度滿足某一閾值,則認為這兩張圖滿足匹配條件。注意,對於同一個物體,拍攝角度不同,亮度不同都應該滿足匹配條件。
可以看到,對於同一個場景的不同拍攝角度的兩張圖片,能找到匹配到的特徵點,但是誤差非常大。我們設定一個閾值,滿足該條件才認為兩個點匹配:
誤差少很多了,匹配到的特徵點也非常正確。
換一組攝像機的照片,前一張和後一張在拍攝時,攝像機角度往左下角有偏移
我們可以看到,雖然拍攝角度不同,但是由於場景類似,仍然能匹配到特徵點(為了減少繪圖方便看清楚,閾值設定非常嚴格,如果放寬一點還能看到更多匹配到的點),而且這些匹配到的點幾乎都正確。對於兩張完全不同的場景照片,匹配到的特徵點非常少或者為零(具體看設定的閾值)
場景不同,匹配到的特徵點只有視訊上的文字。
角度偏移告警
如果攝像機位置不變,前後拍攝兩張照片,那麼這兩張照片匹配到的特徵點的二維物理座標應該是一樣的(可能有輕微偏移,兩張照片尺寸一致)。那麼我們可以根據攝像機前後兩幀(或間隔時間內取得的兩幀)的匹配點物理位置是否有偏移,設定一個偏移閾值,大於該閾值時則認為偏移,否則認為沒偏移(或輕微偏移),當然,如果兩幀匹配到的特徵點非常少(低於一個閾值),那麼我們認為這倆幀完全不一樣了(場景不一樣了),這時候攝像機完全偏移了原來的角度。
注意點:
1)閾值非常重要;
2)前後幀匹配時,要去掉類似攝像機自動加上去的“視訊位置”、“當前時間”等等區域,因為這些區域很多時候能夠匹配到特徵點,並且物理位置座標不會發生變化,造成誤差;
3)在計算特徵點物理位置偏移量時,取所有特徵點物理位置偏移的平均值。
最終效果
間隔時間取視訊中的幀,進行特徵點對比。根據前面的思路分為4個等級:“無偏移”、“輕度偏移(抖動)”、“嚴重偏移”、“完全偏移”。
原始碼
最重要的是程式碼,很簡單,直接貼上來即可。加起來不到160行。測試很多場景,效果都不錯。
1 ''' 2 視訊幀匹配指令碼 3 ''' 4 import numpy as np 5 import cv2 6 7 #至少10個點匹配 8 MIN_MATCH_COUNT = 10 9 #完全匹配偏移 d<4 10 BEST_DISTANCE = 4 11 #微量偏移 4<d<10 12 GOOD_DISTANCE = 10 13 14 15 # 特徵點提取方法,內建很多種 16 algorithms_all = { 17 "SIFT": cv2.xfeatures2d.SIFT_create(), 18 "SURF": cv2.xfeatures2d.SURF_create(8000), 19 "ORB": cv2.ORB_create() 20 } 21 22 ''' 23 # 影象匹配 24 # 0完全不匹配 1場景匹配 2角度輕微偏移 3完全匹配 25 ''' 26 def match2frames(image1, image2): 27 img1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY) 28 img2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY) 29 30 size1 = img1.shape 31 size2 = img2.shape 32 33 img1 = cv2.resize(img1, (int(size1[1]*0.3), int(size1[0]*0.3)), cv2.INTER_LINEAR) 34 img2 = cv2.resize(img2, (int(size2[1]*0.3), int(size2[0]*0.3)), cv2.INTER_LINEAR) 35 36 sift = algorithms_all["SIFT"] 37 38 kp1, des1 = sift.detectAndCompute(img1, None) 39 kp2, des2 = sift.detectAndCompute(img2, None) 40 41 FLANN_INDEX_KDTREE = 0 42 index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) 43 search_params = dict(checks = 50) 44 45 flann = cv2.FlannBasedMatcher(index_params, search_params) 46 47 matches = flann.knnMatch(des1, des2, k=2) 48 49 # 過濾 50 good = [] 51 for m,n in matches: 52 if m.distance < 0.7*n.distance: 53 good.append(m) 54 55 if len(good) <= MIN_MATCH_COUNT: 56 return 0 # 完全不匹配 57 else: 58 distance_sum = 0 # 特徵點2d物理座標偏移總和 59 for m in good: 60 distance_sum += get_distance(kp1[m.queryIdx].pt, kp2[m.trainIdx].pt) 61 distance = distance_sum / len(good) #單個特徵點2D物理位置平均偏移量 62 63 if distance < BEST_DISTANCE: 64 return 3 #完全匹配 65 elif distance < GOOD_DISTANCE and distance >= BEST_DISTANCE: 66 return 2 #部分偏移 67 else: 68 return 1 #場景匹配 69 70 71 ''' 72 計算2D物理距離 73 ''' 74 def get_distance(p1, p2): 75 x1,y1 = p1 76 x2,y2 = p2 77 return np.sqrt((x1-x2)**2 + (y1-y2)**2) 78 79 80 if __name__ == "__main__": 81 pass
測試
1 ''' 2 攝像機角度偏移告警 3 ''' 4 import cv2 5 import do_match 6 import numpy as np 7 from PIL import Image, ImageDraw, ImageFont 8 9 ''' 10 告警資訊 11 ''' 12 def putText(frame, text): 13 cv2_im = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 14 pil_im = Image.fromarray(cv2_im) 15 16 draw = ImageDraw.Draw(pil_im) 17 font = ImageFont.truetype("fonts/msyh.ttc", 30, encoding="utf-8") 18 draw.text((50, 50), text, (0, 255, 255), font=font) 19 20 cv2_text_im = cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR) 21 22 return cv2_text_im 23 24 25 26 27 texts = ["完全偏移","嚴重偏移", "輕微偏移", "無偏移"] 28 29 cap = cv2.VideoCapture('videos/test4_new.mp4') 30 31 if (cap.isOpened()== False): 32 print("Error opening video stream or file") 33 34 first_frame = True 35 pre_frame = 0 36 37 index = 0 38 39 while(cap.isOpened()): 40 ret, frame = cap.read() 41 if ret == True: 42 if first_frame: 43 pre_frame = frame 44 first_frame = False 45 continue 46 47 index += 1 48 if index % 24 == 0: 49 result = do_match.match2frames(pre_frame, frame) 50 print("檢測結果===>", texts[result]) 51 52 if result > 1: # 快取最近無偏移的幀 53 pre_frame = frame 54 55 size = frame.shape 56 57 if size[1] > 720: # 縮小顯示 58 frame = cv2.resize(frame, (int(size[1]*0.5), int(size[0]*0.5)), cv2.INTER_LINEAR) 59 60 text_frame = putText(frame, texts[result]) 61 62 cv2.imshow('Frame', text_frame) 63 if cv2.waitKey(1) & 0xFF == ord('q'): 64 break 65 else: 66 break 67 68 cap.release() 69 cv2.destroyAllWindows()