搜尋此網誌

2025年7月31日 星期四

Raspberry pi PyTorch 影像識別 (二)

緣起:


    接續這篇雖然我在文章最後說我停手了,但後面其實還是有改了些東西,也有把程式搬到我那台裝 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 來操作。


    那天晚上,我跟我爸借了電動工具,找好位置,把監視器鎖在牆上


    在安裝前有做勘查,發現電錶旁就有插座,這個位置非常的棒,就算下大雨,屋簷也能擋下那些雨水。


    wifi 真的方便,只要訊號能涵蓋到就行,省了拉網路線的功夫。


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 小時的監控功能就完成了,這台監視器到晚上時還會自動切成夜間紅外線模式。我前幾天有成功靠這個裝置逮到那隻又想偷大便的賤狗。我當時在看書,聽到警報後,我確認螢幕,看到它鬼鬼祟祟的在我們庭院前面徘徊,我馬上走出門,拿拖鞋丟他,把它給趕跑,送啦,再偷偷過來大便看看啊,臭狗。

沒有留言:

張貼留言