カメラに向かってニッコリ微笑むと、予め登録しているLINEグループに笑顔のスタンプが送られ、スタンプが送られたことを音声で知ることができます。
そんなカメラを作りました。
作るとき考えたこと
- ボタンを押す・文字を入力するなど、面倒なので操作を必要としないようにする。
- カメラの画像を送ると色々と気を使うので、画像は送らないようにする。
- 運用にお金をかけない。(電気代は除く。)
こんな使い方があるかも
- 実家で一人暮らしをしている母の安否確認
- 小学校から帰った子どもの帰宅確認
などなど。
デモ動画
仕様
- カメラに笑顔が写ったときだけ、スタンプを送ります。
- スタンプを送ると、送ったことを音声で知ることができます。
- スタンプは連続投稿しないようにしています。
(一定時間経過するまでスタンプ送信を抑止しています。)
※笑顔検知のみ実装していますが、悲しみ・サプライズ・驚き・恐怖なんかも検知してLINEスタンプを送ることも可能です。
顔検知について
顔検出にはgoogle CloudのCould Vision APIを使うと良いかと思いますが、運用にお金はかけたくなかったので、、以下顔検知のjavacriptライブラリ:face-api.jsを使用しました。
https://github.com/justadudewhohacks/face-api.js/
材料
品名 | 値段 |
---|---|
obniz Borad 1Y | 6930円 |
KKHMF ISD1820 音声録音モジュール | 168円 |
Arducam Mega2560ボード用OV2640カメラモジュール | 3280円 |
ELEGOO Arduino用改良電子エレクトロニクス キット | 1400円 |
回路図
外観
ソースコード
以下【index.html】【index.js】を作成しました。
index.html
<br lang="ja">
<head>
<meta charset="UTF-8">
<title>笑顔を検知するとLINEスタンプを送るカメラ</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/obniz@3.14.0/obniz.js"></script>
<script src="./dist/face-api.min.js"></script>
</head>
<body>
<!-- obniz接続確認 -->
<!-- <div id="obniz-debug"></div> -->
<!-- ナビバー -->
<ul class="nav justify-content-center">
<li class="nav-item">
<button type="button" class="btn btn-primary" id="startAndStop" style="width:300px" disabled>Start</button>
</li>
</ul>
<img id="image" src="./mac.jpg" style="display:none">
<canvas id="outPutImage" class="rounded mx-auto d-block overflow-auto" width=800 height=600 style="border:1px solid;width:800px;-webkit-font-smoothing:none"></canvas>
<div id="notifiedLog" class="mx-auto d-block" style="border:1px solid;overflow-y:scroll;width:800px;height:100px"></div>
<div id="log" class="mx-auto d-block" style="border:1px solid;overflow-y:scroll;width:800px;height:100px"></div>
<ul class="nav justify-content-center">
<li class="nav-item">
<button type="button" class="btn btn-info" id="testSendLine">LINE通知テスト用ボタン</button>
</li>
<li class="nav-item">
<button type="button" class="btn btn-info" id="testNotify">LINE通知音用ボタン</button>
</li>
</ul>
<script>
let obniz = new Obniz("##########"); //obnizIDを入力する
const DO_NOT_NOTIFY_PERIOD = 30000; //LINE通知を抑止する時間
let streaming = false; //ストリーミング判定フラグ
let initialized = false; //初期化判定フラグ
let isLineNotify = true; //LINE通知判定フラグ
let cam = ""; //カメラオブジェクト
let img = document.getElementById('image')
let startAndStop = document.getElementById('startAndStop');
let notifiedLog = document.getElementById('notifiedLog');
let canvas = document.getElementById('outPutImage')
let ctx = canvas.getContext("2d");
// obnizに接続する
obniz.onconnect = async function () {
let cam = obniz.wired("ArduCAMMini", { cs:0, mosi:1, miso:2, sclk:3, gnd:4, sda:6, scl:7, module_version:1 });
// obnizの接続が成功した場合、スタートストップボタンを有効化する
if (obniz.connectionState === "connected") {
startAndStop.disabled = false;
}
// スタートストップボタンをリスナーに登録する
startAndStop.addEventListener('click', () => {
if(!streaming) {
onStreamingStarted(cam);
} else {
onStreamingStoped();
}
});
}
// スタートボタンクリック時
async function onStreamingStarted(cam) {
_log("onStreamingStarted","started");
streaming = true;
startAndStop.innerText = 'Stop';
// 初期化処理
if (!initialized){
await init(cam);
}
await getImage(cam);
}
// ストップボタンクリック時
function onStreamingStoped() {
_log("onStreamingStoped","stoped");
streaming = false;
startAndStop.innerText = 'Start';
}
// スタートボタンクリック時(初期化)
async function init(cam) {
_log("init()","initialized");
initialized = true;
await faceapi.nets.faceExpressionNet.loadFromUri('./weights/')
await faceapi.nets.tinyFaceDetector.load("./weights/")
// obnizのカメラセットアップ
await cam.startupWait();
}
/**
* カメラから画像を取得する
*
*/
async function getImage(cam) {
async function processImage() {
if (!streaming) {
return;
}
try {
// カメラから画像を取得する
const data = await cam.takeWait('800x600'); //'160x120'or'176x144'or'320x240'or'352x288'or'640x480'or'800x600'or'1024x768'or'1280x960'or'1600x1200'を指定する
const base64 = cam.arrayToBase64(data);
document.getElementById("image").src = "data:image/jpeg;base64, " + base64;
// 画像を解析する
await detectFace();
// 繰り返し
window.requestAnimationFrame(processImage);
} catch (err) {
_log("processImage", "error");
}
}
// 初回
window.requestAnimationFrame(processImage);
}
/**
* 取得したカメラ画像から顔検出情報を取得して、canvasへ描写する
*
*/
async function detectFace() {
const displaySize = { width: img.naturalWidth, height: img.naturalHeight }
const detections = await faceapi.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions()).withFaceExpressions()
const resizedDetections = await faceapi.resizeResults(detections, displaySize)
// console.log(detections);
await ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
await faceapi.draw.drawDetections(ctx,resizedDetections);
if (!detections.length || !isLineNotify) {
return;
}
detectData(detections);
}
/**
* 顔検出結果から表情を取得して、LINEへリクエストする
*
*/
async function detectData(detections) {
const resultExpression = await detections.map( data => {
return judgeExpression(data);
});
if (!isLineNotify) {
return;
}
_log("detectData_resultExpression", resultExpression)
if(resultExpression != "no_expression"){
fetch('/sendLine/' + resultExpression).then(response => {
if (response.status == "200") {
notifiedLine(response, resultExpression);
console.log(response);
};
});
}
}
/**
* 表情検出した結果から感情を抽出する。
*
*/
function judgeExpression(data) {
if (data.expressions.happy >= 0.8) {
return "happy"
};
// 他の表情を検出する場合には、以下のコードをコメントアウトする
// if (data.expressions.sad >= 0.8) {
// return "sad"
// }
// if (data.expressions.angry >= 0.8){
// return "angry"
// }
// if (data.expressions.surprised >= 0.8){
// return "suprised"
// }
return "no_expression";
};
/**
* LINE通知をお知らせして、ログにスタンプを送ったことを表示する
*
*/
function notifiedLine(response, resultExpression) {
// 音声モジュールを再生する
obniz.io11.output(true);
setTimeout(obniz.io11.end(), 100);
let date1 = new Date();
notifiedLog.innerText = date1.toLocaleString("ja") + ":" + resultExpression + "スタンプ送ったよ"
isLineNotify = false;
_log("notifiedLine", "isLineNotify=false")
setTimeout(function(){
isLineNotify = true;
_log("notifiedLine", "isLineNotify=true")
}, DO_NOT_NOTIFY_PERIOD);
}
/**
* ログ出力用の関数
*
*/
function _log(methodName, msg) {
const date1 = new Date();
let log = document.getElementById("log");
log.innerHTML += date1.toLocaleString() + ":" + methodName + ":" + msg + "<br/>";
log.scrollTop = log.scrollHeight;
}
/**
* テスト用
*/
// LINE通知テスト用のスクリプト
let test = document.getElementById('testSendLine');
test.addEventListener('click', () => {
fetch('/sendLine/happy').then(response =>{
notifiedLine(response);
_log("testSendLine", response.state);
});
});
// LINE通知音声テスト用のスクリプト
let noti = document.getElementById('testNotify');
noti.addEventListener('click', () => {
obniz.io11.output(true);
setTimeout(obniz.io11.end(), 100);
_log("testSendLine", "execute");
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</body>
</html>
index.js
const axios = require('axios');
const qs = require('querystring');
const app = express();
const LINE_NOTIFY_API_URL = 'https://notify-api.line.me/api/notify';
const LINE_NOTIFY_TOKEN = '######################################'; // LINEトークン
// 表情をキーにスタンプ情報を保持している
const expressionObj = {
happy: {
stickerId: "1988",
stickerPackageId: "446",
message: '元気だよー',
},
// 他の表情を有効すする場合には以下コメントを有効化する
// sad: {
// stickerId: "2008",
// stickerPackageId: "446",
// message: '悲しいー',
// },
// angry: {
// stickerId: "2019",
// stickerPackageId: "446",
// message: '怒ーーー',
// },
// suprised: {
// stickerId: "2011",
// stickerPackageId: "446",
// message: '!!!',
// }
}
/**
* LINE通知用リクエスト内容を作成する
*
*
* @param {String} expression 感情を表す文字列
* @returns
*/
function configExpression(expression) {
return config = {
url: LINE_NOTIFY_API_URL,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer ' + LINE_NOTIFY_TOKEN
},
data: qs.stringify(expressionObj[expression])
}
}
/**
* LINEへリクエストする
*
* @param {String} expression 感情を表す文字列
*/
async function sendRequest(expression) {
try {
const responseLINENotify = await axios.request(configExpression(expression));
console.log(responseLINENotify.data);
} catch (error) {
console.error(error);
}
}
app.use(express.static('web'));
app.get('/sendLine/:expression', (req, res) =>{
sendRequest(req.params.expression);
res.json(req.params.expression);
});
app.listen(3000, ()=> console.log('Listening on port 3000'));
pakage.json (インストールパッケージはaxios、express、face-api.jsです)
"name": "obniz_1",
"version": "0.1.0",
"description": "obniz_test",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"express": "^4.17.1",
"face-api.js": "^0.22.2"
}
}
階層構造の参考までに、ソースコードは以下GitHubリポジトリに格納しています。
https://github.com/mako999/obniz_line_camera
実行手順
1)ソースコードを保存したいディレクトリに移動して、以下コマンドでGitHubからGit cloneでダウンロードする。
# git clone https://github.com/mako999/obniz_line_camera
2)obniz_line_cameraディレクトリが生成されるので、このディレクトリに移動する。
# cd obniz_line_camera
3)npm(パッケージマネージャー)で依存パッケージをインストールする。
# npm install
4)以下コマンドでプログラムを実行する。
# node index.js
5)WEBブラウザで「localhost:3000」にアクセスする。
6)画面に表示される【start】ボタンをクリックするとカメラ画像が表示され、
カメラ画像に笑顔の人物が表示されると、LINEに通知する。
まとめ
・obnizはプログラミングで躓きがちな環境構築がとても簡単。
・obnizパーツライブラリの部品は、簡単に連携できる。
・javascriptのライブラリが使える。
電子工作は初めてでしたが、アイデアと部品さえあれば大概作れそうな気にさせてくれます^^。(部品は高いですが。。。。)
obnizはWEBブラウザからブロックプログラミングできるので、今度は子供と一緒にブロックプログラミングで何か作ってみたいと思います。
-
makoto
さんが
2021/05/08
に
編集
をしました。
(メッセージ: 初版)
-
makoto
さんが
2021/05/08
に
編集
をしました。
ログインしてコメントを投稿する