Python 與 OpenCV 實作移動偵測程式教學,打造智慧型監視器

這裡示範如何使用 Python 與 OpenCV 實作移動偵測程式,分析攝影機影片,自動挑選出有物體移動的畫面。

OpenCV 是一個很好用的影像處理函式庫,裡面有非常多在影像處理上常會用到的工具函數,我們只需要拿幾個簡單的函數組合起來,就可以打造一個效果還不錯的智慧型監視器,自動且即時的分析攝影機的影像,當偵測到有人或物體在移動時,讓程式自動觸發某些動作(例如送出通知的 Email 等),以下是幾個簡單的實作範例。

攝影機畫面移動偵測

我們先示範從網路攝影機擷取即時的畫面,偵測畫面中移動的物體,偵測物體的方法是先以移動平均(moving average)演算法計算出背景影像,然後將每幅影格都跟這幅背景影像比較,只要有差異的話,就判斷是有物體在移動。

而在影像處理的過程,有加入一些模糊化、擴張、縮減等處理,目的就是要減少雜訊干擾、突顯主體,這些步驟與其中的參數都要自己調整。以下是完整的程式碼:

import cv2
import numpy as np

# 開啟網路攝影機
cap = cv2.VideoCapture(0)

# 設定影像尺寸
width = 1280
height = 960

# 設定擷取影像的尺寸大小
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)

# 計算畫面面積
area = width * height

# 初始化平均影像
ret, frame = cap.read()
avg = cv2.blur(frame, (4, 4))
avg_float = np.float32(avg)

while(cap.isOpened()):
  # 讀取一幅影格
  ret, frame = cap.read()

  # 若讀取至影片結尾,則跳出
  if ret == False:
    break

  # 模糊處理
  blur = cv2.blur(frame, (4, 4))

  # 計算目前影格與平均影像的差異值
  diff = cv2.absdiff(avg, blur)

  # 將圖片轉為灰階
  gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)

  # 篩選出變動程度大於門檻值的區域
  ret, thresh = cv2.threshold(gray, 25, 255, cv2.THRESH_BINARY)

  # 使用型態轉換函數去除雜訊
  kernel = np.ones((5, 5), np.uint8)
  thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
  thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)

  # 產生等高線
  cntImg, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

  for c in cnts:
    # 忽略太小的區域
    if cv2.contourArea(c) < 2500:
      continue

    # 偵測到物體,可以自己加上處理的程式碼在這裡...

    # 計算等高線的外框範圍
    (x, y, w, h) = cv2.boundingRect(c)

    # 畫出外框
    cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

  # 畫出等高線(除錯用)
  cv2.drawContours(frame, cnts, -1, (0, 255, 255), 2)

  # 顯示偵測結果影像
  cv2.imshow('frame', frame)

  if cv2.waitKey(1) & 0xFF == ord('q'):
    break

  # 更新平均影像
  cv2.accumulateWeighted(blur, avg_float, 0.01)
  avg = cv2.convertScaleAbs(avg_float)

cap.release()
cv2.destroyAllWindows()

這個程式執行之後,就會開啟一個視窗,顯示即時的物體偵測結果。

移動偵測程式

儲存畫面

如果想要把偵測的結果儲存下來,可以參考 OpenCV 的圖片寫入OpenCV 影片寫入教學,把想要紀錄的畫面儲存下來。

以下示範把偵測的結果寫入影片檔。在一開始的地方先設定輸出檔案:

# 使用 XVID 編碼
fourcc = cv2.VideoWriter_fourcc(*'XVID')

# 建立 VideoWriter 物件,輸出影片至 output.avi
out = cv2.VideoWriter('output.avi', fourcc, 16.0, (width, height))

然後在顯示偵測結果影像的地方,把結果寫入檔案:

# 寫入影格
out.write(frame)

這樣就可以產生偵測結果的影片了。

若只想要儲存有物體移動的畫面,只要把輸出影片的程式碼移動到有偵測到物體的地方就可以了,上面這段程式碼只要稍加修改,就可以變化出許多應用,例如當偵測到物體移動時,送出 Email 通知管理者等。

快速篩選監視器影像

現在許多社區或大樓都會裝設監視攝影機,若發生特別的事件時(例如遭小偷)就可以調閱這些攝影機的影片,協助警方辦案,而這類的攝影機所錄製下來的影片中,通常大部分都是沒有人的畫面,可能一小時中只有幾十秒是真的有拍到人,其餘都是空的。

最常遇到的問題就是如何在這些大量監視攝影機的影片中,找出有人或是物體移動的畫面,在以前這種狀況是真的用人眼把上百小時的影片看完,非常費時又費力。

若遇到這樣的狀況,就可以使用 Python 與 OpenCV 幫助我們快速篩選出有物體在移動的畫面,先把空拍的部份去除,用這種方式來分析影片,可以省下非常大量的時間。

以下是用來篩選大量監視器影像的指令稿,其實整個大架構跟上面的程式差不多,只是畫面來源改從影片檔案讀取,而將有變動的畫面輸出成為單張的圖檔,沒有變動的部分就直接捨棄。

import cv2
import numpy as np
import os

# 影片檔案
videoFile = "my_video.m4v"

# 輸出目錄
outputFolder = "my_output"

# 自動建立目錄
if not os.path.exists(outputFolder):
  os.makedirs(outputFolder)

# 開啟影片檔
cap = cv2.VideoCapture(videoFile)

# 取得畫面尺寸
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)

# 計算畫面面積
area = width * height

# 初始化平均畫面
ret, frame = cap.read()
avg = cv2.blur(frame, (4, 4))
avg_float = np.float32(avg)

# 輸出圖檔用的計數器
outputCounter = 0

while(cap.isOpened()):
  # 讀取一幅影格
  ret, frame = cap.read()

  # 若讀取至影片結尾,則跳出
  if ret == False:
    break

  # 模糊處理
  blur = cv2.blur(frame, (4, 4))

  # 計算目前影格與平均影像的差異值
  diff = cv2.absdiff(avg, blur)

  # 將圖片轉為灰階
  gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)

  # 篩選出變動程度大於門檻值的區域
  ret, thresh = cv2.threshold(gray, 25, 255, cv2.THRESH_BINARY)

  # 使用型態轉換函數去除雜訊
  kernel = np.ones((5, 5), np.uint8)
  thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
  thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)

  # 產生等高線
  cntImg, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

  hasMotion = False
  for c in cnts:
    # 忽略太小的區域
    if cv2.contourArea(c) < 2500:
      continue

    hasMotion = True

    # 計算等高線的外框範圍
    (x, y, w, h) = cv2.boundingRect(c)

    # 畫出外框
    cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

  if hasMotion:
    # 儲存有變動的影像
    cv2.imwrite("%s/output_%04d.jpg" % (outputFolder, outputCounter), frame)
    outputCounter += 1

  # 更新平均影像
  cv2.accumulateWeighted(blur, avg_float, 0.01)
  avg = cv2.convertScaleAbs(avg_float)

cap.release()

將監視攝影機的影片輸入進去之後,就會自動篩選出有偵測到物體移動的畫面,輸出至指定的目錄,讓程式先篩選過之後,人再來看的話,就會方便很多。

篩選過的影像

參考資料:pyimagesearchRobin DavidMatthias Stein

程式設計

15 留言

  1. 新手~学习起来~~~~~

  2. Kevin

    存好的影片出現 0xc10100be 無法撥放此檔案,有甚麼解決的辦法嗎

  3. Proms

    謝謝您的教學!
    不知是版本的關係,還是其它原因?

    我需要把以下這行的 cntImg, cnts, _ 改成 cnts, cntImg 才能讓程式執行

    cntImg, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    • Kai

      是版本差異,我使用OpenCV 2.4,保留 cnts, _ 才可跑這程式窩~~

    • allen

      cv2有再更新版本,要去官網看就知道改哪了~

  4. Ken

    run程式的時候出現
    error: OpenCV(4.1.0) C:\projects\opencv-python\opencv\modules\core\src\matrix.cpp:757: error: (-215:Assertion failed) dims 0 in function ‘cv::Mat::locateROI’
    是版本不符嗎?

    • fox

      我也是不知道是版本的關係,還是其它原因?
      需要把cap = cv2.VideoCapture(0)換成

      camera_number = 0
      cap = cv2.VideoCapture( camera_number + cv2.CAP_DSHOW)

      才能執行

      • Show

        不好意思我改了跟您@fox一樣卻無法執行,也是跑出跟 @Ken大大一樣,是不是還缺了什麼?!

  5. fox

    Proms
    謝謝您的教學!
    不知是版本的關係,還是其它原因?

    我需要把以下這行的 cntImg, cnts, _ 改成 cnts, cntImg 才能讓程式執行

    cntImg, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    這個也要改喔!

  6. Sam

    謝謝您的教學!
    但是我有一個疑問,當我執行此代碼時出現了錯誤 cntImg, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    ValueError: not enough values to unpack (expected 3, got 2)

  7. Roy

    請問有完整的原始碼檔案嗎?
    還踏入不了Python,無法執行,
    有心想學,但是力不從心。

    • disconct

      import cv2
      import numpy as np

      # 開啟網路攝影機
      cap = cv2.VideoCapture(0)

      # 設定影像尺寸
      width = 1280
      height = 960

      # 設定擷取影像的尺寸大小
      cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
      cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)

      # 計算畫面面積
      area = width * height

      # 初始化平均影像
      ret, frame = cap.read()
      avg = cv2.blur(frame, (4, 4))
      avg_float = np.float32(avg)

      while(cap.isOpened()):
      # 讀取一幅影格
      ret, frame = cap.read()

      # 若讀取至影片結尾,則跳出
      if ret == False:
      break

      # 模糊處理
      blur = cv2.blur(frame, (4, 4))

      # 計算目前影格與平均影像的差異值
      diff = cv2.absdiff(avg, blur)

      # 將圖片轉為灰階
      gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)

      # 篩選出變動程度大於門檻值的區域
      ret, thresh = cv2.threshold(gray, 25, 255, cv2.THRESH_BINARY)

      # 使用型態轉換函數去除雜訊
      kernel = np.ones((5, 5), np.uint8)
      thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
      thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)

      # 產生等高線
      cntimg, cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

      for c in cntimg:
      # 忽略太小的區域
      if cv2.contourArea(c) < 2500:
      continue

      # 偵測到物體,可以自己加上處理的程式碼在這裡…

      # 計算等高線的外框範圍
      (x, y, w, h) = cv2.boundingRect(c)

      # 畫出外框
      cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

      # 畫出等高線(除錯用)
      cv2.drawContours(frame, cntimg, -1, (0, 255, 255), 2)

      # 顯示偵測結果影像
      cv2.imshow('frame', frame)

      if cv2.waitKey(1) == 27:
      break

      # 更新平均影像
      cv2.accumulateWeighted(blur, avg_float, 0.01)
      avg = cv2.convertScaleAbs(avg_float)

      cap.release()
      cv2.destroyAllWindows()

  8. Renny

    您好:
    請問在”快速篩選監視器影像”可以設定成1分鐘內只截一張圖嗎?
    完全沒有學過程式,在網路搜尋到您的文章才開始使用opencv,
    但試了兩天實在試不出來一分鐘只截一張圖,所以才來麻煩您。

  9. sakura

    請問大大,
    是否可以提供資料影片檔呢?謝謝><

Comments are Closed