緣起:
接續這篇,雖然我在文章最後說我停手了,但後面其實還是有改了些東西,也有把程式搬到我那台裝
ssd 的 pi 5
上跑,那執行的速度確實快上了不少,而且我們神明廳就算加了鐵網,也還是阻止不了那臭狗在我們庭院前的其它地方大便,所以此專案還是有繼續執行的必要。
我那時是直接把 pi 跟螢幕搬到門口附近,然後用膠帶把 usb
網路攝影機黏在鐵椅上,放到外面。超蠢的,攝影機到晚上時就沒作用了,一片黑;下雨時也是要把它收進來,每天重新放回去又要在那邊橋角度;東西擺門口也很擋路;當蜂鳴器響起時我還是要離開位置跑到門口查看.....。幾天後就懶得再每天手動擺那些東西了,所以最後還是沒起到什麼作用啊
!!!
我上禮拜真的就受不了,直接跑去外面買了台室外用的 wifi
監視器,果然還是得用專業的器具才能確實的解決我的問題。
Tapo C310:
在跑出門前有去查,有確認那些 wifi 的監視器大部份都能開啟 rtsp
的功能,這代表我能使用它官方軟體外的渠道來擷取影像,提供的很大的彈性,這對需要自己客製功能的我來說是很重要的一件事。我跑去燦坤買的,那邊有一個櫃子都是這類的
wifi 監視器,哇嗚,只要 999 就能買到功能這麼好的監視器,真棒 !!
買回家拆開後才想到,哎呀,tp-link 這公司不是跟共匪有關嗎 ?
算了,錢都花了,反正我只在區網內使用,問題應該不大
(?)。讓我覺得很煩的是,我要調整初始設定還要載它的
app,然後還要建帳號,我在添加設備時它還要我開啟位置,乾,好噁啊。我只有在初次使用,給它韌體做更新時連上我手機的
wifi,之後就都只連為它準備的 wifi 分享器,完全在區網內作業。
在 app 上設定好 C310 要自動連上的 wifi,還有啟用它的 rtsp
跟 ONVIF 功能後,我就盡量不
去使用它的 app 了。那個 ONVIF
是我在的學習過程中認識的一個標準,它讓監控設備的設定調整變得更容易整合,有了它,我就不需要再透過
app
來調整監視器的設定,雖然不是什麼功能都能調整,但對我來說很夠用了,Windows
上可以載 ODM 來操作。
include-system-site-packages:
在重新調整程式、測試執行時,我發現一個問題,python venv
執行的程式無法存取 pi 的 GPIO,怪了,上次跑的時候可以,怎麼這次就不行 ?
既虛擬環境調用 RPi.GPIO
程式庫會有問題,那是不是該換調用系統的 RPi.GPIO 程式庫 ? 去查了資料後得知,你可以編輯你 Python 虛擬環境裡的 pyvenv.cfg 檔,把
include-system-site-packages 那行改成
include-system-site-packages=true
這樣你 python 虛擬環境的程式就能存取安裝在你本系統的 python
程式庫,我再來把虛擬環境的 RPi.GPIO
程式庫砍掉,再執行程式後,它就正常運行了,所以真的是虛擬環境無法存取 GPIO
的問題。
雖然我後來不是用 pi 的 GPIO
來控制蜂鳴器,但這個雷點還是紀錄個。
Python OpenCV Rtsp:
用 python 的 opencv 程式庫擷取 Rtsp
的串流影像超簡單的,改一下傳入 cv2.VideoCapture 的字串就行了,改成
rtsp://{帳號}:{密碼}@{ip位置}:554/stream1
監視器傳回的影像,左上角有時間,這就讓我發現一個問題,就是程式當下截取到的畫面一定會落後當前實際畫面。就算沒執行影像識別的程式碼,落後的情況還是會存在,而且會愈來愈嚴重,這是個大問題,沒辦法即時識別到狗子的話,我這專案就沒有用了。
我程式的主架構簡單來說是這樣
我一直以為,我在用 cv2.VideoCapture 類別的 capture 擷取
rtsp 來源的影像時,在我程式架構下它會這樣運作
後來去請教 GPT 大神,它給我這樣的回覆
GPT 也有給我一個解法,寫了一個 VideoStream 類別,開 Thread
不斷從 rtsp
讀取最新的影像,等到主程式需要影像時,再回傳當前影像。使用這個類別來取得
rtsp 的影像後,就沒有延遲的問題了
class VideoStream:
def __init__(self, src):
self.capture = cv2.VideoCapture(src)
self.src=src
self.ret, self.frame = self.capture.read()
self.running = True
self.lock = threading.Lock()
threading.Thread(target=self.update, daemon=True).start()
def update(self):
while self.running:
ret, frame = self.capture.read()
if not ret:
continue
with self.lock:
self.ret = ret
self.frame = frame
def read(self):
with self.lock:
return self.ret, self.frame.copy()
def stop(self):
self.running = False
self.capture.release()
完整程式:
超醜程式碼警告,當初就只想著趕快做出來,所以只依最簡單最直接想得到的方式來把程式拼湊出來
class VideoStream:
def __init__(self, src):
self.capture = cv2.VideoCapture(src)
self.src=src
self.ret, self.frame = self.capture.read()
self.running = True
self.lock = threading.Lock()
threading.Thread(target=self.update, daemon=True).start()
def update(self):
while self.running:
ret, frame = self.capture.read()
if not ret:
continue
with self.lock:
self.ret = ret
self.frame = frame
def read(self):
with self.lock:
return self.ret, self.frame.copy()
def stop(self):
self.running = False
self.capture.release()
if __name__ == '__main__':
import sys
import torch
import cv2
import time
import threading
import RPi.GPIO as GPIO
import warnings
from datetime import datetime
import serial
warnings.filterwarnings('ignore')
arduino_serial = serial.Serial('/dev/ttyUSB0', 9600)
latest_reconnect_time = datetime.now()
rtsp_url='rtsp://{帳號}:{密碼}@{ip位置}:554/stream1'
model = torch.hub.load('ultralytics/yolov5', 'yolov5n', pretrained=True)
stream=VideoStream(rtsp_url)
acc_time=0
buzz_time=0
while True:
time.sleep(0.05)
if buzz_time > 0:
buzz_time-=1
if buzz_time == 0:
arduino_serial.write('0'.encode('utf-8'))
else:
acc_time+=1
ret, frame=stream.read()
frame_h, frame_w, _ = frame.shape
if acc_time >= 20 and buzz_time < 1:
acc_time=0
results=model(frame)
labels = results.pandas().xyxy[0]['name'].values
if 'dog' in labels:
arduino_serial.write('1'.encode('utf-8'))
buzz_time=30
resized = cv2.resize(frame, (1920, 1080), interpolation=cv2.INTER_AREA)
cv2.imshow('cam', resized)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
stream.stop()
37 行 : 執行時,pytorch 會一直印警告,說某些方法已經適用還什麼的,很煩,所以加這個給它消音。
39 行 : 電子元件的控制,我後來就交給 arduino 來做,因為那個 Raspberry pi 官方的 SSD Hat 不知有啥毛病,裝上之後,那些 GPIO 孔是沒辦法用杜邦線公頭插入的。arduino 直接用 usb 線接到 pi 上,序列通訊, Arduino 那邊收到 1 就向蜂鳴器輸電。
44、45 行 : 這兩個數字是紀錄幾個週期用,while 裡的 sleep 是 0.05 秒,也就是每 0.05 秒一個週期,每個週期會擷取影像並更新 window 畫面。為了減少 cpu 跟 gpu 負荷,所以每 20 輪才做一次影像識別 (那個 acc_time變數名取得很不好)。然後當識別到狗子時,向 arduino 發送訊號,啟動蜂鳴器,持續 30 個週期後再發送訊號,關閉蜂鳴器。
69 行 : 我監視器是抓 2K 畫質的畫面,直接在 1080 p 的螢幕上呈現的話會太大,所以在 imshow 前調整它的大小。
哦對了,這邊再提一件事,原本 Pi 是 Wifi 連接 Wifi 分享器的,但後來發現每次跑個 30 分鐘左右,rtsp 就斷線,那時還以為是監視器有休眠還什麼的問題,測了好一段時間後才發現,原來就只是 Pi 的 Wifi 連線很不穩,用網路線連接後就沒問題了。
至此,這個 24 小時的監控功能就完成了,這台監視器到晚上時還會自動切成夜間紅外線模式。我前幾天有成功靠這個裝置逮到那隻又想偷大便的賤狗。我當時在看書,聽到警報後,我確認螢幕,看到它鬼鬼祟祟的在我們庭院前面徘徊,我馬上走出門,拿拖鞋丟他,把它給趕跑,送啦,再偷偷過來大便看看啊,臭狗。