Node.js
画像処理
動画
node-red
bosch

BOSCH製IPカメラを利用した人数カウント(EEN連携編)

人数カウントデータをEagle Eye Networksに保存する

前回の投稿ではBluemix上でBOSCHのIPのカメラから送信されたライン超えアラートを数えて、Cloudantに挿入するところまでできました。今回は更にBluemixからEENに対して人数カウントデータを送り、保存するところまで行いたいと思います。

この内容は多分以下の3本立てになると思います。今回は以下の3番に該当します。

  1. BOSCH製IPカメラでの人数カウントを行う設定
  2. BOSCH製IPカメラから人数カウントデータを受け取る
  3. 人数カウントデータをEagle Eye Networksに保存する

なぜEENに人数カウントデータを保存するのか

前回までご覧になった方の中には、「カウントデータはCloudantに入れば自由に加工や入出力が可能なのに、なぜEENに保存するのか?」という疑問を持っている方もいらしゃるかと思います。ご存知のとおりCloudantは非常に高速で、スキーマの拡張性も高く、PGからアクセスするという環境の限りでは最も優れたJSONストアの一つと考えられております。しかし、DBという側面を持っている限り「上書き」や「削除」が可能であるということから逃れられません。EENの画像や動画はセキュリティの観点から特定部分だけ削除や上書きを行うことができません。この特性を保持しつつユーザー独自の情報を特定のカメラの特定のタイムスタンプに保持することが新たに可能になりました。この機能が「アノテーション機能」と呼ばれます。この機能を使うことで、業務上の重要なデータを動画とともに、消去される危険性からも開放されながら安全に保管することが可能になります。

アノテーション機能の詳細

ここでは詳しく述べませんので、以下のURLで詳細をご覧ください。

EEN アノテーション(注釈)API

環境、事前確認

前回の投稿の本項目をご参照下さい。

Bluemix側の準備

Bluemix側では前回使用した環境をご用意下さい。
前回インポートしたフローのあるタブに、今回もフローをインポートしていきたいと思います。
そのためには事前に「環境、事前確認」の内容を参考に、環境を整えておいてください。

フローのインポート

以下の内容をコピーして、Node-REDにインポートを行って下さい。

[{"id":"27863746.31c148","type":"debug","z":"2016f88.2cc7808","name":"","active":false,"console":"false","complete":"payload","x":1470,"y":120,"wires":[]},{"id":"b4be2f09.03fa4","type":"http request","z":"2016f88.2cc7808","name":"Get Camera ID","method":"GET","ret":"obj","url":"https://login.eagleeyenetworks.com/g/device/list?A={{auth_key}}&t=camera&s=ATTD","tls":"","x":1140,"y":240,"wires":[["237fc928.708fd6"]]},{"id":"237fc928.708fd6","type":"function","z":"2016f88.2cc7808","name":"Pull CameraID and set JSON","func":"var camLength = msg.payload.length;\n\nfor (itr = 0; itr < camLength; itr++){\n    var camIdx = msg.payload[itr].indexOf(msg.camName);\n    if ( camIdx > 0 ) {\n        msg.camID = msg.payload[itr][camIdx - 1];\n    }\n}\n\nmsg.payload = {\n    \"count\": 1,\n    \"entrance\": msg.entrance\n};\n\nreturn msg;","outputs":1,"noerr":0,"x":1180,"y":180,"wires":[["80ed19d1.722e48","be152d43.10f24","3419a025.99247"]]},{"id":"80ed19d1.722e48","type":"http request","z":"2016f88.2cc7808","name":"","method":"PUT","ret":"txt","url":"https://login.eagleeyenetworks.com/annt/set?c={{camID}}&ts={{st_UTC}}&ns=70000&A={{auth_key}}","tls":"","x":1130,"y":120,"wires":[["27863746.31c148"]]},{"id":"be152d43.10f24","type":"debug","z":"2016f88.2cc7808","name":"","active":false,"console":"false","complete":"true","x":1450,"y":240,"wires":[]},{"id":"3419a025.99247","type":"debug","z":"2016f88.2cc7808","name":"","active":false,"console":"false","complete":"false","x":1470,"y":180,"wires":[]},{"id":"f0eced9f.99cd5","type":"function","z":"2016f88.2cc7808","name":"Get Auth Head","func":"if (msg.req) {\n    msg.orgheaders=msg.req.headers;\n}\n\nmsg.auth_key = global.get(\"auth_key\");\n\nif (msg.st) {\n    var st_UTC = msg.st;\n} else {\n    var st_UTC = \"2017/1/1 00:00:00\";\n}\n\nvar st_UTC_do = new Date(st_UTC);\nmsg.st_UTC = [st_UTC_do.getFullYear(),\n                ( '0' + (st_UTC_do.getMonth() + 1)).slice(-2),\n                ( '0' + st_UTC_do.getDate() ).slice(-2),\n                ( '0' + st_UTC_do.getHours() ).slice( -2 ),\n                ( '0' + st_UTC_do.getMinutes() ).slice( -2 ),\n                ( '0' + st_UTC_do.getSeconds() ).slice( -2 ) + \".000\"\n                ].join(\"\");\n\nvar st_JST_do = new Date(st_UTC_do.getTime() + 32400000);\nmsg.st_JST = [st_JST_do.getFullYear() + \"/\",\n                ( '0' + (st_JST_do.getMonth() + 1)).slice(-2) + \"/\",\n                ( '0' + st_JST_do.getDate() ).slice(-2) + \" \",\n                ( '0' + st_JST_do.getHours() ).slice( -2 ) + \":\",\n                ( '0' + st_JST_do.getMinutes() ).slice( -2 ) + \":\",\n                ( '0' + st_JST_do.getSeconds() ).slice( -2 )\n                ].join(\"\");\n\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":240,"wires":[["b4be2f09.03fa4"]]}]

インポートが完了すると、以下のようなフローが表示されます。
red2.png

今回はこのフローを前回のフローに結合します。「Set time and count」というノードと、「Get Auth Head」というノードを接続します。接続すると以下のようなフローになるはずです。
red3.png

これでアノテーションの書き込み準備ができました。
しかしこのままではアノテーションを読み込む方法がありません。そこでEEN上のアノテーションを読み込むフローもインポートしてみましょう。以下のJSONをNode-REDでインポートしてみてください。

[{"id":"8eb269a9.40feb8","type":"function","z":"93a3d3ac.ce43b","name":"Get Auth Head","func":"if (msg.req) {\n    msg.orgheaders=msg.req.headers;\n}\n\nif (msg.payload.auth_key) {\n    msg.auth_key = msg.payload.auth_key;\n} else {\n    msg.auth_key = global.get(\"auth_key\");\n}\n\nmsg.camName = msg.payload.camName;\n\nif (!msg.payload.start) {\n    var now_UTC_do = new Date();\n    msg.et_UTC = [now_UTC_do.getFullYear(),\n        ( '0' + (now_UTC_do.getMonth() + 1)).slice(-2),\n        ( '0' + now_UTC_do.getDate() ).slice(-2),\n        ( '0' + now_UTC_do.getHours() ).slice( -2 ),\n        ( '0' + now_UTC_do.getMinutes() ).slice( -2 ),\n        ( '0' + now_UTC_do.getSeconds() ).slice( -2 )\n        ].join(\"\");\n            \n    var st_UTC_do = new Date(now_UTC_do.getTime() - 604800000);\n    msg.st_UTC = [st_UTC_do.getFullYear(),\n        ( '0' + (st_UTC_do.getMonth() + 1)).slice(-2),\n        ( '0' + st_UTC_do.getDate() ).slice(-2),\n        ( '0' + st_UTC_do.getHours() ).slice( -2 ),\n        ( '0' + st_UTC_do.getMinutes() ).slice( -2 ),\n        ( '0' + st_UTC_do.getSeconds() ).slice( -2 )\n        ].join(\"\");\n} else {\n    var st_JST_do = new Date(msg.payload.start);\n    var st_UTC_do = new Date(st_JST_do.getTime() - 32400000);\n    msg.st_UTC = [st_UTC_do.getFullYear(),\n        ( '0' + (st_UTC_do.getMonth() + 1)).slice(-2),\n        ( '0' + st_UTC_do.getDate() ).slice(-2),\n        ( '0' + st_UTC_do.getHours() ).slice( -2 ),\n        ( '0' + st_UTC_do.getMinutes() ).slice( -2 ),\n        ( '0' + st_UTC_do.getSeconds() ).slice( -2 )\n        ].join(\"\");\n    \n    var et_JST_do = new Date(msg.payload.end);\n    var et_UTC_do = new Date(et_JST_do.getTime() - 32400000);\n    msg.et_UTC = [et_UTC_do.getFullYear(),\n        ( '0' + (et_UTC_do.getMonth() + 1)).slice(-2),\n        ( '0' + et_UTC_do.getDate() ).slice(-2),\n        ( '0' + et_UTC_do.getHours() ).slice( -2 ),\n        ( '0' + et_UTC_do.getMinutes() ).slice( -2 ),\n        ( '0' + et_UTC_do.getSeconds() ).slice( -2 )\n        ].join(\"\");\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":360,"y":740,"wires":[["13e8aea2.2e3351"]]},{"id":"523d10e1.b00f8","type":"debug","z":"93a3d3ac.ce43b","name":"","active":true,"console":"false","complete":"true","x":830,"y":800,"wires":[]},{"id":"13e8aea2.2e3351","type":"http request","z":"93a3d3ac.ce43b","name":"Get Camera ID","method":"GET","ret":"obj","url":"https://login.eagleeyenetworks.com/g/device/list?A={{auth_key}}&t=camera&s=ATTD","tls":"","x":360,"y":800,"wires":[["9a858e59.e9d2e"]]},{"id":"9a858e59.e9d2e","type":"function","z":"93a3d3ac.ce43b","name":"Pull CameraID","func":"var camLength = msg.payload.length;\n\nfor (itr = 0; itr < camLength; itr++){\n    var camIdx = msg.payload[itr].indexOf(msg.camName);\n    if ( camIdx > 0 ) {\n        msg.camID = msg.payload[itr][camIdx - 1];\n    }\n}\n\nmsg.headers = {\n    'cookie': 'auth_key=' + msg.auth_key,\n    'Content-type': 'application/json'\n};\n\nmsg.payload = {};\n\nreturn msg;","outputs":1,"noerr":0,"x":620,"y":740,"wires":[["129bb51a.785dab","f9fc4909.f6b5a8"]]},{"id":"129bb51a.785dab","type":"http request","z":"93a3d3ac.ce43b","name":"","method":"GET","ret":"obj","url":"https://login.eagleeyenetworks.com/annt/annt/list?id={{camID}}&st={{st_UTC}}.000&et={{et_UTC}}.000","tls":"","x":610,"y":800,"wires":[["523d10e1.b00f8","8478308d.7d863"]]},{"id":"f9fc4909.f6b5a8","type":"debug","z":"93a3d3ac.ce43b","name":"","active":true,"console":"false","complete":"true","x":830,"y":680,"wires":[]},{"id":"569482e3.83c5bc","type":"inject","z":"93a3d3ac.ce43b","name":"","topic":"","payload":"{\"start\":\"2017/08/28 08:00:00\",\"end\":\"2017/08/28 09:00:00:00\",\"camName\":\"BOSCH_DINION_IP_ultra_8000\"}","payloadType":"json","repeat":"","crontab":"","once":false,"x":110,"y":680,"wires":[["8eb269a9.40feb8"]]},{"id":"38ba92a7.6c394e","type":"http in","z":"93a3d3ac.ce43b","name":"get Annotations","url":"/getAnnt","method":"get","swaggerDoc":"","x":140,"y":740,"wires":[["8eb269a9.40feb8"]]},{"id":"24f9687f.aee938","type":"http response","z":"93a3d3ac.ce43b","name":"","x":1070,"y":740,"wires":[]},{"id":"54b564bc.f7a9ac","type":"comment","z":"93a3d3ac.ce43b","name":"BOSCH + EEN Annotation","info":"","x":170,"y":620,"wires":[]},{"id":"8478308d.7d863","type":"contrib-json","z":"93a3d3ac.ce43b","engine":"JSONPath","command":"jq","expr":"$..[?(@.count == 1)]","complete":"property","prop":"payload","name":"Filter count","x":850,"y":740,"wires":[["24f9687f.aee938","e13f91d.ec3337"]]},{"id":"e13f91d.ec3337","type":"debug","z":"93a3d3ac.ce43b","name":"","active":true,"console":"false","complete":"false","x":1090,"y":800,"wires":[]}]

インポートが完了すると、以下のようなフローが読み込まれるはずです。

red4.png

試してみよう!

前回カメラ側の設定が完了していれば、前回同様のテストを行うだけです。
各デバッグノードをオンにして、実際にカメラの前を横切ってみましょう。
LineOver.png

上のスナップショットは便宜的に緑色の線を入れておりますが、ここではこの部分にライン超えの線があると仮定しています。

前回同様、横切った際にカメラが認識しBluemixに情報を転送され、Bluemix側で受け取れれば、以下のようなメッセージが「debug」タブにJSONオブジェクトとして表示されます。この情報は構成済みのCloudantにも記録されます。

{ timestamp: "2017-07-25-02.43.50.000000", 
count: 1, 
entrance: "Exit", 
camera: "BOSCH_DINION_IP_ultra_8000" }

Eagle Eyeと連携してみよう

今回は同様にEagle Eye上にも記録されているはずですので、記録を参照してみましょう。
今回インポートしたノードの中に「Inject」ノードがありますので、これを一部編集します。

red5.png

Injectの中身はJSONなのですが、JSONの記述欄の右端の「…」ボタンを押すと上記の画面が表示されます。
検出対象のカメラ名、検索開始/終了のタイムスタンプを編集し、保存します。
保存できたらInjectのボタンを押してみましょう。次の画面のようなJSON応答がデバッグ画面に表示されるはずです。

red6.png

これでBOSCHの人数カウント機能からBluemixに連動し、Eagle Eye上にデータを保管するところまでできました。
あとは必要に応じてデータ加工し、可視化などするといいかもしれません。

ライセンス、著作権等

本スクリプト、ノード構成、エクスポート内容についてはMIT Licenseを適用します。
Eagle Eye Networksの商標およびロゴについてはEagle Eye Networks, Inc.に帰属します。
IBMおよびIBM Bluemix、Node-Redの商標およびロゴについてはInternational Business Machines Corporationに帰属します。
node.jsの商標およびロゴについてはNode.js contributorsに帰属します。
BOSCH及びBOSCHロゴは、、 Robert Bosch GbmHおよびその関連会社(以下、「ボッシュ」)の著作権として、著作権に関する各種国際条約、各国の著作権法およびその他の各種法律(以下、「法律等」)によって保護されています。