1. 程式人生 > 程式設計 >Python樹莓派學習筆記之UDP傳輸視訊幀操作詳解

Python樹莓派學習筆記之UDP傳輸視訊幀操作詳解

本文例項講述了Python樹莓派學習筆記之UDP傳輸視訊幀操作。分享給大家供大家參考,具體如下:

因為我在自己膝上型電腦上沒能成功安裝OpenCV-Contrib模組,因此不能使用人臉識別等高階功能,不過已經在樹莓派上安裝成功了,所以我想實現把樹莓派上採集的視訊幀傳輸到PC的功能,這樣可以省去給樹莓派配顯示屏的麻煩,而且以後可能可以用在遠端監控上。

1 UDP還是TCP

首先考慮用哪種傳輸方式,平常TCP用的非常多,但是像視訊幀這種資料用TCP不是太合適,因為視訊資料的傳輸最先要考慮的是速度而不是準確性,視訊幀的資料量很大,幀間隔也非常短,需要儘量保證傳輸速度,同時丟失一些資料是無所謂的。TCP需要維護連線、保證資料包正確,會耗費一些時間,因此應該使用UDP,就像所有參考書上說的,UDP不在乎是否建立連線,也不管資料是否能被準確接收,只關心能否把資料傳送出去而已。

在Python的socket程式碼中也可直觀地看到UDP的特點,對於傳送方,我們通過server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)建立UDP套接字物件,然後執行server.connect((HOST,PORT)) 指定傳送方地址,但其實connect函式直接就返回了,不像TCP中的客戶端會等待連線成功,接著就可直接在套接字物件上呼叫send函式傳送資料了,這個過程根本沒確立連線。

2 影象傳輸中的編解碼

但是用UDP傳輸影象有一個很關鍵的問題需要考慮,就是影象的大小。根據UDP協議,單個包的資料大小最大隻能65507個位元組(去掉包頭),而一般直接從攝像頭採集的影象幀的大小比這個數要大得多,以我的邏輯C270為例,單幅影象的大小為480X640X3個位元組,遠大於65507,因此一個包是沒法傳送完的。解決方法有兩種,一種是把影象拆成幾次進行傳送,相應的接收端用個迴圈多次接收,這種方法可以完整地接收資料,但是速度肯定受到影響,而且可能要新增一些自定義規則,徒增麻煩;另一種方法就是傳送前先對影象進行編碼壓縮,接收後再解碼,清晰度會有所下降,但是可以保持速度上的優勢,這種方式比較合適。

OpenCV中的imencode和imdecode方法可分別用於影象的編碼和解碼。imencode根據指定的標識將影象資料編碼並存入快取區,函式原型為cv2.imencode(ext,img[,params]) → retval,buf,ext為副檔名,指定了儲存格式,如'.jpg';img為需要編碼的影象資料; params為指定的編碼標識,其形式為paramId_1,paramValue_1,paramId_2,paramValue_2,... ,對於jpg格式,可以指定標識為CV_IMWRITE_JPEG_QUALITY ,其對應的值在0到100之間,表示了壓縮質量,值越大壓縮率越大,編碼後的資料量越小,但解碼後的影象質量也越差。

imdecode從快取區讀取影象資料,通過指定標識,可以實現指定的解碼格式。imdecode的函式原型為cv2.imdecode(buf,flags) → retval ,其中flags指定影象的讀取型別,實際上就是指定了以多少深度多少通道讀取影象,比如CV_LOAD_IMAGE_ANYDEPTH(即整數2)表示單個通道,深度不變的灰度圖;CV_LOAD_IMAGE_COLOR(即整數1)表示3通道、8位深度的彩色圖。

3 樹莓派程式

結合套接字物件和編解碼函式,就可以編寫傳送端的程式碼了,不過還有一個需要注意的地方是傳送和接收的資料格式問題,套接字的傳送和接收都是位元組流,或者說是byte陣列,傳送資料時需要以位元組流格式傳送,接收資料後需要把位元組流型別轉換成合適的資料型別。

從攝像頭獲取的影象是480X640X3的numpy.ndarray型別,通過imencode編碼,得到?X1的numpy.ndarray物件,經測試,這個物件可以直接傳送出去;在接收端,獲得的是byte陣列,這個陣列直接做imdecode的引數會報錯,經除錯,發現還需要把陣列轉換成numpy.ndarray型別。樹莓派作為傳送端,其Python程式碼如下:

import cv2
import numpy
import socket
import struct
HOST='192.168.1.122'
PORT=9999
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #socket物件
server.connect((HOST,PORT))
print('now starting to send frames...')
capture=cv2.VideoCapture(0) #VideoCapture物件,可獲取攝像頭裝置的資料
try:
  while True:
    success,frame=capture.read()
    while not success and frame is None:
      success,frame=capture.read() #獲取視訊幀
  result,imgencode=cv2.imencode('.jpg',frame,[cv2.IMWRITE_JPEG_QUALITY,50]) #編碼
  server.sendall(struct.pack('i',imgencode.shape[0])) #傳送編碼後的位元組長度,這個值不是固定的
  server.sendall(imgencode) #傳送視訊幀資料
  print('have sent one frame')
except Exception as e:
  print(e)
  server.sendall(struct.pack('c',1)) #傳送關閉訊息
  capture.release()
  server.close()

在程式碼中,首先把編碼後的位元組長度傳送了過去,目的是讓接收端可以進行簡單的校驗,並且接收端可以據此判斷是否應該關閉程式,相應的,自定義單位元組的1為關閉訊息。

4 PC端程式

自己的電腦作為接收端,為了解碼資料,需要把原始位元組流轉成numpy.ndarray物件,程式碼如下:

import cv2
import numpy
import socket
import struct
HOST='192.168.191.122'
PORT=9999
buffSize=65535
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #建立socket物件
server.bind((HOST,PORT))
print('now waiting for frames...')
while True:
  data,address=server.recvfrom(buffSize) #先接收的是位元組長度
  if len(data)==1 and data[0]==1: #如果收到關閉訊息則停止程式
    server.close()
    cv2.destroyAllWindows()
    exit()
  if len(data)!=4: #進行簡單的校驗,長度值是int型別,佔四個位元組
    length=0
  else:
    length=struct.unpack('i',data)[0] #長度值
  data,address=server.recvfrom(buffSize) #接收編碼影象資料
  if length!=len(data): #進行簡單的校驗
    continue
  data=numpy.array(bytearray(data)) #格式轉換
  imgdecode=cv2.imdecode(data,1) #解碼
  print('have received one frame')
  cv2.imshow('frames',imgdecode) #視窗顯示
  if cv2.waitKey(1)==27: #按下“ESC”退出
    break
server.close()
cv2.destroyAllWindows()

5 測試

因為我樹莓派上的OpenCV只關聯了Python2,因此以python2 UDP_Frame_Send.py 的命令啟動傳送程式(接好攝像頭);電腦上,在開始選單中輸入cmd進入Windows的控制檯,進入程式檔案目錄,輸入python UDP_Frame_Recv.py啟動接收程式,結果表明可以比較流暢地視窗顯示,不過有幾個問題,一是在樹莓派上,程式有時候打不開攝像頭,需要重啟幾次程式,二是在電腦上,recvfrom這個函式是阻塞式的,在Windows系統的控制檯中似乎沒辦法用鍵盤中斷強制從這個函式退出,所以如果傳送端出錯接收端的程式就沒法正常退出了,對此可以用TCP&UDP除錯助手手動傳送單個位元組的1來終止程式。

更多關於Python相關內容可檢視本站專題:《Python Socket程式設計技巧總結》、《Python資料結構與演算法教程》、《Python函式使用技巧總結》、《Python字串操作技巧彙總》、《Python入門與進階經典教程》及《Python檔案與目錄操作技巧彙總》

希望本文所述對大家Python程式設計有所幫助。