〜Teachable Machine と HTML/CSS/JavaScript で完結〜
お金がなくてもできる!ブラウザだけで記憶テスト開発
社内で新規事業を立ち上げて4年。シニアケアをされている方々とメーカーさまをお繋ぎし、サポートするプラットフォーム事業を進めています。
お客さまへのウェブサービスや店舗でのイベントに加え、実際に店舗を構えて商品やサービスをご案内しながら、シニアケアや介護の備えを日常的にお伝えする取り組みも重ねてきました。
そうした活動を通じて、シニアの方やそのご家族、介護施設では認知機能の変化に早めに気づくことがとても重要だと感じています。
しかし本格的な検査機器を購入・レンタルするには高額で、新規事業の私たちには手が出せません…
そこで「お買い物やイベントのついでに、気軽に試せる記憶テスト」を
Webブラウザだけで作ることに挑戦しました。
#今回の目標は
- 専用アプリ不要。パソコンやタブレットのブラウザだけで動作
- 写真を見て覚え、30秒後に描き出すシンプルなテスト
- AIによる5段階自動採点
を実現することです。
使用した技術
今回の仕組みは驚くほどシンプルです。
役割 | 使用した技術 |
---|---|
学習とモデル公開 | Teachable Machine |
AI推論エンジン |
TensorFlow.js + @teachablemachine/image
|
画面UI / ロジック | HTML + CSS + JavaScript |
サーバーやデータベースは不要。
作ったファイル(index.html
と画像1枚)を持ち運べば、
どのPCでもブラウザでそのまま起動できます。
開発プロセス
1. 記憶する絵の作成
-
Canvaを使って
-
A4横向き・塗りなし・線は均一。
-
ファイル名は
scene.jpg
として保存。
この一枚の画像を、来店者に30秒見てもらいます。
2. Teachable Machine でAIモデルを学習
- Teachable Machine にGoogleアカウントでログイン。
- 「使ってみる」→「Image Project」→「Standard image model」。
- クラスを5つ作成し、名前を
score_1
,score_2
,score_3
,score_4
,score_5
とします。
- 各クラスに学習用画像をアップロード。
例:score_5
はほぼ完全に描いた絵、score_1
はほとんど描かない、など。
※それぞれのscore用に5つのサンプルを手書きで用意(Makeに作らせてもOK)
5. Train Model(モデルをトレーニング)をクリック。
6. 学習完了後、Export Model → Tensorflow.js → Upload (Hosted) → Upload My Model
を押してモデルを公開。
→ 共有可能なURLが発行されます
(例:https://teachablemachine.withgoogle.com/models/DhnU5RhaX/
)。
このURLを控えておきます。
ここがAI判定の心臓部になります。
3. Webページ(index.html)の作成
プロジェクト用フォルダを作成し、
以下の2つを置きます。
memory-test/
├ index.html ← 完成したHTML
└ scene.jpg ← 記憶する風景画像
エディタ(VS Codeやメモ帳)で index.html
を新規作成し、
以下のコードをそのまま貼り付けます。
実際のコード全文
(Teachable MachineのモデルURLは自分のURLに置き換えてください)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>記憶テスト(風景)</title>
<style>
body{font-family:sans-serif; max-width:900px; margin:24px auto; padding:0 12px;}
.hidden{display:none;}
#scene{max-width:100%; border:1px solid #ddd;}
#timer{font-size:20px; font-weight:700; margin:8px 0 16px;}
canvas{border:1px solid #ddd; width:100%; max-width:900px; height:480px; touch-action:none; background:#fff}
button{padding:10px 16px; margin:8px 8px 0 0;}
#score{font-size:20px; font-weight:700; margin-top:8px;}
.pill{display:inline-block; background:#f5f5f5; border:1px solid #e5e5e5; border-radius:9999px; padding:6px 10px; margin:4px 6px;}
</style>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@latest/dist/teachablemachine-image.min.js"></script>
</head>
<body>
<h1>記憶テスト(風景)</h1>
<section id="phase1">
<p>下の画像を<strong>30秒</strong>で記憶してください。</p>
<div id="timer">のこり 30 秒</div>
<img id="scene" src="scene.jpg" alt="記憶する風景" />
</section>
<section id="phase2" class="hidden">
<h2>再現タイム(自由描画)</h2>
<canvas id="pad" width="900" height="480"></canvas><br/>
<button id="clear">消す</button>
<button id="finish">描き終えた</button>
</section>
<section id="phase3" class="hidden">
<h2>評価</h2>
<div id="checklist"></div>
<button id="calc">採点する</button>
<div id="score"></div>
<div id="rubric"></div>
<hr/>
<h3>AI自動採点</h3>
<button id="aiFromCanvas">キャンバスをAIで採点</button>
<input type="file" id="photo" accept="image/*" capture="environment" />
<button id="aiFromPhoto">写真をAIで採点</button>
<div id="aiResult" style="margin-top:12px; font-weight:700;"></div>
</section>
<script>
const phase1=document.getElementById('phase1');
const phase2=document.getElementById('phase2');
const phase3=document.getElementById('phase3');
const timerEl=document.getElementById('timer');
let remain=30;
const tick=setInterval(()=>{
remain--;
timerEl.textContent=`のこり ${remain} 秒`;
if(remain<=0){clearInterval(tick);phase1.classList.add('hidden');phase2.classList.remove('hidden');}
},1000);
// お絵描き
const canvas=document.getElementById('pad');
const ctx=canvas.getContext('2d');
let drawing=false,lastX=0,lastY=0;
function startDraw(x,y){drawing=true;lastX=x;lastY=y;}
function draw(x,y){if(!drawing)return;ctx.lineJoin='round';ctx.lineCap='round';ctx.lineWidth=3;ctx.strokeStyle='#000';ctx.beginPath();ctx.moveTo(lastX,lastY);ctx.lineTo(x,y);ctx.stroke();lastX=x;lastY=y;}
function stopDraw(){drawing=false;}
canvas.addEventListener('mousedown',e=>startDraw(e.offsetX,e.offsetY));
canvas.addEventListener('mousemove',e=>draw(e.offsetX,e.offsetY));
canvas.addEventListener('mouseup',stopDraw);
canvas.addEventListener('mouseleave',stopDraw);
// スマホ対応
canvas.addEventListener('touchstart',e=>{e.preventDefault();const r=canvas.getBoundingClientRect();const t=e.touches[0];startDraw((t.clientX-r.left)*(canvas.width/r.width),(t.clientY-r.top)*(canvas.height/r.height));},{passive:false});
canvas.addEventListener('touchmove',e=>{e.preventDefault();const r=canvas.getBoundingClientRect();const t=e.touches[0];draw((t.clientX-r.left)*(canvas.width/r.width),(t.clientY-r.top)*(canvas.height/r.height));},{passive:false});
canvas.addEventListener('touchend',e=>{e.preventDefault();stopDraw();},{passive:false});
document.getElementById('clear').onclick=()=>ctx.clearRect(0,0,canvas.width,canvas.height);
document.getElementById('finish').onclick=()=>{phase2.classList.add('hidden');phase3.classList.remove('hidden');};
// 手動採点
const FEATURES=[
'左上に太陽がある','雲が2つある','三角形の山が2つある',
'中央に家がある','家に窓が2つある','家にドアがある',
'木が6本ある','前景に川がある'
];
const checklist=document.getElementById('checklist');
checklist.innerHTML=FEATURES.map(f=>`<label class="pill"><input type="checkbox"> ${f}</label>`).join('');
document.getElementById('calc').onclick=()=>{
const boxes=checklist.querySelectorAll('input[type="checkbox"]');
let got=0;boxes.forEach(b=>{if(b.checked)got++;});
const pct=Math.round(100*got/FEATURES.length);
let grade=1;if(pct>=90)grade=5;else if(pct>=70)grade=4;else if(pct>=50)grade=3;else if(pct>=30)grade=2;
document.getElementById('score').textContent=`スコア: ${pct}点(${grade}/5)`;
document.getElementById('rubric').textContent=`評価: ${'★'.repeat(grade)}${'☆'.repeat(5-grade)}`;
};
// AI判定
const TM_URL="https://teachablemachine.withgoogle.com/models/DhnU5RhaX/"; // ★自分のURLに置き換え
let tmModel=null;
async function loadTM(){
const modelURL=TM_URL+"model.json";
const metadataURL=TM_URL+"metadata.json";
tmModel=await window.tmImage.load(modelURL,metadataURL);
}
loadTM();
async function predictBest(img){
const preds=await tmModel.predict(img);
const best=preds.reduce((a,b)=>a.probability>b.probability?a:b);
const grade=parseInt(best.className.replace(/\D/g,''),10)||1;
const prob=Math.round(best.probability*100);
return {grade,prob};
}
document.getElementById('aiFromCanvas').onclick=async()=>{
const res=document.getElementById('aiResult');
res.textContent="判定中...";
const img=new Image();
img.onload=async()=>{const {grade,prob}=await predictBest(img);res.textContent=`AI判定: ${grade}/5(${'★'.repeat(grade)}${'☆'.repeat(5-grade)}) 確信度: ${prob}%`;};
img.src=canvas.toDataURL("image/png");
};
document.getElementById('aiFromPhoto').onclick=async()=>{
const res=document.getElementById('aiResult');
const file=document.getElementById('photo').files[0];
if(!file){res.textContent="写真を選んでください";return;}
res.textContent="判定中...";
const img=new Image();
img.onload=async()=>{const {grade,prob}=await predictBest(img);res.textContent=`AI判定: ${grade}/5(${'★'.repeat(grade)}${'☆'.repeat(5-grade)}) 確信度: ${prob}%`;};
img.src=URL.createObjectURL(file);
};
</script>
</body>
</html>
使い方
scene.jpg と上記 index.html を同じフォルダに置く
Chrome で index.html を開く
30秒見て覚える → キャンバスに描画 →「AI自動採点」ボタンを押す
反省とこれからの改善点
今回の実装は「まず動くものを最短で作る」ことを優先したため、
以下のような課題や改善の余地が見えてきました。
-
学習データの少なさ
手描きサンプルを最小限にしたため、筆圧や描き方の個人差に弱い。
→ 今後は来店者が描いたデータを匿名化して蓄積し、モデルの精度を向上させたい。 -
UI/UXのさらなる工夫
タブレットやスマホでは描画中に指が当たると誤作動する場合がある。
→ タッチ操作の最適化、描画色やペン太さの選択機能を追加したい。 -
利用シーンの広がり
店舗イベントだけでなく、在宅介護や地域コミュニティでも活用できるはず。
→ オフライン利用や印刷用QRコードを検討中。 -
評価の多角化
現状は記憶再現の「形」だけを評価している。
→ 経時変化を記録し、連続テストで傾向を見られる仕組みにしたい。
こうした改善に取り組むことで、
単なる「試せる仕組み」から、
シニアの健康維持や認知症予防をより確実に支援できるサービスへと育てていけると感じています。
ブラウザ完結でここまでできる!
サーバー不要・低コストで実現した“使えるAIアプリ”の実感。
ブラウザだけで動くため、特別なサーバーやアプリは不要
モデルURLを差し替えれば精度向上版へ簡単にアップデート可能
シニアの来店イベントや健康啓発の場で、
気軽に「記憶力チェック」を体験してもらえる
本格的な検査機器を買わなくても
こうした「試せる仕組み」を用意するだけで、
シニアケアの第一歩として大きな価値があると感じました。
これで、誰でもすぐにブラウザだけで「記憶テスト」が提供できます。
ぜひ活用してみてください!