📷

ラズパイ×Webカメラで簡易的な「混雑率推定システム」をつくる

2022/01/27に公開

概要

過去に RaspberryPi でエッジコンピューティングに入門する記事を書いたので,今回は実際に役に立つシステムを開発してみたいと思います.

背景

未だにコロナウィルス(オミクロン株)が猛威を奮っており,なかなか接触が怖い状況が続いていますので,RaspberryPi とWebカメラを用いて簡易的な「混雑率推定のためのシステム」を開発していきます.なるべく汎用的で,手軽で,シンプルな構成にするよう心がけて開発していきます!

やること

  1. Webカメラで画像を撮影する
  2. 画像を Google の Cloud Vision API に送る
  3. Cloud Vision API で人の検出 (およびカウント) をおこなう
  4. 返ってきた結果から混雑率を計算する
  5. データベースに格納する
  6. 定期的にプログラムが実行されるようにする

システム構成

システム構成図

前提

  • 開発と並行して書いている記事です
    • 勉強しながら書いています!
  • セットアップやアカウント登録などは省きます
  • 最終的にラズパイで動かすことを目指しますが,開発段階ではMacで動作確認しながら進めています
  • RaspberryPi (および Ubuntu) のセットアップは過去の記事で書いておきました

開発環境

  • Local : macOS Monterey (12.1)
    • Node.js (v12.21.0)
    • npm (7.5.2)
  • RaspberryPi 4 Model B (RAM 4GB)
    • OS : Ubuntu Desktop ( 20.04.3 64-bit )
    • Node.js (v12.21.0)
    • npm (7.5.2)

1. Webカメラから画像を取得する

Webカメラのセッティング

まずは,Mac/RaspberryPi 共にWebカメラをプログラムから操作できるようにしておきます.RaspberryPi でWebカメラを使えるようにするためのセットアップ方法はこちらの記事に書いていますが,ざっくりと手順だけ記します.

  1. RaspberryPi にWebカメラをつなぐ
  2. ターミナルから lsusb コマンドを実行して認識されたUSB機器の中にWebカメラがあるか確認
  3. ls /dev/video* を実行しデバイスファイルがあるか確認 (dev/video0dev/video1 があれば利用可能)
  4. sudo apt install fswebcam でラズパイでWebカメラを利用するためのパッケージをインストール
    • ローカル (mac) で開発しているときには brew install imagesnap で imagesnap をを入れておく
  5. fswebcam img.jpg を実行して該当のファイル名の画像が撮影できていたら成功

プロジェクトの作成

今回は Node.js で開発を行います.Node.js や npm はインストール済みとします.

  1. 任意のディレクトリで mkdir congestion-estimation を実行し今回のプロジェクトディレクトリを作成
  2. npm initpackage.json を作成する
    • たくさん質問されますが,特に指定はないのですべて何も入力せずに return する
    • package.json が作成され中身もデフォルトの設定が入っていれば OK
  3. npm install node-webcam で Node.js からWebカメラにアクセスするためのパッケージをインストール
  4. mkdir src でソースコードを格納するディレクトリを作成
  5. src 内にプログラムを作成していきます

ディレクトリ構成

基本的なディレクトリ構成は以下のようになっています.どのファイルを編集するかは,この構成を元に説明していきます(必要なファイルだけ記述しています).

congestion-estimation
     L package.json
     L README.md
     L img.jpg (Webカメラで撮影した画像)
     L src
         L index.js (これはルーティング用なので今回使わない)
	     L edge.js (ラズパイではこのファイルを実行するだけ)
	     L config.js (認証情報など)
	     L services
	         L webcam
	             L WebcamControlService.js (Webcam操作系)
	         L ComputerVision
	             L ObjectDetectionService.js (Cloud Vision API を利用する)
	         L CRUD
	         L FirestoreService.js (DB操作系)

基本的に services 以下に必要な機能を実装し,それを edge.js から呼び出していきます.

Webカメラで写真を撮影する

まずはWebカメラの操作を行うプログラムを作成します.ファイルのの PATH は前述したディレクトリ構成を参照してください.

  • WebcamContorolService.js にWebカメラを操作するための処理を書く
  • ソースコードは公式のサンプルコードを参考にして書いていく
    • クラスに分割したかったのでいろいろ変えています
  • WebcamContorolService.js は以下
const NodeWebcam = require('node-webcam');

// Webcam 操作のためのクラス
class WebcamControlService {
  // PCに接続したWebカメラで写真を撮影する
  static captureImage() {
    // Default Options
    let opts = {
      width: 1280,
      height: 720,
      quality: 100,
      frames: 60,
      delay: 0,
      saveShots: true,
      output: 'jpeg',
      device: '/dev/video0',
      callbackReturn: 'location',
      verbose: false,
    };
    
    // Create webcam instance
    const Webcam = NodeWebcam.create(opts);
    Webcam.capture('img', function (err, data) {}); // Will automatically append location output type
    NodeWebcam.capture('img', opts, function (err, data) {}); // Also available for quick use

    // Get list of cameras
    Webcam.list(function (list) {
      // Use another device
      let anotherCam = NodeWebcam.create({ device: list[0] });
    });

    // Return type with base 64 image
    opts = {
      callbackReturn: 'base64',
    };
    NodeWebcam.capture('img', opts, function (err, data) {
      let image = "<img src='" + data + "'>";
    });

    return;
  }
}

module.exports = WebcamControlService;
  • edge.js からこのクラスを呼び出すソースコードは以下
const WebcamControlService = require('./services/webcam/WebcamControlService.js');

// Webカメラで写真を撮影する
WebcamControlService.captureImage();
  • node edge.js で実行
  • コマンドを実行した階層に img.jpg というファイルが作成されればOK
    • Ubuntu Desktop なので デスクトップから確認して画像が問題なく撮影できていることも確認できました
    • mac で実行しても同じように成功

2. Computer Vision API で画像処理

機械学習のモデルを自作するのはハードルが高いので,画像処理に関しては API を利用します.今回は Google の Cloud Vision API を採用します.

料金

クラウドサービスを利用する際にはまず料金体系を確認しておく必要があります.公式の料金ページで詳細を確認できます.

  • 料金は画像一枚ごとに発生する
    • 料金の計算は1処理を表す「1ユニット」ごと
    • 例えば,一枚の画像に「ラベル検出」と「顔検出」という2つの処理を適用すると2ユニット分の請求が発生する
  • 毎月1000ユニットは無料
  • 以降は従量課金制

Cloud Vision API の設定

Google Cloud のアカウントは事前に作成済みとします.プロジェクトの作成や課金登録もクイックスタートや「認証のスタートガイド」を参考にしながら設定します.ここでは,APIを利用するのに必要なサービスアカウントの作成手順を簡単に説明します.

  1. APIキーを作成する
  2. メニューから「APIとサービス」を選択
  3. 「認証情報」を選択
  4. 画面上部にある「認証情報を作成」を選択
  5. サービスアカウント」を選択
  6. アカウント名や権限を選択
  7. 「キー」のタブから新しく鍵を作成
  8. json 形式のファイルとしてダウンロードし任意の場所に保存する
  9. export GOOGLE_APPLICATION_CREDENTIALS="PATH/TO/JSON/File" を実行し環境変数へ
    • プロセスが終了すると環境変数は消えるので,必要な場合は .zshrc.bashrc などに追記する

実装

Cloud Vision API では Computer Vision に関する様々な機能が提供されていますが,今回は「複数のオブジェクトを検出する」のドキュメントを参考にオブジェクト検出を行います.また, API の呼び出し方法は複数ありますが,ここでは,専用のクライアントライブラリを利用して呼び出す方法を採用します.

  • npm install @google-cloud/vision で Cloud Vision API の SDK をインストール
  • services/ComputerVision 以下に ObjectDetectionService.js を作成し以下のように書く
class ObjectDetectionService {
  static async detectMultipleObject() {
    // Imports the Google Cloud client libraries
    const vision = require('@google-cloud/vision');
    const fs = require('fs');

    // Creates a client
    const client = new vision.ImageAnnotatorClient();

    // PATH to local image
    const fileName = `./img.jpg`;
    const request = {
      image: { content: fs.readFileSync(fileName) },
    };

    const [result] = await client.objectLocalization(request);
    const objects = result.localizedObjectAnnotations;
    objects.forEach((object) => {
      console.log(`Name: ${object.name}`);
      console.log(`Confidence: ${object.score}`);
      const vertices = object.boundingPoly.normalizedVertices;
      vertices.forEach((v) => console.log(`x: ${v.x}, y:${v.y}`));
    });
    return objects;
  }
}

module.exports = ObjectDetectionService;
  • これで Cloud Vision API に指定した画像を送信する処理は完成
  • edge.js にこのクラスを呼び出す処理を追記する
const WebcamControlService = require('./services/webcam/WebcamControlService.js');
const ObjectDetectionService = require('./services/ComputerVision/ObjectDetectionService.js');

// Webカメラで写真を撮影する
WebcamControlService.captureImage();

// 撮影した画像を Cloud Vision API でオブジェクト検出
ObjectDetectionService.detectMultipleObject();
  • node edge.js を実行
  • 成功するとコンソールに検知したモノと確信度などが出力されます
    • Response は json で返ってきています

4. 混雑率の計算

ここまでで画像の分析はできているので,返ってきた結果を元に混雑率を計算する処理を書いていきます.ここまでの実装では結果がコンソールに出力されるだけなので,結果を受け取って処理します.
ObjectDetectionService.js は最後に結果を return しているので以下のように edge.js 側で受け取って処理します.

const WebcamControlService = require('./services/webcam/WebcamControlService.js');
const ObjectDetectionService = require('./services/ComputerVision/ObjectDetectionService.js');

// Webカメラで写真を撮影する
WebcamControlService.captureImage();

// 撮影した画像を Cloud Vision API でオブジェクト検出
const detectObject = async () => {
  const objects = await ObjectDetectionService.callVisionAPI();
  console.log('objects: ', objects);
  return objects;
};

const detectionResult = detectObject();
console.log('detectionResult: ', detectionResult);
  • 非同期にしないと値が受け取れないので async 関数にしました
  • これを実行すると結果の json が画面に表示されます

画像内に写っている人数を取得する

続いて,この結果を用いて混雑率を計算します.この処理は edge.js にそのままの流れで書いていきます.追記した edge.js は以下です.

const WebcamControlService = require('./services/webcam/WebcamControlService.js');
const ObjectDetectionService = require('./services/ComputerVision/ObjectDetectionService.js');

// Webカメラを用いた混雑率推定
const congestionEstimation = async () => {
  // Webカメラで写真を撮影する
  await WebcamControlService.captureImage()
    .then(async (res) => {
      // VisionAPIで写真のオブジェクト検出
      await ObjectDetectionService.detectMultipleObject()
        .then((res) => {
          const objects = res;
          let objectNames = [];
          objects.forEach((object) => {
            objectNames.push(object.name);
          });

          // 画像内の人の数をカウントする
          let peopleCount = 0;
          for (let i = 0; i < objectNames.length; i++) {
            if (objectNames[i] === 'Person') {
              peopleCount++;
            }
          }
          console.log('検知した人数: ', peopleCount);
        })
        .catch((err) => {
          console.log('Error: ', err);
        });
    })
    .catch((err) => {
      console.log('Error: ', err);
    });
};

congestionEstimation();
  • 非同期で順番に処理する必要があるので async の関数にしました
  • 返り値の json では人を検知すると Person というラベルが返ってきます
  • peopleCount に格納した数が画像内に写っている人数の値です
    • 0人, 1人, 2人 と結果が変わるのも確認できました

これで返ってきた json に含まれている Person の数を表示できるようになりました.人数のカウントは成功です.

混雑率を計算する

ここまで来たら最後は混雑率を計算すればやりたいことは大体できるようになります.混雑率を計算する方法はいくつかあるようですが,ここでは鉄道の混雑率を計算する方法を使います(参考文献1).鉄道の混雑率は 輸送人員 / 輸送力 ということなのでここでは単純に 人数 / 定員 します.一つ前のソースコードでいうと peopleCount / 定員 です.肝となるのはWebカメラに写る範囲の定員を何人に設定するかですが,これはわからないので勘です.edge.js の人数をカウントする処理のあとに追記します.

          // 画像内の人の数をカウントする
          let peopleCount = 0;
          for (let i = 0; i < objectNames.length; i++) {
            if (objectNames[i] === 'Person') {
              peopleCount++;
            }
          }
          console.log('\n検知した人数: ', peopleCount);
	  const congestionDegree = peopleCount / 10; // 追加: 混雑率
          console.log('congestionDegree: ', congestionDegree);  // 追加	  
  • 混雑率が計算されれば完成!

これで画像を取得し,物体検知を行い,人数をカウントし,混雑率を計算するところまでは完成です.

5. データベースにデータを格納する

次に取得したデータをデータベースに格納します.DB にさえ入れてしまえば,フロントで表示したり,可視化したり様々なことができるので,それを見越して入れておきます.DB は FirebaseCloud Firestore を使用します.Firebase のアカウントやプロジェクトはすでに作成済みとします.

Firestore のコレクションは以下のような構造になっています.

edge (collection)
    L document (エッジデバイス単位)
        L observatoin-data (subcollection)
	    L document (観測データ単位)

Firestoreの利用設定

ここも公式ドキュメントを参考にすすめます.

  1. ローカルで必要なパッケージをインストールする
    • Firebase SDK : npm install firebase@8.10.0 --save
    • Firebase admin : npm install firebase-admin --save
  2. Firebase のプロジェクトページにいく
  3. 歯車アイコンから「ユーザと権限」を開く
  4. 「サービスアカウント」タブにいく
  5. 「新しい秘密鍵の生成」→「キーを生成」で JSON ファイルがダウンロードされる
  6. JSON ファイルをプロジェクトルートに配置
  7. 忘れずに .gitignore に指定

Firestore への書き込み

  • 秘密鍵生成のときに表示されていた以下のコードをコピーする
var admin = require("firebase-admin");
var serviceAccount = require("path/to/serviceAccountKey.json");
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});
  • FirestoreService.js に DB への書き込み処理を追加する
    • serviceAccount の json パスはどは適宜変える
    • 書き込み先はサブコレクションになっています
require('date-utils');  // 現在日時を取得するライブラリもインストールしておく
const admin = require('firebase-admin');
const serviceAccount = require('PATH/TO/JSON/FILE');
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

class FirestoreService {
  // Firestore へのデータの書き込み
  static async writeDataToFirestore(peopleCount, congestionDegree) {
    const db = admin.firestore();

    // 現在時刻を取得
    const date = new Date();
    const formattedDate = date.toFormat('YYYYMMDDHH24MISS');

    // Firestore へ書き込み
    await db
      .collection('edge')
      .doc('boTrseygojrZeVzm3oUz')
      .collection('observation-data')
      .add({
        timestamp: formattedDate,
        peopleCount: peopleCount,
        congestionDegree: congestionDegree,
      })
      .then((res) => {
        return res;
      })
      .catch((err) => {
        console.log('Error: ', err);
      });
  }
}
module.exports = FirestoreService;
  • edge.jsFirestoreService.js のメソッドを呼び出す部分を追記する
    • 以下は edge.js の完成形
const WebcamControlService = require('./services/webcam/WebcamControlService.js');
const ObjectDetectionService = require('./services/ComputerVision/ObjectDetectionService.js');
const FirestoreService = require('./services/CRUD/FirestoreService');

// Webカメラを用いた混雑度推定
const congestionEstimation = async () => {
  // Webカメラで写真を撮影する
  await WebcamControlService.captureImage()
    .then(async (res) => {
      // VisionAPIで写真のオブジェクト検出
      await ObjectDetectionService.detectMultipleObject()
        .then(async (res) => {
          const objects = res;
          let objectNames = [];
          objects.forEach((object) => {
            objectNames.push(object.name);
          });

          // 画像内の人の数をカウントする
          let peopleCount = 0;
          for (let i = 0; i < objectNames.length; i++) {
            if (objectNames[i] === 'Person') {
              peopleCount++;
            }
          }
          console.log('\n検知した人数: ', peopleCount);
          const congestionDegree = peopleCount / 10; // 混雑率
          console.log('混雑率: ', congestionDegree, '%');

          // Firestore への書き込みを実行 New!
          await FirestoreService.writeDataToFirestore(peopleCount, congestionDegree)
            .then((res) => {
              console.log('Success: ', res);
            })
            .catch((err) => {
              console.log('Error: ', err);
            });
        })
        .catch((err) => {
          console.log('Error: ', err);
        });
    })
    .catch((err) => {
      console.log('Error: ', err);
    });
};

congestionEstimation();

これでプログラムはすべて完成です.実行すると Firestore にデータが書き込まれて増えていくはずです.

result

きちんとデータが増えていきました.

6. プログラムを定期実行する

ここまでで必要なプログラムは完成しました.フィールドに設置して混雑率を測ることを想定しているので,このプログラムが定期的に実行されるようにします.

Cron の設定

cron を設定してプログラムの定期実行をします.ただし,クラウドサービスを利用しているため(あと cron の設定詳しくないので怖い),設定ミスや切り忘れなどで料金がかかってしまうことを避けるため,Linux の cron の設定ではなく Node.js の cron ライブラリである node-cron をインストールし,プログラムが動いている間だけ定期実行されるようにします.

  • npm install node-cronnode-cron をインストール
  • edje.js の最初の行に const cron = require('node-cron'); を追記し
  • 最終行を以下のように変更する
cron.schedule('1 * * * * *', () => {
  congestionEstimation();
});
  • 定期実行させる時間は任意で変更してください
  • これでプログラムを実行すると定期的に処理が走っていることが確認できるはず!

cron の書式についてはnode-cron の公式ドキュメントを参考にすればいいと思います.

7. RaspberryPi で動かす

開発はローカル (MAC) で行っていたので RaspberryPi (Ubuntu Desktop) でも同じように動作させる手順もメモします.RaspberryPi へは SSH で接続して作業を行います.Webカメラを操作するための fswebcam の設定は最初にやってあるので,それ以外のサービスアカウント部分だけ設定すればOKです.

  1. ソースコードは GitHub などに push しておく
    • RaspberryPi で git clone or git pull
  2. Cloud Vision API のキーファイル (json) をローカルから RaspberryPi に送る
    • scp key.json user@xxx.xxx.xx.xx:~/path/to/optional/directory
    • 任意のディレクトリに大切に保管する
  3. 環境変数に json ファイルまでの PATH を追加する
    • .bashrc などに export GOOGLE_APPLICATION_CREDENTIALS="/path/to/json/file を追記する (/からの絶対パス)
    • source .bashrc などで設定を反映させる
  4. Firebase のキーファイルも RaspberryPi に送る
    • scp key.json user@xxx.xxx.xx.xx:~/path/to/project/root
    • Firebase の鍵ファイルはプロジェクトのルートに配置します
  5. 必要なライブラリ,パッケージをインストールする
    • npm i
    • Firebase SDK : npm install firebase@8.10.0 --save
    • Firebase admin : npm install firebase-admin --save
  6. node ./src/edge.js で正常に動作すればOK

基本的にはローカルで開発していたときと同様の設定をすればいいですが,環境設定周りで PATH などをミスっていると動かないので注意が必要です.

まとめ

Webカメラを使って簡易的に混雑率を推定するためのシステムを開発しました.クラウドサービスを活用することで,機械学習のモデルを独自で構築することなく力技で実現することができました.データも DB に格納してあるので,フロントから見られるようにしたり,貯めたデータを分析するなり自由自在です.認識の精度などはこれからフィールドワークなどを通して確かめていきたいと思います.

調べながら開発しているので,ベストプラクティスやアンチパターンなど,知見があれば教えていただけると助かります!

ソースコードは GitHub のリポジトリで公開してありますが,こちらはアップデートなどを行って記事の内容と変わる可能性がありますのでご了承ください.もちろんこの記事も随時アップデート予定ではあります!

Reference

  1. 一般社団法人 日本民営鉄道協会, 混雑率.

Discussion