EyetribeUnity250506

EyetribeはずっとProcessingで動かしてきたが,それが足かせとなり込み入った開発が難しい状況にあった。なにせ古いものなので調査も難航し,結局「そんな複雑なものじゃないので,ソケットでデータを受け取るプログラムを作っては?」と言われ,Python版ができた。んならUnity版も作ってもらおうてなことで,Unityへの移行が進んだ。Processing版はなにせパフォーマンスが芳しく無く,これ以上の開発は不毛な印象なので,同じ機能をUnity版に移植した。動画の読み込みが速い,動作がもたつかないなど,余裕がある感じで大変よろしい。オープンキャンパスなど,しばらくはこれで行こうと思う。実行ファイル3は,result内のCSVファイルの頭に「MGAZE_20250506_150602.csv」のようにMをつけることで,男性参加者のデータを青で表示することができる。
実行ファイル1 実行ファイル2 実行ファイル3

一方で,ファッション雑誌の静止画で視線測定をするのが訴求力あるという意見もあり,妙に納得である。実写は版権が難しいので,イラストにしてもらい測るのもありかなと。視線測定よりAIでイラスト生成のほうが楽しかったりして。

using UnityEngine;
using System;
using System.Net.Sockets;
using System.IO;
using System.Threading;

// JSON マッピング用クラス
[Serializable]
public class Avg { public float x; public float y; }
[Serializable]
public class Frame { public Avg avg; public bool fix; public int state; }
[Serializable]
public class Values { public Frame frame; }
[Serializable]
public class TrackerMessage { public Values values; }

public class EyeTribeTCP : MonoBehaviour
{
    private const string HOST = "127.0.0.1";
    private const int PORT = 6555;
    private const string REQ_PUSH = "{\"category\":\"tracker\",\"request\":\"set\",\"values\":{\"push\":true,\"version\":1}}\n";
    private const string REQ_HEARTBEAT = "{\"category\":\"heartbeat\",\"request\":null}\n";

    private TcpClient client;
    private NetworkStream stream;
    private StreamReader reader;
    private Thread heartbeatThread;
    private Thread receiveThread;
    private bool running;

    [Header("=== Gaze Data (Read-Only) ===")]
    [Tooltip("現在の注視座標X")]
    public float GazeX;
    [Tooltip("現在の注視座標Y")]
    public float GazeY;
    [Tooltip("現在の注視固定状態")]
    public bool IsFixated;
    [Tooltip("現在のトラッキング状態")]
    public bool IsTracking;
    [Tooltip("デバッグモード")]
    public bool IsDebugmode;


    void Start()
    {
        Connect();
    }

    void Connect()
    {
        try
        {
            client = new TcpClient(HOST, PORT);
            stream = client.GetStream();
            reader = new StreamReader(stream);

            Send(REQ_PUSH);

            running = true;

            heartbeatThread = new Thread(() =>
            {
                while (running && stream != null)
                {
                    Send(REQ_HEARTBEAT);
                    Thread.Sleep(250);
                }
            });
            heartbeatThread.IsBackground = true;
            heartbeatThread.Start();

            receiveThread = new Thread(ReceiveLoop);
            receiveThread.IsBackground = true;
            receiveThread.Start();

            Debug.Log("EyeTribe: 接続完了し、スレッドを開始しました。");
        }
        catch (Exception e)
        {
            Debug.LogError($"EyeTribe 接続失敗: {e.Message}");
        }
    }

    private void Send(string message)
    {
        if (stream == null) return;
        try
        {
            var buf = System.Text.Encoding.UTF8.GetBytes(message);
            stream.Write(buf, 0, buf.Length);
        }
        catch (Exception e)
        {
            Debug.LogWarning($"Send エラー: {e.Message}");
        }
    }

    void ReceiveLoop()
    {
        try
        {
            while (running && reader != null)
            {
                string line = reader.ReadLine();
                if (string.IsNullOrEmpty(line))
                    continue;

                try
                {
                    var msg = JsonUtility.FromJson<TrackerMessage>(line);
                    if (msg?.values?.frame != null)
                    {
                        var f = msg.values.frame;
                        bool tracking = (f.state & 0x1) != 0;

                        // フィールドにセット(Inspector に反映される)
                        GazeX = f.avg.x;
                        GazeY = f.avg.y;
                        IsFixated = f.fix;
                        IsTracking = tracking;

                        if (IsDebugmode) {
                            Debug.Log($"x={GazeX:F3}, y={GazeY:F3}, fix={IsFixated}, tracking={IsTracking}");
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.LogWarning($"JSON 解析エラー: {ex.Message} | raw: {line}");
                }
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning($"ReceiveLoop 終了: {e.Message}");
        }
    }

    void OnDestroy()
    {
        running = false;
        heartbeatThread?.Join(500);
        receiveThread?.Join(500);
        reader?.Close();
        stream?.Close();
        client?.Close();
        Debug.Log("EyeTribe: 接続を閉じました。");
    }
}

川。ー卒論の刺激づくりについてー

今回は就活のポートフォリオの作成のため、自分が作成した風景のまとめをします。

上の画像は、2024年の12月頃に作成していた埼玉県長瀞市の川沿いをunityで作成したものです。この頃はunityの標準機能とNatureManufactureとRiverAutoMaterialとVegetationStudioを使用して長瀞を作成していた。今見るとこっちのほうがきれいに見えてくる・・・

こっちの画像はいま現段階でMicroverseを使用して作成している長瀞だ。こっちのほうが環境も大きくいいと感じるが、細部をまだ凝る必要がある。また、見どころがあまりないのが困ったところ。以上が現状の報告である。

ブラウザで認識機能

コミュニケーションに必要な認識はブラウザで行う時代に!
WebGL版AIエージェントの音声認識に試用しているWebGL Speechというアセットが動作しなくなり(ブラウザのセキュリティ強化のせい?)音声会話に支障が出ている。WebGL版OpenGLの遅さや、Web版Mediapipeの充実を鑑みるに、「CGなどUnityでしかできないものはWebGLに、音声や表情など各種認識はブラウザに」という機能の棲み分けが今後一般的になるのではないか!?コミュニケーションに必要な認識能力はブラウザに統合されるという時代の流れがあるとの読みだ。

音声認識
ちょいとo4miniに作成してもらう。「Web Speech API(webkitSpeechRecognition)でブラウザに表示したテキストエリアに、発話した内容を表示するデモプログラムを作成してくれませんか?Javascriptで」完成したデモは十分すぎる仕上がりだ。音声認識はブラウザで受けて、それをWebGLアプリで吸い上げる作戦だ。

表情認識
表情認識は、Googleのデモをベースにやはりo4miniに作成してもらう。こちらは少々苦戦したが、左右反転なしでFaceMeshも表示されるデモを作ってもらえた。

なによりも朗報なのは、iPadでも動作するということだろうか。


音声と表情をブラウザで同時に認識する
上記で作成したJSは,Unityのプラグインとして記述し,WebGLビルドに含めることができる。晴れて「ブラウザで認識した音声+表情をWebGLへ送る」アイデアが実現した。o3様々だ。しかし,デバッグするためにビルドする必要があるという問題が残る。Unityエディタで開発中は,ブラウザで認識した情報を受取ることはできないの?o3曰く,Websocketを使えば良い。そのためには,Websocket-sharpが必要とのこと。これで開発中も音声+表情認識が無事に使えるようになった。やった!!o3の使用制限はあと24回になっちゃったけど。

音声認識も表情認識も行うデモ
websocket-sharp


ブラウザでFER2013を動かす
MediaPipeがJSで動いて、その結果をUnityWebGLに呼び込めるなら、OpenCVで使っていたFER2013モデルも読み込めないのだろうか?これは、TensorFlows.jsというものがあり、そのラッパーであるface-api.jsを利用すれば良いようだ。モデルをDLして配置して、エラーを見ながらo4-miniに相談すると、無事ブラウザで動かすことに成功した。楽しい~!これもアリですね。
一連の会話
ブラウザでFER2013動かすデモ

患者セリフ集

理学療法中の患者さんが抱えやすい「つらさ」「不安」「あきらめ」「無力感」「怒り」の5つの感情に注目し、実際に口にしそうな言葉を集めました。AIに感情表現を学習させるための素材として作成しましたが、同時に患者さんの心の声に耳を傾ける手がかりにもなればと思います。


■ つらさ(身体的・精神的な苦痛)
毎回痛みに耐えるだけで、ぐったりしてしまうんです。
やるたびに痛みが増していく気がして、怖いです。
「慣れれば楽になりますよ」って言われても、全然楽にならない。
少し動かしただけで、こんなにつらいなんて思わなかった。
人と話す気力も、笑う余裕も、もう残ってないです。
寝ても起きても痛くて、逃げ場がありません。
リハビリに行くのが、だんだん怖くなってきました。
体だけじゃなく、心まで壊れていくような気がしてます。


■ 不安(先が見えないことへの恐れや心配)
もしこのまま治らなかったら…って考えると夜も眠れません。
退院後に一人で生活できる気がしなくて、不安で仕方ないです。
「もう少しですね」って言葉が、逆にプレッシャーになります。
この先、自分に何ができるのかまったく見えないんです。
良くなるって信じたいけど、実感がないと不安になります。
周りに「がんばれ」って言われるほど、不安だけが大きくなります。
わたし、ちゃんと前に進めてるんでしょうか。
毎日が不安の塊で、息苦しくなります。


■ あきらめ(希望を失ったり、努力する意味を見いだせない状態)
もう何をしても、変わらない気がしてきました。
リハビリに行く意味、最近はわからなくなってます。
回復を信じたい気持ちが、日に日に薄れていきます。
毎日同じ訓練をしてるだけで、空虚な気持ちになります。
頑張っても報われないなら、もう頑張らなくてもいいかなって。
期待するたびに、裏切られてる気がするんです。
希望を持つのが怖くなってきました。
「いつかきっと良くなるよ」って言葉が、ただの慰めにしか聞こえません。


■ 無力感(できない・役に立てない自分への落胆)
自分の体が、自分のものじゃないみたいに感じます。
周りが助けてくれてるのに、何もできない自分がつらいです。
手すりにすがって立つだけなのに、なぜこんなにもつらいんでしょう。
日常のちょっとしたことさえできなくなってしまって、情けないです。
リハビリ中に「またダメだった」と思う瞬間が、一番つらいです。
自分がどんどん役立たずになっていく気がして、涙が出ます。
できてたことができなくなっていくのを、ただ見てるしかないんです。
これ以上、家族に迷惑かけたくないけど、できることが何もない。


■ 怒り(他者や状況、自分自身への苛立ちや怒り)
なんで自分だけが、こんな目にあうんですか。
痛いって何度も言ってるのに、わかってもらえないのが腹立たしいです。
「がんばれば良くなる」って簡単に言わないでほしい。
なんであの日、怪我なんかしたんだろうって、自分が憎くなる。
他人事みたいな口ぶりで励まされても、正直ムカつきます。
周りは普通に歩いてるのに、自分だけ…って考えると悔しくて涙が出ます。
痛みが続くたびに、どうしてこんな体になったんだって怒りが湧いてきます。
もうこれ以上、「もうちょっとだけ」とか言われたくないんです!

FreetokenWithOpenAI

Geminiで会話履歴をPrefsに覚えさせ、会話を続けられるようにしていたが、やはり1日50requestで「Gemini API Error: HTTP/1.1 429 Too Many Requests」となる事が判明・・・やはりOpenAIで作ったほうが得策か。。。そういえば、無料トークンの話あったよな。
OpenAI API を毎日無料で 100 万トークン使えるらしい?によれば、条件をみたすと100万トークン毎日無料で使えるようになるという。本当に?!ブログに従って無料トークン用のAPIキーを作成してみる。Pythonで試してみようか・・・あ、動きますね!これなら無駄に会話を続けてもダメージは最小限に抑えられる。これで開発しようか。

Gemini再来

UezoさんのChatdollkitを見て気がつく。まえにもBard使ってたけど、かなり賢いGeminiがなんと使い放題らしい。人格も維持し、JSON出力もキープしてくれる。これはひょっとして・・・。Unityから使う方法も一応紹介されている。もっと簡単に使える方法はないものか。。
C#でAIと無料で会話できるようにしてみた
これとか良いのではないか・・・。dllが必要!?

GASを使うと3分でできるというが・・・。めんどくさい。やるしかないのか。
Gemini↔️GAS↔️UnityでGeminiを使ってみる(テキスト編)


Geminiは、ChatGPTにUnityでの使い方を聞いたらあっさり動作した。・・・がしかし、50リクエスト/日とある。50リクエストしかできないって事じゃ??はぁ~

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using TMPro;
using UnityEngine.UI;

public class GeminiChat : MonoBehaviour
{
    public string apiKey = ""; // あなたのGemini APIキー
    public TextMeshProUGUI outputText;
    public InputField IF1;
    public Text outputmessage;

    private List<Content> conversationHistory = new List<Content>(); // 会話履歴(最大100)

    void Start()
    {
        // 初期化時は空の会話でもOK
    }

    public void procMessage(string message)
    {
        string userInput = IF1.text;
        if (!string.IsNullOrEmpty(userInput))
        {
            AddToHistory("user", userInput); // ユーザー発言を履歴に追加
            StartCoroutine(SendRequestToGemini());
        }
    }

    void AddToHistory(string role, string text)
    {
        // Gemini APIのフォーマットは content[].parts[].text を使用
        Content content = new Content
        {
            role = role,
            parts = new Part[]
            {
                new Part { text = text }
            }
        };

        conversationHistory.Add(content);

        // 最大100件までに制限(roleごとではなく全体で100)
        if (conversationHistory.Count > 100)
        {
            conversationHistory.RemoveAt(0);
        }
    }

    IEnumerator SendRequestToGemini()
    {
        string url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro-exp-02-05:generateContent?key=" + apiKey;

        GeminiRequest requestData = new GeminiRequest
        {
            contents = conversationHistory.ToArray()
        };

        string jsonData = JsonUtility.ToJson(requestData, true);
        byte[] postData = System.Text.Encoding.UTF8.GetBytes(jsonData);

        UnityWebRequest request = new UnityWebRequest(url, "POST");
        request.uploadHandler = new UploadHandlerRaw(postData);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            string result = request.downloadHandler.text;
            GeminiResponse response = JsonUtility.FromJson<GeminiResponse>(result);

            string reply = response.candidates[0].content.parts[0].text;

            Debug.Log("Gemini Response: " + reply);

            if (outputText != null)
                outputText.text = reply;

            outputmessage.text = reply;

            // AIの返答を履歴に追加(role: "model")
            AddToHistory("model", reply);
        }
        else
        {
            Debug.LogError("Gemini API Error: " + request.error);
            outputmessage.text = request.error;
        }
    }

    // JSON構造(Gemini API仕様)
    [System.Serializable]
    public class GeminiRequest
    {
        public Content[] contents;
    }

    [System.Serializable]
    public class Content
    {
        public string role; // "user" or "model"
        public Part[] parts;
    }

    [System.Serializable]
    public class Part
    {
        public string text;
    }

    [System.Serializable]
    public class GeminiResponse
    {
        public Candidate[] candidates;
    }

    [System.Serializable]
    public class Candidate
    {
        public Content content;
    }
}

GroomingStack

GroomingStack – Hair, Fur and More
良いのか悪いのか、情報が少なくて評価しにくいアセット。Fluffy Grooming Toolなら紹介が見つかるのだけれど。しかしHDRPが動作する環境なら、これくらいの表現力があっても良いのではないか。しかしこの髪データ、どうやって作るんだ・・・。これはBlenderParticleHairという仕組みで作成したものを取り込めるらしい。CardtoStrandというツールが付いているようだが、実際どの程度の表現力なのかわからない。・・・おおお、Blender であらゆるヘアカードをグルーミング ストランドに変換するなどのチュートリアルが!ハイフィデリティな髪はこうやって作ってるのか!!すぅげぇ~!

HDRP250317

深みにはまるHDRP。もう何が良いのか悪いのかわからなくなってきた。Sun>Shadows>ContactShadowsをmedium以上にすると、髪の毛の影が顔に落ちるようになった。何が違うのかポニーテールの場合は、髪テクスチャのアラが目立ちにくい。これはカメラのPostAntialiasingをTAA(High)にしてる効果が大きいと思われる。リアルタイムレンダリングとしては、かなり良いところまで来た気がする。

ショートカットは、コンタクトシャドウがつくと、とたんに前髪が邪魔そうな印象になる。まぁそれはそれでリアルなのだろうが・・・。

ロングヘアーのヒトは印象がすっかり変わってしまい、モードな雰囲気に・・・。影の濃さを変えるには、環境光(HDRI)のExposureを変える必要があるようだ。が、そうすると背景も変わってしまう。難しいところだ。HDRPの画作りが真実に近いと言うなら、見る側もそれに慣れなければいけないのか。でも全員に言われそう。「前髪がジャマ」って。それは、IdaFaber(髪アセット作者)に言ってくれ。

ところで、HDRPの作画を画像検索にかけると、高い確率で「いかがわしいCG」に認定されてしまう。しかし、ポニーテールとショートカットは、とうとうGoogleに「個人の写真は検索できない(つまり人間)」認定をうけた。やった。残念ながらロングヘアーは相変わらずいかがわしい画像認定であった。

UnityHiarSystem

Unityでのリアルな髪の毛表現には、Unity製のHairsystemというアセットがあるらしいのだが、これはAlembic形式というファイルを作成する必要があるらしい。MayaやMAXなどの環境が必要なのだろうか。。いつか作れる日がくるのだろうか?

Alembicファイルはここから手に入るらしい。気が遠くなるほどセットアップ手順が長い。
Unityがデジタルヒューマン構築に消極的で、行く先が割と絶望的であることが議論されている。やはりUEに移行するしかないのか。

MediapipeWithWeb

MediaPipeはブラウザでも動作するだと?!そんなことあってたまるか・・・。KDDIのTOYOTAさんの記事によればMediaPipeStudioでは、様々なWeb版MediaPipeの機能を体験できるという。

顔の検出手の検出、確かにWebのみで動作する。MediaPipeは、以前にBarracuda経由でためしたものよりバージョンアップして機能や精度が改善されているようだ。52の顔ブレンドシェイプ値を出してくれるのが良い。しかし・・・実際に動作を確認すると「笑顔の検出が鈍い(そのぶん飽和しないので、強い笑顔の検出はできる)」「ネガティブ表情の検出が非常に悪い」などの問題があるように感じた。こういうAIは一長一短で、なかなかパーフェクトなものは存在しないようだ。いちいち確かめるのは不毛とも言える。

DAZMen250215

経緯
Unityを使ったAIチャットボットの開発を学習課題に導入予定であるが、ちょうどよい利用可能なキャラがない(軍人やEV社長は有料アセットだから配布できない)。Mixamoでリグをセットしたキャラを口パク可能なように改造してみたが、手間が現実的じゃない。

仕方ないのでDAZでキャラクタ開発
今更だが、新しい男性キャラクタを開発。DAZの覚書によれば、DAZtoUnityを使うようだが、マテリアルが正しくインポートされないよ・・・しかたない手動だ。毎度わからなくなるのでメモ・・・涙
・Cornea(角膜)→テクスチャなし。透明に。目の輝きに寄与。
・EyeMoisture(白目部分)→透明に。輝くようにsmoothnessなどを調整。
・Pupils(瞳孔)、Irises(光彩)、Sclera→目のテクスチャ
・Eyesocket→顔の素材で良い
半透明テクスチャをGIMPで作成する手順は以下の通り。

髪の毛の素材はすべて両サイドのStandardDS(SpecularSetup)に。Specularのテクスチャをセットすると、DAZっぽい見た目に。目の輝きが足りない?

やりすぎたか、輝きすぎた!?

長野声(21)がAI化される

AIボイスには出来不出来がある。「はぁ~なんていい声なんだ!」と思うようなものもあれば「いらんわっ!(怒)」といったものも多い。どうすれば、澄んだきれいなAI声を作成できるのだろう?てことで、合成音声用データ作成の経験値を高めるべく、自分の声をAI化してみた。わかったことは、

①録音機材が大事。クリアな声を録音しないと、AIボイスもクリアにならない。
②理想の抑揚をつけた声も必要。
③いくつかのニューラルネットから良いとこ取りをすると質が高いものができる。

である。2021年の声からけっこう良い長野ボイスができて、「老いを感じる」という罰ゲームが待っているとは、当初は気づかないのであった。

AI会話研究パタンBase

0-1.プロフィールの登録
0-2.AIエージェントとの会話(練習)

1-3.AIエージェントとの会話(男性B/あり)
1-4.AIエージェントとの会話(男性B/なし)
1-1.AIエージェントとの会話(男性A/あり)
1-2.AIエージェントとの会話(男性A/なし)

2-3.AIエージェントとの会話(女性B/あり)
2-4.AIエージェントとの会話(女性B/なし)
2-1.AIエージェントとの会話(女性A/あり)
2-2.AIエージェントとの会話(女性A/なし)

3-1.アンケート調査に回答

5.その他