徒然なる日々を送るソフトウェアデベロッパーの記録(2)

技術上思ったことや感じたことを気ままに記録していくブログです。さくらから移設しました。

Raspberry Pi2 で監視カメラ

単に監視カメラとして使うなら motion をインストールして使えば言い訳ですが、車輪の再発明をしてみる。

基本的なアイデアは以下の通り。

  1. 背景が動かない時は何もしない。
  2. 背景に変化があったらその瞬間の画像を保存する。背景が動いているかどうかはグレー画像の差分画像のヒストグラムをとってみて、交流成分(つまり0から遠い成分)が一定のスレッショルドを超えるかどうかで判定する。
  3. 一度画像変化を検出したら連続して似たような画像を検出しないよう、一定の時間変化を検出しないようにする。

プログラムは以下のようになりました。

/**
 * 変化検知型ビデオ画像保存プログラム: liveCamera.cc
 * 2015 by minosys.com
 */
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>

// cv 名前空間をデフォルトとして使用
using namespace cv;

// パラメータ値
int MorIterations = 3; // MORPH_OPEN を実行する回数
int HistoBinWidth = 256; // ヒストグラムのビンの幅; 8ビット画像に合わせている
int ThresholdBin = 10; // 色変化がこれ以上だった領域を計測する
float Threshold = 0.1; // 色変化を観測した領域の大きさが全体に比べてこの値よりも大きかったら背景が変化したものとみなす
int Interval = 5; // ガードタイム; 変化を検出したらこの時間(秒)は検出しない
char *imageFileName = 0; // 背景が変化した時の画像を保存するファイル名。設定がない時は保存しない

/*
 * メインロジック
 */
int main(int argc, char **argv) {
  int opt;
  // 引数の解析
  while ((opt = getopt(argc, argv, "m:b:t:i:f:h")) != -1) {
    switch (opt) {
    case 'b':
      ThresholdBin = atoi(optarg);
      break;
    case 't':
      Threshold = atof(optarg);
      break;
    case 'i':
      Interval = atof(optarg);
      break;
    case 'm':
      MorIterations = atoi(optarg);
      break;
    case 'f':
      imageFileName = optarg;
      break;
    case 'h':
      std::cout << "usage: liveCamera -b <ThresholdBin> -t <Threshold> -i <Intervfal> -m <Morphology iterations> -f <save file>" << std::endl;
      return 0;
    }
  }

  // ビデオをオープンする
  VideoCapture cap(0);
  if (!cap.isOpened()) {
    std::cout << "failed to open camera" << std::endl;
    return 1;
  }
  cap.set(CV_CAP_PROP_FRAME_WIDTH, 320);
  cap.set(CV_CAP_PROP_FRAME_HEIGHT, 240);
  namedWindow("Detected", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
  namedWindow("Live Image", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);

  // モルフォロジーカーネルの作成
  Mat kernel = getStructuringElement(MORPH_RECT, Size(1, 1));
  
  // 1つ前の画像
  Mat lastImage;
  bool bHasLastImage = false;
  time_t lastSaveTime = time(0);

  while(1) {
    Mat frame;
    cap >> frame;

    // グレースケール化
    Mat gray;
    cvtColor(frame, gray, CV_RGB2GRAY);

    if (bHasLastImage) {
      // 画像の差分を求める
      Mat imgDiff;
      absdiff(lastImage, gray, imgDiff);

      // ノイズ成分の除去
      Mat imageDenoise;
      morphologyEx(imgDiff, imageDenoise, MORPH_OPEN, kernel, Point(-1,-1), MorIterations);

      // ヒストグラムを求める
      Mat histo;
      const int histSize[] = { HistoBinWidth };
      const float range1[] = { 0, 256 };
      const float *ranges[] = { range1 };

      const int channels[] = { 0 };
      calcHist(&imageDenoise, 1, channels, Mat(), histo, 1, histSize, ranges, true, false);

      // ガードタイム経過すれば動画像の変化を検出する
      time_t currentTime = time(0);
      if (currentTime - lastSaveTime > Interval) {
        // ヒストグラムの総和およびスレッショルド以上の和を求める
        float accum = 0.0;
        float part = 0.0;
        for (int i = 0; i < HistoBinWidth; ++i) {
          float h = histo.at<float>(i);
          accum += h;
          if (i >= ThresholdBin) {
            part += h;
          }
        }

        // スレッショルド以上画像が変化したらウィンドウに書き出す
        if (accum > 0.0 && part / accum > Threshold) {
          struct tm *lt = localtime(&currentTime);
          char s[128];
          strftime(s, sizeof(s), "%F %T", lt);

          Mat tmpImage = frame.clone();
          putText(tmpImage, s, Point(50, 30), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 100, 200), 2, CV_AA);
          imshow("Detected", tmpImage);
          if (imageFileName) {
            imwrite(imageFileName, frame);
          }
          std::cout << "Motion Detected at " << s << std::endl;
          lastSaveTime = currentTime;
        }
      }
    }

    imshow("Live Image", frame);
    if (waitKey(100) > 0) {
      break;
    }

    // 現在の画像を保存
    lastImage = gray.clone();
    bHasLastImage = true;
  }
  return 0;
}

カメラ自体は 24 fps で動作しますが、raspberry pi 2 ではきつそうだったので 10 fps に落としました。まあ、監視用ならこの程度でも十分かと。CPU パワーは意外に食いませんが、メモリは100メガくらい持っていきます。top を見る限り DRAM の空きは通常 800M 以上あるので問題ありません。

昨日構築したクロス環境で開発しましたが、問題なく動作しています。