この記事では、人工知能、または少なくとも機械学習を使用して、静的であるはずの環境内で変化が生じた場合 (例えば納屋に収納されている収穫後の干し草が乾燥してきた場合)、アラームが起動されるようにする方法を説明します。この仕組みを実現するために、視覚認識と画像比較という 2 つの手法を使用します。
視覚認識にはかなりの処理が必要になるため、Raspberry Pi 上で簡単に対処することはできません。そこで、写真を IBM Cloud にアップロードし、IBM Watson Visual Recognition を利用して写真の中からオブジェクトを識別することにしました。新しいオブジェクトが出現した場合、または期待されるオブジェクトが消えた場合 (オブジェクトは特定の光の状態でのみ識別可能なため、丸 1 日消えていることを条件とします)、この AI システムはアラームを起動します。
IBM Watson Visual Recognition を利用してオブジェクトを認識するには、IBM Cloud に写真をアップロードするためにかなりの量の帯域幅が必要になるため、LoRa 接続のような低帯域幅のネットワーク接続でも機能する AI システムを設計しました。このような環境内で変化を検出するために、この記事の後半では画像比較を使用します。10 分間隔で画像を撮影し、その都度、24 時間前に撮影した画像と比較します。こうすれば、光の状態が変わっても、誤報を防止できる程度の小さな影響に抑えることができます。
このアプリケーションを作成するために必要なもの
- Raspberry Pi
- Raspberry Pi カメラ・モジュール
- HDMI モニター
- USB キーボードおよびマウス (ワイヤレスと有線式 USB のどちらでもかまいません)
- microSD カード (8 GB の容量で十分過ぎるほどです)
アーキテクチャー
短距離通信アーキテクチャー
クラウド内での視覚認識の実装には、最初の記事で説明した短距離通信アーキテクチャーに基づくアーキテクチャーを使用します。納屋内のデバイスは母屋の中にある、インターネットに接続されたアクセス・ポイントと通信するために Wi-Fi を使用します。最初の記事で使用した NodeMCU センサーと DHT11 センサーに加え、納屋にはオブジェクトを検出する Raspberry カメラ・モジュール搭載の Raspberry Pi があります。
図 1. 短距離通信アーキテクチャー
この使用ケースには、長距離通信アーキテクチャーは適していません。その理由は、データ転送速度が問題になるためです。Raspberry カメラで撮影された写真は、それぞれ約 3.5 MB の大きさになります。LoRa のデータ転送速度は約 50 kbps です。3.5 MB は 3584 kB です (1 メガバイトは 1024 キロバイトです)。つまり、28,672 kb になります (1 バイトあたり 8 ビット)。したがって 1 つの画像を転送するのに 570 秒、つまり 10 分近くの時間がかかります。この環境ではセンサーの測定値といった他の情報も転送する必要があるため、これでは時間がかかりすぎです。
そのため、このソリューションでは Raspberry Pi でインターネットを使用して Object Storage と Cloud Functions という 2 つの IBM Cloud サービスにアクセスすることにしました。Raspberry Pi は写真を撮った後、それを IBM Cloud Object Storage にアップロードします。続いて新しい写真に関する情報を渡すために、IBM Cloud Functions 上でアクションを呼び出します。このアクションは、IBM Watson Visual Recognition サービスを利用してオブジェクトを検出し、IBM Cloudant データベースにアクセスして現在のオブジェクトと過去に観測されたオブジェクトと比較します。大幅な違いを検出した場合は、SendGrid を使用してメッセージを人間に送信します。
IBM Watson IoT Platform に写真を保管することもできましたが、その場合、Raspberry Pi をデバイスとして登録するといった複雑さに対処しなければならないため、付加価値はもたらされません。ただし、センサーでは IoT Platform を利用します。それは、センサー測定値をアップロードして保管するのに適した高可用性インフラストラクチャーであるためです。一方、IBM Cloud Object Storage も同じく高可用性インフラストラクチャーであり、IoT Platform とは対照的に、カメラで撮った写真などの大容量の情報保管を目的に最適化されています。
長距離通信アーキテクチャー (エッジ上)
エッジ上での視覚認識の実装には、最初の記事で説明した長距離通信アーキテクチャーに基づくアーキテクチャーを使用します。前のアーキテクチャーとの 1 つの違いは、カメラを搭載した Raspberry Pi が追加されることです。この Raspberry Pi は Wi-Fi を使用して納屋の ESP32 に接続します。納屋の ESP32 から、LoRa を使用して母屋の ESP32 にアラームを送信し、そこから IBM IoT Platform にアラームが送信されるという仕組みです。
_図 2: 長距離通信アーキテクチャー
ソース・コード
このソリューションでは反復型開発モデルを使用しています。そのため、GitHub 上にはソフトウェアの複数のバージョンを置いてあります。
以下のプログラム一式を使用して、クラウド内に視覚認識を作成します。
- 31_take_picture.js: Raspberry Pi 上で実行される、Raspberry カメラで写真を撮るための Node.js コード。
- 32_upload_picture.js: 写真を IBM Cloud Object Storage にアップロードするためのコードが追加されています。
- 33_visual_recognition.js: 写真内のオブジェクトを識別する IBM Watson Visual Recognition を呼び出すためのコードが追加されています。単純にするために、この時点ではプログラム全体が引き続き Raspberry Pi 上で実行されます。
- 34_classifier_output.json: プログラムのように見えますが、IBM Watson Visual Recognition によって生成された出力の一例です。
- 35_action.js: IBM Watson Visual Recognition を呼び出し、IBM Cloudant データベースを使用して変化を識別する IBM Cloud Functions のアクション。
- 36_raspberry_final.js: Raspberry Pi Node.js プログラムの最終バージョン。IBM Watson Visual Recognition を呼び出すのではなく、IBM Cloud Functions のアクションを呼び出します。
- 37_action_with_email.js: IBM Cloud Functions のアクションの最終バージョン。必要に応じて人間に e-メールを送信する、SendGrid の呼び出しが追加されています。
以下の 2 つの最終プログラムを使用して、エッジ上に視覚認識を作成します。
- 38_compare_images.js: 画像を取り、31_take_picture.js で以前に撮られた画像と比較します。
- 39_send_alerts.js: LoRa を使用して納屋を接続する完全なソリューション。
クラウド内に視覚認識を実装する
このアーキテクチャーでは IBM Watson Visual Recognition を利用します。Raspberry Pi を使用してローカルで写真を撮りますが、IBM Watson Visual Recognition を利用するために、インターネットを介して IBM Cloud に写真をアップロードします。写真がアプロードされると、IBM Watson Visual Recognition 内の AI がその写真内のオブジェクトを識別します。オブジェクトを識別した後は、同じカメラで前に観測されたオブジェクトと比較できます。
1. Raspberry Pi をセットアップする
今のところは、Raspberry Pi を独自のネットワークに接続してセットアップします。デプロイできる状態になったら、ネットワーク・パラメーターを必要に応じて変更し、納屋内で Raspberry Pi を接続します。
1a Raspberry Pi、カメラ・モジュール、オペレーティング・システムをセットアップする
Raspberry Pi を監視カメラとして使用できるように構成するには、以下の手順に従います。
- NOOBS (ライト・バージョンで十分です) をダウンロードして microSD カード上にインストールします。
- microSD カードを Raspberry Pi に挿入します。HDMI、キーボード、マウス、電源を接続します (micro-USB 接続)。
- Raspberry Pi カメラ・モジュールを接続します。
- インターネットに接続するように Wi-Fi ネットワークを構成します。
- 「Raspbian」を選択して「Install (インストール)」をクリックします (インストール手順で必要なのは、画像でカメラが写真を撮っていることを確認することだけです。画像ファイルをコピーして別の場所で開く場合は、「Raspbian Lite」を選択できます)。
- インストールが完了したら、Raspberry Pi を再起動します。
- ログインするよう求められたら、pi としてログインします。パスワードは raspberry です。Wi-Fi 設定を変更する必要はありません。NOOBS に指定した Wi-Fi の値はそのまま維持されています。
- デフォルトのパスワードを
passwd pi
に変更します。 - ここで説明されている手順に従って、SSH を有効にします。
- SSH クライアント (Windows 上での私のお気に入りは Bitvise です) を使用して Raspberry Pi に接続します。IP アドレスを特定するためにルーターに接続しなければならない場合があります。
1b 写真撮影ソフトウェアを Raspberry Pi にインストールする
ハードウェアとオペレーティング・システムが稼動中になったら、次のステップとして、必要なソフトウェアをダウンロードします。
NodeJS をインストールします。この環境では、JavaScript でプログラミングすることができます。通常のリポジトリーから
apt-get
を使用することで NodeJS をインストールできますが、この場合にインストールされるバージョンはかなり古いものです。sudo apt-get update sudo apt-get dist-upgrade curl -sL https://deb.nodesource.com/setup_10.x | sudo \ -E bash – sudo apt-get install -y nodejs node -v
NodeJS パッケージ・マネージャーの
npm
をインストールします。sudo apt-get install npm
このプロジェクトに必要な NodeJS モジュールをインストールします。
npm install pi-camera ibm-cos-sdk watson-developer-cloud
プログラムをダウンロードして実行します。
Raspberry Pi 上のブラウザー内で file:///tmp/test.png を開き、撮影した写真を確認します。
最初の行で、pi-camera モジュールのオブジェクトを作成します。この参照は変わらないことになっているので (常に同じオブジェクトを参照します)、const
キーワードを使用して参照を宣言できます。
const PiCamera = require('pi-camera');
注: const
キーワードによって変数内のオブジェクトの値が保護されることはありません。このステートメントを確認するには、以下の行を実行します。
const c = {a:1}; c.b=2; console.log(c);
以下の行で、写真を撮るオブジェクトを指定します。
const myCamera = new PiCamera({
mode: 'photo',
output: '/tmp/test.png',
nopreview: true
});
最後に、このコードで写真を撮ります。
myCamera.snap()
.then(result => console.log('Success ' + result))
.catch(err => console.log('Error ' + err));
2. 画像をアップロードする
画像で何らかの処理を行うには、その前に、必要なサービスがインターネット上で画像にアクセスできるようにする必要があります。IBM Cloud Object Storage が、画像にアクセスするための場所になります。
2a IBM Cloud Object Storage バケットを作成する
- IBM Cloud コンソールにログオンします。
- 「Create resource (リソースを作成)」をクリックします。
- 「Storage (ストレージ)」 > 「Object Storage」の順に選択します。
- スクロールダウンして、サービスの名前として「
BarnImages
」と入力し、「Create (作成)」をクリックします。 - 「Create Bucket (バケットを作成)」をクリックします。
- バケットの名前を入力し、「Create bucket (バケットを作成)」をクリックします (私は「
images4detection
」という名前を付けましたが、これとは別の名前が必要になる場合があります)。 - Raspberry Pi のサービス資格情報を作成します。
- 左側のサイドバーにある「Service credentials (サービス資格情報)」をクリックします。
- 「New credential (新規資格情報)」をクリックします。
- 資格情報の名前として「
RaspberryUploadCred
」と入力します。 - 「Select Service ID (サービス ID を選択)」 > 「Create New Service ID (新規サービス ID を作成)」の順に選択します。
- サービス ID の名前として「
RaspberryUploadCred
」と入力します。 - 「Add (追加)」をクリックします。
- 「View credentials (資格情報を表示)」をクリックし、資格情報をクリップボードにコピーします。
- 左側のサイドバーにある「Buckets (バケット)」をクリックし、「images4detection」をクリックします。
- 「Endpoint (エンドポイント)」をクリックします。
- 適切な場所 (us-geo、eu-geo、または ap-geo) を選択します。
- パブリック・エンドポイントのいずれかをコピーします。この情報がコード内で必要になります。
2b Raspberry Pi から写真をアップロードする
このプログラムをダウンロードし、プログラム内の serviceAcctCred
、bucket
、storageEndpoint
の各定数を実際の値で置き換えます。プログラムを実行し、前のステップで作成したストレージを表示します。ストレージ内に picture.png オブジェクトがあるはずです。このオブジェクトをダウンロードして表示します。これが、Raspberry Pi が撮った写真です。
以下のコードで、ライブラリーを使用して IBM Cloud Object Storage に接続します。
const ObjectStorageLib = require("ibm-cos-sdk");
const objectStorage = new ObjectStorageLib.S3({
endpoint: storageEndpoint,
apiKeyId: serviceAcctCred.apikey,
ibmAuthEndpoint: 'https://iam.ng.bluemix.net/oidc/token',
serviceInstanceId: serviceAcctCred.resource_instance_id
});
カメラ制御コードは、写真をファイルに書き出します (このコードは、コマンド・ライン・インターフェースのラッパーです)。したがって、ファイルから写真を読み取るためにファイル・システム・モジュールが必要になります。
const fs = require("fs");
オブジェクトを IBM Cloud Object Storage 上に配置するには、2 つの方法があります。簡単なほうの方法はシングル・パート・アップロードですが、サイズは 5 MB 以下に制限されます。これよりも複雑な方法では、ファイルを複数のチャンクとしてアップロードすることになりますが、幸い、画像のサイズは 5 MB 未満なので、複雑な方法を使用する必要はありません。
// The image file size is less than 5 MB, so there's no need
// for a multi-part upload
ファイル名、バケット名、オブジェクト・ストレージの構成は不変ですが、キーは変わる場合があります。複数の Raspberry Pi インスタンスを稼動させるとしたら、写真のソースを区別することをお勧めします。写真を維持する場合は、時間別にも区別できるようにしてください。
const uploadImage = (key, callback) => {
fs.readFile(fname, (err, data) => {
if (err) {
ファイルを読み取ります。エラーが発生した場合は、プロセスを停止する必要がありますが、エラーが発生することはないはずです。
console.log(`Error reading file ${fname}: ${err}`);
process.exit();
}
オブジェクトを IBM Cloud Object Storage 内に格納します。このデータは、ファイルから読み取られたデータです。
objectStorage.putObject({
Bucket: bucket,
Key: key,
アクセス制御リストを使用して、誰でも情報を読み取れるようにすることができます。こうすると、認証トークンを IBM Watson Visual Recognition と共有する必要がなくなるため、プログラムを単純化できます。
ACL: "public-read",
Body: data
}).promise()
.then(callback)
重要となるのは、エラーを正常に処理することです。アップロード・エラーが発生することはよくあります。例えばインターネット接続に問題がある場合は写真をアップロードするとはできません。
.catch(e => console.log("Image upload error: " + e))
});
};
写真を撮って、アップロードします (成功した場合)。
myCamera.snap()
.then(uploadImage("picture.png", () => {
console.log("Done");
何らかの理由で IBM Cloud Object Storage ライブラリーがアップロードの完了後もアプリケーションを実行し続けることがあります。この問題を回避するために、process.exit()
を呼び出して終了します。
process.exit();
}))
.catch(err => console.log('myCamera.snap error ' + err));
2c URL から写真をダウンロードする
このドキュメントによると、オブジェクトを読み取るための URL は https://<エンドポイント>/<バケット名>/<オブジェクト名>
です。ブラウザーで写真の URL にアクセスしてください。Content-Type は設定されていないため、ブラウザーはこれが写真であることを認識しません。ファイルとみなして、そのまま保存するだけです。
画像内のオブジェクトを分類する
クラウド上の画像を取得できるようになったので、次のステップでいよいよ IBM Watson Visual Recognition を活用します。
- IBM Cloud コンソールにログオンします。
- 「Create resource (リソースを作成)」をクリックします。
- 「AI」 > 「Visual Recognition」の順に選択します。
- インスタンスの名前として「
barnImages
」と入力し、「Create (作成)」をクリックします。 - 「Show (表示)」をクリックして API キーを表示し、資格情報をコピーします。
- このプログラムをコピーし、内容を編集して独自の資格情報 (
serviceAcctCred
とvisualRecognitionCred
) ならびにストレージのエンドポイント (storageEndpoint
) を反映させます。 - プログラムを実行し、結果を確認します。
画像分類は API を使用して行うこともできます。その方法は以下のとおりです。
API を使用して画像を分析する際の最初のステップは、API のオブジェクト・クラスを取得することです。IBM Cloud Object Storage ライブラリーには、IBM Watson の一部となっている他の API も含まれています。
const VisualRecognitionV3 =
require('watson-developer-cloud/visual-recognition/v3');
次に、実際の API オブジェクトを作成します。将来のアップグレードに備えて、API オブジェクトでは、使用する API のバージョン知る必要があります (この記事を書いている時点で最新のバージョンは、2018 年 3 月 19 日にリリースされたものです)。また、使用 (および必要に応じて課金) するアカウントを把握するには API キーも必要です。
const visualRecognition = new VisualRecognitionV3({
version: '2018-03-19',
iam_apikey: visualRecognitionCred.apikey
});
以下のコードが、写真を撮ってアップロードした後、Visual Recognition API を呼び出します。
myCamera.snap()
.then(uploadImage(objName, () => {
console.log("Done uploading");
Visual Recognition のパラメーターは以下のとおりです。ここで使用しているパラメーターは、画像の取得先の URL だけですが、他にも役立つ可能性のパラメーターがあります。
const params = {
url:
`https://${storageEndpoint}/${bucket}/${objName}`
};
console.log(`Picture located at ${params.url}`);
以下のコードで、実際に分類子を呼び出します。レスポンスは JSON 形式で返されます。
visualRecognition.classify(params, (err, response) => {
if (err)
console.log(err);
else
console.log(JSON.stringify(response,
null, 2));
process.exit();
});
}))
.catch(err => console.log('myCamera.snap error ' + err));
画像分類によって生成されたサンプル JSON はこちらで確認できます。画像の数が 1 つの場合、検出されたオブジェクトのクラスは .images[0].classifiers[0].classes
で使用できます。
4. 変化を検出する
カメラと視覚的分類を使用する目的は、人間に報告しなければならないような納屋内の変化を識別することです。変化を識別するアルゴリズムは以下のとおりです。ただし、ほとんどの納屋は太陽の光だけに照らされること、日中の光の角度は変わること、夜間は真っ暗になるという事実は、このアルゴリズムでは考慮されていません。
- Raspberry Pi が定期的に写真を撮って、それを IBM Cloud Object Storage にアップロードします。
- Raspberry Pi から IBM Cloud Functions 上で実行されるサーバー・コードに情報を渡します。
- サーバー・コードがオブジェクトを識別するために IBM Watson Visual Recognition を呼び出します。
- サーバー・コードが IBM Cloudant データベースを使用して、識別されたオブジェクトを最近観測されたオブジェクトと比較します。
- オブジェクトが追加または削除されている場合、アップロードされた写真を組み込んだ e-メールを送信して人間に報告します。
- オブジェクトに変化がなければ、ストレージ容量を節約するために写真を消去します。
- 必要に応じて IBM Cloudant データベースを更新します。
4a IBM Cloudant データベースを構成する
次のステップでは、データ・ストレージとそれを使用するサーバー・コードを構成します。
各デバイス上で識別されたオブジェクトを保管するデータベースが必要です。
- IBM Cloud コンソールにログオンします。
- 「Create resource (リソースを作成)」をクリックします。
- 「Databases (データベース)」 > 「Cloudant」の順に選択します。
- サービス・インスタンスの名前として「
barnImages
」と入力し、認証メソッドとして「Use both legacy credentials and IAM (レガシー資格情報と IAM の両方を使用)」を選択します。 - 「Create (作成)」をクリックします。
- Cloudant データベースのプロビジョニングが完了したら、「Services (サービス)」を選択し、リソース・リストから「barnImages」を選択します。
- データベースのサービス資格情報を作成します。
- 左側のサイドバーにある「Service credentials (サービス資格情報)」をクリックします。
- 「New credential (新規資格情報)」をクリックします。
- 資格情報の名前として「
deviceObjects
」と入力します。 - 役割は「Manager (マネージャー)」のままにします。
- 「Add (追加)」をクリックします。
- 「View credentials (資格情報を表示)」をクリックし、資格情報をクリップボードにコピーします。
- 左側のサイドバーにある「Manage (管理)」をクリックし、「LAUNCH CLOUDANT DASHBOARD (CLOUDANT ダッシュボードを起動)」をクリックします。
- 左側のサイドバーにある「Databases (データベース)」(またはアイコン ) をクリックします。
- 「Create Database (データベースを作成)」をクリックし、データベースの名前として「
device_objects
」と入力し、「Non-partitioned (パーティショニングなし)」を選択してから「Create (作成)」をクリックします。
4b IBM Cloud Functions を構成する
サーバー・コードは IBM Cloud Functions 内で実行します。こうすることで、実際に必要なときにだけリソースが使用されることになります。ここでは、サーバー・コードを実行するアクションを 1 つだけ作成します。
- IBM Cloud コンソールにログオンします。
- 「Create resource (リソースを作成)」をクリックします。
- 「Compute (コンピューティング)」 > 「Serverless Compute (サーバーレス・コンピューティング)」 > 「Functions」の順に選択します。
- 「Start Creating (作成開始)」をクリックします。
- 「Actions (アクション)」をクリックし、「Create (作成)」をクリックします。
- 「Create Action (アクションを作成)」をクリックします。
- アクションのパッケージを作成します。
- 「Create Package (パッケージを作成)」をクリックします。
- パッケージの名前として「
barnImages
」と入力し、「Create (作成)」をクリックします。
- アクションの名前として「
processBarnImage
」と入力します。 - ランタイム環境が Node.js 10 以降のバージョンであることを確認します。
- 「Create (作成)」をクリックします。
- デフォルトのプログラムを GitHub 内に用意されているプログラムで置き換えます。このプログラムについては、このプログラムを呼び出す Raspberry Pi コードと併せて後述します。
- 左側のサイドバーにある「APIs (API)」をクリックします。
- 「Create Managed API (マネージド API を作成)」をクリックします。
- API の名前として「
Process Barn Images
」と入力します。 - 「Create operation (処理を作成)」をクリックします。
以下のパラメーターを使用して処理を作成します。
- Path (パス):
/image
- Verb (動詞): GET
- Package containing action (アクションを含めるパッケージ): barnImages
- Action (アクション): processBarnImages
- Response content type (レスポンスのコンテンツ・タイプ):
application/json
- Path (パス):
その他の設定はデフォルト値のままにして、「Create (作成)」をクリックします。
- 後で使用できるようにルートをコピーしておきます。
IBM Cloud Function コードは部分的に、Raspberry Pi 上の Node.js で行ったのとまったく同じ処理を行います。以下では、それ以外の新しい部分について説明します。
まず、IBM Cloudant データベースに接続します。レガシー資格情報をサポートする場合、URL にはユーザー名とパスワードが組み込まれます。このほうが、IAM 資格情報を使用するよりも単純です。
// Credential for the IBM Cloudant database
const cloudantCred = {
… redacted …
}
const cloudant = require('@cloudant/cloudant')({
url: cloudantCred.url
});
次に、使用するデータベースを指定します。
const database = "device_objects";
const db = cloudant.db.use(database);
main
関数に渡すパラメーターは、IBM Cloud Object Storage にアップロードしたオブジェクトの名前だけです。この名前には、デバイス ID とタイムスタンプの両方が含まれます。
const main = params => {
以下のコードでオブジェクト名を解析します。オブジェクト名は pict_<デバイス ID>_<タイムスタンプ>.png
形式になっているため、拡張子を削除してから下線 (_)
文字を基準に名前を分割します。
// Parse the object name to get the information encoded in it
const splitObjName = params.objName.split(".")[0].split("_");
const devID = splitObjName[1];
const timestamp = splitObjName[2];
IBM Cloud Function は即時に結果を返せない場合、Promise
オブジェクトを返します。このオブジェクトには、システムが呼び出す関数が格納されています。この関数自体が 2 つの関数 (success と failure) を取り、プロセスが成功または失敗すると、いずれか該当するほうの関数が呼び出されます。
この例では、処理に時間がかかるプロセスが少なくとも 3 つあります。具体的には、画像内のオブジェクトの認識、デバイスに対応する現在のデータベース・エントリーの読み取り、更新されたエントリーの書き込みです。以下のようにして、多くのリソースを占有しすぎることなく、これらのプロセスを実行できるようにします。
// Classification takes time, so we need to return a
// Promise object.
return new Promise((success, failure) => {
以下の方法でオブジェクトを認識します。この方法は、Raspberry Pi 上で使用した方法と同じです。
visualRecognition.classify(visRecParams,
(err, response) => {
if (err) {
エラーが発生した場合は、failure
関数を呼び出します。この関数が期待する構造体として、ここではエラーの場所とエラーそのものを渡しています。問題が発生して failure
が呼び出される可能性のあるステップは 1 つだけではないため、エラーの場所を渡すと役立ちます。
failure({
errLoc: "visualRecognition.classify",
err: err
});
return; // Exit the function
}
オブジェクト名だけを使用するために、信頼度とオブジェクト・タイプの階層内での場所についての情報を削除して、オブジェクト名の配列だけ残します。
const objects = response.images[0].classifiers[0].classes.map(x => x.class);
デバイスに対応するエントリーを取得します。このエントリーは、オブジェクト名とそのデバイスからの画像内でそれぞれのオブジェクトが前回認識された時間からなる連想配列です。
db.get(devID, (err, result) => {
エラー・ステータス 404 は、デバイスに対応するエントリーがないことを意味します。デバイスが新しい場合、これは期待される結果です。このエラーに対処するために、オブジェクトを作成してデータベース内に挿入します。
// Not really an error, this is just a new device without an entry yet
if (err && err.statusCode == 404) {
var dataStruct = {}; // Data structure
// to write to database
objects.map(obj =>
dataStruct[obj] = timestamp);
db.insert({_id: devID, data: dataStruct},
(err, result) => {
if (err) {
failure({
errLoc: "db.insert (new entry)",
err: err
});
return; // Exit the function
}
新しいデバイスを追加した時点で、ユーザーにデバイスの追加を通知することもできます。
// Send message to the user here
success
関数を呼び出します。関数が正常に実行されても、変更する対象の前の状態がないので、報告される情報はありません。
success({});
}); // end of db.insert
この時点で db.get
コールバックに制御が渡されています。関数を終了して以降の処理を停止する必要があります。新しいデバイスの場合、その必要はありません。
return;
}
db.get
が 404 以外のエラーで失敗した場合、それは実際のエラーであるため、エラーを報告する必要があります。
// If we got here, it's a real error
if (err) {
failure({
errLoc: "db.get",
err: err
});
return; // Exit the function
}
読みやすくするために、残りの処理は別の関数 compareData
内で行います。
// Compare the old data with the new one,
// update as needed and update the database
// (also, inform the user if needed)
compareData(objects, result,
timestamp, success, failure);
}); // end of db.get
}); // end of visualRecognition.classify
}); // end of new Promise
}; // end of main
// Compare the old data with the new one, update as needed
// and update the database
// (also, inform the user if needed)
const compareData = (objects, result, timestamp,
success, failure) => {
実用的なプログラミングの見本を作っているのではありませんが、データベースに書き込む更新データ、新しいオブジェクトの配列のリスト、欠落しているオブジェクトの配列のリストを以下の変数内で作成します。
var data = result.data;
var newObjects = [];
var missingObjects = [];
var informHuman = false;
IBM Visual Recognition から取得したオブジェクトのリストを繰り返し処理します。オブジェクトが新しいものである場合は、それを新しいオブジェクトのリストに追加します。また、前回認識された時間をタイムスタンプに応じて更新します。
objects.map(object => {
// The object is new, insert it to the new object list
if (!result.data[object])
newObjects.push(object);
// Either way, update the time it was last seen
data[object] = timestamp;
});
次は、データベース内に格納されているオブジェクトのリストを繰り返し処理し、24 時間前に認識されたオブジェクトのうち、なくなっているオブジェクトがあるかどうかを確認します。これに該当するオブジェクトは欠落オブジェクトのリストに追加して、そのタイムスタンプを削除します (再び出現するまで、以降の呼び出しで毎回欠落しているオブジェクトとして示されないようにするため)。
// Look for objects that haven't been seen in 24 hours
const deadline = new Date(new Date(timestamp) – 24*60*60*1000);
Object.keys(data).map(object => {
const lastSeen = new Date(data[object]);
if (lastSeen < deadline) {
missingObjects.push(object);
delete data[object];
}
}); // end of Object.keys(data).map
新しいオブジェクトまたは欠落しているオブジェクトがある場合、それを人間に通知する必要があります。この機能は、記事の後のほうのステップで実装します。
// Do we need to inform a human?
if (newObjects.length > 0 || missingObjects.length > 0)
informHuman = true;
Cloudant データベース・ドキュメントを更新するには、ドキュメントのリビジョン・エントリー (_rev
) が必要です。この要件により、古くなったデータに基づく変更は拒否されるため、Cloudant の整合性を維持することができます。
// Update the data in the database (that will always
// be required
// because if nothing else the timestamps changed)
const newEntry = {
_id: result._id,
_rev: result._rev,
data: data
};
db.insert(newEntry, (err, result) => {
if (err) {
failure({
errLoc: "db.insert",
err: err
});
return; // Exit the function
};
success({
new: newObjects,
missing: missingObjects,
data: data
});
}); // end of db.insert
}; // end of compareData
4c オブジェクトを分類する
GitHub にある Raspberry Pi の最終バージョンをコピーしてください。APIUrl
の部分には、前のステップで IBM Cloud Functions 上で作成した API のルートに設定します。serviceAcctCred
の部分には、IBM Cloud Object Storage 資格情報を設定します。storageEndpoint
の部分は、実際の場所に応じた正しい値に変更します。10 分間隔となっている更新頻度を変更する場合は、freq
の値を変更します。
この Raspberry Pi プログラムの最終バージョンでは、写真を撮ってアップロードしてから、さらに処理するためにサーバー・コードを呼び出します。最終バージョンのプログラムの大部分は Raspberry Pi 上で前に行った処理とまったく同じ処理を行います。以下では、それ以外の新しい部分について説明します。
写真を処理するコードは関数内にラップされて、呼び出しやすくなっています。
const processPicture = () => {
...
uploadImage(currentObjName, () => {
console.log("uploaded " + currentObjName);
アップロードが完了したら、https.get を使用して IBM Cloud Functions コードを呼び出します。POST を使用し、HTTP ヘッダー内にパラメーターを書き込むという方法もありますが、パラメーターの数が少ない場合はクエリー・ストリングとして送信したほうが簡単です。この例ではパラメーターとして渡すのは、アップロードされたオブジェクトの名前だけです。
https.get(`${APIUrl}${funcPath}?objName=${currentObjName}`,
IBM Cloud Functions からのレスポンスを受信すると呼び出される以下のコールバックは、必須というわけではありません。ここでは、このオブジェクトに対してサーバー・コードが実行されたことを確認するために、デバッグ目的で使用しているだけです。
(res) => {
console.log(
`Done with ${currentObjName}`);
console.log("-----------------------");
}); // end of https.get
...
}; // end of processPicture
この直後に processPicture
を 1 回実行し、その後は数分間隔 (freq
変数で指定された間隔) で再実行します。JavaScript はミリ秒で時間を追跡するため、分数を 60,000 で乗算した値を使用してください。
processPicture();
// Process a picture every freq minutes
setInterval(processPicture, 1000*60*freq);
5. 人間に通知する
まだ実装していない機能は、オブジェクトが新しい場合、または過去 24 時間観測されていない場合に人間にそれを通知する機能です。通知の手段としては、SendGrid の e-メール・サービスを使用できます。
- https://sendgrid.com/free/ にアクセスします。
- 「Try for Free (無料で試す)」をクリックし、自分のアカウントを作成します。
- 「Integrate using out Web API (Web API を使用して統合)」タイルを見つけて「Start (開始)」をクリックします。
- 「Web API」を選択します。
- 「cURL」を選択します。この例でのアクションでは Node.js ランタイム環境を使用します。別のライブラリーを IBM Cloud Functions に追加することもできますが、その場合は多少複雑な作業になります。ライブラリーの追加方法を確認するには、開発ツールチェーンに関するこの記事を参照してください。
- API キーに名前を付けて (例えば、
informPeople
)、「Create Key (キーを作成)」をクリックします。 - 「Create Key (キーを作成)」ボタンの下にあるテキスト・フィールドからキーをコピーします。
- 受信した e-メールを表示して、アカウントを確認します。
このアクションでは HTTPS を使用して直接 SendGrid サービスを呼び出します。
以下の関数で、リストを HTML 順序なしリストに変換します。この関数は map 関数を使用してリストの項目 (例: [“it
“, “ems
“]) を HTML リストのリスト ([“
<ul><li>it</li><li>ems</li></ul>
」) に変換します。
const list2HTML = (lst) => {
if (lst.length == 0)
return "None";
return `<ul>${lst.map(x => "<li>"+x+"</li>").
reduce((a,b) => a+b, " ")}</ul>`;
};
以下の関数は送信する HTML 全体を作成し、メッセージの本文にオブジェクトの変化自体と、それらの変化を示している写真を組み込みます。
const makeMsg = (newObjs, missingsObjs, pictureURL) => {
return `
<H2>Changes</H2>
<h4>New objects:</h4>
${list2HTML(newObjs)}
<h4>Missing objects:</h4>
${list2HTML(missingsObjs)}
<H2>Picture</H2>
<img src="${pictureURL}" height="500" width="800">
`;
}; // makeMsg
以下の関数は、サーバーが識別したオブジェクトの変化に関するすべての情報を提供する e-メールをユーザーに送信します。
const informUserFunc = (newObjs, missingObjs,
devID, pictureURL, success) => {
SendGrid API 呼び出しを使用して e-メールを送信するために、HTTPS POST リクエストを送信します。ヘッダーにはコンテンツ・タイプと、このサービスの使用に対する承認情報を含める必要があります。
const req = https.request({
method: "POST",
hostname: "api.sendgrid.com",
path: "/v3/mail/send",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SendGridAPIKey}`
}
}, // end of options for https.request
リクエストが処理された後、以下の関数が呼び出されます。これが、写真に対するアクションの最後のステップです。
(res) => {
success({loc: "informUserFunc",
statusCode: res.statusCode,
statusMessage: res.statusMessage
});
}); // end of https.request
SendGrid は以下のオブジェクトを期待します。content.value
フィールドは、makeMsg
関数によって作成された HTML に該当します。
const mailToSend =
{"personalizations": [{"to": [{"email": destEmail}]}],
"from": {"email": sourceEmail},
"subject": `Changes in device ${devID}`,
"content": [{
"type": "text/html",
"value": makeMsg(newObjs, missingObjs, pictureURL)
}]
};
以下のコードで、メール・オブジェクトの JSON を POST リクエストの本文に書き込みます。ヘッダー・フィールドではなく本文であるため、上記の https.request
呼び出しではなく、このコードで書き込みます。
req.write(JSON.stringify(mailToSend));
req.end();
}; // end of informUserFunc
6. デプロイする場合の準備
このプログラムは説明のために作成したプロトタイプであり、実際にデプロイすることを目的に作成された完全に機能するプログラムではありません。本番環境に実際にデプロイする場合は、以下の機能を追加する必要があります。
- 未使用の写真を削除する。Visual Recognition でユーザーに通知すべきオブジェクトが識別されなかった場合、
deleteBucket
を使用して、その写真を IBM Cloud Object Storage から削除します。Storage は低コストですが無料ではありません。 - デバイスのオフライン状態を識別する。一定の期間にわたってデバイスからメッセージを受け取らない場合は、アラートを送信します。それには、定期的トリガーを使用し、IBM Cloudant データベース内のすべてのエントリーを読み取って各エントリーの最終更新日をチェックするアクションを実行します。例えば 1 時間にわたってエントリーが更新されていなければ、ユーザーに通知します。
- デバイスに有用な ID を割り当てる。Cloudant 上で別のデータベースを使用して、MAC アドレス ID (
b8:27:eb:5f:aa:73
など) を「Barn #5, the one next to the Creek (小川の横の第 5 納屋)」といった有用な ID に変換します。
エッジ上に視覚認識を実装する
このシリーズでは、LoRa でネットワークに接続する長距離通信アーキテクチャーも使用します。 この場合、写真をアップロードするのは不可能です。Raspberry Pi 上で Tensor Flow ライブラリーを実行してオブジェクトを識別することは可能ですが、それはかなり複雑なプロセスになります。この複雑なプロセスを完成させたとしても、Raspberry Pi の RAM は低容量なので (最大 1 GB)、使用できる視覚認識モデルは限られます。したがって、検出の精度にも限りがあります。
それよりも適切なソリューションは、写真を撮って、それを前の写真と構造的類似性を基準に比較することです。こうすれば、光の状態の違い (晴れ、曇り、雨) が補われると同時に、大幅な変化を識別できます。
1. Raspberry Pi をセットアップする
セットアップは、短距離通信アーキテクチャーの場合とほぼ同じです。
今のところは、Raspberry Pi を独自のネットワークに接続してセットアップします。デプロイできる状態になったら、ネットワーク・パラメーターを必要に応じて変更し、納屋内で Raspberry Pi を接続します。最初の記事で使用した構成を流用する場合、SSID は barn-net
です。パスワードはありません。
1a Raspberry Pi、カメラ・モジュール、オペレーティング・システムをセットアップする
Raspberry Pi を監視カメラとして使用できるように構成するには、以下の手順に従います。
- NOOBS (ライト・バージョンで十分です) をダウンロードして microSD カード上にインストールします。
- microSD カードを Raspberry Pi に挿入します。HDMI、キーボード、マウス、電源を接続します (micro-USB 接続)。
- Raspberry Pi カメラ・モジュールを接続します。
- インターネットに接続するように Wi-Fi ネットワークを構成します。
- 「Raspbian」を選択して「Install (インストール)」をクリックします (画像でカメラが写真を撮っていることを確認するだけでインストールできます。画像ファイルをコピーして別の場所で開く場合は、「Rasberry Lite」を選択できます)。
- インストールが完了したら、Raspberry Pi を再起動します。
- ログインするよう求められたら、
pi
としてログインします。パスワードはraspberry
です。Wi-Fi 設定を変更する必要はありません。NOOBS に指定した Wi-Fi の値はそのまま維持されています。 - デフォルトのパスワードを
passwd pi
に変更します。 - ここで説明されている手順に従って、SSH を有効にします。
- SSH クライアント (Windows 上での私のお気に入りは Bitvise です) を使用して Raspberry Pi に接続します。IP アドレスを特定するためにルーターに接続しなければならない場合があります。
1b 写真撮影ソフトウェアを Raspberry Pi にインストールする
ハードウェアとオペレーティング・システムが稼動中になったら、次のステップとして、必要なソフトウェアをダウンロードします。
NodeJS をインストールします。この環境では、JavaScript でプログラミングすることができます。通常のリポジトリーから
apt-get
を使用することで NodeJS をインストールできますが、この場合にインストールされるバージョンはかなり古いものです。sudo apt-get update sudo apt-get dist-upgrade curl -sL [https://deb.nodesource.com/setup_10.x](https://deb.nodesource.com/setup_10.x) | sudo -E bash – sudo apt-get install -y nodejs node -v
NodeJS パッケージ・マネージャーの npm をインストールします。
sudo apt-get install npm
このプロジェクトに必要な NodeJS モジュールをインストールします。
npm install pi-camera
https://github.com/qbzzt/IoT/blob/master/201801/Hay_Bale_Fire/31_take_picture.js からプログラムをダウンロードし、実行します。このプログラムについては、前に視覚認識アーキテクチャーの一部として説明しています。
Raspberry Pi 上のブラウザー内で file:///tmp/test.png を開き、撮影した写真を確認します。
2. 画像を比較する
画像の比較に使用するパッケージは、img-ssim です。このパッケージを使用するには、以下の手順に従います。
この画像比較用 Node.js パッケージをインストールします。時間がかかることにご注意ください。Raspberry Pi 3 Model B 上では、約 17 分かかります。このプロセスによって、類似性を計算する C コードがコンパイルされます。コンパイル中に多数の警告メッセージが出されますが、無視してかまいません。
npm install img-ssin
このファイルをダウンロードし、compare_img.js という名前を付けた後、以下のコマンドを使用して実行します。
node compare_img.js `
画像比較は非常に複雑なアルゴリズムですが、呼び出しに関しては極めて単純です。比較する 2 つのファイルの名前と、2 つのパラメーター (エラーと類似性スコア) を取るコールバック関数を指定するだけで、呼び出すことができます。類似性スコアはゼロから 1 までの範囲の値です。
imgSSIM(
"/tmp/pict1.png",
"/tmp/pict2.png",
(err, similarity) => console.log(err || similarity);
); // end of imgSSIM
3. 画像の比較後にアラームを送信する
1 日の時間帯によって写真は異なって見えます。通常、納屋には人工照明が取り付けられていません。光源となる太陽の光が差し込む角度は、当然のことながら時間とともに変わっていきます。光の問題については、1 日の同じ時刻に写真を撮ることで解決します。
このプログラムは、10 分間隔で実行されるように設計されています。実行されると毎回写真を撮り、前日に撮った写真がある場合は 2 つの写真を比較します。写真の間に十分な違いがある場合は、アラームを起動します。違いの有無に関わらず、最後に古い写真を新しい写真で置き換えます。こうすることで、季節による太陽光の強度と角度の違いを補います。
このプログラムはまず、構成パラメーターから始まります。最初のパラメーター timeDiff
で、24 時間を 10 分単位で分割し、144 個のタイム・スライスにします。同じタイム・スライスに属する 2 つの写真が比較されます。
// Divide time into ten minute slices
const timeDiff = 10*60*1000;
以下の類似性スコアを下回ると、プログラムはアラームを起動するのに十分な変化であると判断します。
// Threshold at which there's an alarm
const threshold = 0.75;
写真は以下のディレクトリーに保管されます。
// Directory to store pictures
const pictDir = "/tmp";
この特定の納屋でのデバイス ID は以下のとおりです。NodeMCU センサーと区別するために負の数値になっています。NodeMCU センサーの ID は常に正の数値です。
// Identifier for this device
const devID = -1;
プログラムの次の部分では、必要なライブラリーを宣言しています。node-webcam
と img-ssim
に加え、fs
モジュールも必要です。このモジュールにより、写真などのファイルを操作できるようになります。ここではこのモジュールを使用して、古い写真を新しい写真で置き換えます。アラートを ESP32 に送信し、そこから LoRa を使用してインターネットに送信するには、Node.js 用の HTTP ライブラリーが必要になります。
const NodeWebcam = require( "node-webcam" );
// Takes about 20 minutes to install
const imgSSIM = require("img-ssim");
// File manipulation
const fs = require("fs");
// HTTP client code
const http = require('http');
以下の関数が現在のタイム・スライスのタイムスタンプを取得します。
// Get a timestamp for the current slice of time
const getTimeStamp = () => {
const now = Date.now(); // msec from start of epoch
const dateObj = new Date();
Math.floor(x/y)*y
を使用して、x を上限として、y の倍数の最大数を取得します。この例の場合、この値は現在のタイム・スライスが開始した時点 (エポック開始からのミリ秒数) を意味します。
dateObj.setTime(Math.floor(now / timeDiff) * timeDiff);
タイムスタンプは <時間>_<分>
形式の文字列です。従来の区切り文字としてコロン (: ) を使用してみましたが、画像比較ライブラリーが混乱する結果となってしまいました。このライブラリーは URL 形式で写真を受け入れることができます (例えば、https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/IBM_logo.svg/800px-IBM_logo.svg.png)。したがって、/tmp/pict_1:20.png をこの構文の URL として /tmp/pict_1 と解釈します。このライブラリーはスキーマの処理方法については把握しません。
return `${dateObj.getHours()}_${dateObj.getMinutes()}`;
};
次の 2 つの定義は、ファイル名に関するものです。getFname
関数は現在のタイム・スライスでのファイル名を取得します。newPictFname
は、撮影する写真に付ける名前です。写真の名前にタイム・スライス名を使用することはできません。そうすると、既存の写真が削除されて、写真を比較できなくなります。
// Get the filename for a picture taken at this time
const getFname = () => `${pictDir}/pict_${getTimeStamp()}.png`;
// The filename of the new picture, the one in processing
const newPictFname = `${pictDir}/new_pict.png`;
以下の processPicture
関数が、このプログラムの処理のほとんどを行います。具体的には、新しい写真を撮り、それを古い写真と比較し、新しい写真の名前を変更して古い写真を上書きします。2 つの写真の間に十分な違いがある場合は、コンソールにメッセージも書き出します。
const processPicture = () => {
関数が実行する getFname() の戻り値は、時間に依存することから変わる場合があります。そのため、以下のようにして一定の値にしています。
const currentPict = getFname();
// Take new picture
Webcam.capture(newPictFname, (err, data) => {
if (err) {
console.log(`Webcam.capture error: ${err}`);
return ; // Exit the function call
}
fs.existsSync
関数は、ファイルが存在するかどうかをチェックします。接尾辞の -Sync
は、これが同期関数であることを意味します。つまり、コールバックを実行するのではなく、結果を返す関数です。サーバー・プロセスでは、一般に同期関数を使用するのは賢明とは言えません。同期関数によって他のすべてのリクエストがブロックされるためです。けれどもこのプログラムは 1 つのことだけを処理します。その結果を得るまでは処理を続行できないため、コールバックを使用してさらに複雑にする必要はありません。
if (fs.existsSync(currentPict)) {
ファイルが存在する場合は、新しい画像を古い画像と比較します。
imgSSIM(currentPict, newPictFname, {
enforceSameSize: false,
resize: true
}, (err, score) => {
if (err) {
console.log(`imgSSIM error: ${err}`);
return ;
};
2 つの画像が同様でなければ、アラームを起動します。アラームを意味のあるものにするためには、LoRa を使用してインターネットに送信する必要があります。
この長距離通信アーキテクチャーでは LoRa を使用するために、納屋に ESP32 アクセス・ポイントを設けています。このアクセス・ポイントは http://10.0.0.1/<チップ ID>/<温度>/<湿度>
で HTTP リクエストを受信します。HTTP リクエストで使用されるチップ ID、温度、湿度の 3 つの値はすべて 10 進値です (この記事「(Integrating LPWAN networking and edge computing into your IoT solutions)」のステップ 2c を参照)。これらの値が LoRa 経由で母屋の ESP32 に送信され、そこから IBM Watson IoT Platform に送信されます (IBM Watson IoT Platform を Node-RED アプリケーションに接続することもできます。このシリーズのパート 3 を参照してください)。
ESP32 プログラムに変更を加えなくても済むようにするには、遥かに単純な方法があります。NodeMCU のチップ ID は常に正の数値です。したがって、チップ ID を負の数値にすれば、納屋の Raspberry Pi の ID を区別することができます。クラウド上では、負の数値となっているチップ ID を区別して、そこからセンサー測定値を取得するたびにアラートを起動することで別の処理が可能になります。
// If the score is too low, raise the alarm.
if (score < threshold)
ESP32 への通知を実際に処理するのは http.get
関数です。この関数で温度と湿度の値を両方とも 99 に設定して、クラウド上のアプリケーションが更新されていないとしてもアラームが起動されるようにします。ここまでの温度 (煮え立つほどの暑さ) と湿度 (ほぼ飽和状態) は警戒状態です。したがって、結果を取得する必要はないのでコールバック関数は使用しません。
http.get(`http://10.0.0.1/${devID
}/99/99`);
currentPict
内の古い写真を新しい写真で置き換える必要があります。この処理は写真を置き換えるときだけでなく、fs.existsSync()
の else セクションでも呼び出されます。if ステートメントの外部に配置したくなりますが (既存のファイルの有無に関わらず、名前を変更する必要があるため)、この 2 つの名前変更処理はかなり異なるタイミングで呼び出されます。以下の名前変更処理は画像比較のコールバック関数内にあり、古い写真との画像比較が完了してからでないと呼び出されません。もう一方の名前変更処理の呼び出しは、比較対照の古い写真が存在しないときに行われます。つまり、新しい写真が撮影された直後に呼び出されます。
// Replace the picture with the new one.
fs.rename(newPictFname, currentPict, (err) =>
{
if (err)
console.log(`Rename to ` +
`${currentPict} error: ` +
` ${err}`);
}); // fs.rename
}); // imgSSIM
} else { // if (fs.existsSync())
fs.rename(newPictFname, currentPict, (err) => {
if (err)
console.log(`NO ${currentPict} yet,` +
` rename error: ${err}`);
}); // fs.rename
} // if (fs.existsSync())
}); // Webcam.capture
}; // processPicture definition
最後に、processPicture
関数を実行します。
processPicture();
このプログラムを 10 分間隔で実行するために、crontab -e を実行して以下の行を追加します。
*/10 * * * * /usr/bin/node.js ~pi/<directory>/app.js
まとめ
この記事では、Raspberry Pi を使用して写真を撮る方法と、写真内の変化を識別する 2 つの方法を説明しました。
* 最初の方法では、写真を IBM Cloud Object Storage にアップロードし、IBM Visual Recognition を利用してオブジェクトを識別し、そのオブジェクトの変化を IBM Cloud Functions と IBM Cloudant データベースを使用して識別した後、最終的に SendGrid e-メールでユーザーに変化を通知します。
* 2 番目の方法では、写真を Raspberry Pi 上に保管し、それぞれ別の日の同じ時刻に撮影された 2 つの写真を比較します。2 番目の方法で入手できる情報は正確さに劣りますが、この方法は写真をアップロードできない低帯域幅の接続でも使用できます。