2.Teachable Machineを利用した接触判定システム

Teachable Machineをp5.jsとTensorflow.jsで動かし、それをwebsocket通信でTouchdesignerに送信します。

Teachable Machineについて

誰でも簡単に画像認識やPoseNet、音声認識ができるサービスです。
これをp5.jsで動かすには、Danniel ShiffmanのYouTubeチャンネルThe coding trainがわかりやすいです。

今回は、手がペンに触れたか触れていないかの判定を、Teachable Machineの画像認識を用いて行います。

Teachable Machineで画像認識

まず、Teachable Machineのサイトに飛びます。「使ってみる」から「画像プロジェクト」を選びます。※日本語サイトの仕様に沿っています
今回は、触れているか触れていないかの2つを判定するので、class名はtouchedとuntouchedにしておきます。

スクリーンショット 2021-02-10 17.38.05

手元の画像が必要なため、webカメラで事前に撮影した動画を、ffmpegを使って画像にしました。こちらのサイトを参考にしています。
https://qiita.com/livlea/items/a94df4667c0eb37d859f
画像を読み込ませたあとはこんな感じ。

スクリーンショット 2021-03-16 22.37.28

大体1200枚ずつくらい学習させてます。
あとはDannielの動画の通りです。
p5.jsの公式のwebEditorを使えば、きちんと動くと思います。

Teachable Machineの結果をNode.jsとwebsocket通信で送信する

公式のwebEditorで動いたTeachable Machineですが、結果を外部のソフトに送信するためには、Node.jsというのを使う必要があります。
p5.jsとNode.jsを使うチュートリアルは、これまたDannielの動画があります。

Node.jsの使い方については、Dannielが詳しく動画で言ってくれているので、ここでは説明しません。
ただし、Dannielの動画内で使用しているsocket.ioというサービスは、Touchdesigner2020ではver1.6.1までしかサポートされていません。(2019版は完全に使えません。現在のバージョンは試していないのでわかりません)
そして、Teachable MachineとNode.jsを合体させようとしたところ、Dannielの動画のやり方のままではうまく動きませんでした。
私がぶつかった問題点と、どうやって解決したかを書いていきます。

問題点1:Teachable Machineで使用しているml5.jsがlocalサーバーでは動かなかった

上記のDannielの動画では、ml5.jsというライブラリを使用していましたが、node.jsでローカルサーバーを立て動かしたところ、「ml5.jsはローカルサーバーでは使えないよ」的なエラーが出ました。

(2021/8 追記):調べたところ、問題なく使えるっぽい?です。当時の私の調べ方が足りなかったのだと思います。すみません。
結局制作にあたってml5.jsは使わなかったので、実際に使ったやり方を書いておきます。

困っていたところ、Tensorflow.jsというライブラリなら動くらしい記事を見つけ、さらにこのgithubのページを見つけました。
https://github.com/yining1023/teachable-machine-p5
拝みました。ありがたく使わせていただきます。
このプロジェクトの中の、"imageclassifier"の中の"imageclassifier-on-webcam"というファイルを使っていきます。とりあえず動かしてみるとうまくいったので、Teachable MachineのURLを自分で作成したものに変更します。
さらに今回はwebサイトとして見た目を綺麗にしなくていいので、htmlとsketch.jsを整理します。jsonファイル・weightsファイルの読み込みのあたりだとか、予測の自信度(?)を表示しているあたりは消しました。

問題点2:Touchdesignerがsocket.ioに対応していない

既に上で書きましたが、Touchdesigner2020でもsocket.ioのver1.6.1までしか対応してません。代わりに、websocket DATが使えるとあったので、以下のサイトを参考にして、server.jsとsketch.jsを書き直しました。
https://github.com/kodai100/WebSocketWithTouchDesigner-WS

コード載せておきます。

sketch.js

//connect to local server
let host = location.hostname;
let ws = new WebSocket("ws://" + host + ":13253");


//Teachable Machine url
const modelURL = 'https://teachablemachine.withgoogle.com/models/3IM7iUU5e/';
const checkpointURL = modelURL + "model.json";
const metadataURL = modelURL + "metadata.json";


let webcam;
let w = 640,
 h = 480;

let model;
let totalClasses;

//load model
async function load() {
 model = await tmImage.load(checkpointURL, metadataURL);
 totalClasses = model.getTotalClasses();
 console.log("Number of classes, ", totalClasses);
}

async function loadWebcam() {
 webcam = new tmImage.Webcam(w, h); 
 await webcam.setup();
 await webcam.play();
 window.requestAnimationFrame(loopWebcam);
}

async function loopWebcam(timestamp) {
 webcam.update(); // update the webcam frame
 await predict();
 window.requestAnimationFrame(loopWebcam);
}


async function setup() {
 myCanvas = createCanvas(w, h);
 ctx = myCanvas.elt.getContext("2d");

//for check connection
 ws.onmessage = function (event) {
   console.log(event.data);
 }

 // Call the load function, wait until it finishes loading
 await load();
 await loadWebcam();
}

async function predict() {
 // in this case we set the flip variable to true since webcam 
 // was only flipped in CSS 
 const prediction = await model.predict(webcam.canvas, true, totalClasses);

 // Sort prediction array by probability
 // So the first classname will have the highest probability
 const sortedPrediction = prediction.sort((a, b) => - a.probability + b.probability);
 let message = sortedPrediction[0].className;

 //show the classname in html
 const judge = select('#judgeResult');
 judge.html(message);
 
 //send classname to server
 ws.send(message);

 if (webcam.canvas) {
   ctx.drawImage(webcam.canvas, 0, 0);
 }
}

server.js

let express = require('express'),
   http = require('http'),
   url = require('url'),
   path = require('path'),
   webSocket = require('ws');

let app = express(),
   server = http.createServer(app),
   wss = new webSocket.Server({ server:server });

// client connections
let connects = []

app.use(express.static(path.join(__dirname, '/public')));

// Called when success building connection
wss.on('connection', function (ws, req) {
   var location = url.parse(req.url, true);

   var initMessage = {message:"connection"};
   ws.send(JSON.stringify(initMessage));
   connects.push(ws);
   console.log("New Client Connected : " + connects.length);

   // Callback from client message
   ws.on('message', function (message) {
       //console.log('received: %s', message);
       broadcast(message);  // Return to client
   });

   ws.on('close', function () {
       console.log('A Client Leave');
       connects = connects.filter(function (conn, i) {
           return (conn === ws) ? false : true;
       });
   });

});

server.listen(13253, function listening() {
   console.log('Listening on %d', server.address().port);
});

// Implement broadcast function because of ws doesn't have it
function broadcast (message) {
   connects.forEach(function (socket, i) {
       socket.send(message);
   });
}

問題点1と2で引用したサイトを悪魔合体させただけです。参考までに。
重要なのは、

server.listen(13253, function listening() {
   console.log('Listening on %d', server.address().port);
});

この部分でポート番号(13253)を指定しています。TouchDesigner側でもこの値を使います。

この記事が気に入ったらサポートをしてみませんか?