秋風(fēng)閣——北溪入江流:https://focus-wind.com/
秋風(fēng)閣——基于OpenCv+Django的網(wǎng)絡(luò)實(shí)時(shí)視頻流傳輸(前后端分離)
使用OpenCv捕獲攝像機(jī)畫面后,我們有時(shí)候需要將畫面顯示在界面上。本博客基于Django的前后端分離模式,將視頻流從后端讀取,傳送給前端顯示。
Django流傳輸實(shí)例:StreamingHttpResponse
在使用Django進(jìn)行視頻流傳輸時(shí),無(wú)法使用HttpResponse,JsonResponse等對(duì)象對(duì)內(nèi)容直接傳輸,需要使用StreamingHttpResponse流式傳輸一個(gè)響應(yīng)給瀏覽器。StreamingHttpResponse不是HttpResponse的子類,因此他們之間的API略有不同。StreamingHttpResponse與HttpResponse之間有以下顯著區(qū)別:
- 應(yīng)該給StreamingHttpResponse一個(gè)迭代器,產(chǎn)生字節(jié)字符串作為內(nèi)容。
- 不應(yīng)該直接訪問(wèn)StreamingHttpResponse的內(nèi)容,除非通過(guò)迭代器響應(yīng)對(duì)象本身。
- StreamingHttpResponse沒(méi)有content屬性。相反,他有一個(gè)streaming_content屬性。
- 無(wú)法使用類文件對(duì)象的tell()何write()方法。這樣會(huì)引起一個(gè)異常。
Django傳輸視頻流
因?yàn)槭褂肈jango的StreamingHttpResponse類進(jìn)行流傳輸,所以我們首先需要生成一個(gè)視頻流的迭代器,在迭代器中,需要將從opencv中獲取到的numpy.ndarray三維數(shù)組轉(zhuǎn)換為字節(jié)類型的,然后傳輸?shù)角岸恕?/p>
傳輸視頻流:
- 讀取圖片
- 圖片壓縮(針對(duì)分辨率較高的界面)
- 對(duì)圖片進(jìn)行解碼
- 轉(zhuǎn)換為byte類型
- 傳輸視頻流
import cv2
from django.http import StreamingHttpResponse
def gen_display(camera):
"""
視頻流生成器功能。
"""
while True:
# 讀取圖片
ret, frame = camera.read()
if ret:
# 將圖片進(jìn)行解碼
ret, frame = cv2.imencode('.jpeg', frame)
if ret:
# 轉(zhuǎn)換為byte類型的,存儲(chǔ)在迭代器中
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n')
def video(request):
"""
視頻流路由。將其放入img標(biāo)記的src屬性中。
例如:<img src='https://ip:port/uri' >
"""
# 視頻流相機(jī)對(duì)象
camera = cv2.VideoCapture(0)
# 使用流傳輸傳輸視頻流
return StreamingHttpResponse(gen_display(camera), content_type='multipart/x-mixed-replace; boundary=frame')
在使用海康威視等分辨率較高的相機(jī)時(shí),直接解碼,延遲過(guò)高,所以需要先對(duì)圖片進(jìn)行壓縮,然后解碼。
經(jīng)測(cè)試,海康相機(jī)使用0.25的壓縮倍率顯示壓縮效率較好,當(dāng)大于0.25時(shí),延遲較高,小于0.25時(shí),界面顯示較差
迭代器優(yōu)化:
def gen_display(camera):
"""
視頻流生成器功能。
"""
while True:
# 讀取圖片
ret, frame = camera.read()
if ret:
frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
# 將圖片進(jìn)行解碼
ret, frame = cv2.imencode('.jpeg', frame)
if ret:
# 轉(zhuǎn)換為byte類型的,存儲(chǔ)在迭代器中
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame.tobytes() + b'\r\n')
前端顯示視頻流
在Django中配置路由后,在瀏覽器端直接訪問(wèn)視頻url即可看到視頻顯示畫面。
在前端HTML5中,將視頻路由寫入img標(biāo)簽的src屬性中,即可訪問(wèn)視頻流界面。例如:<img src=‘https://ip:port/uri’
前端顯示視頻流:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基于OpenCv+Django的網(wǎng)絡(luò)實(shí)時(shí)視頻流傳輸(前后端分離)</title>
</head>
<body>
<!-- 顯示視頻流 -->
<img src="http://127.0.0.1:8000/api/cv/display">
</body>
</html>
顯示結(jié)果:
在前端顯示視頻流中,可以通過(guò)調(diào)整img標(biāo)簽的屬性來(lái)調(diào)整界面顯示位置,顯示大小。所以在進(jìn)行視頻流前后端傳輸中,在保證視頻顯示清晰度的情況下,建議使用前端來(lái)調(diào)整界面大小。
調(diào)整界面前端顯示視頻樣式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基于OpenCv+Django的網(wǎng)絡(luò)實(shí)時(shí)視頻流傳輸(前后端分離)</title>
<style>
#video {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<!-- 顯示視頻流 -->
<div align="center"><img src="http://127.0.0.1:8000/api/cv/display" id="video"></div>
</body>
</html>
顯示結(jié)果:
視頻流傳輸優(yōu)化
在項(xiàng)目中,我們可能經(jīng)常需要對(duì)多個(gè)相機(jī)進(jìn)行處理,而不是對(duì)一個(gè)相機(jī)進(jìn)行操作,所以我們可以使用相機(jī)工廠來(lái)獲取相機(jī)。在實(shí)例化相機(jī)后,需要開啟一個(gè)線程,及時(shí)更新緩存隊(duì)列,確保OpenCv不會(huì)因?yàn)榫彺孢^(guò)多而造成緩存區(qū)堵塞,界面延遲。
- 使用線程實(shí)時(shí)讀取OpenCv的內(nèi)容到隊(duì)列中
- 使用相機(jī)工廠來(lái)獲取相機(jī)
在示例代碼中,camera_model為自定義model,其中代碼需要用到的數(shù)據(jù)有數(shù)據(jù)表記錄的唯一標(biāo)識(shí)id,相機(jī)的訪問(wèn)api:camera_api
相機(jī)類:
import queue
import threading
import cv2
from apps.device.models import Camera
class CameraException(Exception):
message = None
# 初始化異常
def __init__(self, message: str):
# 初始化異常,定位異常信息描述
self.message = message
def __str__(self):
return self.message
class BaseCamera:
# 相機(jī)操作對(duì)象
cam = None
# 保存每一幀從rtsp流中讀取到的畫面,使用opencv讀取,為BGR圖片
queue_image = queue.Queue(maxsize=10)
# 后臺(tái)取幀線程
thread = None
# 相機(jī)Model
camera_model = None
# 相機(jī)基類
def __init__(self, camera_model: Camera):
"""
使用rtsp流初始化相機(jī)參數(shù)
rtsp格式:rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream
username: 用戶名。例如admin。
password: 密碼。例如12345。
ip: 為設(shè)備IP。例如 192.0.0.64。
port: 端口號(hào)默認(rèn)為554,若為默認(rèn)可不填寫。
codec:有h264、MPEG-4、mpeg4這幾種。
channel: 通道號(hào),起始為1。例如通道1,則為ch1。
subtype: 碼流類型,主碼流為main,輔碼流為sub。
"""
self.cam = cv2.VideoCapture(camera_model.camera_api)
if self.cam.isOpened():
# 相機(jī)打開成功,啟動(dòng)線程讀取數(shù)據(jù)
self.thread = threading.Thread(target=self._thread, daemon=True)
self.thread.start()
else:
# 打開失敗,相機(jī)流錯(cuò)誤
raise CameraException("視頻流接口訪問(wèn)失敗")
def _thread(self):
"""
相機(jī)后臺(tái)進(jìn)程,持續(xù)讀取相機(jī)
opencv讀取時(shí)會(huì)將信息存儲(chǔ)到緩存區(qū)里,處理速度小于緩存區(qū)速度,會(huì)導(dǎo)致資源積累
"""
# 線程一直讀取視頻流,將最新的視頻流存在隊(duì)列中
while self.cam.isOpened():
ret, img = self.cam.read()
if not ret or img is None:
# 讀取相機(jī)失敗
pass
else:
# 讀取內(nèi)容成功,將數(shù)據(jù)存放在緩存區(qū)
if self.queue_image.full():
# 隊(duì)列滿,隊(duì)頭出隊(duì)
self.queue_image.get()
# 隊(duì)尾添加數(shù)據(jù)
self.queue_image.put(img)
else:
# 隊(duì)尾添加數(shù)據(jù)
self.queue_image.put(img)
# 直接讀取圖片
def read(self):
"""
直接讀取從rtsp流中獲取到的圖片,不進(jìn)行額外加工
可能為空,需做判空處理
"""
return self.queue_image.get()
# 讀取視頻幀
def get_frame(self):
"""
獲取加工后的圖片,可以直接返回給前端顯示
"""
img = self.queue_image.get()
if img is None:
return None
else:
# 壓縮圖片,否則圖片過(guò)大,編碼效率慢,視頻延遲過(guò)高
img = cv2.resize(img, (0, 0), fx=0.25, fy=0.25)
# 對(duì)圖片進(jìn)行編碼
ret, jpeg = cv2.imencode('.jpeg', img)
return jpeg.tobytes()
class CameraFactory:
"""
相機(jī)工廠
"""
# 存儲(chǔ)實(shí)例化的所有相機(jī)
cameras = {}
@classmethod
def get_camera(cls, camera_id: int):
# 通過(guò)相機(jī)id獲取相機(jī)
camera = cls.cameras.get(camera_id)
if camera is None:
# 查看是否存在相機(jī),存在訪問(wèn)
try:
camera_model = Camera.objects.get(id=camera_id)
base_camera = BaseCamera(camera_model=camera_model)
if base_camera is not None:
cls.cameras.setdefault(camera_id, base_camera)
return cls.cameras.get(camera_id)
else:
return None
except Camera.DoesNotExist:
# 相機(jī)不存在
return None
except CameraException:
# 相機(jī)實(shí)例失敗
return None
else:
# 存在相機(jī),直接返回
return camera
Django views.py:
from django.http import StreamingHttpResponse
from apps.device.Camera import CameraFactory, BaseCamera
def gen_display(camera: BaseCamera):
"""
視頻流生成器功能。
"""
while True:
# 讀取圖片
frame = camera.get_frame()
if frame is not None:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
def video(request):
"""
視頻流路由。將其放入img標(biāo)記的src屬性中。
例如:<img src='https://ip:port/uri' >
"""
# 視頻流相機(jī)對(duì)象
camera_id = request.GET.get('camera_id')
camera: BaseCamera = CameraFactory.get_camera(camera_id)
# 使用流傳輸傳輸視頻流
return StreamingHttpResponse(gen_display(camera), content_type='multipart/x-mixed-replace; boundary=frame')
本方案存在的問(wèn)題及解決方向
鑒于本文對(duì)大家的幫助,特針對(duì)評(píng)論區(qū)一些熱點(diǎn)問(wèn)題和依照本文實(shí)現(xiàn)的方法存在的問(wèn)題提出描述,供大家探討(本方案是之前在做與視覺(jué)有關(guān)領(lǐng)域的項(xiàng)目時(shí)寫的,目前博主已不做這個(gè)方向,也沒(méi)有對(duì)相關(guān)領(lǐng)域進(jìn)行深入研究,所以博主僅對(duì)解決方向提供一些個(gè)人的見(jiàn)解。存在的問(wèn)題和后續(xù)的解決方案可自行探索)
公網(wǎng)無(wú)法訪問(wèn)
本方案是通過(guò)rtsp協(xié)議讀取相機(jī)內(nèi)容,若將服務(wù)放在公網(wǎng)上,公網(wǎng)無(wú)法訪問(wèn)本地的相機(jī),自然無(wú)法讀取數(shù)據(jù)。針對(duì)公網(wǎng)無(wú)法訪問(wèn),主要的解決思路是使公網(wǎng)可以獲取到相關(guān)的數(shù)據(jù)。
- 考慮向運(yùn)營(yíng)商獲取公網(wǎng)IP,將視頻讀取任務(wù)單獨(dú)分離出來(lái)(減少運(yùn)維成本),通過(guò)本地公網(wǎng)IP讀取視頻內(nèi)容
- 若無(wú)法獲取IPv4,可退而求其次考慮IPv6,在使用IPv6時(shí),請(qǐng)測(cè)試其對(duì)現(xiàn)有設(shè)備的兼容性
- 采用推流的方式,將本地抓取到的流視頻推送到公網(wǎng),然后公網(wǎng)再拉流讀取視頻內(nèi)容(推薦)
畫面混合問(wèn)題
在本方案中,為更好的使用多相機(jī)展示畫面,采用了單例模式并將所有的相機(jī)存儲(chǔ)在字典里面方便用戶直接根據(jù)相機(jī)的標(biāo)識(shí)獲取相機(jī)。若出現(xiàn)畫面混合問(wèn)題,檢查代碼,是否在存儲(chǔ)相機(jī)的字典相關(guān)的結(jié)構(gòu)中,將數(shù)據(jù)混合在一塊,導(dǎo)致混合輸出。檢查代碼,請(qǐng)仔細(xì)確定被一個(gè)相機(jī)的數(shù)據(jù)流流向,確保不同相機(jī)的數(shù)據(jù)流不發(fā)生混合。
沒(méi)有apps.device.models.Camera類
Camera類在這里主要對(duì)相機(jī)進(jìn)行管理,主要是存儲(chǔ)了相機(jī)的rtsp協(xié)議流,關(guān)于rtsp協(xié)議的讀取和相關(guān)內(nèi)容參考:基于OpenCv的視頻流處理方法,具體代碼,不予提供(懶的找了)
本方案的改進(jìn)策略
在做本方案相關(guān)需求時(shí),考慮到僅是通過(guò)前后端到視頻傳輸?shù)綄?shí)現(xiàn),未考慮到效率因素。
假設(shè)在視頻傳輸中硬件條件如下:
- 視頻圖像文件在傳輸時(shí)一般有3個(gè)通道
- 像素點(diǎn)在存儲(chǔ)時(shí)以8bit(1B)的大小進(jìn)行存儲(chǔ)
- 相機(jī)到分辨率為1920x1080
- 相機(jī)每秒采樣30幀
按照以上的數(shù)據(jù)計(jì)算的話,那么每一秒需要傳輸?shù)臄?shù)據(jù)有3 x 1 x 1920 x 1080 x 30,在不考慮壓縮的優(yōu)化的情況下,每一秒所傳的數(shù)據(jù)是海量的。在本方案的探索過(guò)程中,未進(jìn)行任何壓縮的情況下,在本機(jī)上前端讀取都存在著3~4秒的延遲。更何況是將服務(wù)部署到同一網(wǎng)段甚至公網(wǎng)上。 所以本方案只是提供一個(gè)前后端分離到視頻顯示傳輸?shù)教剿鳌?br> 關(guān)于本方案存在的問(wèn)題,目前可想到的改進(jìn)方案有:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-474996.html
- 對(duì)視頻進(jìn)行下采樣(同比咧縮?。?,在普通的監(jiān)控場(chǎng)景下不需要有較高的分辨率需求,可以考慮將分辨率縮小
- 采用相關(guān)的視頻壓縮算法對(duì)視頻進(jìn)行壓縮,如h264、h265、mpeg-4、hevc等方法
- 將視頻轉(zhuǎn)換為常見(jiàn)的視頻格式進(jìn)行硬件解碼播放
- 考慮到在視頻監(jiān)控等特殊場(chǎng)景,一般的視頻畫面之間變化不大,是否可以采用差幀法的方式
- 當(dāng)未檢測(cè)到界面變化的情況下,不進(jìn)行幀傳輸
- 在界面變化較小的情況下,只傳輸部分變化幀
由于博主本人并未深入了解相關(guān)領(lǐng)域,以上的各個(gè)解決方案只是個(gè)人的一些思路和想法,并未得到有效驗(yàn)證,歡迎大家就相關(guān)方向進(jìn)行討論分享文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-474996.html
到了這里,關(guān)于基于OpenCv+Django的網(wǎng)絡(luò)實(shí)時(shí)視頻流傳輸(前后端分離)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!