TECH PLAY

電子工作

イベント

マガジン

技術ブログ

はじめに こんにちは!トモニテで開発をしている吉田です。 2026/3/20〜2026/3/22に開催されたPHPerKaigi 2026にスタッフとして参加してきました! PHPerKaigi(ペチパーカイギ)とは以下のようなイベントです! PHPerKaigi(ペチパーカイギ)は、PHPer、つまり、 現在PHPを使用している方、過去にPHPを使用していた方、 これからPHPを使いたいと思っている方、そしてPHPが大好きな方たちが、 技術的なノウハウとPHP愛を共有するためのイベントです。 phperkaigi.jp しかしながら私自身、普段の業務でPHPは書いていません。どうして私が今回参加したのか、PHPを書いていない私が参加してどうだったのか書いていきます。 参加経緯 一番初めのきっかけは昨年開催されたiOSDC 2025で当日スタッフをしたことです。普段はGo言語やTypeScriptを書いていてiOSとは無縁でしたが、スタッフとして参加できました。 iOSDCとPHPerKaigiは実行委員長が同じです。その関係でiOSDC 2025終了後にスタッフの方からPHPerKaigiのスタッフについても話を聞いて、ぜひやってみたいと思い参加させてもらうことにしました! そんな私がどうして普段触る技術領域以外のカンファレンスでスタッフをしているのか。それは 仕事をしてるだけだと出会わない人たちと出会ってみたい!新しい世界を知りたい! と思ったからです。 ここからは実際に参加してみての感想を書いていきます。 参加してみて スタッフについて スタッフにはコアスタッフと当日スタッフの2種類があります。当日スタッフは会期前に事前の顔合わせを行い、当日の運営を担当します。コアスタッフは開催に向けて早い段階から事前準備を進めていてます。 私はコアスタッフとして参加しました。事前準備では名札を首から下げるためのストラップ制作を担当しました。 会期中はTrack Aを担当し、セッションごとに司会や演出を担当したりしていました。演出は場面に応じてスクリーンに映す内容を切り替えるといった役割です。 Track Aではオープニングや通常セッションに加えて、PHPer コードバトルやルーキーズLT、LT大会といった多様なコンテンツが行われていました。 もちろんメインは担当としての仕事ですが、シフトの合間にはスポンサーブースを回ったり、セッションも聞いていました。PHPのカンファレンスではありますが、スピーカーが話す内容はPHPを書いていないと分からないということはなく、それぞれが課題に対してどういうアプローチを取ったのかという手法の部分は、普段の技術領域にも活かせることがあるのではと感じました。 聞いたもの全て興味深かったのですが、特に面白かったものを紹介します。 PHPer コードバトル PHPer コードバトルは、指示された動作をする PHP コードをより短く書けた方が勝ちという 1 対 1 の対戦コンテンツです。予選を勝ち上がったプレイヤー6名がトーナメント形式で対決します。 スコアはコードの空白を除去したバイト数になります。 普段のサービスを動かすためのコードとはまた違うテクニック的な要素も必要になります。 ルールは分かるのですが、正直プレイヤーが具体的にどんなテクニックを使っているのかは分かりません(笑)。 ただ、会場のスクリーンにはリアルタイムにプレイヤーが書いているコードやその瞬間のプログラムサイズが表示され、解説者による解説があります! 個人的にはさながらスポーツ観戦をしているような臨場感で、プレイヤーが大きくプログラムサイズを減らすと会場がどよめくような瞬間もありました。 何より解説があるのでプレイヤーがどういう工夫をしているのか観戦者も知ることができます。 私がコードバトルで学んだのは && と and は優先順位が違うということです。いつかどこかで役立てたいと思います。 参考: PHP: 論理演算子 - Manual コードバトルはシフトが当たっていなくても会場に見に行っていたくらい面白かったです。Track Aの担当にならなければ見ることはなかったと思うので、スタッフをやったからこそ知れた面白さでした! ルーキーズLT/LT大会 ルーキーズLT大会はPHPerKaigiで初めてトークする「ルーキー」たちによる5分のショートトーク、LT大会はスピーカーを限定しないLT大会です。LT大会では参加者がペンライトを振る場面があるのですが、これが会場にとても綺麗な彩りを添えていました! 特に印象に残ったトークを2つ紹介します。まずルーキーズLTから: AI時代の脳疲れと向き合う「言語学としてのPHP」 - プロポーザル / 登壇資料 AI疲れは私自身実感していましたが、それを言語学の観点から考察しているのが新鮮で勉強になりました。ハイコンテキストな日本語話者がローコンテキストな指示を出そうとしていて、これが疲れの原因らしいです...。 「なんでこんな疲れるんだろう...」の原因を知ることができたので、これからは対策が取れそうです。 LT大会からは以下のトークです。 よし、PHPで円でも描いてみるか - プロポーザル / 登壇資料 PHPerKaigi 2024の登壇でもらった質問から「PHPで円を書いてみよう」ということになったそうです! Webやコンソールで描いてみたり、途中では電子工作をされていたり、最終的にはアニメーションする円を実現されていたりと、多種多様な円をPHPで描かれていました。実現過程も面白かったですが話術もすごくてたくさん笑わせてもらいながら聞いていました。 最後に PHPを書いていない私でも参加してみてどうだったかというと、十分に楽しめたし学びもありました。セッションで語られる課題へのアプローチは言語を問わず通じるものが多く、普段の開発にも持ち帰れる気づきがありました。また、コードバトルのようにスタッフとしてTrack Aを担当したからこそ出会えたコンテンツもあり、「仕事をしてるだけだと出会わない人たちと出会ってみたい、新しい世界を知りたい」という動機は十分に満たされました。 これからもカンファレンスのスタッフ活動を続けていきたいと思います!
作業の合間に窓の外を眺めることはありますか? 筆者の席は会社のビルの西側で、午後になると大きな窓から強烈な西日が射し込んできます。 それがあまりに強烈なので、いつもブラインドが閉まっていて外があまり見えません。 そのせいでオフィス内の移動中などに窓から見えた空が真っ暗で驚くということがよくあります。それで困るというわけではありませんが、少し味気ない感じがします。 空の色で時間の移ろいを感じたい。 隙間から覗くこともある というわけで、机に置いて空の色で時間帯を知らせてくれるデバイスを作ることにしました。それだけでもつまらないので、机の上にあったら嬉しいポモドーロタイマーの機能も付けてみ
こんにちは、Insight Edgeの小林まさみつです。本記事は Insight Edge Advent Calendar 2025 の8日目の記事です。 最近は生成AIをソフトウェア領域に応用した開発をしていますが、今回は趣向を変えてハードウェアと組み合わせたシステムを作成してみたので紹介します。 目次 1. はじめに 1.1 なぜ作ったのか 1.2 完成システムの紹介 1.3 この記事で分かること 2. システム概要 2.1 全体構成図 2.2 使用技術スタック 2.3 動作の流れ 3. ハードウェア編:振動モーター制御回路 3.1 必要な部品リスト 3.2 回路図と配線 3.3 動作確認とコード 4. ソフトウェア編:姿勢判定システム 4.1 カメラ設置とPythonでの画像取得 4.2 生成AI(Bedrock Claude Sonnet 4)との連携 4.3 Arduino との通信(シリアル通信) 4.4 統合プログラム 5. 実際に使ってみて 5.1 効果を実感した点 5.2 振動の強さについて 5.3 生成AIの判定精度 6. 今後の展望 7. まとめ 1. はじめに 1.1 なぜ作ったのか 長時間のデスクワークで肩こりや腰痛に悩まされ、「姿勢を良くしなければ」と思いつつも、集中していると姿勢のことなど忘れてしまいます。 市販の姿勢矯正グッズも検討しましたが、高価なものが多く、効果も不透明。そこで「技術で解決できるのでは?」と考え、自作することにしました。 当初はエアバッグで物理的に姿勢を矯正する構想もありましたが、コストと複雑さを考慮し、 振動で姿勢の悪化を通知する シンプルなシステムに方針転換しました。 従来の画像認識ライブラリでは、照明条件や服装の変化で精度が不安定になりがちです。一方、生成AIであれば自然言語で評価基準を定義でき、より柔軟な判定が期待できます。そこで 生成AIで姿勢を評価し、振動で身体に直接フィードバックする 実験的なシステムを構築しました。 1.2 完成システムの紹介 システムは以下の3つの要素で構成されています: Webカメラ : 横から姿勢を撮影 PC : 生成AIで姿勢評価 Arduino+振動モーター : 姿勢の悪い箇所をユーザーに通知 Arduino とは、電子工作を簡単に始められる小型のコンピューター基板(=マイコン)です。 姿勢が悪くなると、該当する部位が振動して通知されます。例えば猫背なら腰が、左に傾いていれば左太ももが振動する仕組みです。 動作イメージは以下の通りです。 Webカメラが5分ごとにユーザーの姿勢を撮影 左の画像は良い姿勢、右は悪い姿勢の例です。左足が浮いており、腰も前傾していることがわかります。右側のように姿勢が悪くなったタイミングで振動モーターを動作させます。 結果に応じてArduinoに指示を送り、振動モーターを動作 結果に応じて、左足・右足・腰のいずれか、または複数が振動して姿勢の悪化を通知します。これを人の足にテープなどで固定して使用します。 1.3 この記事で分かること 本記事では、以下の内容を解説します: 生成AI活用 : AWS Bedrock Claude Sonnet 4による姿勢評価の実装方法 ハードウェア制御 : Arduino + 振動モーターの制御回路の設計と配線 システム統合 : Webカメラ ⟷ PC ⟷ Arduino間の通信の実装 2. システム概要 2.1 全体構成図 システムの全体像は以下の通りです。 カメラで撮影した画像をPCで取得し、生成AIに送信して姿勢を評価。その結果に基づいてArduinoに指示を送り、適切な振動モーターを動作させます。 各コンポーネントは疎結合で、それぞれ独立してテスト・改善できる構成になっています。 2.2 使用技術スタック システム全体で使用した技術は以下の通りです。 レイヤー 技術 用途 言語 Python 3.13 システム全体の制御 AI推論 AWS Bedrock Claude Sonnet 4 姿勢評価 画像取得 OpenCV (Python) カメラ制御 通信 pySerial PC-Arduino間 マイコン Arduino Uno モーター制御 また、利用したツールは以下の通りです。 ツール 用途 Arduino IDE Arduinoコード開発/マイコンへの書き込み Tinkercad 回路設計とシミュレーション Webカメラ こちら を参考にiPhoneをWebカメラ化しました 2.3 動作の流れ システムの動作フローは以下の通りです: 画像取得 :Webカメラが5分ごとにユーザーの姿勢を撮影する AI評価 : PythonでAWS Bedrock(Claude Sonnet 4)に画像を送信し、姿勢を評価 コマンド送信 :評価結果に応じてシリアル通信でArduinoに指示 各部位のスコアをもとに、振動すべき箇所を3ビットで表現する 左足・右足・腰の順番に0/1とし、問題がある箇所を 1 に設定 例: 000 (全て良好)、 100 (左足が悪い)、 111 (全て悪い) モーター制御 :Arduinoが該当する振動モーターを動作させる ループ :1に戻り、継続的に監視 評価頻度は5分に1回としました。 当初は30秒ごとの評価も検討しましたが、以下の理由で5分間隔を選択しました。 APIコストの削減 姿勢改善には継続的な意識づけが重要で、頻繁すぎる通知は逆効果になるため 3. ハードウェア編:振動モーター制御回路 3.1 必要な部品リスト 回路の構築に用いた部品は以下の通りです。 部品名 型番・仕様 個数 用途 マイコン Arduino Uno 1 モーター制御 振動モーター FM34E 3 触覚フィードバック トランジスタ DTC143EL 3 スイッチング 抵抗 1kΩ 3 ベース電流制限 ダイオード 1N4007 3 逆起電力保護 ブレッドボード 標準サイズ 1 配線用 ジャンパーワイヤー オス-オス 11本 回路の接続用 ジャンパーワイヤー オス-メス 6本 振動モーター接続用 ジャンパーワイヤー (必要に応じて) オス-メス 任意の本数 振動モーター延長用 振動モーターを延長する場合は、オス-メスのジャンパーワイヤーを追加で24本程度用意することを推奨します。 3.2 回路図と配線 回路図の作り方 回路設計の経験が無かったため、生成AIに回路図の作成を依頼しました。 出力された回路図をもとに、 Tinkercad でシミュレーションと配線図の作成をしました。 初心者でも簡単に始められ、無料で利用できるため非常に便利です。 基本回路(1個のモーター) まず、1個の振動モーターを制御する基本回路を説明します。 回路の動作原理: Arduinoのデジタルピン(D3)から信号を出力し、抵抗を経由してトランジスタのベースに接続します。トランジスタはスイッチとして機能し、ベースに電圧がかかるとコレクタ-エミッタ間が導通し、モーターに電流が流れます。 ダイオードはモーターと並列に逆向きで接続され、モーター停止時の逆起電力を吸収します。これにより、Arduinoや他の回路が逆電圧で破損することを防ぎます。 PWM制御の必要性: 使用する振動モーターの定格は3Vですが、Arduinoの出力は5Vです。そのまま接続すると過電圧になるため、PWM(Pulse Width Modulation)制御を使って実効電力を調整します。具体的には、 analogWrite(pin, 153) とすることで、約60%のデューティサイクル(153/255)で動作し、平均電圧を約3Vに下げることができます。 完全な配線(3個のモーター) 次に、3個のモーターを制御する完全な回路です。 配線した回路 配線時の重要な注意点: トランジスタの向き :平らな面を手前に向けて挿入します。左からエミッタ(E)、コレクタ(C)、ベース(B)の順です。 ダイオードの向き :銀色の帯のある方がカソード(+側)で、モーターのプラス側に接続します。逆に接続すると短絡の原因になります。 PWM対応ピンの使用 :Arduino Unoでは、D3/D5/D6/D9/D10/D11がPWM対応です。今回はD3/D5/D6を使用しています。 3.3 動作確認とコード Arduino IDEのシリアルモニタから手動でコマンドを送信して、動作を確認できます。 Arduinoコード // ピン定義 const int LEFT_LEG_PIN = 3; // 左脚用振動モーター const int RIGHT_LEG_PIN = 5; // 右脚用振動モーター const int WAIST_PIN = 6; // 腰用振動モーター // 振動設定 const int VIBRATION_DURATION = 5000; // 振動時間(ミリ秒) const int VIBRATION_INTENSITY = 153; // PWM値(0-255) 5V電源をモーターに約3Vで供給するため153に設定 String receivedData = ""; bool dataComplete = false; void setup() { // シリアル通信を開始(9600 baud) Serial.begin(9600); // ピンを出力モードに設定 pinMode(LEFT_LEG_PIN, OUTPUT); pinMode(RIGHT_LEG_PIN, OUTPUT); pinMode(WAIST_PIN, OUTPUT); // 初期状態:全モーター停止 analogWrite(LEFT_LEG_PIN, 0); analogWrite(RIGHT_LEG_PIN, 0); analogWrite(WAIST_PIN, 0); // 起動確認用フラッシュ startupFlash(); } void loop() { // シリアルデータが利用可能かチェック if (Serial.available()) { String receivedString = Serial.readString(); if (receivedString.length() > 0) { receivedData = receivedString; dataComplete = true; } } // データ受信完了時の処理 if (dataComplete) { processPostureFeedback(receivedData); receivedData = ""; dataComplete = false; } } void processPostureFeedback(String data) { // 3桁のバイナリ文字列を期待(例:"101") if (data.length() != 3) { Serial.println("Error: Invalid data format. Expected 3 digits."); return; } // 各桁をチェックして対応する振動を制御 bool leftLegVibrate = (data.charAt(0) == '1'); bool rightLegVibrate = (data.charAt(1) == '1'); bool waistVibrate = (data.charAt(2) == '1'); // 振動パターンを実行 executeVibrationPattern(leftLegVibrate, rightLegVibrate, waistVibrate); } void executeVibrationPattern(bool leftLeg, bool rightLeg, bool waist) { // 全モーター停止 stopAllMotors(); // 振動が必要な部位があるかチェック if (!leftLeg && !rightLeg && !waist) { return; } // 対象部位を振動 if (leftLeg) { analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY); } if (rightLeg) { analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY); } if (waist) { analogWrite(WAIST_PIN, VIBRATION_INTENSITY); } // 振動時間待機 delay(VIBRATION_DURATION); // 全モーター停止 stopAllMotors(); } void stopAllMotors() { analogWrite(LEFT_LEG_PIN, 0); analogWrite(RIGHT_LEG_PIN, 0); analogWrite(WAIST_PIN, 0); } void startupFlash() { // 起動時に全モーターを短時間点灯してテスト Serial.println("System test - All motors flash"); for (int i = 0; i < 3; i++) { analogWrite(LEFT_LEG_PIN, VIBRATION_INTENSITY); analogWrite(RIGHT_LEG_PIN, VIBRATION_INTENSITY); analogWrite(WAIST_PIN, VIBRATION_INTENSITY); delay(200); stopAllMotors(); delay(200); } Serial.println("System test completed"); } 4. ソフトウェア編:姿勢判定システム 4.1 カメラ設置とPythonでの画像取得 前提:姿勢を表すためのモデル定義 以下のPydanticモデルを用いて、生成AIからの姿勢評価を受け取りArduinoに送信する形式へ変換します。 姿勢評価のPydanticモデルコード from pydantic import BaseModel, Field POSTURE_THRESHOLD = 0.7 # 姿勢スコアの閾値 class FeedbackModel (BaseModel): left_leg_score: float = Field(..., description= "左脚の姿勢スコア" , ge= 0.0 , le= 1.0 ) right_leg_score: float = Field(..., description= "右脚の姿勢スコア" , ge= 0.0 , le= 1.0 ) waist_score: float = Field(..., description= "腰の姿勢スコア" , ge= 0.0 , le= 1.0 ) remarks: str = Field( "" , description= "スコア評価の備考" ) def to_should_vibrate (self) -> str : """ 振動フィードバックが必要かどうかを判定 3桁の0, 1の組み合わせで返す。 左足・右足・腰の順番 例: - 000: 全て良好 - 100: 左足が悪い - 111: 全て悪い """ res = "" res += "1" if self.left_leg_score < POSTURE_THRESHOLD else "0" res += "1" if self.right_leg_score < POSTURE_THRESHOLD else "0" res += "1" if self.waist_score < POSTURE_THRESHOLD else "0" return res Pythonでの画像取得 OpenCVを使ってカメラから画像を取得します。 画像取得のコード import cv2 from datetime import datetime # Webカメラのキャプチャを開始 cap = cv2.VideoCapture( 0 ) # キャプチャがオープンしている間続ける while (cap.isOpened()): ret, frame = cap.read() if ret: # カメラ映像を表示 cv2.imshow( 'Webcam Live' , frame) # 's'キーが押されたらスクリーンショットを保存 if cv2.waitKey( 1 ) & 0xFF == ord ( 's' ): timestamp = datetime.now().strftime( "%Y%m%d_%H%M%S" ) filename = f "screenshot/screenshot_{timestamp}.png" cv2.imwrite(filename, frame) # 'q'キーが押されたらループから抜ける if cv2.waitKey( 1 ) & 0xFF == ord ( 'q' ): break else : break # キャプチャをリリースし、ウィンドウを閉じる cap.release() cv2.destroyAllWindows() ポイント: cv2.VideoCapture(0) の引数は環境によって異なります。複数のカメラが接続されている場合は、番号を変えて試してください。 動作確認用のため、 's' キーでスクリーンショットを保存するようにしています。最終的には5分ごとに自動で保存するロジックを追加します。 4.2 生成AI(Bedrock Claude Sonnet 4)との連携 AWS Bedrockのセットアップ AWS Bedrockを使用するには、事前にAWSアカウントとCLIの設定が必要です。 必要な準備: 1. AWSアカウントの作成 2. IAMユーザーの作成(Bedrock実行権限付与) 3. AWS CLIのインストールと設定( aws configure ) 4. Bedrock APIへのアクセス権限の確認(リージョンは ap-northeast-1 など) 姿勢評価の実装 Claude Sonnet 4に画像を送信して姿勢を評価するクラスを実装します。 Claudeを呼び出し姿勢を評価するコード import json import boto3 from model import FeedbackModel MODEL_ID = "" # Bedrockで使用するモデルIDを指定してください。 class BedrockClient (): def __init__ (self): self.client = boto3.client( 'bedrock-runtime' ) # 姿勢評価 def evaluate_posture (self, img_bytes: bytes ): system_prompt = ( "ユーザーから提供されるbase64エンコードされた画像を解析し、姿勢評価を行ってください。 \n " "画像横から撮影した人が映っています。座り姿勢が良いか悪いかを判定し、以下のJSON形式で返してください。 \n\n " "1. left_leg_score: 左脚の姿勢スコア(0-1のfloat、1が最良) \n " "2. right_leg_score: 右脚の姿勢スコア(0-1のfloat、1が最良) \n " "3. waist_score: 腰の姿勢スコア(0-1のfloat、1が最良) \n\n " "JSON形式の例: {{ \" left_leg_score \" : 0.8, \" right_leg_score \" : 0.9, \" waist_score \" : 0.7}}" ) messages = [{ "role" : "user" , "content" : [ { "image" : { "format" : "png" , "source" : { "bytes" : img_bytes } } } ] }] response = self.client.converse( modelId=MODEL_ID, system=[{ "text" : system_prompt}], inferenceConfig={ "temperature" : 0 }, messages=messages, toolConfig={ "tools" : [ { "toolSpec" : { "name" : "extract_Feedback_Model" , "description" : "inputSchemaに沿ってFeedbackというPydanticモデルを出力するツール" , "inputSchema" : { "json" : FeedbackModel.model_json_schema()} } } ], "toolChoice" : { "tool" : { "name" : "extract_Feedback_Model" } } } ) # Bedrockのレスポンスから姿勢評価結果のJSONを抽出し、FeedbackModelに変換して返す tool_res = response[ "output" ][ "message" ][ "content" ][ 0 ][ "toolUse" ][ "input" ] if isinstance (tool_res, str ): tool_res = json.loads(tool_res) return FeedbackModel(**tool_res) ポイント ユーザーから提供される画像を読み取り、バイト列としてClaude Sonnet 4に送信します。 BedrockにおけるClaudeはtoolConfigを利用することで、Pydanticモデルを用いたJSONレスポンスを受け取ることができます。一方、必ず意図した形式で返ってくるとは限らないため、必要に応じてエラーハンドリングを追加してください。 4.3 Arduino との通信(シリアル通信) シリアル通信の基礎 PythonからArduinoに指示を送るには、シリアル通信を使用します。pySerialライブラリをインストールする必要があります。 Arduinoコントローラーの実装 姿勢評価結果に基づいてArduinoに適切なコマンドを送信するクラスを作成します。 Arduinoコントローラーのコード import serial from model import FeedbackModel def run_vibration_feedback (arduino: serial.Serial, feedback: FeedbackModel) -> None : """ Arduinoを使用して振動フィードバックを提供する関数 """ # 1. フィードバックを解析 should_vibrate = feedback.to_should_vibrate() # 2. フィードバック信号をArduinoに送信 arduino.write( bytes (should_vibrate, encoding= "ascii" )) # 生成AIからのフィードバックをモック化 feedback = FeedbackModel( left_leg_score= 0.4 , right_leg_score= 0.6 , waist_score= 0.7 , remarks= "Test feedback" ) # Arduinoのシリアルポートとボーレートを設定。環境に応じて適切に変更してください。 ARDUINO_PORT = '/dev/cu.usbmodem114301' ARDUINO_BAUDRATE = 9600 # Arduinoに接続する arduino = serial.Serial(ARDUINO_PORT, ARDUINO_BAUDRATE) # 振動フィードバックを実行 run_vibration_feedback(arduino, feedback) # 接続を閉じる arduino.close() ポイント 1. 振動フィードバックの実行には、Arduinoが接続されている必要があります。 2. Arduinoのシリアルポートやボーレートは環境によって異なります。後述の確認方法を参考に適切に設定してください。 COMポートの確認方法: Windows : デバイスマネージャーで「ポート(COMとLPT)」を確認。 COM3 , COM4 など。 Mac : ターミナルで ls /dev/cu.* を実行。 /dev/cu.usbserial-XXXX など。 Linux : ターミナルで ls /dev/ttyUSB* または ls /dev/ttyACM* を実行。 Arduino IDEのシリアルモニタを開いている場合は、ポートが占有されているため、Pythonから接続できません。必ず閉じてから実行してください。また、ポート番号はArduino IDEで確認すると便利です。 4.4 統合プログラム すべてのコンポーネントを統合し、システム全体を動作させるメインプログラムです。 コード全文 main.py # main.py from datetime import datetime import cv2 import time import serial from bedrock import BedrockClient from model import FeedbackModel # Arduinoのシリアルポートとボーレートを設定。環境に応じて適切に変更してください。 ARDUINO_PORT = '/dev/cu.usbmodem114301' ARDUINO_BAUDRATE = 9600 interval_sec = 300 def get_image (frame) -> bytes : """OpenCVのフレームを画像バイトに変換して返す""" timestamp = datetime.now().strftime( "%Y%m%d_%H%M%S" ) filename = f "screenshot/screenshot_{timestamp}.png" # スクリーンショットを保存 cv2.imwrite(filename, frame) res: bytes = b "" # 画像をバイトに変換 with open (filename, "rb" ) as img_file: res = img_file.read() return res def run_capture_analysis (frame): # -> Feedback: """ OpenCVでキャプチャしたフレームを元に姿勢分析を行う関数 """ # 1. frameをbyteで送信 img_bytes = get_image(frame) # 2. Bedrockに送信して姿勢推定 bedrock_client = BedrockClient() return bedrock_client.evaluate_posture(img_bytes) def run_vibration_feedback (arduino: serial.Serial, feedback: FeedbackModel) -> None : """ Arduinoを使用して振動フィードバックを提供する関数 """ # 1. フィードバックを解析 should_vibrate = feedback.to_should_vibrate() # 2. フィードバック信号を送信 arduino.write( bytes (should_vibrate, encoding= "ascii" )) def main (): # ウェブカメラのキャプチャを開始 cap = cv2.VideoCapture( 0 ) arduino = serial.Serial(ARDUINO_PORT, ARDUINO_BAUDRATE) # キャプチャがオープンしている間続ける while (cap.isOpened()): time.sleep(interval_sec) ret, frame = cap.read() if not ret: break # デバッグ用にフレームを表示 cv2.imshow( 'Webcam Live' , frame) feedback = run_capture_analysis(frame) run_vibration_feedback(arduino, feedback) # 'q'キーが押されたらループから抜ける if cv2.waitKey( 1 ) & 0xFF == ord ( 'q' ): break # キャプチャをリリースし、ウィンドウを閉じる cap.release() cv2.destroyAllWindows() arduino.close() if __name__ == "__main__" : main() bedrock.py # bedrock.py import json import boto3 from model import FeedbackModel MODEL_ID = "" # Bedrockで使用するモデルIDを指定 class BedrockClient (): def __init__ (self): self.client = boto3.client( 'bedrock-runtime' ) # 姿勢評価 def evaluate_posture (self, img_bytes: bytes ): system_prompt = ( "ユーザーから提供されるbase64エンコードされた画像を解析し、姿勢評価を行ってください。 \n " "画像横から撮影した人が映っています。座り姿勢が良いか悪いかを判定し、以下のJSON形式で返してください。 \n\n " "1. left_leg_score: 左脚の姿勢スコア(0-1のfloat、1が最良) \n " "2. right_leg_score: 右脚の姿勢スコア(0-1のfloat、1が最良) \n " "3. waist_score: 腰の姿勢スコア(0-1のfloat、1が最良) \n\n " "JSON形式の例: {{ \" left_leg_score \" : 0.8, \" right_leg_score \" : 0.9, \" waist_score \" : 0.7}}" ) messages = [{ "role" : "user" , "content" : [ { "image" : { "format" : "png" , "source" : { "bytes" : img_bytes } } } ] }] response = self.client.converse( modelId=MODEL_ID, system=[{ "text" : system_prompt}], inferenceConfig={ "temperature" : 0 }, messages=messages, toolConfig={ "tools" : [ { "toolSpec" : { "name" : "extract_Feedback_Model" , "description" : "inputSchemaに沿ってFeedbackというPydanticモデルを出力するツール" , "inputSchema" : { "json" : FeedbackModel.model_json_schema()} } } ], "toolChoice" : { "tool" : { "name" : "extract_Feedback_Model" } } } ) # Bedrockのレスポンスから姿勢評価結果のJSONを抽出し、FeedbackModelに変換して返す tool_res = response[ "output" ][ "message" ][ "content" ][ 0 ][ "toolUse" ][ "input" ] if isinstance (tool_res, str ): tool_res = json.loads(tool_res) return FeedbackModel(**tool_res) model.py # model.py from pydantic import BaseModel, Field POSTURE_THRESHOLD = 0.7 # 姿勢スコアの閾値 class FeedbackModel (BaseModel): left_leg_score: float = Field(..., description= "左脚の姿勢スコア" , ge= 0.0 , le= 1.0 ) right_leg_score: float = Field(..., description= "右脚の姿勢スコア" , ge= 0.0 , le= 1.0 ) waist_score: float = Field(..., description= "腰の姿勢スコア" , ge= 0.0 , le= 1.0 ) remarks: str = Field( "" , description= "スコア評価の備考" ) def to_should_vibrate (self) -> str : """ 振動フィードバックが必要かどうかを判定 3桁の0, 1の組み合わせで返す。 左足・右足・腰の順番 例: - 000: 全て良好 - 100: 左足が悪い - 111: 全て悪い """ res = "" res += "1" if self.left_leg_score < POSTURE_THRESHOLD else "0" res += "1" if self.right_leg_score < POSTURE_THRESHOLD else "0" res += "1" if self.waist_score < POSTURE_THRESHOLD else "0" return res motor.ino // ピン定義 const int LEFT_LEG_PIN = 3 ; // 左脚用振動モーター const int RIGHT_LEG_PIN = 5 ; // 右脚用振動モーター const int WAIST_PIN = 6 ; // 腰用振動モーター // 振動設定 const int VIBRATION_DURATION = 5000 ; // 振動時間(ミリ秒) const int VIBRATION_INTENSITY = 153 ; // PWM値(0-255) String receivedData = "" ; bool dataComplete = false ; void setup () { // シリアル通信を開始(9600 baud) Serial. begin ( 9600 ); // ピンを出力モードに設定 pinMode (LEFT_LEG_PIN, OUTPUT); pinMode (RIGHT_LEG_PIN, OUTPUT); pinMode (WAIST_PIN, OUTPUT); // 初期状態:全モーター停止 analogWrite (LEFT_LEG_PIN, 0 ); analogWrite (RIGHT_LEG_PIN, 0 ); analogWrite (WAIST_PIN, 0 ); // 起動確認用フラッシュ startupFlash (); } void loop () { // シリアルデータが利用可能かチェック if (Serial. available ()) { String receivedString = Serial. readString (); if (receivedString. length () > 0 ) { receivedData = receivedString; dataComplete = true ; } } // データ受信完了時の処理 if (dataComplete) { processPostureFeedback (receivedData); receivedData = "" ; dataComplete = false ; } } void processPostureFeedback (String data) { // 3桁のバイナリ文字列を期待(例:"101") if (data. length () != 3 ) { Serial. println ( "Error: Invalid data format. Expected 3 digits." ); return ; } // 各桁をチェックして対応する振動を制御 bool leftLegVibrate = (data. charAt ( 0 ) == '1' ); bool rightLegVibrate = (data. charAt ( 1 ) == '1' ); bool waistVibrate = (data. charAt ( 2 ) == '1' ); // 振動パターンを実行 executeVibrationPattern (leftLegVibrate, rightLegVibrate, waistVibrate); } void executeVibrationPattern ( bool leftLeg, bool rightLeg, bool waist) { // 全モーター停止 stopAllMotors (); // 振動が必要な部位があるかチェック if (!leftLeg && !rightLeg && !waist) { return ; } // 対象部位を振動 if (leftLeg) { analogWrite (LEFT_LEG_PIN, VIBRATION_INTENSITY); } if (rightLeg) { analogWrite (RIGHT_LEG_PIN, VIBRATION_INTENSITY); } if (waist) { analogWrite (WAIST_PIN, VIBRATION_INTENSITY); } // 振動時間待機 delay (VIBRATION_DURATION); // 全モーター停止 stopAllMotors (); } void stopAllMotors () { analogWrite (LEFT_LEG_PIN, 0 ); analogWrite (RIGHT_LEG_PIN, 0 ); analogWrite (WAIST_PIN, 0 ); } void startupFlash () { // 起動時に全モーターを短時間点灯してテスト Serial. println ( "System test - All motors flash" ); for ( int i = 0 ; i < 3 ; i++) { analogWrite (LEFT_LEG_PIN, VIBRATION_INTENSITY); analogWrite (RIGHT_LEG_PIN, VIBRATION_INTENSITY); analogWrite (WAIST_PIN, VIBRATION_INTENSITY); delay ( 200 ); stopAllMotors (); delay ( 200 ); } Serial. println ( "System test completed" ); } プログラムの構成: 初期化フェーズ :カメラとArduinoを初期化 メインループ : 5分待機 カメラから画像を取得 AIで姿勢評価 Arduinoにコマンド送信 終了処理 :終了時リソースを適切に解放 5. 実際に使ってみて システムを1週間使用した結果、以下のような効果と課題が見えてきました。 5.1 効果を実感した点 最初はほぼ毎回振動させられ、そのたびに「ハッとして」姿勢を直していました。振動というフィードバックは、視覚や聴覚と比べて邪魔にならず、作業を中断させない点が良かったです。 4日目くらいから、少しずつ姿勢が改善され、振動される頻度が減ってきました。座り姿勢を意識する習慣がついたように感じます。 5.2 振動の強さについて 使用した3V振動モーターは、服の上からでもはっきりと分かるため、通知としての役割は十分果たせています。 一方で、長時間使用すると慣れてしまい、振動に対する感度が下がる傾向も見られました。振動パターンをランダム化する、または強弱をつけるなどの工夫が必要かもしれません。 5.3 生成AIの判定精度 Claude Sonnet 4の姿勢評価は想像以上に精密で、「確かにその通り」と思える判定が多くありました。 特に、微妙な体の傾きや脚の位置なども的確に指摘してくれるため、自分では気づかない姿勢の問題を発見できました。 6. 今後の展望 このシステムは実験的なプロトタイプですが、以下のような発展性があります。 複数部位への拡張 : 現在は3箇所ですが、首、肩、背中など5〜7箇所に増やすことで、より細かい姿勢の矯正が可能になります。 モーターの種類変更 : 振動モーター以外に、エアポンプとエアバッグを利用することで良い姿勢に矯正させることができます。 バッテリー駆動化 : USB給電からバッテリー駆動に変更し、ケーブルレスで使用できるようにできます。 同様の「自然言語での評価基準定義 + 物理フィードバック」のパターンは、評価が主観的になりがちな他の分野でも参考になるかもしれません。 7. まとめ 本記事を書くにあたり、大学以来久しぶりに電子工作をしました。生成AIを活用して回路図を作成し、シミュレータで動作確認をすることで、初学者でも簡単に取り組めました。ソフトウェアアプリケーションの領域だけでなく、ハードウェア制御の分野にも生成AIを活用できることを実感し、自身で取り組める範囲が広がったと感じています。本記事が、同様の課題を持つ方々の参考になれば幸いです。

動画

書籍