ランタイムで読み込んだHumanoidモデルに読み込んだモーションを適用する

こんにちは、エンジニアの渡辺(@mochi_neko_7)です。

今回は Unity でランタイム(アプリケーション実行中)に読み込んだ Humanoid モデルに、同じくランタイムに読み込んだモーションを適用できるようにするまでの流れを解説します。

ユースケースが多くないのか世の中にあまり情報が多くない印象だったので、同様のことをされる方に参考になれば幸いです。

想定しているユースケースの一例としてはモーションビューワーで、ランタイムで外部からモーションデータを読み込んで、適当な Humanoid モデルにモーションを適用するものです。

少し前の記事 でチラッと紹介したようにモーション生成 AI も出始めているので、それを見越したものにもなります。

特別記事の内容に影響はありませんが、検証に使用している Unity のバージョンは下記です。

  • Unity 2022.3.0f1

ワークフローの確認

全体の見通しが良くなるようワークフローを先にまとめます。

  1. Humanoid モデルの読み込み
  2. モーションデータの読み込み
  3. Generic アニメーションの Path の変換
  4. T-Pose の異なるアバター間のモーション変換
  5. Playable API によるモーション適用

これらを Unity Editor 上の開発時ではなく、ランタイム(アプリケーション実行中)に行っていきます。

あくまで一例に過ぎませんが、実現するまでに以外と多くのステップを挟む必要があります。

一つずつ順番に見ていきましょう。

Humanoid モデルの読み込み

Humanoid モデルの読み込みに関しては過去に記事を書いていますので、こちらを参照してください。

synamon.hatenablog.com

今回はシンプルに UniVRM を用いて VRM アバターをロードして使用します。

読み込んだ Humanoid モデル用にセットアップされている isHuman = trueAvatar があることを確認してください。

Generic アニメーションの使用

Unity におけるモーションデータの表現として一般的なものは AnimationClip になります。

AnimationClip にはいくつか属性がありますが、特に下記の2点に注意が必要です。

  • legacy
    • Animator で再生する場合は falseAnimation で再生する場合は true に設定します。
  • humanMotion
    • Humanoid アニメーションの場合は true、Generic アニメーションの場合は false が返ってきます。

Humanoid モデルのアニメーションは Avatar を Animator にセットして Humanoid アニメーションを使用するのが基本だと思います。

ただ動的に生成した AnimationClip の humanMotiontrue にする方法(条件)が分からないため、本記事では仕方なく Generic アニメーションを使用することを前提にして話を進めます。

※ もしランタイムで生成した AnimationClip を humanMotion = true にできる方法をご存知の方がいましたら教えていただきたいです。*1

Generic アニメーションでは各ボーンの Transform 上の名前でアニメーションを組みますが、モーションデータをロードした際にはそのモーションに入っているモデルのボーン名を用いた Path でロードされることが多いです。

しかし今回のようにアニメーションデータ内のモデルとは別のモデルにアニメーションを適用したい場合には、アニメーションを適用したい先のモデルのボーン名を用いた Path で AnimationClip を作成する必要があります。

そのため、Generic アニメーションの AnimationClip 生成の際の Path の指定の仕方に配慮する必要があります。

BVH のモーションの読み込み

BVH(.bvh)はテキストベースのモーションファイルフォーマットの一つで、Biovision 社という現存しない会社が提唱したものですが、モーション関連では現在でも一般的に使われている印象です。

qiita.com

BVH のランタイム読み込むはもちろん Unity は標準対応していないため*2外部ライブラリが必要ですが、実は UniVRM の中の UniGLTF、の中の UniHumanoid に BVH Importer が実装されています。

基本的にはこちらを使用すればよいですが、後述するようにボーン名の変換や T-Pose の差分の考慮が必要になるため、実際には BVH の Parse されたモーションデータ(UniHumanoid.Bvh)だけ拝借して AnimationClip 生成部分は自分で書き直すことになります。

FBX のモーションの読み込み

汎用 3D モデルフォーマットの FBX(.fbx)もモーションデータを持つことができるため、モーション関連で使用されることが多いかと思います。

UnityEditor では FBX のインポートに標準対応していますが、ランタイムでのインポートには対応していません。

FBX 用のランタイムライブラリとして自分がよく使用するのは、TriLib という有料アセットです。 assetstore.unity.com

各種フォーマット、各種プラットフォームにも対応していて使い勝手が良いです。

TriLib には AssetLoaderOptions というロード設定があり、「Animations」設定もありますが、「Rig」の設定で Humanoid に設定しても生成される AnimationClip は Generic アニメーションになります。

この時生成される AnimationClip は当然 FBX ファイル内のモデルに対する Generic アニメーションですので、BVH 同様ロードされたモーションのデータのみ使用し、AnimationClip は Path の変換や T-Pose の補正をかけて自分で再生成することになります。

ロードされたモーションの元データは AssetLoaderContext.RootModel.AllAnimations の中に入っていて、AnimationClip の生成に必要な情報は揃っています。

ちなみに今回は触れませんが glTF も FBX 同様モーションデータを持つことができますが、TriLib は glTF にも対応しているためほぼ同様の手順で glTF のアニメーションデータも読み込むことができます。

Generic アニメーションのための Path の変換

Generic アニメーションの使用 で説明したように、とあるモデルに合わせて作成されている Generic アニメーションのデータはそのモデル固有のボーン名の Path で構成されており、異なるモデルにそのまま適用しても Path が異なる場合にはアニメーションさせることはできません。

ですが Humanoid 想定のモデルであれば Avatar が生成でき、Unity の HumanBodyBones でキーポイントとなるボーンが規格化されています。

ということは下記のようなステップで一方のモデルの Path から相互の Avatar を通してもう一方の Path に変換することは難しくありません。

  1. とあるボーンの Path の末尾のボーン名を取得する
  2. そのモデルの Avatar.humanDescription.human からボーン名で検索をし、HumanBone.humanName を特定する
  3. 同じ humanName をもつ HumanBone を適用先の Avatar から検索をし、ボーン名を特定する
  4. 適用先の Transform から特定したボーン名の Transform の子供を探し、親に辿って Path を構成する

注意点としては当然 Humanoid のボーンの仕様外のアニメーションは変換できませんが、それは Humanoid アニメーションも同じ仕様なので大きな問題にはならないかと思います。

T-Pose の異なるアバター間のモーション変換

3D モデルの作り方には標準規格がなく、Humanoid モデルといっても作り手によって様々な仕様で作られるモデルが存在します。

使用する 3D CG ソフトの違いによる右手系 / 左手系や y-up / z-up の座標系の違いも有名ですが、Humanoid のボーンの向きの付け方にも様々あります。

例えば VRM モデルは T-Pose で全てのボーンがほぼ無回転、つまり 右側が x 方向、上側が y 方向、前側が z 方向を向きますが、とある FBX モデルではパーツの末端方法に y 軸が向く、いわゆる y-forward なものも存在します。*3

後者では T-Pose 時に y-forward 分の回転が入るため、同じ T-Pose でも VRM と FBX では姿勢が異なる、という場面が起こります。

  • VRM の Left Upper Leg:

  • とある FBX の Left Upper Leg

このような T-Pose の姿勢の違うモデル間でアニメーションをそのまま適用しても、結果が同じ姿勢にはなりません。

結果の例 をご覧ください)

VRM モデルの T-Pose が無回転に近いので、簡単のために適用先のモデルを VRM として適用先の回転を無視する場合を考えてみましょう。

この VRM モデルに対して、y-forward な FBX モデルのアニメーションを適用するための変換式は、FBX モデルの T-Pose 時のとあるボーンの Local Rotation を  L、Transform の親の World Rotation を  P、元の FBX モデルの該当ボーンのアニメーションのとあるフレームの Local Rotation を  A とすると、VRM モデルに適用するために変換されたアニメーションの Local Rotation  \Delta A'

 \Delta A' = P A L^{-1} P^{-1}

と表現できます。

これらの量は全て Quaternion なので積が非可換であること、右肩の -1 は Unity での Quaternion.Inverse(...) に相当することに注意します。

 A L^{-1} の部分は FBX の T-Pose の Local Rotation  L を差し引いているものです。

 P ( ... ) P^{-1} のように親の World Rotation で挟んでいるのは、Transform の回転は親の影響を受けるために親の回転を引いた姿勢で Local Rotation を再計算するためです。

Transform 親から順番に回転を適用していくので、例えば 1 -> 2 -> 3 の順で親から子に辿った時の子の World の回転は  W_3 = L_3 L_2 L_1 のように右から Quaternion をかけていきます。

この親の回転を一度打ち消すために右から  P^{-1} をかけ、更に親の回転自体は Local Rotation 自体の計算には不要であくまで軸が回転していることだけを取り入れたいため、もう一度  P をかけて Local の回転に戻してあげます。(親の回転分だけ座標系が回転しているので、座標系間の変換をしているイメージです)

簡単のために VRM の回転を無視していますが、実際には VRM モデルの Local Rotation、親の World Rotation も追加で考慮する必要があります。

VRM は上記だけでもそれなりに綺麗に変換できるためあまり検証できていませんが、おそらく同様に VRM の Local Rotation を加え、VRM の親の World Rotation で両側から挟むような形になることと、変換の対称性*4を考えると下記のような形になるかと思います。

 A' = P'^{-1} \Delta A' P' L' = P'^{-1} ( P A L^{-1} P^{-1} ) P' L'

同様の説明が VRM Animation の「ポーズデータの互換性について」 にも記載があります。

式の見かけは一見違いますが、 W = L P で書き直すと一致することが確認できます。

Playable API によるモーション適用

通常 UnityEditor 上で FBX などの 3D モデルデータをインポートして AnimationClip を作成し、AnimatorControllerTimeline に載せてモーションを適用することが多いかと思います。

しかし今回のアプローチでは適用先のモデルも適応したいモーションもランタイムで読み込んで使用することを想定しています。

(Runtime) AnimatorController の API をよく眺めてみると、AnimationClip を追加・編集するような API は見当らず、あくまで Readonly で参照できるだけのようです。

代替手段としては少し古い API の Animation *5、もしくは Timeline のベースにもなっている Playable API を使用することができます。

今回はより新しいアプローチの Playable API を使用します。

Playable API による AnimationClip の再生に関しては下記記事などを参照してください。

light11.hatenadiary.com

一つの 3D モデルに複数のモーションデータが入っている場合もあるため、AnimationMixerPlayer を使うのがベターかと思います。

tsubakit1.hateblo.jp

結果の例

Mixamo の Hip Hop Dancing(FBX)を実際に適用した例です。

ちなみに閲覧注意ですが上記を T-Pose の補正をせずにモーションを適用したのが下記です。

別サービスのモーションの例として、Vmotionize の Text to Motion で生成された泳ぎモーション(FBX)を使用したのが下記です。

VRM モデルは VRoid 製の自作のものを使用しています。

要点

処理をややこしくしているのは主に下記の点になります。

  1. 外部ファイルのランタイムでの読み込みに外部のライブラリを使用する必要がある
  2. 動的に読み込んだ AnimationClip を Humanoid にする設定方法が分からず、Humanoid アニメーションが使用できないこと
  3. Generic アニメーションにおける適切な Path 設定が必要なこと
  4. T-Pose の異なるモデル間のモーションの変換が必要なこと

結果的に前述のアプローチでうまくアニメーションを適用することには成功しましたが、Humanoid アニメーションとしてロードする方法が分かればもう少し楽できるはずです。

おわりに

Unity のアニメーションの仕組みに関してはそこまで理解していなかったですが、UnityEngine.Avatar や Quaternion の知識はあったおかげで最終的には見られる状態まで持っていくことができました。

本来はいつものようにライブラリを公開したいところですが、外部のライブラリに依存している部分が大きいこと、様々なパターンのモデルを網羅的にデバッグするとこまでは検証ができていないことから、本記事では考え方の解説に留めます。

Generic アニメーションで遠回りな実装ではありますが、改めて普段何気なく利用していた Humanoid アニメーションのありがたみを感じます。

あとはモーションデータのフォーマットとしても BVH は仕様が少し曖昧かつテキストベースでデータの無駄が多いこと、FBX や glTF は汎用フォーマットで情報量が多いことなど気になる点が多いですし、もう少しシンプルで扱いやすいフォーマットがあってもいいのではと思いました。( VRM Animation のモチベーションのそれかもしれませんが、まだ使える場面が限定的です。)

単位や座標系の違いをフォーマットの仕様で制限して、ライブラリ側で各 3D エンジン向けの変換をする方が賢い気がします。

将来的にはポーズデータを生成系 AI でリアルタイムに生成してアニメーションさせる、なんて時代が来た場合には外部から取り込んだモーションデータの適用が重要になるかもしれません。

以上、Unity におけるランタイムでのアニメーションの読み込みと適用に関して参考になれば幸いです。

*1:時間がなくて試せてないのですが、AnimationClip.SetCurve の引数の type に UnityEngine.Avatar を指定するとか...?

*2:Unity はランタイムでの外部ファイルのインポートは基本的に標準対応していないため、何かしらの外部ライブラリを導入する必要があります。

*3:z-forward で作る人もいると昔聞いたことがあります

*4:P = P'、L = L' の時に A = A' にならないとおかしいので

*5:Animation コンポーネントを使用する際には AnimationClip.legacy = true にする必要があります。

生成AIを使ってUnityのカメラアプリを作ってみた

こんにちは。Activ8の岡村です。

最近Activ8社内で生成AIハッカソンが行われたため、それに向けて作った簡単なUnityアプリをせっかくなので紹介します。*1

アプリの内容は、カメラで撮影した画像をAIに見せてプロンプトを生成し、生成したプロンプトで画像生成する、といったものです。

デモ

汚くて申し訳ないですが、自分の机の写真を用意しました。

The photo appears to depict a cluttered workstation from a first-person perspective. At the forefront, there is a black, full-sized keyboard with Japanese characters on the keys alongside the standard English letters and numbers, suggesting a bilingual user interface. To the right of the keyboard is a person’s right hand controlling a vertical ergonomic mouse, and further right is a clear glass of water positioned close to the edge of the desk.

Just beyond the keyboard, across the bottom edge of the photo, a white earbud with a long cable trails across the work surface, and next to it, a black wristwatch is placed face-down. To the left, there’s a large gaming headset resting on top of a closed laptop, its size dominating a significant part of the desk’s left side. Next to the headset is a small stack of personal items including what seems to be a card pack for a collectible card game. In the background, there are two monitors; the left one has a colorful display that could be a puzzle or strategy game, and the right one is turned off, reflecting the surrounding room. There are various cables and other objects, including a Nintendo Switch, revealing a certain unorganized, lived-in quality to the space. The entire scene is indicative of a tech-savvy individual with various interests in gaming, technology, and possibly language studies or translation work. The image captures a candid, everyday moment of someone's personal workspace.

すごく似ている、というわけでもないのですが、画像に映っているところどころのオブジェクトは再現されており、不思議な近親感を持つ画像に仕上がっています。

前提

  • 2023年12月9日時点の情報
  • OpenAI API
  • Unity 2022.3.11f1
  • Windows 10

Unityを使っているのは単純に私自身がUnityに慣れているからです。あと、時間があればUIを作りこんだりスマホ対応したかったな……というのもあります。

構想

発想当時はChatGPTが画像認識、画像生成に対応したタイミングであったため、このマルチモーダル機能を活かしたものを作りたいと考えていました。特に画像認識能力は既にOpenAI APIにも搭載されており、gpt-4-vision-previewモデルとして利用可能になっています。なので、画像を認識して説明し、それを基に加工された画像を用意する、といった仕組みの作成に挑戦してみました。

ちなみに、似た画像を生成するだけならDALL-EにCreate image variation機能があるのですが、こちらはプロンプトの指定ができません。プロンプトの指定ができると、画像の加工時に情報の追加や削除が柔軟にできて可能性が広がるのではと考え、一度プロンプトに変換する方法を試してみました。

実装

突貫で作ったのもありあんまり見せられたコードではないのですが、かいつまんで紹介していきます。

アプリ自体のつくりはシンプルで、撮影し、加工して、結果を見るという3ステートで構成されており、ステートの管理はImtStateMachineを使用しました。

github.com

撮影ステートでは、カメラの映像をWebCamTextureで取得し、撮影ボタンを押したら新しいテクスチャを作成してデータをコピーします。

public Texture TakeSnapShot()
{
    var destTexture = new Texture2D(texture.width, texture.height, DefaultFormat.Video, TextureCreationFlags.None);
    destTexture.SetPixels32(texture.GetPixels32());
    destTexture.Apply();
    return destTexture;
}

加工ステートではテクスチャをjpegに変換し、OpenAIにアップロードしています。

var gpuRequest = await AsyncGPUReadback.Request(texture, 0, TextureFormat.RGBA32);
if (gpuRequest.hasError)
{
    throw new Exception("Failed to readback texture.");
}
var data = gpuRequest.GetData<Color32>();
var jpg = ImageConversion.EncodeNativeArrayToJPG(data, texture.graphicsFormat, (uint)texture.width,
    (uint)texture.height, 0);
return Convert.ToBase64String(jpg);

zenn.dev

base64エンコードしたjpeg画像を、「この画像を画像生成のプロンプトに使えるくらい詳細に説明してください」といった感じのプロンプトとともにHttpRequestで投げています。

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions");
request.Headers.Add("Authorization", $"Bearer {apiKey}");
request.Content = new StringContent(
@$"{{
    ""model"": ""gpt-4-vision-preview"",
    ""messages"": [
        {{
            ""role"": ""user"",
            ""content"": [
                {{
                    ""type"": ""text"",
                    ""text"": ""Please describe the photo in enough detail to use it as a prompt for model generation.""
                }},
                {{
                    ""type"": ""image_url"",
                    ""image_url"": {{
                        ""url"": ""data:image/jpg;base64,{await GetBase64EncodedJpegTexture(image.Texture)}""
                    }}
                }}
            ]  
        }}
    ],
    ""max_tokens"": 300
}}", Encoding.UTF8, "application/json");

var responseMessage = await httpClient.SendAsync(request, token);

こうして受け取ったプロンプトを基に、DALL-E3で画像生成を行います。

var request = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/images/generations");
request.Headers.Add("Authorization", $"Bearer {apiKey}");
var content = new ByteArrayContent(Encoding.UTF8.GetBytes(
@$"{{
    ""model"": ""dall-e-3"",
    ""prompt"": ""{HttpUtility.JavaScriptStringEncode(prompt)}"",
    ""n"": 1,
    ""size"": ""1792x1024"" 
}}"));
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
request.Content = content;
var responseMessage = await httpClient.SendAsync(request, token);

最後に画像生成した結果をダウンロードし、テクスチャとして使えるようにしておしまいです。

var request = new HttpRequestMessage(HttpMethod.Get, url);
var responseMessage = await httpClient.SendAsync(request, token);
var response = await responseMessage.Content.ReadAsByteArrayAsync();
var texture = new Texture2D(1, 1);
texture.LoadImage(response);

振り返り

Unityでの画像の扱いは以前からGPU周りの事情などが絡んでいて負荷を避けるのが難しいのですが、最近はAsyncGPUReadbackImageConversionなどのAPIが生えていて、だいぶ楽になっているように感じました。上記コードではまだ妥協して古いAPIを使ってしまっているところが多いので、後でよりフレームレートに優しいコードに変えておきます。

OpenAI APIをC#から直接叩く際、Jsonペイロードの用意が中々面倒でした。元々C#とJsonの相性がいまいちなのもありますが、特に理不尽なところとして、chat/completionsではContent-TypeヘッダがStringContentクラスが生成するapplication/json; charset=utf-8のままでも問題なく使えたのですが、images/generationsではapplication/jsonでないとエラーが出てしまい、回避のためにByteArrayContentクラスを使わなければなりませんでした。 APIKeyの流出問題もあるので、公開するアプリではきちんと間にサーバーを挟んであげることをおすすめします。

また、生成の結果によってはコンテンツポリシーに違反しているというエラーが発生することもあり、日常の画像を使う際にはこういったエラーのハンドリングにも気を付ける必要がありそうです。

400 - BadRequest
{
  "error": {
    "code": "content_policy_violation",
    "message": "Your request was rejected as a result of our safety system. Your prompt may contain text that is not allowed by our safety system.",
    "param": null,
    "type": "invalid_request_error"
  }
}

画像を取り扱うAPIはコストも高く、2023年12月9日現在、プロンプト生成→画像生成の一連の流れで0.1ドル程度かかってしまっていたので、試す場合はコストにもご注意ください。画像認識のdetailを下げることで多少は回避できますが、クオリティとのトレードオフになります。

*1:体調不良で結局間に合いませんでした……

リアルタイムお絵描きツール「flowty-realtime-lcm-canvas」を触ってみた

はじめに

こんにちは、フロントエンドエンジニアの堀江(@nandemo_3_)です。

社内の生成AIハッカソンのネタを探しているときに、XでLCMを用いたリアルタイムお絵描きツールがいくつかあることを知りました。

その中でも、flowty-realtime-lcm-canvasが非常に導入しやすく、初学者のような自分にとって画像生成AIの学びのきっかけになると思い、今回紹介しようと思います。

※注意:初学者のため、間違っていることがあるかもしれません。ご了承ください。

LCMとは

LCM(Latente Consistency Model: 潜在的一貫性モデル)は2023月10月に論文が発表されたかなり新しい生成モデルです。

LCMの特徴は、高速かつ高品質な画像を生成できることで、画像生成は2~4ステップで完了します。

Latent Diffusion Modelsの効率を向上させるために開発された新世代の生成モデルとなります。

この章だけで、テックブログ1回分の量になるくらい情報量なので、概要はこれくらいにします。

Consistency Model(一貫性モデル)について詳しく知りたい方はこちらをご覧ください。非常に分かりやすいです。

zenn.dev

また、驚くことに、LCMはOSSのため、無料で使うことができます。

flowty-realtime-lcm-canvasの紹介

その無料で使えるLCMのツールがflowty-realtime-lcm-canvasです。

flowtというコミュニティが開発している、LCMをWeb UI上で試すことができるツールです。

Web UI部分は gradio libraryを使用しているようです。

操作感は、Stable Diffusionに近いので、すでにStable Diffusionを触られている方は問題なく使えると思います。

github.com

インストール、起動

インストールから起動まで非常に簡単です。今回はMacで実行しました。

※Google Colab Proを持ちの方は、以下のコマンド実行でサクッと試すことができます

!git clone https://github.com/flowtyone/flowty-realtime-lcm-canvas.git
%cd flowty-realtime-lcm-canvas
!pip install -r requirements.txt
!python ui.py --share

では、Macでの手順となります。

まず、venvを用いて仮想環境を立ち上げます。(my_venvはプロジェクト名)

$ python3 -m venv /path/my_venv
$ source /path/my_venv/bin/activate

venvが起動するとターミナルの頭が、(my_venv)のようにvenvのプロジェクト名になります。

続いて、PrTorchをインストールします。

(my_venv) $ pip install torch torchvision torchaudio

そして、flowty-realtime-lcm-canvas本体のクローンとインストールをします。

(my_venv) $ git clone https://github.com/flowtyone/flowty-realtime-lcm-canvas.git
(my_venv) $ cd flowty-realtime-lcm-canvas

(my_venv) $ pip install -r requirements.txt

最後に、ツールの起動をします。

実行を待つと以下のようにlocalhostのURLが表示されるので、ブラウザでアクセスしてください。

(my_venv) $ python ui.py

...
Running on local URL:  http://127.0.0.1:7860

To create a public link, set `share=True` in `launch()`.

この画面が表示されたら、準備完了です。

デモ

ここからは、思う存分画像生成を楽しむことができます。

画面上部にいくつか設定項目がありますが、Stable DiffusionのようにPromptに生成したいイメージの情報を入力します。

デフォルトでは、「Scary warewolf, 8K, realistic, colorful, long sharp teeth, splash art」となっています。

これを、「sunflower」としてみます。

かなり幼稚な絵でも割と完成度の高い画像になります。

続いて、「mountain, road, rice fieald」としてみると、そこそこいい感じの画像になりました。

性能ですが、Apple M2のMac Book Proを使っていますが、デフォルトの設定では、5~7秒程度で画像が生成されました。

Windowsの場合やスペックによっては、動かないという問題もあるようです。

まとめ

今回は、最近話題のリアルタイムお絵描きツール「flowty-realtime-lcm-canvas」を触ってみました。

厳密に言うとリアルタイムではなく、ワンクリックごとに画像生成の処理が走るのですが、

ものの数秒で自分で描いたイメージに近い画像が生成されるので、非常に面白いですね。

ハッカソンではDiffutionモデルを用いたデモを開発した方がいらっしゃいましたが、速度感は圧倒的にLCMが早いなという印象でした。

別のツールでは、絵を描くと同時に(ほぼ)画像が生成されるものもあり、性能については時間の問題のようです。

FPSが30~60出るようになれば、もはやアニメやゲームの背景は、LCMで生成可能になりそうです。

最後まで読んでくださりありがとうございました。

参考文献

self-development.info

makereal(tldraw)を試してみる

はじめに

こんにちは、エンジニアのクロ(@kro96_xr)です。

先日以下のようなポストが流れてきました。

言葉にするならワイヤーフレームからHTMLを生成できるツールでしょうか。
元々Miroライクなホワイトボードアプリとしてtldrawがあり、それにOpenAIのGPT-4Vを組み合わせたツールとしてmakerealが公開されたようです。

github.com

github.com

私自身メインはサーバサイドであり、フロントエンドは得意ではないので早速使ってみることにしました。

なお、https://makereal.tldraw.com/にアクセスすればブラウザ上でも使用できますが、リポジトリも公開されているのでローカル上で試してみます。

使い方

環境構築に必要な作業は以下の通りです。

  1. リポジトリのクローン
  2. OpenAIのAPIキー取得
  3. プロジェクトを立ち上げる
  4. APIキーを設定
  5. 図を描く
  6. 生成!

リポジトリのクローン

Gitに慣れている方であれば説明不要だと思いますが、https://github.com/tldraw/make-realにアクセスしてリポジトリをクローンします。

===以下ChatGPTで作成===

  1. まず、コンピュータにGitがインストールされていることを確認します。インストールされていない場合は、Gitの公式サイトからダウンロードしてインストールしてください。
  2. コマンドラインまたはターミナルを開きます。
  3. 次のコマンドを使用して、tldraw/make-realリポジトリをクローンします。
git clone https://github.com/tldraw/make-real.git

クローンが完了したら、次のコマンドで新しく作成されたディレクトリに移動します。

cd make-real

===ここまでChatGPTで作成===

不明点などがあれば公式のリファレンスもあわせてご覧ください

docs.github.com

OpenAIのAPIキー取得

GPT-4Vを使用するためにOpenAIのAPIキーが必要になります。

OpenAIのサービスにログインの上、API KeysにアクセスしてAPIキーを生成しましょう。
Create new secret keyをクリックし、必要情報を入力するとAPIキーを生成できます。発行したAPIキーは後ほど使うので安全な場所にコピペしておきましょう。

また、APIの使用は有料なので、Payment Methodからクレジットカードを登録しておきましょう。

プロジェクトを立ち上げる

ここまでできたらクローンしたプロジェクトを立ち上げます。
手順はREADMEにも書いてある通りです。

Node.jsをインストールしていない場合はまずインストールする必要があります。
公式サイトからインストーラーをダウンロードしてインストールするか、nvmを使ってインストールしましょう。
nvmの使い方は割愛しますので、各自検索をお願いします。

公式

Node.js

Windowsの場合

GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go.

Mac/Linuxの場合

GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

Node.jsをインストールしたら、クローンしたディレクトリ内に移動して以下のコマンドを実行し、依存するパッケージをインストールします。

npm i

パッケージのインストールが正常に終了したら、サービスを立ち上げます。

npm run dev

正常に立ち上がったらhttp://localhost:3000/にアクセスするとホワイトボードが表示されます。

APIキーを設定する

画面中央下にYour OpenAI API Keyと書かれた入力欄があるので、先に発行しておいたAPIキーをコピペします。

ここまでで環境構築は完了です。

図を描く

いよいよ実際に図を描いていきます。
tldrawの機能で描いてもいいですし、スクショをペーストして作ってもらうことも可能です

ちょうどMiroで適当に書いたECサイトのトップページみたいなものが手元にあったのでペーストしてみます。

生成

図を描き終えたらGPT-4Vに渡したい範囲を選択して、右上のMake Realボタンを押すだけです。

うーん、それぞれの要素の配置はあっていますが、ちょっと違いますね。
特にバナーは完全に消えてヘッダとくっついているように見えます。

どうやらテキストでの補足もできるようなので試してみます。

指示を英語にした影響か、サイト内の文章も英語になりましたが、カルーセルも実装してくれました。

ただ、このままではECサイトっぽくないのでもう少し雑な指示にチャレンジしてみます。

基本的な要素は変わらず、バナーより下の部分のテキストと画像サイズが変わりましたね。
たしかに元のデザインはサムネイルが小さすぎるので、良い変更だと思います。

こうなってくるとサムネイルに画像を追加したいですね。
(greatの綴りを間違えていることにスクショ撮り終わってから気づきました)

図とテキストの指示を組み合わせてここまで作ることができました。
一旦今回はこのくらいにしておきます。

HTLMの確認

生成したサイトのHTMLはクリップボードにコピーすることができます。

今回生成したコードが以下になります。
内部の画像はunsplash.comから取得しているようで、商用利用も無料なので安心ですね。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>E-commerce Site</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* Additional styles if needed */
        .carousel-indicator {
            transition: background-color 0.3s;
        }
        .carousel-indicator.active {
            background-color: #4B5563; /* Gray-600 */
        }
    </style>
</head>
<body class="bg-white text-gray-800">
    <div class="container mx-auto px-4 py-8">
        <header class="text-white text-center p-6 mb-6" style="background-color: #1E40AF;">
            <h1 class="text-3xl font-bold">Your Shop</h1>
        </header>

        <section class="mb-6">
            <div id="carousel" class="relative">
                <div class="flex justify-center space-x-2 mb-4">
                    <span class="carousel-indicator h-3 w-3 bg-gray-300 rounded-full cursor-pointer active"></span>
                    <span class="carousel-indicator h-3 w-3 bg-gray-300 rounded-full cursor-pointer"></span>
                    <span class="carousel-indicator h-3 w-3 bg-gray-300 rounded-full cursor-pointer"></span>
                </div>
                <div class="flex overflow-hidden">
                    <img src="https://source.unsplash.com/random/800x300?sig=1" alt="Banner 1" class="block w-full">
                    <img src="https://source.unsplash.com/random/800x300?sig=2" alt="Banner 2" class="block w-full">
                    <img src="https://source.unsplash.com/random/800x300?sig=3" alt="Banner 3" class="block w-full">
                </div>
            </div>
        </section>

        <section class="mb-6">
            <h2 class="text-2xl font-semibold text-center mb-4">Featured Products</h2>
            <div class="grid grid-cols-3 gap-4">
                <!-- Placeholder for product thumbnails -->
                <img src="https://source.unsplash.com/random/200x200?product,1" alt="Product 1" class="w-full h-48 rounded-lg">
                <img src="https://source.unsplash.com/random/200x200?product,2" alt="Product 2" class="w-full h-48 rounded-lg">
                <img src="https://source.unsplash.com/random/200x200?product,3" alt="Product 3" class="w-full h-48 rounded-lg">
            </div>
        </section>

        <section>
            <h2 class="text-2xl font-semibold text-center mb-4">Latest Arrivals</h2>
            <div class="grid grid-cols-3 gap-4">
                <!-- Placeholder for product thumbnails -->
                <img src="https://source.unsplash.com/random/200x200?product,4" alt="Product 4" class="w-full h-48 rounded-lg">
                <img src="https://source.unsplash.com/random/200x200?product,5" alt="Product 5" class="w-full h-48 rounded-lg">
                <img src="https://source.unsplash.com/random/200x200?product,6" alt="Product 6" class="w-full h-48 rounded-lg">
            </div>
        </section>
    </div>

    <script>
        // Simple carousel functionality
        const indicators = document.querySelectorAll('.carousel-indicator');
        const slides = document.querySelectorAll('#carousel img');
        let currentSlide = 0;

        indicators.forEach((indicator, i) => {
            indicator.addEventListener('click', () => {
                indicators[currentSlide].classList.remove('active');
                slides[currentSlide].classList.add('hidden');
                currentSlide = i;
                slides[currentSlide].classList.remove('hidden');
                indicator.classList.add('active');
            });
        });

        // Auto-rotate slides every 5 seconds
        setInterval(() => {
            indicators[currentSlide].classList.remove('active');
            slides[currentSlide].classList.add('hidden');
            currentSlide = (currentSlide + 1) % slides.length;
            slides[currentSlide].classList.remove('hidden');
            indicators[currentSlide].classList.add('active');
        }, 5000);
    </script>
</body>
</html>

おわりに

以上、簡単に触ってみましたが、ワイヤーフレームとテキストからそれらしいものが生成できたので、モック制作や個別のコンポーネント制作の時などに役立ちそうかなと思いました。

一方でチーム開発や運用を見据えるとデザインシステムの構築なども必要であり、この生成AIで開発が完結することはないと思います。ただ、AIの進歩は凄まじいので1ヶ月後はわかりませんね。

いずれにせようまく活用して業務効率化していきたいですね!

Rustのcandleを使って顔検出を実装してみる

こんにちは、エンジニアの渡辺(@mochi_neko_7)です。

今回は Rust の candle という ML(Machine Learning) フレームワークを使用して、BlazeFace というモデルを用いた顔検出(Face Detection)を趣味開発で実装した話を紹介します。

Rust で ML をやること自体まだ珍しいかと思いますし、Rust で実装されている candle は開発中というのもあり情報が少ないので使用例の一つとして参考になればと思います。

BlazeFace を選んだ理由は、私自身 ML は初心者のため実装が比較的シンプルなモデル*1を自分で実装してみたかったこと、VTuber 向けの Face Tracking システムがどのようにできているか興味を持った中で Face Detection がワークフローの最初のステップに実行される汎用的なもの*2であることで、実用性よりは個人的な勉強の色が強いです。

ですが candle は Pure Rust で実装されていてクロスプラットフォームの対応ができること、WebAssembly 対応がありブラウザでの利用も可能なこと、Rust の性能とメンテナンス性の高さ*3、Unity/C# への組み込みも現実的であること*4から、プロダクションにおける実用性もパフォーマンス次第ではあるかもしれません。

本記事では下記のような読者を想定しています。

  • ML を触ったことがあり Rust / candle での実装に興味がある方
    • PyTorch の実装との比較ができるかと思います
  • ML 初心者で基礎理論は理解していて、具体的な実装例を見てみたい方
    • 私自身がこの立場のため
    • 個人的には Python より Rust の方が型が明示的で実装が読みやすいのではと思っています

ソースコードは下記の Repository で公開していますが、crate として公開する準備をまだ整えていないこと、クロスプラットフォームの動作検証ができていないことからすぐに製品利用することは難しいことにご注意ください。

github.com

使用している環境は下記になります。

  • Windows 11
  • NVIDIA GeForce RTX 4090
  • CUDA 11.8 / cuDNN 8
  • Rust v1.73.0
  • candle v0.3.0 (#73d02f4f57c788c43f3e11991635bc15701c25c0)

candle とは

candle は HuggingFace が開発している Rust 製の ML フレームワークです。

github.com

API の比較表があるように PyTorch に近い API 構成をしていますので、PyTorch を触ったことのある方なら直感的に触れるかと思います。*5

既存の PyTorch や TensorFlow の ML フレームワーク は良くも悪くも成熟していて、Rust Bindings (例えば PyTorch に対する tch-rs) のバイナリサイズが大きくなってしまいます。

それに対して candle は Pure Rust で軽量な ML フレームワークを提供します。

そもそも Python は GIL などパフォーマンスに難があり、実戦でそのまま導入するには懸念が出るケースもあります。(実際に Mojo のような言語によるアプローチも開発されています。)

その点 Rust は実行速度が速かったり、WebAssembly にビルドしてブラウザで高速に動かすこともしやすいなどのメリットがあります。*6

反面、Rust での ML は環境が未成熟であったり、candle 自身もまだ開発中だったりする点に注意が必要です。

candle は examples で最新のモデルの実装例があり興味を持ったこと、API が PyTorch に近いので PyTorch の勉強にもなることから採用しました。

BlazeFace とは

今回実装する機械学習モデルは、BlazeFace という顔検出(Face Detection)のモデルです。

arxiv.org

画像中の人間の顔を検出し、それぞれの顔の Bounding Box と6点のキーポイント(右目、左目、鼻、口、右耳、左耳)の位置、スコアを推定します。

Google が開発したモバイル端末の CPU でも動作することを目指したモデルで、MediaPipe の中に含まれている Face Detection でも使用されています。

developers.google.com

BlazeFace のモデルのバックボーンとして、論文の Figure 1 に書かれているような BlazeBlock という二層の CNN をベースとした構造を利用します。

これは顔検出タスクでよく使用されている MobileNetV2 のパフォーマンスを改良したものになります。

顔検出タスクではニューラルネットワークを使用しない CV(Conputer Vision)の実装として例えば dlib の Face Detector などもよく使用されていますがロバストネスに課題があり、ニューラルネットワークを使用したモデルではこれを改善できるとのことです。*7

スマホのフロントカメラを想定した 128x128 の解像度の画像を入力とする Front モデル、バックカメラを想定した 256x256 の解像度の画像を入力とする Back モデルの二種類が実装されており、それぞれアーキテクチャもパラメータも異なります。

candle は PyTorch のパラメータファイル(.pth)も対応していますが挙動が不安定な部分*8もありますので、Safetensors(.safetensors)に変換して利用します。

BlazeFace の API は下記のような形です。

  • 入力
    • 128x128 もしくは 256x256 の画像(3チャネル)
    • バッチ処理、つまり一度に複数の画像を入力することも可能
  • 出力:
    • 各画像で検出した人数分の17成分の1次元 Tensor のリスト
    • 1 ~ 4:顔の Bounding Box の 左上(y min、x min)と右下(y max, x max)
    • 5 ~ 16:顔の6点のキーポイント(右目、左目、鼻、口、右耳、左耳)の座標(y、x)
    • 17: 信頼度スコア

candle を使った BlazeFace の再実装

BlazeFace 本家は TensorFlow で実装されていますが、candle により近い PyTorch 実装をされている Repository があったのでこちらを参考にさせてもらいました。

github.com

BlazeFace の実装はいくつかのステップを踏む必要があります。

  1. BlazeBlock の実装
  2. Front モデルと Back モデルの実装
  3. 推論結果の後処理の実装
  4. Non-maximum Supression の実装
  5. 最終的な BlazeFace の API の実装
  6. 画像の入出力処理の実装

1. BlazeBlock の実装

バックボーンとなる BlazeBlock および FinalBlazeBlock を PyTorch の実装を参考に candle で実装します。

face-tracking-rs/src/blaze_face/blaze_block.rs at main · mochi-neko/face-tracking-rs · GitHub

BlazeBlock は Stride が1と2の場合で処理に分岐があるため、上記ではそれぞれを明確に分けて実装しています。

また、Back モデルでは Stride は2でも Max Pooling と Channel Padding を適用しない FinalBlazeBlock も使用されるため、こちらも別で実装します。

face-tracking-rs/src/blaze_face/final_blaze_block.rs at main · mochi-neko/face-tracking-rs · GitHub

mod tests { ... } で Shape のチェックだけするテストコードも書いておきます。

2. Front モデルと Back モデルの実装

Conv2d レイヤーと BlazeBlock、FinalBlazeBlock を組み合わせた Front モデルと Back モデルをそれぞれ PyTorch 実装を参考にしながら実装します。

face-tracking-rs/src/blaze_face/blaze_face_front_model.rs at main · mochi-neko/face-tracking-rs · GitHub

face-tracking-rs/src/blaze_face/blaze_face_back_model.rs at main · mochi-neko/face-tracking-rs · GitHub

基本的には入り口の Conv2d → ReLU 活性化 → 複数の BlazeBlock → Classifier と Regression というフローの推論をします。

最後は Classifier にかけてスコアの推定を、Regression にかけて Bounding Box や Keypoint の位置の回帰推定をします。

ライブラリの Tensor という構造体は C# における Object 型のようなもので、実際の値に加えて Device(CPU or GPU)、DType(32bit float、16bit float などのパラメータの値型)、Shape(Tensor の形状、各成分の次元)があり、コンパイルは通っても実行時にそれらの不整合があるとエラーを出してしまいます。

特に Shape は不整合が起こりやすいため、それぞれ入力の Tensor の Shape が (batch_size, 3, 128, 128)(batch_size, 3, 256, 256) であることを念頭に、計算過程の Tensor の Shape がどうなっているか確認しながら実装をし、丁寧にテストコードも書いておきます。

candle では .pth (PyTorch のパラメータファイル)、.safetensors (HuggingFace が定義しているパラメータファイル)などのパラメータファイルから candle_nn::VarBuilder を通して Weight や Bias などのパラメータをロードできますので、それに合わせたロード処理も実装しておきます。

3. 推論結果の後処理の実装

生の推論結果の Tensor はそのまま使用するのではなく、

  • 回帰の結果を Anchor を使用して Pixel の座標系に変換(デコード)する
  • Bounding Box は Center + Size → Min + Max に変換する
  • スコアのフィルタリングをする

などをして扱いやすいよう加工しておきます。

face-tracking-rs/src/blaze_face/blaze_face.rs at 7030448ae4d9924f881f54f86bd0238fe5ca5a6c · mochi-neko/face-tracking-rs · GitHub

4. Non-maximum Supression の実装

物体検出で一般的に使用される Non-maximum Supression を実装して、同じ人物の顔の検出結果をまとめます。

face-tracking-rs/src/blaze_face/non_max_suppression.rs at main · mochi-neko/face-tracking-rs · GitHub

一定以上の重なりのある Box 同士を判定し、Score による重心の計算をして同じ顔の位置を補正し、Score は平均を取ります。

Non-maximum Supression の詳しい解説は下記を参照してください。

meideru.com

5. 最終的な BlazeFace の API の実装

2 ~ 4 をつなぎ合わせて、最終的な API を実装します。

  • 入力
    • 128x128 もしくは 256x256 の画像(3 Channels)
    • バッチ処理、つまり一度に複数の画像を入力することも可能
  • 出力:
    • 各画像で検出した人数分の17成分の一次元 Tensor のリスト
    • 1 ~ 4:顔の Bounding Box の 左上(y min、x min)と右下(y max, x max)
    • 5 ~ 16:顔の6点のキーポイント(右目、左目、鼻、口、右耳、左耳)の座標(y、x)
    • 17: 信頼度スコア

face-tracking-rs/src/blaze_face/blaze_face.rs at 7030448ae4d9924f881f54f86bd0238fe5ca5a6c · mochi-neko/face-tracking-rs · GitHub

また、Rust 上で結果を扱う場合には Tensor ではなく明示的に構造体を用意した方が触りやすいため、Tensor → 構造体(FaceDetection)の変換処理もオプションで用意しておきます。

face-tracking-rs/src/blaze_face/face_detection.rs at main · mochi-neko/face-tracking-rs · GitHub

各モデルの forward 処理の終盤の permute、resize をよく追うと座標の順番が x、y ではなく y、x の順番になっている点に注意します。*9

face-tracking-rs/src/blaze_face/blaze_face_front_model.rs at 7030448ae4d9924f881f54f86bd0238fe5ca5a6c · mochi-neko/face-tracking-rs · GitHub

PyTorch の実装ではなぜかこの順番が不自然だったので修正をして実装をしています。

6. 画像の入出力処理の実装

Rust 上での画像の入出力は image crate、加工処理は imageproc crate が利用できます。

face-tracking-rs/examples/utilities.rs at 7030448ae4d9924f881f54f86bd0238fe5ca5a6c · mochi-neko/face-tracking-rs · GitHub

face-tracking-rs/examples/utilities.rs at 7030448ae4d9924f881f54f86bd0238fe5ca5a6c · mochi-neko/face-tracking-rs · GitHub

Tensor の順番や解像度などに気を遣う必要があるので少し注意が必要ですが、これらの外部 crate に直接依存するのは避けたいため現在は examples 内に実装を用意しています。

もしかしたら後で本体側にも実装を用意するかもしれません。

実行例

photoAC からライセンスフリーの写真をお借りして BlazeFace を実行してみた例が下記です。

一人の場合は多少角度がついていても検出自体はできていることが分かります。

ただ Bounding Box のサイズや Keypoint の位置のずれが気になりますし、多人数の場合の結果が近い顔に密集していて不自然なので、Non-maximum Supression などの実装かパラメータ設定に不備があるかもしれません。

ベンチマーク

ベンチマークで実際の処理負荷の計測結果を公開したいところですが、普段開発に利用しているのが Docker (WSL2)環境であること、CUDA を使用すると実行時に謎のエラーが出たり、CPU では MKL のコンパイルが通らないことなどあり、まともな結果をまだお見せすることができません。

Windows ローカルで MKL なし、Front モデルが 21ms 前後と想定より遅すぎるので、最適化すべき実装が多分に残っていると思われます。

正しく計測できた際には Repository の README に記載する予定ですので、少しお待ちください。

活用例

BlazeFace の Face Detection は顔関係のタスク、例えば Face Landmark Detection(顔の特徴点検出)の前処理としても利用されます。

実例としては Media Pipe の Face Landmarker でも BlaceFace が使用されています。

Face landmark detection guide  |  MediaPipe  |  Google for Developers

そのため、Face Detection 単体ではなくより高度なタスクと組み合わせることでより BlazeFace の軽量さに価値が出るかと思います。

おわりに

実行例でお見せしたように、一応実際に candle / Rust を使った BlaceFace の Face Detection が実装できました。

とはいえ想定より少し精度が低い点、複数人の場合の処理が怪しい点、ベンチマークによる最適化対応ができていない点など課題も多く残っています。

これらの対応が落ち着いたら、次の目標の Face Landmark Detection に挑戦しようと思います。

candle / Rust で BlazeFace を実装してみた感想もいくつか述べておきます。

  • Rust の型システムのおかげで途中処理が追いやすい
    • Python だと Type Hints を使っている実装が少なくて理解に時間がかかる場合も多いですが、Rust だと明示的で理解しやすいと個人的に思います
    • とはいえ Tensor は candle でも PyTorch 同様、実装ミスによる Shape や DType などの実行時エラーが起こりやすく、テストコードを書きながら実装しないとデバッグが大変になりそうです
  • ML の基礎の再確認になった
    • CNN の挙動やパラメータファイルの IO など、ML 初心者の自分には手を動かすことで勉強になりました
  • PyTorch との差分は当然ある
    • 特に Tensor の Setter がない点、Mask の実装に少し工夫が必要な点に注意が必要でした
    • ただし candle のアップデートで変わる可能性もあります
  • 再実装でもそれなりに時間はかかる
    • 参考になる PyTorch 実装があっても、テストコードを書きながら実装してもやはりそれなりに時間がかかりました(慣れの問題もありますが)
  • Rust の cargo(パッケージ管理)、rustfmt(フォーマッター)、criterion(ベンチマーク)などのエコシステムが優秀で開発が快適

なにより Rust で ML を書ける、学べるのは楽しいので、Rust が好きで ML に興味がある方は candle から入門してみるのもありかもしれません。

*1:BlazeFace は CNN ベースで特に難しい構造は登場しません

*2:例えば顔の形状を取る Face Landmark Detection でも始めに Face Detection をして人物の有無、顔の位置の特定をします

*3:Python と比較して

*4:csbindgen で Rust -> C# API の変換ができるため

*5:名前も Torch(松明)に対する蝋燭(Candle)というアナロジーでしょうね

*6:もちろん Python でも裏側の実装を C や C++ で書いて高速化するケースも多いですが、再実装の手間やメモリ安全性の問題などもあります。

*7:詳細:https://arxiv.org/pdf/2101.10808.pdf

*8:Linux では問題なかったですが、Windows だとロードエラーになりました。

*9:元が height, width の順番なので

M5Stackを使ってタイムコード生成器を試作する

はじめに

エンジニアの松原です。以前紹介したオーディオ・映像機器間の同期方法に関しての取り組み(その1その2) を記事にしました。
前回記事ではオーディオ信号にLTC信号を送り、マイク入力から受け取ったLTC信号をUnity上でデコード、表示する方法について取り上げましたが、今回は前回記事とは逆に、LTC信号を送る側として、マイコンとして扱いやすいM5Stack(今回利用したのはM5StackCoreS3)とM5Stack用RCAモジュールを使ってLTC信号を生成するデバイスを試作してみました。今回はまだ試作段階で詰めが甘いところもあるため、コード公開はまた別の機会に行いたいと思います。

docs.m5stack.com

しくみ解説

タイムコードの最低単位としてフレーム(1秒以下の単位、タイムコードでは24~30フレーム)ごとにマイコン上でカウントアップし、このフレーム単位に合わせてLTC信号として信号の生成を行います。
まず総フレーム数をそれぞれ時間、分、秒、フレームの単位に変換し、それらをタイムコードとして80bitのデータに変換します。
そのタイムコードのデータを実際のオーディオデータとして再生するために48000Hzの信号データ(16bitの量子化データ)に変換後、DACを通して実際にオーディオ信号として出力します。
これらをM5Stackに対してプログラミングを行い、信号をRCAに出力します。

タイムコードの80bitのデータに関しては以前の記事でも取り上げましたがWikipediaにタイムコードのデータフォーマット表を参考にするのが分かりやすいかと思います。

実際にオーディオ信号として出力されたものをオシロスコープで信号のパターンを見てみました。

Sync Wordのパターンが見えるので、上手くいっているようです。波形が荒れているため、今後DACに送る際の信号データに何かしら加工を加える必要があるかもしれません。

生成されたLTC信号をUnity上でデコードしてみる

前回の記事の方法を使ってデコードしてみました。

synamon.hatenablog.com

放置してしばらく時間経過後、同時に並列で走らせていたストップウォッチの時間とのずれがどれぐらい出たか比べてみました。半日ぐらい放置した画像が下の画像です。

厳密なズレまでは計測できていませんが、ストップウォッチで時間を確認したところ、数時間で数秒ぐらいのズレで済んでいるので、初めて取り組んだ割にはよくできたのではないかと思います。

まとめ

今回まだ試作段階ですが、マイコンを使ってタイムコードを自作して、実際にUnity側でデコードできるか試した結果、思ったよりも結構うまくいきました。
ただ、タイムコードのデバイスとしてちゃんとM5Stack側のGUI部分の作成などがまだできていないのと、LTCの規格としてちゃんと信号が出せているかは確認できていないため、今後さらにブラッシュアップしていければと考えています。

JavaScriptランタイム最新動向(Node.js vs Deno vs Bun)

こんにちは、フロントエンドエンジニアの堀江(@nandemo_3_)です。

2023年9月にJavaScript RuntimeのBunが、バージョン1.0をリリースしました。

bun.sh

JavaScriptランタイムといえばNode.jsですが、DenoやBunとの違いは何か。

自分は恥ずかしながら、最近、Deno、Bunという新しいJavaScriptランタイムがあるということを知り、この際学び直しをしました。

JavaScriptランタイムってなんだ?DenoやBunってなんだ?っていう初心者向けの記事となります。

ランタイムとは

そもそもJavaScriptランタイムのランタイムとは何か。

実行すること、実行時に必要な部品。

実行環境と表現されたりします。

つまり、JavaScriptランタイムとはJavaScriptを実行する環境のことを意味し、主にサーバサイドのプログラムが実行されます。

JavaScriptランタイムの種類

それでは、JavaScriptランタイムの種類を紹介していきます。

Node.js

Node.jsは、JavaScriptランタイムといったらこれと言っても過言ではないくらい、デファクトスタンダードとなっていますよね。

Node.jsはRyan Dahlによって開発され、2009年に発表されました。

14年前(2023年現在)から存在するんだとびっくりしますが、

元々、フロントエンドのみでしか使えなかったJavaScriptが、Node.jsの登場でバックエンドでも利用可能となり、革命と言っても過言ではない出来事がこの時起きました。

圧倒的に使われているNode.js

ここで、この後紹介するDenoやBunを含むJavaScriptランタイムのGitHubのStar数(2023年10月30日時点)を比較してみます。

Node.jsとDenoに大きな差はなく、Bunも熱量の高さを感じられます。

一方、State of JavaScript 2022*1によると、

普段使用しているランタイムはNode.jsが圧倒的だということがわかります。

https://assets.devographics.com/captures/js2022/en-US/runtimes.png

2022.stateofjs.com

Node.jsの問題点

Node.jsは現在もJavaScriptランタイムの王者として君臨し、巨大なコミュニティとサポートを提供しています。

StackOverflowを見れば、大体の課題の解決策があるでしょう。

しかし、諸行無常。

Node.jsには課題があります。

Node.jsの発展とともに多くのモジュールが開発され使われるようになりましたが、それに伴ってモジュールが何重にも使われ、複雑で遅くなりました。

その要因の一つとして挙げられる課題は、Node.jsが採用しているJavaScriptモジュールシステムがあります。

登場してすぐのNode.jsは、モジュールシステムを仕組みとしてなかったため、Common JSを採用しました。

しかし、ES6で標準化されたモジュールシステムはES Modulesでした。

Common JSのコード

const { hoge } = require('./hoge')

ES Modulesのコード

import { hoge } from './hoge.js'

Node.jsで使われるモジュール類はこのどちらかを使っており、Node.jsはそのどちらも対応することにしました。

これにより、どちらのモジュールシステムを採用しているかによるトラブルが発生。

例えば、Common JS準拠のモジュールから、ES Modules準拠のモジュールを呼び出すことはできません。

その他にも、Node.jsには、セキュリティやTypeScriptサポートの課題があります。(詳しくはDenoのセクションで記述します)

もっと詳しく知りたい方は、こちらの記事にRyan Dahl氏が述べたNode.jsの後悔について詳しく記載されているので、ご覧ください。

yosuke-furukawa.hatenablog.com

こういった課題から登場したのが、DenoとBunです。

Deno

DenoはRustベースのJavaScriptランタイムです。

Node.jsと同様、Ryan Dahl氏によって、Node.jsが提供しているものの改善を目的として作成され、立ち上げられました。

2020年5月にバージョン 1.0をリリースしました。

Node.jsの開発者がDenoを作ったというのに驚きですね。

Denoが開発された背景

1. セキュリティ

Node.jsのセキュリティを改善したものがDenoと言われます。

Node.jsでは、スクリプトがデフォルトでフルアクセス権限を持っています。

一方、Denoは、sandboxモデルになっており、デフォルトではネットワークアクセスやファイルの書き込み権限がありません。

明示的に--allow-net--allow-writeといったオプションをつけることでアクセス権限が付与されます。

deno run --allow-read=/etc https://deno.land/std/examples/cat.ts /etc/passwd

2. モジュールシステム

Node.jsでは、上記でも述べた通りCommon JSとES Modulesの2つのモジュールシステムを採用しています。

また、npm経由でパッケージ管理されます。

一方、DenosではURLまたはファイルパスから直接importします。(リモート or ローカル)

これは、コード内にimportしたものしかimportしないということを表しています。

ちなみに、import には拡張子は必要になります。

// remote import 
import { add, multiply } from "https://x.nest.land/ramda@0.27.0/source/index.js";

// local import 
import { add, multiply } from "./arithmetic.ts";

3. TypeScriptサポート

Node.jsでTypeScriptを使う場合、型定義ファイルをインストールする必要がありますが、 Denosではネイティブサポートしているので、意識することなくTypeScriptを使うことができます。

Node.jsの場合

npm install --save-dev typescript @types/node

とはいえ、Node.jsもTypeScriptサポートが活発化している*2のも事実ではあります。

Denoはまた、「Fresh」というDenoのために作られたウェブフレームワークや、「Lume」という静的サイトジェネレーターといったエコシステムもあります。

Bun

Bunは、2023年9月に1.0をリリースしたばかりのJavaScriptランタイムです。

Bunの特徴

1. パフォーマンス

Bunの特徴は、Node.jsとDenoと比べて圧倒的にパフォーマンスが高いということです。

公式サイト*3によると、Node.jsより最大4倍早く起動すると書かれています。

これは、BunがEdgeで動くJavaScriptランタイムでシェアNo.1を取ることをゴールていることに由来します。*4

2. モジュールシステム

面白いことに、BunはCommonJSとES Modulesどちらもサポートしており、同じファイルに以下のコードのようにCommonJSとES Modulesのimportを書くことができます。

この時、package.jsonなどの設定ファイルを変更する必要はなく、どちらもimportするだけで動くようです。

import lodash from "lodash";
const _ = require("underscore");

3. TypeScriptサポート

BunもDeno同様、TypeScriptをサポートし、JSXやTSXファイルも外部ライブラリなしで利用可能*5です。

DenoでHello World

それでは、DenoでHello Worldを出力するチュートリアル*6を行っていきたいと思います。

インストール

Homebrewでインストール

$ brew install deno
$ deno -V
deno 1.37.2

サンプルコード

外部APIをfetchしてレスポンスをログ出力する簡単なコードです。

// index.ts
async function func () {
  const textResponse = await fetch("https://pokeapi.co/api/v2/pokemon?limit=1");
  const textData = await textResponse.text();
  console.log(textData);
}

func();

deno run--allow-netオプションをつけて実行することで、レスポンスが出力されます。

$ deno run --allow-net index.ts
{"count":1292,"next":"https://pokeapi.co/api/v2/pokemon?offset=1&limit=1","previous":null,"results":[{"name":"bulbasaur","url":"https://pokeapi.co/api/v2/pokemon/1/"}]}

--allow-netをつけずに実行すると、このようにwarningが出るので、問題があった時は非常に分かりやすいですね。

$ deno run index.ts
┌ ⚠️  Deno requests net access to "pokeapi.co".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) > 

BunでHello World

インストール

$ brew tap oven-sh/bun # for macOS and Linux
$ brew install bun
$ bun --version
1.0.7

init

$ bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (bun): test
entry point (index.ts): 

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts

bun initするとNode.jsと同様なファイル群が生成されます。 bunもnode_modulesやpackage.jsonを使うようです。

$ tree
.
├── README.md
├── bun.lockb
├── index.ts
├── node_modules
├── package.json
└── tsconfig.json

index.tsを見てみるとconsole.log("Hello via Bun!");だけが書かれてました。

bun run index.tsで実行することができます。

$ bun run index.ts
Hello via Bun!

サンプルコード

せっかくなのでJSXを書いてみたいと思います。

function Component(props: {message: string}) {
  return (
    <body>
      <h1 style={{color: 'red'}}>{props.message}</h1>
    </body>
  );
}

console.log(<Component message="Hello world!" />);
$ bun install react
$ bun run index.tsx
<NoName message="Hello world!" />

エラーなく出力されました。

が、NoNameになっているので何かおかしい気もしますが、ここでは一旦見なかったことにします。

Bunは、公式HPにAll in Oneと謳っていますが、JSXやNext.jsに対応しているという点で確かにそうだなと思いました。

まとめ

今回は、JavaScriptランタイムの最新動向をまとめてみました。

初心者向けの記事なので、より詳しく知りたい方は、参照サイトをご覧ください。

結局のところ、一般的には、実績のあるNode.jsが最も安全な選択肢であることに変わりはないと思います。

DenoとBunは、新しいものを作りたい方や、新しい技術の最先端に乗りたい場合に選択すべきなのかなと思いました。

特に、Bunに関しては、Next.js対応JavaScriptランタイムということで、非常興味深いなと感じました。

BunはEdgeで動くのが目的で、起動が速いのが特徴ですが、

lambdaとの相性が非常に良いですし、サーバレスフロントエンド、サーバレスNext.jsなどといった、フロントエンドのトレンドでも活用できそうだと思いました。

最後まで読んでくださりありがとうございました。

*1:JavaScriptに関心のある世界中のITエンジニア3万9472人が回答したアンケートの結果

*2:https://github.com/openjs-foundation/summit/issues/368

*3:https://bun.sh/blog/bun-v1.0

*4:https://gihyo.jp/article/2023/01/tfen005-bun

*5:下記サンプルコードにて、JSXをログ出力しましたが、reactをインストールしないとエラーになりました

*6:https://deno-ja.vercel.app/manual@v1.9.1/getting_started